Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 130 additions & 19 deletions bot/exts/fun/off_topic_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import random
from functools import partial

from discord import ButtonStyle, Colour, Embed, Interaction
from discord import ButtonStyle, Colour, Embed, HTTPException, Interaction
from discord.abc import GuildChannel
from discord.ext import tasks
from discord.ext.commands import Cog, Context, group, has_any_role
from discord.ui import Button, View
from pydis_core.site_api import ResponseCodeError
from pydis_core.utils.channel import get_or_fetch_channel

from bot.bot import Bot
from bot.constants import Bot as BotConfig, Channels, MODERATION_ROLES, NEGATIVE_REPLIES
Expand All @@ -23,6 +25,7 @@
OTN_FORMATTER = "ot{number}-{name}"
OT_NUMBER_INDEX = 2
NAME_START_INDEX = 4
MAX_RENAME_ATTEMPTS = 3

log = get_logger(__name__)

Expand All @@ -46,29 +49,137 @@ async def cog_unload(self) -> None:
self.update_names.clear_exception_types()
self.update_names.stop()

@tasks.loop(time=datetime.time(), reconnect=True)
async def update_names(self) -> None:
"""Background updater task that performs the daily channel name update."""
await self.bot.wait_until_guild_available()

async def _fetch_ot_names(self, count: int) -> list[str]:
try:
channel_0_name, channel_1_name, channel_2_name = await self.bot.api_client.get(
"bot/off-topic-channel-names", params={"random_items": 3}
return await self.bot.api_client.get(
"bot/off-topic-channel-names", params={"random_items": count}
)
except ResponseCodeError as e:
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)
@tasks.loop(time=datetime.time(), reconnect=True)
async def update_names(self) -> None:
"""Background updater task that performs the daily channel name update."""
await self.bot.wait_until_guild_available()

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))
ot_channels = [await get_or_fetch_channel(self.bot, channel) for channel in CHANNELS]
num_ot_channels = len(CHANNELS)

channel_name_pool = iter(await self._fetch_ot_names(num_ot_channels))

renamed_ot_channels: set[int] = set()
deactivated_ot_names: list[str] = []

exit_early = False
for ot_indx, ot_channel in enumerate(ot_channels):
if exit_early:
break

attempt = 0
while attempt < MAX_RENAME_ATTEMPTS:
attempt += 1
try:
new_name = next(channel_name_pool)
except StopIteration:
mod_meta = await get_or_fetch_channel(self.bot, Channels.mod_meta)

failed_to_rename = [
ot_channel.mention for ot_channel in ot_channels if ot_channel.id not in renamed_ot_channels
]
await mod_meta.send(
f":x: The pool of off-topic names ran out whilst attempting to rename {ot_channel.mention}.\n"
f"The following off-topic channels have not been renamed: {' '.join(failed_to_rename)}"
)
exit_early = True
break

new_channel_name = OTN_FORMATTER.format(number=ot_indx, name=new_name)
try:
old_channel_name = ot_channel.name
log.debug(
f"Attempt #{attempt} / {MAX_RENAME_ATTEMPTS} to rename "
f"#{old_channel_name} to #{new_channel_name}"
)

await ot_channel.edit(name=new_channel_name)
log.debug(f"Successfully updated off-topic name #{old_channel_name} to #{new_channel_name}")
except HTTPException as e:
# We need to handle code 50035 ("invalid form body"),
# which we get when the new channel name isn't allowed.
#
# For more information see https://github.com/python-discord/bot/issues/2500
if (e.code != 50035):
# The error isn't the one we want to handle so re-raise
log.error(f"Failed to rename #{ot_channel.name} to #{new_channel_name}")
raise

# Deactivate the name since it's not valid
log.info(
f"Failed to rename #{ot_channel.name} to #{new_channel_name} as it's not "
"a valid name for servers in Server Discovery so removing it from the rota."
)
await self.bot.api_client.patch(
f"bot/off-topic-channel-names/{new_name}",
data={"active": False}
)
deactivated_ot_names.append(new_name)
log.debug(f"Successfully removed {new_name} from the pool of off-topic channel names.")

# Add a replacement off-topic channel name to the pool if it will be used
if len(deactivated_ot_names) < (num_ot_channels * MAX_RENAME_ATTEMPTS):
channel_name_pool = iter([*channel_name_pool, *await self._fetch_ot_names(1)])
else:
renamed_ot_channels.add(ot_channel.id)
break

if deactivated_ot_names:
failed_to_rename = [ot_channel for ot_channel in ot_channels if ot_channel.id not in renamed_ot_channels]
await self.handle_failed_renames(self.bot, deactivated_ot_names, failed_to_rename)

@staticmethod
async def handle_failed_renames(
bot: Bot,
deactivated_names: list[str],
ot_channels_not_renamed: list[GuildChannel]
) -> None:
"""Sends an appropriate warning/error message to mod-meta for each ot channel that had a failed rename."""
num_failures = len(deactivated_names)

# Handle pluralisations
if num_failures == 1:
name_or_names = "name"
its_or_theyre = "it's"
deactivated_names_joined = f"`{deactivated_names[0]}`"
else:
name_or_names = "names"
its_or_theyre = "they're"
deactivated_names_joined = (
", ".join(f"`{name}`" for name in deactivated_names[:-1]) +
f", and `{deactivated_names[-1]}`"
)

log.debug(
"Updated off-topic channel names to"
f" {channel_0_name}, {channel_1_name} and {channel_2_name}"
message = (
f":warning: The following {num_failures} off-topic channel {name_or_names} failed, as {its_or_theyre} "
f"not valid for servers in Server Discovery: {deactivated_names_joined}."
)
if num_ot_channels_not_renamed := len(ot_channels_not_renamed):
if num_ot_channels_not_renamed == 1:
ot_channels_not_renamed = ot_channels_not_renamed[0].mention
else:
ot_channels_not_renamed_joined = (
", ".join(ot_channel.mention for ot_channel in ot_channels_not_renamed[:-1]) +
f" and {ot_channels_not_renamed[-1].mention}"
)
message += (
f"\n:x: Was unable to rename {ot_channels_not_renamed_joined} "
f"within the configured maximum {MAX_RENAME_ATTEMPTS} attempts."
)
else:
message += ("\n:white_check_mark: All off-topic channels were successfully renamed to other names.")

mod_meta_channel = await get_or_fetch_channel(bot, Channels.mod_meta)
await mod_meta_channel.send(message)

async def toggle_ot_name_activity(self, ctx: Context, name: str, active: bool) -> None:
"""Toggle active attribute for an off-topic name."""
Expand Down Expand Up @@ -185,24 +296,24 @@ async def re_roll_command(self, ctx: Context, ot_channel_index: int | None = Non
"bot/off-topic-channel-names", params={"random_items": 1}
)
try:
new_channel_name = response[0]
new_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)
name=OTN_FORMATTER.format(number=old_channel_name[OT_NUMBER_INDEX], name=new_name)
)
log.info(
f"{ctx.author} Off-topic channel re-named from `{old_ot_name}` "
f"to `{new_channel_name}`."
f"to `{new_name}`."
)

await ctx.message.reply(
f":ok_hand: Off-topic channel re-named from `{old_ot_name}` "
f"to `{new_channel_name}`. "
f"to `{new_name}`. "
)

try:
Expand Down