def __init__(self, bot: Bot): self.bot = bot self.webhook_id = constants.Webhooks.duck_pond self.webhook = None self.ducked_messages = [] scheduling.create_task(self.fetch_webhook(), event_loop=self.bot.loop) self.relay_lock = None
async def claim_channel(self, message: discord.Message) -> None: """ Claim the channel in which the question `message` was sent. Move the channel to the In Use category and pin the `message`. Add a cooldown to the claimant to prevent them from asking another question. Lastly, make a new channel available. """ log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.") await self.move_to_in_use(message.channel) await _cooldown.revoke_send_permissions(message.author, self.scheduler) await _message.pin(message) try: await _message.dm_on_open(message) except Exception as e: log.warning("Error occurred while sending DM:", exc_info=e) # Add user with channel for dormant check. await _caches.claimants.set(message.channel.id, message.author.id) self.bot.stats.incr("help.claimed") # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time. timestamp = arrow.Arrow.fromdatetime(message.created_at).timestamp() await _caches.claim_times.set(message.channel.id, timestamp) await _caches.claimant_last_message_times.set(message.channel.id, timestamp) # Delete to indicate that the help session has yet to receive an answer. await _caches.non_claimant_last_message_times.delete(message.channel.id) # Removing the help channel from the dynamic message, and editing/sending that message. self.available_help_channels.remove(message.channel) # Not awaited because it may indefinitely hold the lock while waiting for a channel. scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}")
async def claim_channel(self, message: discord.Message) -> None: """ Claim the channel in which the question `message` was sent. Move the channel to the In Use category and pin the `message`. Add a cooldown to the claimant to prevent them from asking another question. Lastly, make a new channel available. """ log.info( f"Channel #{message.channel} was claimed by `{message.author.id}`." ) await self.move_to_in_use(message.channel) await _cooldown.revoke_send_permissions(message.author, self.scheduler) await _message.pin(message) try: await _message.dm_on_open(message) except Exception as e: log.warning("Error occurred while sending DM:", exc_info=e) # Add user with channel for dormant check. await _caches.claimants.set(message.channel.id, message.author.id) self.bot.stats.incr("help.claimed") # Must use a timezone-aware datetime to ensure a correct POSIX timestamp. timestamp = datetime.now(timezone.utc).timestamp() await _caches.claim_times.set(message.channel.id, timestamp) await _caches.unanswered.set(message.channel.id, True) # Not awaited because it may indefinitely hold the lock while waiting for a channel. scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}")
def __init__(self, bot: Bot): self.bot = bot self.webhook_names = {} self.webhook: t.Optional[discord.Webhook] = None scheduling.create_task(self.get_webhook_names(), event_loop=self.bot.loop) scheduling.create_task(self.get_webhook_and_channel(), event_loop=self.bot.loop)
def __init__(self, bot: Bot, supported_infractions: t.Container[str]): self.bot = bot self.scheduler = scheduling.Scheduler(self.__class__.__name__) scheduling.create_task( self.reschedule_infractions(supported_infractions), event_loop=self.bot.loop)
async def _parse_queue(self) -> None: """ Parse all items from the queue, setting their result Markdown on the futures and sending them to redis. The coroutine will run as long as the queue is not empty, resetting `self._parse_task` to None when finished. """ log.trace("Starting queue parsing.") try: while self._queue: item, soup = self._queue.pop() markdown = None if (future := self._item_futures[item]).done(): # Some items are present in the inventories multiple times under different symbol names, # if we already parsed an equal item, we can just skip it. continue try: markdown = await bot.instance.loop.run_in_executor(None, get_symbol_markdown, soup, item) if markdown is not None: await doc_cache.set(item, markdown) else: # Don't wait for this coro as the parsing doesn't depend on anything it does. scheduling.create_task( self.stale_inventory_notifier.send_warning(item), name="Stale inventory warning" ) except Exception: log.exception(f"Unexpected error when handling {item}") future.set_result(markdown) del self._item_futures[item] await asyncio.sleep(0.1) finally: self._parse_task = None log.trace("Finished parsing queue.")
def __init__(self, bot: Bot) -> None: """Instantiate repository abstraction & allow daemon to start.""" self.bot = bot self.repository = BrandingRepository(bot) scheduling.create_task( self.maybe_start_daemon(), event_loop=self.bot.loop) # Start depending on cache.
def __init__(self, bot: Bot): self.bot = bot self.channel = None self.threshold = relativedelta(days=0) self.expiry = None self.scheduler = Scheduler(self.__class__.__name__) scheduling.create_task(self._sync_settings(), event_loop=self.bot.loop)
async def inner(self: Cog, ctx: Context, *args, **kwargs) -> None: if ctx.channel.id == destination_channel: log.trace( f"Command {ctx.command} was invoked in destination_channel, not redirecting" ) await func(self, ctx, *args, **kwargs) return if bypass_roles and any(role.id in bypass_roles for role in ctx.author.roles): log.trace( f"{ctx.author} has role to bypass output redirection") await func(self, ctx, *args, **kwargs) return elif channels and ctx.channel.id not in channels: log.trace( f"{ctx.author} used {ctx.command} in a channel that can bypass output redirection" ) await func(self, ctx, *args, **kwargs) return elif categories and ctx.channel.category.id not in categories: log.trace( f"{ctx.author} used {ctx.command} in a category that can bypass output redirection" ) await func(self, ctx, *args, **kwargs) return redirect_channel = ctx.guild.get_channel(destination_channel) old_channel = ctx.channel log.trace( f"Redirecting output of {ctx.author}'s command '{ctx.command.name}' to {redirect_channel.name}" ) ctx.channel = redirect_channel if ping_user: await ctx.send( f"Here's the output of your command, {ctx.author.mention}") scheduling.create_task(func(self, ctx, *args, **kwargs)) message = await old_channel.send( f"Hey, {ctx.author.mention}, you can find the output of your command here: " f"{redirect_channel.mention}") if RedirectOutput.delete_invocation: await asyncio.sleep(RedirectOutput.delete_delay) with suppress(NotFound): await message.delete() log.trace( "Redirect output: Deleted user redirection message") with suppress(NotFound): await ctx.message.delete() log.trace("Redirect output: Deleted invocation message")
def __init__(self, bot: Bot) -> None: """Prepare `event_lock` and schedule `crawl_task` on start-up.""" self.bot = bot self.incidents_webhook = None scheduling.create_task(self.fetch_webhook(), event_loop=self.bot.loop) self.event_lock = asyncio.Lock() self.crawl_task = scheduling.create_task(self.crawl_incidents(), event_loop=self.bot.loop)
def __init__(self, bot: Bot) -> None: self.bot = bot self.reviewer = Reviewer(self.__class__.__name__, bot, self) self.cache: Optional[defaultdict[dict]] = None self.api_default_params = { 'active': 'true', 'ordering': '-inserted_at' } self.initial_refresh_task = scheduling.create_task( self.refresh_cache(), event_loop=self.bot.loop) scheduling.create_task(self.schedule_autoreviews(), event_loop=self.bot.loop)
def __init__(self, bot: Bot): self.bot = bot self._role_scheduler = Scheduler("ModPingsOnOff") self._modpings_scheduler = Scheduler("ModPingsSchedule") self.guild = None self.moderators_role = None self.modpings_schedule_task = scheduling.create_task( self.reschedule_modpings_schedule(), event_loop=self.bot.loop) self.reschedule_task = scheduling.create_task( self.reschedule_roles(), name="mod-pings-reschedule", event_loop=self.bot.loop, )
def __init__(self, bot: Bot): self.bot = bot self.scheduler = scheduling.Scheduler(self.__class__.__name__) self.guild: discord.Guild = None self.cooldown_role: discord.Role = None # Categories self.available_category: discord.CategoryChannel = None self.in_use_category: discord.CategoryChannel = None self.dormant_category: discord.CategoryChannel = None # Queues self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None self.last_notification: t.Optional[arrow.Arrow] = None self.dynamic_message: t.Optional[int] = None self.available_help_channels: t.Set[discord.TextChannel] = set() # Asyncio stuff self.queue_tasks: t.List[asyncio.Task] = [] self.init_task = scheduling.create_task(self.init_cog(), event_loop=self.bot.loop)
def __init__(self, bot: Bot, destination: int, webhook_id: int, api_endpoint: str, api_default_params: dict, logger: CustomLogger, *, disable_header: bool = False) -> None: self.bot = bot self.destination = destination # E.g., Channels.big_brother_logs self.webhook_id = webhook_id # E.g., Webhooks.big_brother self.api_endpoint = api_endpoint # E.g., 'bot/infractions' self.api_default_params = api_default_params # E.g., {'active': 'true', 'type': 'watch'} self.log = logger # Logger of the child cog for a correct name in the logs self._consume_task = None self.watched_users = defaultdict(dict) self.message_queue = defaultdict(lambda: defaultdict(deque)) self.consumption_queue = {} self.retries = 5 self.retry_delay = 10 self.channel = None self.webhook = None self.message_history = MessageHistory() self.disable_header = disable_header self._start = scheduling.create_task(self.start_watchchannel(), event_loop=self.bot.loop)
def __init__(self): self._init_task = scheduling.create_task( self._init_channel(), name="StaleInventoryNotifier channel init", event_loop=bot.instance.loop, ) self._warned_urls = set()
async def send_instructions(self, message: discord.Message, instructions: str) -> None: """ Send an embed with `instructions` on fixing an incorrect code block in a `message`. The embed will be deleted automatically after 5 minutes. """ log.info(f"Sending code block formatting instructions for message {message.id}.") embed = self.create_embed(instructions) bot_message = await message.channel.send(f"Hey {message.author.mention}!", embed=embed) self.codeblock_message_ids[message.id] = bot_message.id scheduling.create_task(wait_for_deletion(bot_message, (message.author.id,)), event_loop=self.bot.loop) # Increase amount of codeblock correction in stats self.bot.stats.incr("codeblock_corrections")
async def get_markdown(self, doc_item: _cog.DocItem) -> Optional[str]: """ Get the result Markdown of `doc_item`. If no symbols were fetched from `doc_item`s page before, the HTML has to be fetched and then all items from the page are put into the parse queue. Not safe to run while `self.clear` is running. """ if doc_item not in self._item_futures and doc_item not in self._queue: self._item_futures[doc_item].user_requested = True async with bot.instance.http_session.get(doc_item.url) as response: soup = await bot.instance.loop.run_in_executor( None, BeautifulSoup, await response.text(encoding="utf8"), "lxml", ) self._queue.extendleft(QueueItem(item, soup) for item in self._page_doc_items[doc_item.url]) log.debug(f"Added items from {doc_item.url} to the parse queue.") if self._parse_task is None: self._parse_task = scheduling.create_task(self._parse_queue(), name="Queue parse") else: self._item_futures[doc_item].user_requested = True with suppress(ValueError): # If the item is not in the queue then the item is already parsed or is being parsed self._move_to_front(doc_item) return await self._item_futures[doc_item]
def ensure_valid_reminder( self, reminder: dict ) -> t.Tuple[bool, discord.User, discord.TextChannel]: """Ensure reminder author and channel can be fetched otherwise delete the reminder.""" user = self.bot.get_user(reminder['author']) channel = self.bot.get_channel(reminder['channel_id']) is_valid = True if not user or not channel: is_valid = False log.info( f"Reminder {reminder['id']} invalid: " f"User {reminder['author']}={user}, Channel {reminder['channel_id']}={channel}." ) scheduling.create_task( self.bot.api_client.delete(f"bot/reminders/{reminder['id']}")) return is_valid, user, channel
async def send_eval(self, ctx: Context, code: str) -> Message: """ Evaluate code, format it, and send the output to the corresponding channel. Return the bot response. """ async with ctx.typing(): results = await self.post_eval(code) msg, error = self.get_results_message(results) if error: output, paste_link = error, None else: output, paste_link = await self.format_output(results["stdout"] ) icon = self.get_status_emoji(results) msg = f"{ctx.author.mention} {icon} {msg}.\n\n```\n{output}\n```" if paste_link: msg = f"{msg}\nFull output: {paste_link}" # Collect stats of eval fails + successes if icon == ":x:": self.bot.stats.incr("snekbox.python.fail") else: self.bot.stats.incr("snekbox.python.success") filter_cog = self.bot.get_cog("Filtering") filter_triggered = False if filter_cog: filter_triggered = await filter_cog.filter_eval( msg, ctx.message) if filter_triggered: response = await ctx.send( "Attempt to circumvent filter detected. Moderator team has been alerted." ) else: response = await ctx.send(msg) scheduling.create_task(wait_for_deletion(response, (ctx.author.id, )), event_loop=self.bot.loop) log.info( f"{ctx.author}'s job had a return code of {results['returncode']}" ) return response
async def on_message(self, msg: Message) -> None: """Queues up messages sent by watched users.""" if msg.author.id in self.watched_users: if not self.consuming_messages: self._consume_task = scheduling.create_task( self.consume_messages(), event_loop=self.bot.loop) self.log.trace( f"Received message: {msg.content} ({len(msg.attachments)} attachments)" ) self.message_queue[msg.author.id][msg.channel.id].append(msg)
def __init__(self, bot: Bot, validation_errors: Dict[str, str]) -> None: self.bot = bot self.validation_errors = validation_errors role_id = AntiSpamConfig.punishment['role_id'] self.muted_role = Object(role_id) self.expiration_date_converter = Duration() self.message_deletion_queue = dict() # Fetch the rule configuration with the highest rule interval. max_interval_config = max(AntiSpamConfig.rules.values(), key=itemgetter('interval')) self.max_interval = max_interval_config['interval'] self.cache = MessageCache(AntiSpamConfig.cache_size, newest_first=True) scheduling.create_task( self.alert_on_validation_error(), name="AntiSpam.alert_on_validation_error", event_loop=self.bot.loop, )
def schedule_expiration( self, loop: asyncio.AbstractEventLoop, infraction_object: Dict[str, Union[str, int, bool]]) -> None: """Schedules a task to expire a temporary infraction.""" infraction_id = infraction_object["id"] if infraction_id in self.scheduled_tasks: return task: asyncio.Task = create_task( loop, self._scheduled_expiration(infraction_object)) self.scheduled_tasks[infraction_id] = task
def reaction_check( reaction: discord.Reaction, user: discord.abc.User, *, message_id: int, allowed_emoji: Sequence[str], allowed_users: Sequence[int], allow_mods: bool = True, ) -> bool: """ Check if a reaction's emoji and author are allowed and the message is `message_id`. If the user is not allowed, remove the reaction. Ignore reactions made by the bot. If `allow_mods` is True, allow users with moderator roles even if they're not in `allowed_users`. """ right_reaction = ( user != bot.instance.user and reaction.message.id == message_id and str(reaction.emoji) in allowed_emoji ) if not right_reaction: return False is_moderator = ( allow_mods and any(role.id in MODERATION_ROLES for role in getattr(user, "roles", [])) ) if user.id in allowed_users or is_moderator: log.trace(f"Allowed reaction {reaction} by {user} on {reaction.message.id}.") return True else: log.trace(f"Removing reaction {reaction} by {user} on {reaction.message.id}: disallowed user.") scheduling.create_task( reaction.message.remove_reaction(reaction.emoji, user), suppressed_exceptions=(discord.HTTPException,), name=f"remove_reaction-{reaction}-{reaction.message.id}-{user}" ) return False
async def wait_for_dormant_channel(self) -> discord.TextChannel: """Wait for a dormant channel to become available in the queue and return it.""" log.trace("Waiting for a dormant channel.") task = scheduling.create_task(self.channel_queue.get()) self.queue_tasks.append(task) channel = await task log.trace( f"Channel #{channel} ({channel.id}) finally retrieved from the queue." ) self.queue_tasks.remove(task) return channel
def __init__(self, bot: Bot) -> None: self.bot = bot self._session_scheduler = Scheduler(self.__class__.__name__) self.session_token: Optional[ str] = None # session_info["session_token"]: str self.session_expiry: Optional[ float] = None # session_info["session_expiry"]: UtcPosixTimestamp self.headers = BASE_HEADERS self.exports: Dict[int, List[Dict]] = { } # Saves the output of each question, so internal eval can access it self.init_task = scheduling.create_task(self.init_cog(), event_loop=self.bot.loop)
def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): """ Schedules a task to expire a temporary infraction. :param loop: the asyncio event loop :param infraction_object: the infraction object to expire at the end of the task """ infraction_id = infraction_object["id"] if infraction_id in self.scheduled_tasks: return task: asyncio.Task = create_task( loop, self._scheduled_expiration(infraction_object)) self.scheduled_tasks[infraction_id] = task
def make_confirmation_task(self, incident: discord.Message, timeout: int = 5) -> asyncio.Task: """ Create a task to wait `timeout` seconds for `incident` to be deleted. If `timeout` passes, this will raise `asyncio.TimeoutError`, signaling that we haven't been able to confirm that the message was deleted. """ log.trace( f"Confirmation task will wait {timeout=} seconds for {incident.id=} to be deleted" ) def check(payload: discord.RawReactionActionEvent) -> bool: return payload.message_id == incident.id coroutine = self.bot.wait_for(event="raw_message_delete", check=check, timeout=timeout) return scheduling.create_task(coroutine, event_loop=self.bot.loop)
def __init__(self, bot: Bot): # Contains URLs to documentation home pages. # Used to calculate inventory diffs on refreshes and to display all currently stored inventories. self.base_urls = {} self.bot = bot self.doc_symbols: Dict[str, DocItem] = { } # Maps symbol names to objects containing their metadata. self.item_fetcher = _batch_parser.BatchParser() # Maps a conflicting symbol name to a list of the new, disambiguated names created from conflicts with the name. self.renamed_symbols = defaultdict(list) self.inventory_scheduler = Scheduler(self.__class__.__name__) self.refresh_event = asyncio.Event() self.refresh_event.set() self.symbol_get_event = SharedEvent() self.init_refresh_task = scheduling.create_task( self.init_refresh_inventory(), name="Doc inventory init", event_loop=self.bot.loop, )
async def consume_messages(self, delay_consumption: bool = True) -> None: """Consumes the message queues to log watched users' messages.""" if delay_consumption: self.log.trace( f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue" ) await asyncio.sleep(BigBrotherConfig.log_delay) self.log.trace("Started consuming the message queue") # If the previous consumption Task failed, first consume the existing comsumption_queue if not self.consumption_queue: self.consumption_queue = self.message_queue.copy() self.message_queue.clear() for user_channel_queues in self.consumption_queue.values(): for channel_queue in user_channel_queues.values(): while channel_queue: msg = channel_queue.popleft() self.log.trace( f"Consuming message {msg.id} ({len(msg.attachments)} attachments)" ) await self.relay_message(msg) self.consumption_queue.clear() if self.message_queue: self.log.trace( "Channel queue not empty: Continuing consuming queues") self._consume_task = scheduling.create_task( self.consume_messages(delay_consumption=False), event_loop=self.bot.loop, ) else: self.log.trace("Done consuming messages.")
def __init__(self, bot: Bot) -> None: self.bot = bot scheduling.create_task(self._amend_docstrings(), event_loop=self.bot.loop)