from __future__ import annotations import contextlib from datetime import datetime, timedelta, timezone from typing import Optional, cast import attr import discord from redbot.core.utils.chat_formatting import humanize_timedelta from .message import SchedulerMessage @attr.s(auto_attribs=True, slots=True) class Task: nicename: str uid: str author: discord.Member content: str channel: discord.TextChannel initial: datetime recur: Optional[timedelta] = None extern_cog: Optional[str] = None def __attrs_post_init__(self): if self.initial.tzinfo is None: self.initial = self.initial.replace(tzinfo=timezone.utc) def __hash__(self): return hash(self.uid) async def get_message(self, bot): pfx = (await bot.get_prefix(self.channel))[0] content = f"{pfx}{self.content}" return SchedulerMessage(content=content, author=self.author, channel=self.channel) def to_config(self): return { self.uid: { "nicename": self.nicename, "author": self.author.id, "content": self.content, "channel": self.channel.id, "initial": self.initial.timestamp(), "recur": self.recur.total_seconds() if self.recur else None, "extern_cog": self.extern_cog, } } @classmethod def bulk_from_config(cls, bot: discord.Client, **entries): for uid, data in entries.items(): cid = data.pop("channel", 0) aid = data.pop("author", 0) initial_ts = data.pop("initial", 0) initial = datetime.fromtimestamp(initial_ts, tz=timezone.utc) recur_raw = data.pop("recur", None) recur = timedelta(seconds=recur_raw) if recur_raw else None channel = cast(Optional[discord.TextChannel], bot.get_channel(cid)) if not channel: continue author = channel.guild.get_member(aid) if not author: continue with contextlib.suppress(AttributeError, ValueError): yield cls( initial=initial, recur=recur, channel=channel, author=author, uid=uid, **data, ) @property def next_call_delay(self) -> float: now = datetime.now(timezone.utc) if self.recur and now >= self.initial: raw_interval = self.recur.total_seconds() return raw_interval - ((now - self.initial).total_seconds() % raw_interval) return (self.initial - now).total_seconds() def to_embed(self, index: int, page_count: int, color: discord.Color): now = datetime.now(timezone.utc) next_run_at = now + timedelta(seconds=self.next_call_delay) embed = discord.Embed(color=color, timestamp=next_run_at) embed.title = f"Now viewing {index} of {page_count} selected tasks" embed.add_field(name="Command", value=f"[p]{self.content[:990]}") embed.add_field(name="Channel", value=self.channel.mention) embed.add_field(name="Creator", value=self.author.mention) embed.add_field(name="Task ID", value=self.uid) try: fmt_date = self.initial.strftime("%A %B %-d, %Y at %-I%p %Z") except ValueError: # Windows # This looks less natural, but I'm not doing this piecemeal to emulate. fmt_date = self.initial.strftime("%A %B %d, %Y at %I%p %Z") if self.recur: try: fmt_date = self.initial.strftime("%A %B %-d, %Y at %-I%p %Z") except ValueError: # Windows # This looks less natural, but I'm not doing this piecemeal to emulate. fmt_date = self.initial.strftime("%A %B %d, %Y at %I%p %Z") if self.initial > now: description = ( f"{self.nicename} starts running on {fmt_date}." f"\nIt repeats every {humanize_timedelta(timedelta=self.recur)}" ) else: description = ( f"{self.nicename} started running on {fmt_date}." f"\nIt repeats every {humanize_timedelta(timedelta=self.recur)}" ) footer = "Next runtime:" else: try: fmt_date = next_run_at.strftime("%A %B %-d, %Y at %-I%p %Z") except ValueError: # Windows # This looks less natural, but I'm not doing this piecemeal to emulate. fmt_date = next_run_at.strftime("%A %B %d, %Y at %I%p %Z") description = f"{self.nicename} will run at {fmt_date}." footer = "Runtime:" embed.set_footer(text=footer) embed.description = description return embed def update_objects(self, bot): """Updates objects or throws an AttributeError""" guild_id = self.author.guild.id author_id = self.author.id channel_id = self.channel.id guild = bot.get_guild(guild_id) self.author = guild.get_member(author_id) self.channel = guild.get_channel(channel_id) if not hasattr(self.channel, "id"): raise AttributeError() # Yes, this is slower than an inline `self.channel.id` # It's also not slow anywhere important, and I prefer the clear intent