From 3fa08011d38b1d543044ee491cbc008d55306429 Mon Sep 17 00:00:00 2001 From: Arjun Satarkar Date: Thu, 21 Dec 2023 14:43:32 +0530 Subject: Add question_of_the_day --- .gitignore | 3 + README.adoc | 1 + question_of_the_day/__init__.py | 5 + question_of_the_day/info.json | 5 + question_of_the_day/question_of_the_day.py | 221 +++++++++++++++++++++++++++++ 5 files changed, 235 insertions(+) create mode 100644 .gitignore create mode 100644 README.adoc create mode 100644 question_of_the_day/__init__.py create mode 100644 question_of_the_day/info.json create mode 100644 question_of_the_day/question_of_the_day.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1022f18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/venv/ +__pycache__/ +/.vscode/ diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..15d98d9 --- /dev/null +++ b/README.adoc @@ -0,0 +1 @@ += aps-cogs diff --git a/question_of_the_day/__init__.py b/question_of_the_day/__init__.py new file mode 100644 index 0000000..200f05b --- /dev/null +++ b/question_of_the_day/__init__.py @@ -0,0 +1,5 @@ +from .question_of_the_day import QuestionOfTheDay + + +async def setup(bot): + await bot.add_cog(QuestionOfTheDay(bot)) diff --git a/question_of_the_day/info.json b/question_of_the_day/info.json new file mode 100644 index 0000000..adfd4d0 --- /dev/null +++ b/question_of_the_day/info.json @@ -0,0 +1,5 @@ +{ + "author": ["Arjun Satarkar"], + "description": "Post a random question in a specified channel every day. Users can submit questions for approval.", + "short": "Post (customizable) questions every day." +} diff --git a/question_of_the_day/question_of_the_day.py b/question_of_the_day/question_of_the_day.py new file mode 100644 index 0000000..ca3bb5b --- /dev/null +++ b/question_of_the_day/question_of_the_day.py @@ -0,0 +1,221 @@ +from discord.ext import tasks +from redbot.core import Config +from redbot.core import checks +from redbot.core import commands +import redbot.core +import copy +import datetime +import logging +import random +import time + +MAX_QUESTIONS_PER_GUILD = 1000 +MAX_QUESTION_SIZE = 500 + + +class QuestionOfTheDay(commands.Cog): + def __init__(self, bot): + self.logger = logging.getLogger("red.aps-cogs.question_of_the_day") + self.bot = bot + self.config = Config.get_conf( + self, identifier="551742410770612234|038a0658-85c9-416d-93ea-7c0bdb426734" + ) + self.config.register_guild( + questions=[], + post_at={"hour": 0, "minute": 0}, + post_in_channel=None, + enabled=False, + ) + self.config.register_global(last_posted_qotds_at=None, guild_to_post_at={}) + self.post_qotds.start() + + @tasks.loop(seconds=30) + async def post_qotds(self): + async def post_qotds_for_time(hour, minute): + try: + guilds_due = (await self.config.guild_to_post_at())[ + repr((hour, minute)) + ].keys() + except KeyError: + guilds_due = [] + + for guild_id in guilds_due: + guild = await self.bot.fetch_guild(int(guild_id)) + channel_id = await self.config.guild(guild).post_in_channel() + if not channel_id: + self.logger.info( + f"QOTD was due for guild {guild.name} ({guild_id}) but no channel was set, so it was not posted." + ) + async with self.config.guild(guild).questions() as questions: + channel = await guild.fetch_channel(channel_id) + questions_len = len(questions) + if not questions_len: + await channel.send( + "# Question of the Day\n**No questions left!**" + ) + continue + question_index = random.randrange(0, questions_len) + question = questions[question_index] + await channel.send( + f"# Question of the Day\n" + f"{question['question']}\n{redbot.core.utils.chat_formatting.italics((await guild.fetch_member(question['asked_by'])).name)}" + f" ({question['asked_by']})" + ) + del questions[question_index] + self.logger.info( + f"Posted QOTD for guild {guild.name} ({guild_id})." + ) + + current_time = time.time() + + current_datetime = datetime.datetime.fromtimestamp( + current_time, datetime.timezone.utc + ) + hour = current_datetime.hour + minute = current_datetime.minute + + last_posted_time = await self.config.last_posted_qotds_at() + last_posted_datetime = datetime.datetime.fromtimestamp( + last_posted_time, datetime.timezone.utc + ) + if not (hour == last_posted_datetime.hour and minute == last_posted_datetime.minute): + await post_qotds_for_time(hour, minute) + + gap_secs = current_time - (last_posted_time or current_time) + if gap_secs >= 60: + # Posts may have been missed; recover them up to an hour + self.logger.info(f"Detected gap of {gap_secs} seconds.") + gap_minutes = min(int(gap_secs / 60), 60) + for _ in range(gap_minutes): + minute -= 1 + if minute < 0: + minute = 59 + hour -= 1 + await post_qotds_for_time(hour, minute) + + await self.config.last_posted_qotds_at.set(current_time) + + async def cog_unload(self): + self.post_qotds.cancel() + + @commands.group() + async def qotd(self, _ctx): + pass + + @qotd.command() + @checks.admin_or_permissions(manage_server=True) + async def add(self, ctx, *, question: str): + if not await self.check_and_handle_question_length(ctx, question): + return + async with self.config.guild(ctx.guild).questions() as questions: + if len(questions) > MAX_QUESTIONS_PER_GUILD: + await ctx.reply( + f"Error: too many questions already added in this server! Max is {MAX_QUESTIONS_PER_GUILD}." + ) + return + questions.append({"question": question, "asked_by": ctx.author.id}) + await ctx.reply("Question added!") + + @qotd.command() + @checks.admin_or_permissions(manage_server=True) + async def list(self, ctx): + pages = [ + x + for x in redbot.core.utils.chat_formatting.pagify( + redbot.core.utils.common_filters.filter_various_mentions( + "\n".join( + [ + f"{i + 1}. {redbot.core.utils.chat_formatting.bold(question['question'])} by " + f"{redbot.core.utils.chat_formatting.bold(str(await ctx.guild.fetch_member(question['asked_by'])) + ' (' + str(question['asked_by']) + ')')}" + for i, question in enumerate( + await self.config.guild(ctx.guild).questions() + ) + ] + ) + ) + ) + ] + if pages: + await redbot.core.utils.menus.menu(ctx, pages) + else: + await ctx.reply("No questions yet.") + + @qotd.command() + @checks.admin_or_permissions(manage_server=True) + async def remove(self, ctx, question_id: int): + async with self.config.guild(ctx.guild).questions() as questions: + try: + del questions[question_id - 1] + except IndexError: + await ctx.reply(f"Error: no question with id {question_id}.") + return + await ctx.reply(f"Deleted question {question_id}.") + + @qotd.command() + @checks.admin_or_permissions(manage_server=True) + async def post_at(self, ctx, hour_after_midnight_utc: int, minute_after_hour: int): + if ( + hour_after_midnight_utc >= 0 + and hour_after_midnight_utc < 24 + and minute_after_hour >= 0 + and minute_after_hour < 60 + ): + async with self.config.guild(ctx.guild).post_at() as post_at: + old_post_at = copy.copy(post_at) + post_at["hour"] = hour_after_midnight_utc + post_at["minute"] = minute_after_hour + async with self.config.guild_to_post_at() as guild_to_post_at: + try: + del guild_to_post_at[ + repr((old_post_at["hour"], old_post_at["minute"])) + ][repr(ctx.guild.id)] + except KeyError: + pass + try: + guild_to_post_at[(hour_after_midnight_utc, minute_after_hour)][ + ctx.guild.id + ] = 1 + except KeyError: + guild_to_post_at[ + (hour_after_midnight_utc, minute_after_hour) + ] = {ctx.guild.id: 1} + await ctx.reply( + f"The bot will post the question of the day {hour_after_midnight_utc:0>2}:{minute_after_hour:0>2} hours after midnight UTC." + ) + else: + await ctx.reply( + "Error: the conditions 0 ≤ hours < 24 and 0 ≤ minutes < 60 must be observed." + ) + + @qotd.command() + @checks.admin_or_permissions(manage_server=True) + async def post_here(self, ctx): + await self.config.guild(ctx.guild).post_in_channel.set(ctx.channel.id) + await ctx.reply("Questions of the day will be posted in this channel.") + + async def check_and_handle_question_length(self, ctx, question: str): + if len(question.encode("utf-8")) > MAX_QUESTION_SIZE: + await ctx.reply( + f"Error: that question is too long! Maximum length is {MAX_QUESTION_SIZE} bytes." + ) + return False + return True + + @qotd.command() + @checks.admin_or_permissions(manage_server=True) + async def toggle(self, ctx): + should_be_enabled = not await self.config.guild(ctx.guild).enabled() + await self.config.guild(ctx.guild).enabled.set(should_be_enabled) + post_at = await self.config.guild(ctx.guild).post_at() + async with self.config.guild_to_post_at() as guild_to_post_at: + try: + guild_to_post_at[(post_at["hour"], post_at["minute"])][ctx.guild.id] = 1 + except KeyError: + guild_to_post_at[(post_at["hour"], post_at["minute"])] = { + ctx.guild.id: 1 + } + await ctx.reply( + "QOTDs will be posted in this server (provided that the channel has been set with post_here)." + if should_be_enabled + else "QOTDs will no longer be posted." + ) -- cgit v1.2.3-57-g22cb