aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--README.adoc1
-rw-r--r--question_of_the_day/__init__.py5
-rw-r--r--question_of_the_day/info.json5
-rw-r--r--question_of_the_day/question_of_the_day.py221
5 files changed, 235 insertions, 0 deletions
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."
+ )