Skip to content

Commit

Permalink
timers update :o
Browse files Browse the repository at this point in the history
  • Loading branch information
i-am-zaidali committed Sep 25, 2023
1 parent dd61a19 commit 166efea
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 66 deletions.
136 changes: 89 additions & 47 deletions timer/models.py
@@ -1,15 +1,21 @@
import asyncio
import functools
import logging
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Coroutine, Dict, List, Optional
from typing import Any, Coroutine, Dict, List, Optional, TYPE_CHECKING

import discord
from discord.ui import Button, View
from redbot.core.bot import Red
from redbot.core.utils import chat_formatting as cf

from .exceptions import TimerError

if TYPE_CHECKING:
from .timers import Timer

log = logging.getLogger("red.craycogs.Timer.models")


Expand All @@ -19,11 +25,62 @@ class TimerSettings:
emoji: str


class TimerObj:
_tasks: Dict[int, asyncio.Task] = {}
class TimerView(View):
def __init__(self, cog: "Timer", emoji=":tada:", disabled=False):
super().__init__(timeout=None)
self.bot = cog.bot
self.cog = cog
self.JTB = JoinTimerButton(emoji, self._callback, disabled)
self.add_item(self.JTB)

async def _callback(self, button: "JoinTimerButton", interaction: discord.Interaction):
log.debug("callback called")
cog: "Timer" = self.cog

timer = await cog.get_timer(interaction.guild.id, interaction.message.id)
if not timer:
return await interaction.response.send_message(
"This timer does not exist in my database. It might've been erased due to a glitch.",
ephemeral=True,
)

# haha ctrl C + Ctrl V from giveaways go brrrrrrrrr
elif not timer.remaining_seconds:
await interaction.response.defer()
return await timer.end()

log.debug("timer exists")

result = await timer.add_entrant(interaction.user)
kwargs = {}

if result:
kwargs.update(
{"content": f"{interaction.user.mention} you will be notfied when the timer ends."}
)

else:
await timer.remove_entrant(interaction.user)
kwargs.update(
{
"content": f"{interaction.user.mention} you will no longer be notified for the timer."
}
)

await interaction.response.send_message(**kwargs, ephemeral=True)


class JoinTimerButton(Button[TimerView]):
def __init__(self, emoji: str | None, callback, disabled=False, custom_id="JOIN_TIMER_BUTTON"):
super().__init__(
emoji=emoji,
style=discord.ButtonStyle.green,
disabled=disabled,
custom_id=custom_id,
)
self.callback = functools.partial(callback, self)


class TimerObj:
def __init__(self, **kwargs):
gid, cid, e, bot = self.check_kwargs(kwargs)

Expand All @@ -39,7 +96,7 @@ def __init__(self, **kwargs):
self.ends_at: datetime = e

@property
def cog(self):
def cog(self) -> Optional["Timer"]:
return self.bot.get_cog("Timer")

@property
Expand Down Expand Up @@ -67,7 +124,7 @@ def jump_url(self) -> str:
return f"https://discord.com/channels/{self.guild_id}/{self.channel_id}/{self.message_id}"

@property
def remaining_time(self):
def remaining_seconds(self):
return self.ends_at - datetime.now(timezone.utc)

@property
Expand Down Expand Up @@ -131,9 +188,9 @@ def __hash__(self) -> int:
async def get_embed_description(self):
return (
f"React with {self.emoji} to be notified when the timer ends.\n"
f"Remaining time: **{cf.humanize_timedelta(timedelta=self.remaining_time)}**"
f"Remaining time: **<t:{int(time.time() + self.remaining_time)}:R>** (<t:{int(time.time() + self.remaining_time)}:F>)\n"
if (await self.cog.get_guild_settings(self.guild_id)).notify_users
else f"Remaining time: **{cf.humanize_timedelta(timedelta=self.remaining_time)}**"
else f"Remaining time: **<t:{int(time.time() + self.remaining_time)}:R>** (<t:{int(time.time() + self.remaining_time)}:F>)\n"
)

async def get_embed_color(self):
Expand All @@ -152,8 +209,15 @@ async def _get_message(self, message_id: int = None) -> Optional[discord.Message

async def add_entrant(self, user_id: int):
if user_id == self._host:
return
return False
self._entrants.add(user_id)
return True

async def remove_entrant(self, user_id: int):
if user_id == self._host:
return False
self._entrants.remove(user_id)
return True

async def start(self):
embed = (
Expand All @@ -166,46 +230,22 @@ async def start(self):
.set_footer(text=f"Hosted by: {self.host}", icon_url=self.host.display_avatar.url)
)

msg: discord.Message = await self.channel.send(embed=embed)
kwargs = {
"embed": embed,
}
if (await self.cog.get_guild_settings(self.guild_id)).notify_users:
await msg.add_reaction(self.emoji)
self.message_id = msg.id

self._tasks[self.message_id] = asyncio.create_task(self._start_edit_task())
await self.cog.add_timer(self)

async def _start_edit_task(self):
try:
while True:
await asyncio.sleep(self.edit_wait_duration)
kwargs.update({"view": TimerView()})

if self.ended:
await self.end()
break
msg: discord.Message = await self.channel.send(**kwargs)

msg = await self.message
if not msg:
raise TimerError(
f"Couldn't find timer message with id {self.message_id}. Removing from cache."
)

embed: discord.Embed = msg.embeds[0]

embed.description = await self.get_embed_description()

await msg.edit(embed=embed)

return True
self.message_id = msg.id

except Exception as e:
log.exception("Error when editing timer: ", exc_info=e)
await self.cog.add_timer(self)

async def end(self):
msg = await self.message
if not msg:
await self.cog.remove_timer(self)
self._tasks[self.message_id].cancel()
del self._tasks[self.message_id]
raise TimerError(
f"Couldn't find timer message with id {self.message_id}. Removing from cache."
)
Expand All @@ -214,27 +254,29 @@ async def end(self):

embed.description = "This timer has ended!"

await msg.edit(embed=embed)
settings = await self.cog.get_guild_settings(self.guild_id)

view = TimerView(self.cog, settings.emoji, True)

await msg.edit(embed=embed, view=view)

notify = (await self.cog.get_guild_settings(self.guild_id)).notify_users
notify = settings.notify_users

await msg.reply(
rep = await msg.reply(
f"{self.host.mention} your timer for **{self.name}** has ended!\n" + self.jump_url
)

pings = (
"\n".join((i.mention for i in self.entrants if i is not None))
" ".join((i.mention for i in self.entrants if i is not None))
if self._entrants and notify
else ""
)

if pings:
for page in cf.pagify(pings, delims=[" "], page_length=2000):
await msg.channel.send(page)
await msg.channel.send(page, reference=rep.to_reference(fail_if_not_exists=False))

await self.cog.remove_timer(self)
self._tasks[self.message_id].cancel()
del self._tasks[self.message_id]

@classmethod
def from_json(cls, json: dict):
Expand Down
59 changes: 40 additions & 19 deletions timer/timers.py
@@ -1,7 +1,9 @@
import asyncio
import logging
from typing import Dict, List

import discord
from discord.ext import tasks
from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.utils import chat_formatting as cf
Expand All @@ -10,13 +12,14 @@
from .utils import EmojiConverter, TimeConverter

guild_defaults = {"timers": [], "timer_settings": {"notify_users": True, "emoji": "\U0001f389"}}
log = logging.getLogger("red.craycogs.Timer.timers")


class Timer(commands.Cog):
"""Start countdowns that help you keep track of the time passed"""

__author__ = ["crayyy_zee"]
__version__ = "1.0.4"
__version__ = "1.1.0"

def __init__(self, bot: Red):
self.bot = bot
Expand All @@ -43,23 +46,17 @@ def format_help_for_context(self, ctx: commands.Context) -> str:
]
return "\n".join(text)

@classmethod
async def initialize(cls, bot):
self = cls(bot)

async def cog_load(self):
guilds = await self.config.all_guilds()

for guild_data in guilds.values():
for x in guild_data.get("timers", []):
x.update({"bot": bot})
x.update({"bot": self.bot})
timer = TimerObj.from_json(x)
await self.add_timer(timer)
TimerObj._tasks[timer.message_id] = asyncio.create_task(timer._start_edit_task())

self.max_duration: int = await self.config.max_duration()

return self

async def get_timer(self, guild_id: int, timer_id: int):
if not (guild := self.cache.get(guild_id)):
return None
Expand All @@ -69,9 +66,13 @@ async def get_timer(self, guild_id: int, timer_id: int):
return timer

async def add_timer(self, timer: TimerObj):
if await self.get_timer(timer.guild_id, timer.message_id):
return
self.cache.setdefault(timer.guild_id, []).append(timer)

async def remove_timer(self, timer: TimerObj):
if not (guild := self.cache.get(timer.guild_id)):
return
self.cache[timer.guild_id].remove(timer)

async def get_guild_settings(self, guild_id: int):
Expand All @@ -82,23 +83,43 @@ async def _back_to_config(self):
await self.config.guild_from_id(guild_id).timers.set([x.json for x in timers])

async def cog_unload(self):
for message_id, task in TimerObj._tasks.items():
task.cancel()

self.timer_task.cancel()
await self._back_to_config()

@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
if not payload.guild_id or payload.member.bot:
@tasks.loop(seconds=3)
async def end_timer(self):
if self.end_timer._current_loop and self.end_timer._current_loop % 100 == 0:
await self.to_config()

if not self.bot.get_cog("Giveaways"):
# safeguard idk
return

timer = await self.get_timer(payload.guild_id, payload.message_id)
if not timer:
c = self.cache.copy()
timers = [
timer
for data in c.values()
for timer in data
if isinstance(timer, TimerObj) and timer.ended
]

if not timers:
return

await timer.add_entrant(payload.user_id)
results = await asyncio.gather(*[timer.end() for timer in timers], return_exceptions=True)

list(
map(
lambda x: self.remove_from_cache(x),
timers,
)
)

for result in results:
if isinstance(result, Exception):
log.error(f"A timer ended with an error:", exc_info=result)

@commands.group(name="timer", aliases=["t"])
@commands.group(name="timer")
@commands.mod_or_permissions(manage_messages=True)
async def timer(self, ctx: commands.Context):
"""
Expand Down

0 comments on commit 166efea

Please sign in to comment.