diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py index b8206dab42..5c5fa1dd59 100644 --- a/bot/exts/fun/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py @@ -1,18 +1,30 @@ +import asyncio import datetime import difflib +import json +import random +from functools import partial +from typing import Optional from botcore.site_api import ResponseCodeError -from discord import Colour, Embed +from discord import ButtonStyle, Colour, Embed, Interaction from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role +from discord.ui import Button, View from bot.bot import Bot -from bot.constants import Bot as BotConfig, Channels, MODERATION_ROLES +from bot.constants import Bot as BotConfig, Channels, MODERATION_ROLES, NEGATIVE_REPLIES from bot.converters import OffTopicName from bot.log import get_logger from bot.pagination import LinePaginator CHANNELS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2) + +# In case, the off-topic channel name format is modified. +OTN_FORMATTER = "ot{number}-{name}" +OT_NUMBER_INDEX = 2 +NAME_START_INDEX = 4 + log = get_logger(__name__) @@ -45,19 +57,42 @@ async def update_names(self) -> None: 'bot/off-topic-channel-names', params={'random_items': 3} ) except ResponseCodeError as e: - log.error(f"Failed to get new off topic channel names: code {e.response.status}") + log.error(f"Failed to get new off-topic channel names: code {e.response.status}") raise channel_0, channel_1, channel_2 = (self.bot.get_channel(channel_id) for channel_id in CHANNELS) - await channel_0.edit(name=f'ot0-{channel_0_name}') - await channel_1.edit(name=f'ot1-{channel_1_name}') - await channel_2.edit(name=f'ot2-{channel_2_name}') + await channel_0.edit(name=OTN_FORMATTER.format(number=0, name=channel_0_name)) + await channel_1.edit(name=OTN_FORMATTER.format(number=1, name=channel_1_name)) + await channel_2.edit(name=OTN_FORMATTER.format(number=2, name=channel_2_name)) + log.debug( "Updated off-topic channel names to" f" {channel_0_name}, {channel_1_name} and {channel_2_name}" ) + async def toggle_ot_name_activity(self, ctx: Context, name: str, active: bool) -> None: + """Toggle active attribute for an off-topic name.""" + data = { + "active": active + } + await self.bot.api_client.patch(f"bot/off-topic-channel-names/{name}", data=data) + await ctx.send(f"Off-topic name `{name}` has been {'activated' if active else 'deactivated'}.") + + async def list_ot_names(self, ctx: Context, active: bool = True) -> None: + """Send an embed containing active/deactivated off-topic channel names.""" + result = await self.bot.api_client.get('bot/off-topic-channel-names', params={'active': json.dumps(active)}) + lines = sorted(f"• {name}" for name in result) + embed = Embed( + title=f"{'Active' if active else 'Deactivated'} off-topic names (`{len(result)}` total)", + colour=Colour.blue() + ) + if result: + await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False) + else: + embed.description = "Hmmm, seems like there's nothing here yet." + await ctx.send(embed=embed) + @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) async def otname_group(self, ctx: Context) -> None: @@ -109,7 +144,111 @@ async def delete_command(self, ctx: Context, *, name: OffTopicName) -> None: log.info(f"{ctx.author} deleted the off-topic channel name '{name}'") await ctx.send(f":ok_hand: Removed `{name}` from the names list.") - @otname_group.command(name='list', aliases=('l',)) + @otname_group.command(name='activate', aliases=('whitelist',)) + @has_any_role(*MODERATION_ROLES) + async def activate_ot_name(self, ctx: Context, name: OffTopicName) -> None: + """Activate an existing off-topic name.""" + await self.toggle_ot_name_activity(ctx, name, True) + + @otname_group.command(name='deactivate', aliases=('blacklist',)) + @has_any_role(*MODERATION_ROLES) + async def de_activate_ot_name(self, ctx: Context, name: OffTopicName) -> None: + """Deactivate a specific off-topic name.""" + await self.toggle_ot_name_activity(ctx, name, False) + + @otname_group.command(name='reroll') + @has_any_role(*MODERATION_ROLES) + async def re_roll_command(self, ctx: Context, ot_channel_index: Optional[int] = None) -> None: + """ + Re-roll an off-topic name for a specific off-topic channel and deactivate the current name. + + ot_channel_index: [0, 1, 2, ...] + """ + if ot_channel_index is not None: + try: + channel = self.bot.get_channel(CHANNELS[ot_channel_index]) + except IndexError: + await ctx.send(f":x: No off-topic channel found with index {ot_channel_index}.") + return + elif ctx.channel.id in CHANNELS: + channel = ctx.channel + + else: + await ctx.send("Please specify channel for which the off-topic name should be re-rolled.") + return + + old_channel_name = channel.name + old_ot_name = old_channel_name[NAME_START_INDEX:] # ot1-name-of-ot -> name-of-ot + + await self.de_activate_ot_name(ctx, old_ot_name) + + response = await self.bot.api_client.get( + 'bot/off-topic-channel-names', params={'random_items': 1} + ) + try: + new_channel_name = response[0] + except IndexError: + await ctx.send("Out of active off-topic names. Add new names to reroll.") + return + + async def rename_channel() -> None: + """Rename off-topic channel and log events.""" + await channel.edit( + name=OTN_FORMATTER.format(number=old_channel_name[OT_NUMBER_INDEX], name=new_channel_name) + ) + log.info( + f"{ctx.author} Off-topic channel re-named from `{old_ot_name}` " + f"to `{new_channel_name}`." + ) + + await ctx.message.reply( + f":ok_hand: Off-topic channel re-named from `{old_ot_name}` " + f"to `{new_channel_name}`. " + ) + + try: + await asyncio.wait_for(rename_channel(), 3) + except asyncio.TimeoutError: + # Channel rename endpoint rate limited. The task was cancelled by asyncio. + btn_yes = Button(label="Yes", style=ButtonStyle.success) + btn_no = Button(label="No", style=ButtonStyle.danger) + + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + description=( + "Re-naming the channel is being rate-limited. " + "Would you like to schedule an asyncio task to rename the channel within the current bot session ?" + ), + colour=Colour.blurple() + ) + + async def btn_call_back(schedule: bool, interaction: Interaction) -> None: + if ctx.author != interaction.user: + log.info("User is not author, skipping.") + return + message = interaction.message + + embed.description = ( + "Scheduled a channel re-name process within the current bot session." + if schedule + else + "Channel not re-named due to rate limit. Please try again later." + ) + await message.edit(embed=embed, view=None) + + if schedule: + await rename_channel() + + btn_yes.callback = partial(btn_call_back, True) + btn_no.callback = partial(btn_call_back, False) + + view = View() + view.add_item(btn_yes) + view.add_item(btn_no) + + await ctx.message.reply(embed=embed, view=view) + + @otname_group.group(name='list', aliases=('l',), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) async def list_command(self, ctx: Context) -> None: """ @@ -117,17 +256,19 @@ async def list_command(self, ctx: Context) -> None: Restricted to Moderator and above to not spoil the surprise. """ - result = await self.bot.api_client.get('bot/off-topic-channel-names') - lines = sorted(f"• {name}" for name in result) - embed = Embed( - title=f"Known off-topic names (`{len(result)}` total)", - colour=Colour.blue() - ) - if result: - await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False) - else: - embed.description = "Hmmm, seems like there's nothing here yet." - await ctx.send(embed=embed) + await self.active_otnames_command(ctx) + + @list_command.command(name='active', aliases=('a',)) + @has_any_role(*MODERATION_ROLES) + async def active_otnames_command(self, ctx: Context) -> None: + """List active off-topic channel names.""" + await self.list_ot_names(ctx, True) + + @list_command.command(name='deactivated', aliases=('d',)) + @has_any_role(*MODERATION_ROLES) + async def deactivated_otnames_command(self, ctx: Context) -> None: + """List deactivated off-topic channel names.""" + await self.list_ot_names(ctx, False) @otname_group.command(name='search', aliases=('s',)) @has_any_role(*MODERATION_ROLES)