diff options
-rw-r--r-- | bot/constants.py | 1 | ||||
-rw-r--r-- | bot/exts/info/subscribe.py | 4 | ||||
-rw-r--r-- | bot/exts/recruitment/helper_utils.py | 74 | ||||
-rw-r--r-- | bot/helper_questions.py | 144 |
4 files changed, 222 insertions, 1 deletions
diff --git a/bot/constants.py b/bot/constants.py index a0a20373f..71e9bc160 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -164,6 +164,7 @@ class _Roles(EnvConfig): mod_team = 267629731250176001 owners = 267627879762755584 project_leads = 815701647526330398 + new_helpers = 1088292464051368018 # Code Jam jammers = 737249140966162473 diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index aff1302bb..90a3d4529 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -12,6 +12,7 @@ from pydis_core.utils import members from bot import constants from bot.bot import Bot from bot.decorators import redirect_output +from bot.helper_questions import HelperingButton from bot.log import get_logger from bot.utils.channel import get_or_fetch_channel @@ -78,7 +79,8 @@ class RoleButtonView(discord.ui.View): self.interaction_owner = member author_roles = [role.id for role in member.roles] - for index, role in enumerate(assignable_roles): + self.add_item(HelperingButton(constants.Roles.new_helpers in author_roles, 0)) + for index, role in enumerate(assignable_roles, start=1): row = index // ITEMS_PER_ROW self.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) diff --git a/bot/exts/recruitment/helper_utils.py b/bot/exts/recruitment/helper_utils.py new file mode 100644 index 000000000..fe264f647 --- /dev/null +++ b/bot/exts/recruitment/helper_utils.py @@ -0,0 +1,74 @@ +import datetime as dt +import random +import re + +import arrow +from async_rediscache import RedisCache +from discord import Message +from discord.ext.commands import Cog + +from bot.bot import Bot +from bot.constants import Channels, Roles +from bot.log import get_logger + +OT_CHANNEL_IDS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2) +NEW_HELPER_ROLE_ID = Roles.new_helpers + +log = get_logger(__name__) + +URL_RE = re.compile(r"(https?://[^\s]+)", flags=re.IGNORECASE) + + +class NewHelperUtils(Cog): + """Manages functionality for new helpers in April 2023.""" + + # RedisCache[discord.Channel.id, UtcPosixTimestamp] + cooldown_cache = RedisCache() + + CACHE_KEY = "LAST_PING" + + COOLDOWN_DURATION = dt.timedelta(minutes=10) + MESSAGES = [ + f"<@&{NEW_HELPER_ROLE_ID}> can someone please answer this??", + f"Someone answer this <@&{NEW_HELPER_ROLE_ID}> if you want to keep your role", + f"Where are my <@&{NEW_HELPER_ROLE_ID}> at?", + f"<@&{NEW_HELPER_ROLE_ID}>, answer this!", + f"What's the point of having all these new <@&{NEW_HELPER_ROLE_ID}> if questions are going unanswered?", + ] + + def __init__(self, bot: Bot): + self.bot = bot + self.last_pinged = arrow.get(0) # Ready to fire if it can't be loaded from the cache. + + async def cog_load(self) -> None: + """Load the most recent activation time from the cache.""" + self.last_pinged = arrow.get(await self.cooldown_cache.get(self.CACHE_KEY, 0)) + + @staticmethod + def _is_question(message: str) -> bool: + """Return True if `message` appears to be a question, else False!""" + return '?' in URL_RE.sub('', message) + + @Cog.listener() + async def on_message(self, message: Message) -> None: + """ + This is an event listener. + + Ping helpers in off-topic channels whenever someone asks a question, at most + as often `COOLDOWN_DURATION`. + """ + if message.author.bot or message.channel.id not in OT_CHANNEL_IDS: + return + + if arrow.utcnow() - self.last_pinged < self.COOLDOWN_DURATION: + return + + if self._is_question(message.content): + self.last_pinged = arrow.utcnow() + await message.reply(random.choice(self.MESSAGES)) + await self.cooldown_cache.set(self.CACHE_KEY, self.last_pinged.timestamp()) + + +async def setup(bot: Bot) -> None: + """Load the OffTopicNames cog.""" + await bot.add_cog(NewHelperUtils(bot)) diff --git a/bot/helper_questions.py b/bot/helper_questions.py new file mode 100644 index 000000000..a1f69945a --- /dev/null +++ b/bot/helper_questions.py @@ -0,0 +1,144 @@ +from collections import namedtuple +from string import ascii_lowercase, ascii_uppercase +from textwrap import dedent + +import discord + +from bot.constants import Roles + +Question = namedtuple("question", ("question", "answers")) + +questions = [ + Question( + question="How do you print in python?", + answers=( + "`print()`", + "`sys.stdout.write()`", + "None of the above", + "All of the above" + ) + ), + Question( + question=dedent( + """ + A user opens a help channel with the following information: + > Help, my code is broken. + + They are in a hurry, so there's no time for back-and-forth debugging the issue. + Is the solution to their issue: + """ + ).strip(), + answers=( + 'Replace `password == "123" or "456"` with `password in ("123", "456")`', + "Downgrade to 3.10 because `binascii.rldecode_hqx()` was removed in 3.11", + "Restart their computer and try running it again (it worked before)", + ( + "Nothing. They weren't actually getting an error, " + "the import was just greyed out in VSCode because they hadn't used it yet. " + ) + ) + ), + Question( + question="Why is static typing a terrible feature for a programming language?", + answers=( + "It makes it more difficult to apply polymorphism", + "You get TypeErrors before you can even run the code, slowing down development", + "Guido likes static typing now, actually", + "All of the above" + ) + ), + Question( + question="When is Lemon Day?", + answers=( + "January 1", + "April 14", + "August 29", + "Any day that is not Lime Day" + ) + ) +] + +TOTAL_QUESTION_TO_ASK = 4 + +HELPERS_ROLE = discord.Object(Roles.new_helpers) + + +def format_question(question_index: int) -> str: + """Format the question to be displayed in chat.""" + question = questions[question_index] + prompt = f"**Question {question_index+1} of {TOTAL_QUESTION_TO_ASK}**\n\n{question.question}\n\n" + prompt += "\n".join( + f":regional_indicator_{letter}: {answer}" + for letter, answer in zip(ascii_lowercase, question.answers) + ) + return prompt + + +class HelperingView(discord.ui.View): + """A view that implements the helpering logic by asking a series of questions.""" + + def __init__(self, phase: int = 0): + super().__init__() + print(phase) + self.phase = phase + + answers_view = AnswersSelect(phase) + self.add_item(answers_view) + + +class AnswersSelect(discord.ui.Select): + """A selection of answers to the given question.""" + + def __init__(self, phase: int): + question = questions[phase] + answers = [discord.SelectOption(label=answer) for answer in ascii_uppercase[:len(question.answers)]] + + super().__init__(options=answers) + self.phase = phase + + async def callback(self, interaction: discord.Interaction) -> None: + """Move to the next question, or apply the role if enough question were answered.""" + if self.phase + 1 >= TOTAL_QUESTION_TO_ASK: + if isinstance(interaction.user, discord.Member): + await interaction.user.add_roles(HELPERS_ROLE) + await interaction.response.edit_message( + content=":white_check_mark: Added the Helpers role!", view=None + ) + else: + content = format_question(self.phase + 1) + view = HelperingView(self.phase + 1) + await interaction.response.edit_message(content=content, view=view) + + self.view.stop() + + +class HelperingButton(discord.ui.Button): + """The button which starts the helpering process.""" + + def __init__(self, assigned: bool, row: int,): + label = "Add role Helpers" if not assigned else "Remove role Helpers" + style = discord.ButtonStyle.green if not assigned else discord.ButtonStyle.red + super().__init__(style=style, label=label, row=row) + self.assigned = assigned + + async def callback(self, interaction: discord.Interaction) -> None: + """Either remove the Helpers role or start the Helpering process.""" + if self.assigned: + if isinstance(interaction.user, discord.Member): + await interaction.user.remove_roles(HELPERS_ROLE) + self.label = "Add role Helpers" + self.style = discord.ButtonStyle.green + self.assigned = not self.assigned + await interaction.response.edit_message(view=self.view) + await interaction.followup.send("Removed role Helpers", ephemeral=True) + return + + await interaction.response.edit_message(content="Launching Helpering process, good luck!", view=None) + content = format_question(0) + view = HelperingView() + await interaction.followup.send( + content=content, + view=view, + ephemeral=True, + ) + self.view.stop() |