diff --git a/src/bot-utilities.ts b/src/bot-utilities.ts index 21a7f1bf..3afa8f94 100644 --- a/src/bot-utilities.ts +++ b/src/bot-utilities.ts @@ -2,10 +2,11 @@ import { strict as assert } from "assert"; import * as Discord from "discord.js"; -import { Wheatley } from "./wheatley.js"; +import { named_id, Wheatley } from "./wheatley.js"; import { decode_snowflake, is_media_link_embed, make_url, get_thread_owner } from "./utils/discord.js"; import { unwrap } from "./utils/misc.js"; import { colors } from "./common.js"; +import { is_string } from "./utils/strings.js"; type quote_options = { // description template @@ -336,39 +337,68 @@ export class BotUtilities { ); } - async get_channel(id: string) { - const channel = await this.wheatley.client.channels.fetch(id); - if (!channel) { - throw Error(`Channel ${id} not found`); + private async get_channel_internal(channel_info: named_id, case_insensitive: boolean = true) { + let channel: Discord.GuildBasedChannel | null = null; + + try { + channel = await this.wheatley.guild.channels.fetch(channel_info.id); + } catch (e) { + // don't throw when DiscordAPIError[50001]: Missing Access + if (e instanceof Discord.DiscordAPIError && e.code == 50001) { + // unknown channel + channel = null; + } else { + throw e; + } + } + + if (this.wheatley.devmode_enabled && !channel && channel_info.name && is_string(channel_info.name)) { + channel = this.get_channel_by_name(channel_info.name, case_insensitive) ?? null; } - assert(channel instanceof Discord.TextChannel, `Channel ${channel} (${id}) not of the expected type`); + return channel; } - async get_forum_channel(id: string) { - const channel = await this.wheatley.client.channels.fetch(id); - if (!channel) { - throw Error(`Forum channel ${id} not found`); - } - assert(channel instanceof Discord.ForumChannel, `Channel ${channel} (${id}) not of the expected type`); + async get_channel(channel_info: named_id) { + const channel = await this.get_channel_internal(channel_info); + assert( + channel instanceof Discord.TextChannel, + `Channel ${channel?.name} (${channel_info.id}) not of the expected type`, + ); + return channel; } - async get_thread_channel(id: string) { - const channel = await this.wheatley.client.channels.fetch(id); - if (!channel) { - throw Error(`Thread channel ${id} not found`); - } - assert(channel instanceof Discord.ThreadChannel, `Channel ${channel} (${id}) not of the expected type`); + async get_forum_channel(channel_info: named_id) { + const channel = await this.get_channel_internal(channel_info); + + assert( + channel instanceof Discord.ForumChannel, + `Channel ${channel?.name} (${channel_info.id}) not of the expected type`, + ); + return channel; } - async get_category(id: string) { - const category = await this.wheatley.client.channels.fetch(id); - if (!category) { - throw Error(`Category ${id} not found`); - } - assert(category instanceof Discord.CategoryChannel, `Category ${category} (${id}) not of the expected type`); + async get_thread_channel(channel_info: named_id) { + const channel = await this.get_channel_internal(channel_info); + + assert( + channel instanceof Discord.ThreadChannel, + `Channel ${channel?.name} (${channel_info.id}) not of the expected type`, + ); + + return channel; + } + + async get_category(channel_info: named_id) { + const category = await this.get_channel_internal(channel_info, false); + + assert( + category instanceof Discord.CategoryChannel, + `Channel ${category?.name} (${channel_info.id}) not of the expected type`, + ); + return category; } @@ -388,4 +418,20 @@ export class BotUtilities { } } } + + // case-insensitive + get_channel_by_name(name: string, case_insensitive: boolean = true) { + return this.wheatley.guild.channels.cache.find(channel => { + if (case_insensitive) { + return channel.name.toLowerCase() === name.toLowerCase(); + } + + return channel.name === name; + }); + } + + // case-insensitive + get_role_by_name(name: string) { + return this.wheatley.guild.roles.cache.find(role => role.name.toLowerCase() === name.toLowerCase()); + } } diff --git a/src/modules/tccpp/components/anti-self-star.ts b/src/modules/tccpp/components/anti-self-star.ts index 22214c74..f32b561d 100644 --- a/src/modules/tccpp/components/anti-self-star.ts +++ b/src/modules/tccpp/components/anti-self-star.ts @@ -26,7 +26,11 @@ export default class AntiSelfStar extends BotComponent { if (reaction.emoji.name !== "⭐") { return; } - if (message.channelId == this.wheatley.channels.memes && user.id == message.author.id && has_media(message)) { + if ( + message.channelId == this.wheatley.channels.memes.id && + user.id == message.author.id && + has_media(message) + ) { await this.handle_self_star(await departialize(message)); } } diff --git a/src/modules/tccpp/components/auto-reply.ts b/src/modules/tccpp/components/auto-reply.ts index 6fa5a850..d4848508 100644 --- a/src/modules/tccpp/components/auto-reply.ts +++ b/src/modules/tccpp/components/auto-reply.ts @@ -33,7 +33,7 @@ export default class Autoreply extends BotComponent { LLM_REGEX.test(message.content) && !this.is_ratelimited() && Math.random() <= RATELIMIT_PROBABILITY && - message.channel.id !== this.wheatley.channels.bot_spam + message.channel.id !== this.wheatley.channels.bot_spam.id ) { this.last_reply_time = Date.now(); M.log("firing llm auto-reply"); @@ -48,7 +48,7 @@ export default class Autoreply extends BotComponent { MICROSLOP_REGEX.test(message.content) && !this.is_ratelimited() && Math.random() <= RATELIMIT_PROBABILITY && - message.channel.id !== this.wheatley.channels.bot_spam + message.channel.id !== this.wheatley.channels.bot_spam.id ) { this.last_reply_time = Date.now(); M.log("firing microslop auto-reply"); diff --git a/src/modules/tccpp/components/autoreact.ts b/src/modules/tccpp/components/autoreact.ts index 437371be..3bb10784 100644 --- a/src/modules/tccpp/components/autoreact.ts +++ b/src/modules/tccpp/components/autoreact.ts @@ -122,7 +122,7 @@ export default class Autoreact extends BotComponent { this.wheatley.warn("Unable to find emoji nog4g"); } } - if (message.channel.id == this.wheatley.channels.introductions) { + if (message.channel.id == this.wheatley.channels.introductions.id) { if (message.member == null) { // TODO: Ping zelis? M.warn("Why??", message); @@ -132,15 +132,15 @@ export default class Autoreact extends BotComponent { M.log("Waving to new user", message.author.tag, message.author.id, message.url); await message.react("πŸ‘‹"); } - } else if (message.channel.id == this.wheatley.channels.memes && has_media(message)) { + } else if (message.channel.id == this.wheatley.channels.memes.id && has_media(message)) { M.log("Adding star reaction", message.author.tag, message.author.id, message.url); await message.react("⭐"); - } else if (message.channel.id == this.wheatley.channels.server_suggestions) { + } else if (message.channel.id == this.wheatley.channels.server_suggestions.id) { M.log("Adding server suggestion reactions", message.author.tag, message.author.id, message.url); await message.react("πŸ‘"); await message.react("πŸ‘Ž"); await message.react("🀷"); - } else if (message.channel.id == this.wheatley.channels.food && has_media(message)) { + } else if (message.channel.id == this.wheatley.channels.food.id && has_media(message)) { const reaction = message.guild!.emojis.cache.find(emoji => emoji.name === "chefskiss"); if (reaction !== undefined) { await message.react(reaction); @@ -152,7 +152,7 @@ export default class Autoreact extends BotComponent { // reaction blocked if (e instanceof Discord.DiscordAPIError && e.code === 90001) { await message.member?.timeout(1 * MINUTE, "Thou shall not block the bot"); - if (message.channel.id == this.wheatley.channels.server_suggestions) { + if (message.channel.id == this.wheatley.channels.server_suggestions.id) { await message.delete(); } } else { @@ -177,7 +177,7 @@ export default class Autoreact extends BotComponent { if (new_message.createdTimestamp && Date.now() - new_message.createdTimestamp > 5 * MINUTE) { return; } - if (new_message.channel.id == this.wheatley.channels.memes) { + if (new_message.channel.id == this.wheatley.channels.memes.id) { const bot_starred = new_message.reactions.cache.get("⭐")?.users.cache.has(this.wheatley.user.id); // If we haven't stared (or don't know if we've starred) and the new message has media, star if (!bot_starred && has_media(new_message)) { @@ -204,10 +204,7 @@ export default class Autoreact extends BotComponent { } async catch_up() { - const TCCPP = await this.wheatley.client.guilds.fetch(this.wheatley.guild.id); - const introductions_channel = await TCCPP.channels.fetch(this.wheatley.channels.introductions); - assert(introductions_channel); - assert(introductions_channel.type == Discord.ChannelType.GuildText); + const introductions_channel = await this.utilities.get_channel(this.wheatley.channels.introductions); const messages = await introductions_channel.messages.fetch({ limit: 100, cache: false }); for (const [_, message] of messages) { if (await this.is_new_member(message)) { diff --git a/src/modules/tccpp/components/c-help-redirect.ts b/src/modules/tccpp/components/c-help-redirect.ts index 739c5a3d..b9c88e0e 100644 --- a/src/modules/tccpp/components/c-help-redirect.ts +++ b/src/modules/tccpp/components/c-help-redirect.ts @@ -190,8 +190,8 @@ export default class CHelpRedirect extends BotComponent { assert(command.channel instanceof Discord.GuildChannel); // Only allowed in #c-help-text - if (command.channel.id != this.wheatley.channels.c_help_text) { - await command.reply(`Can only be used in <#${this.wheatley.channels.c_help_text}>`, true); + if (command.channel.id != this.wheatley.channels.c_help_text.id) { + await command.reply(`Can only be used in <#${this.wheatley.channels.c_help_text.id}>`, true); return; } @@ -200,12 +200,12 @@ export default class CHelpRedirect extends BotComponent { if (user) { await this.c_help_text.send( `<@${user.id}> Your code looks like C++ code, but this is a C channel. ` + - `Did you mean to post in <#${this.wheatley.channels.cpp_help_text}>?`, + `Did you mean to post in <#${this.wheatley.channels.cpp_help_text.id}>?`, ); } else { await this.c_help_text.send( `This code looks like C++ code, but this is a C channel. ` + - `Did you mean to post in <#${this.wheatley.channels.cpp_help_text}>?`, + `Did you mean to post in <#${this.wheatley.channels.cpp_help_text.id}>?`, ); } } @@ -215,8 +215,8 @@ export default class CHelpRedirect extends BotComponent { assert(command.channel instanceof Discord.GuildChannel); // Only allowed in #cpp-help-text - if (command.channel.id != this.wheatley.channels.cpp_help_text) { - await command.reply(`Can only be used in <#${this.wheatley.channels.cpp_help_text}>`, true); + if (command.channel.id != this.wheatley.channels.cpp_help_text.id) { + await command.reply(`Can only be used in <#${this.wheatley.channels.cpp_help_text.id}>`, true); return; } @@ -225,12 +225,12 @@ export default class CHelpRedirect extends BotComponent { if (user) { await this.cpp_help_text.send( `<@${user.id}> Your code looks like C code, but this is a C++ channel. ` + - `Did you mean to post in <#${this.wheatley.channels.c_help_text}>?`, + `Did you mean to post in <#${this.wheatley.channels.c_help_text.id}>?`, ); } else { await this.cpp_help_text.send( `This code looks like C code, but this is a C++ channel. ` + - `Did you mean to post in <#${this.wheatley.channels.c_help_text}>?`, + `Did you mean to post in <#${this.wheatley.channels.c_help_text.id}>?`, ); } } @@ -256,20 +256,20 @@ export default class CHelpRedirect extends BotComponent { } // Only check messages in help-text channels - if (message.channel.id == this.wheatley.channels.c_help_text) { + if (message.channel.id == this.wheatley.channels.c_help_text.id) { if (this.check_message_for_cpp_code(message)) { this.auto_triggered_users.insert(message.author.id); await message.reply( `<@${message.author.id}> Your code looks like C++ code, but this is a C channel. ` + - `Did you mean to post in <#${this.wheatley.channels.cpp_help_text}>?`, + `Did you mean to post in <#${this.wheatley.channels.cpp_help_text.id}>?`, ); } - } else if (message.channel.id == this.wheatley.channels.cpp_help_text) { + } else if (message.channel.id == this.wheatley.channels.cpp_help_text.id) { if (this.check_message_for_c_code(message)) { this.auto_triggered_users.insert(message.author.id); await message.reply( `<@${message.author.id}> Your code looks like C code, but this is a C++ channel. ` + - `Did you mean to post in <#${this.wheatley.channels.c_help_text}>?`, + `Did you mean to post in <#${this.wheatley.channels.c_help_text.id}>?`, ); } } diff --git a/src/modules/tccpp/components/forum-channels.ts b/src/modules/tccpp/components/forum-channels.ts index a79d53be..cd3c5bb2 100644 --- a/src/modules/tccpp/components/forum-channels.ts +++ b/src/modules/tccpp/components/forum-channels.ts @@ -294,7 +294,9 @@ export default class ForumChannels extends BotComponent { is_non_forum_mirror_channel(thread: Discord.ThreadChannel) { return ( thread.parentId != null && - [this.wheatley.channels.code_review, this.wheatley.channels.showcase].includes(thread.parentId) + [this.wheatley.channels.code_review, this.wheatley.channels.showcase].some( + channel_info => channel_info.id === thread.parentId, + ) ); } @@ -344,7 +346,7 @@ export default class ForumChannels extends BotComponent { await this.mirror_forum_post( message, `New ${ - thread.parentId == this.wheatley.channels.code_review ? "code review" : "showcase" + thread.parentId == this.wheatley.channels.code_review.id ? "code review" : "showcase" } post: ${thread.name}`, true, this.general_discussion, diff --git a/src/modules/tccpp/components/memes.ts b/src/modules/tccpp/components/memes.ts index accd976f..105b130c 100644 --- a/src/modules/tccpp/components/memes.ts +++ b/src/modules/tccpp/components/memes.ts @@ -56,7 +56,7 @@ export default class Memes extends BotComponent { }); await user.send({ content: - `Your message in <#${this.wheatley.channels.memes}> was deleted because it didn't contain ` + + `Your message in <#${this.wheatley.channels.memes.id}> was deleted because it didn't contain ` + `any images, videos, or media embeds. This channel is for sharing memes only. For commentary ` + `please open a thread.`, embeds: quote.embeds, @@ -88,7 +88,7 @@ export default class Memes extends BotComponent { if (message.guildId !== this.wheatley.guild.id) { return; } - if (message.channel.id !== this.wheatley.channels.memes) { + if (message.channel.id !== this.wheatley.channels.memes.id) { return; } if (await this.should_skip(message)) { @@ -109,7 +109,7 @@ export default class Memes extends BotComponent { if (new_message.guildId !== this.wheatley.guild.id) { return; } - if (new_message.channel.id !== this.wheatley.channels.memes) { + if (new_message.channel.id !== this.wheatley.channels.memes.id) { return; } const message = await departialize(new_message); diff --git a/src/modules/tccpp/components/permissions-manager.ts b/src/modules/tccpp/components/permissions-manager.ts index 1af49ca5..7dcb2031 100644 --- a/src/modules/tccpp/components/permissions-manager.ts +++ b/src/modules/tccpp/components/permissions-manager.ts @@ -6,30 +6,30 @@ import { M } from "../../../utils/debugging-and-logging.js"; import { HOUR } from "../../../common.js"; import { BotComponent } from "../../../bot-component.js"; import SkillRoles from "./skill-roles.js"; -import { Wheatley } from "../../../wheatley.js"; +import { named_id } from "../../../wheatley.js"; import { unwrap } from "../../../utils/misc.js"; import { CommandSetBuilder } from "../../../command-abstractions/command-set-builder.js"; import { set_timeout } from "../../../utils/node.js"; const categories_map = { - staff_logs: "1135927261472755712", - staff: "873125551064363028", - meta: "360691699288113163", - tutoring: "923430684041818153", - cpp_help: "897465499535949874", - c_help: "931970218442493992", - discussion: "855220194887335977", - specialized: "360691955031867392", - community: "1131921460034801747", - off_topic: "360691500985745409", - misc: "506274316623544320", - bot_dev: "1166516815472640050", - voice: "360692425242705921", - archive: "910306041969913938", - private_archive: "455278783352537099", - challenges_archive: "429594248099135488", - meta_archive: "910308747929321492", -}; + staff_logs: { id: "1135927261472755712", name: "Staff Logs" }, + staff: { id: "873125551064363028", name: "Staff" }, + meta: { id: "360691699288113163", name: "Meta" }, + tutoring: { id: "923430684041818153", name: "Tutoring" }, + cpp_help: { id: "897465499535949874", name: "C++ Help" }, + c_help: { id: "931970218442493992", name: "C Help" }, + discussion: { id: "855220194887335977", name: "Discussion" }, + specialized: { id: "360691955031867392", name: "Specialized" }, + community: { id: "1131921460034801747", name: "Community" }, + off_topic: { id: "360691500985745409", name: "Off-Topic" }, + misc: { id: "506274316623544320", name: "Miscellaneous" }, + bot_dev: { id: "1166516815472640050", name: "Bot Dev" }, + voice: { id: "360692425242705921", name: "Voice" }, + archive: { id: "910306041969913938", name: "Archive" }, + private_archive: { id: "455278783352537099", name: "Private Archive" }, + challenges_archive: { id: "429594248099135488", name: "Challenges Archive" }, + meta_archive: { id: "910308747929321492", name: "Meta Archive" }, +} satisfies { [key: string]: named_id }; type permissions_entry = { allow?: bigint[]; @@ -174,25 +174,25 @@ export default class PermissionManager extends BotComponent { }, }; - this.add_category_permissions(categories_map.staff_logs, mod_only_channel); - this.add_category_permissions(categories_map.staff, mod_only_channel); - this.add_category_permissions(categories_map.meta, default_permissions); - this.add_category_permissions(categories_map.cpp_help, default_permissions); - this.add_category_permissions(categories_map.c_help, default_permissions); - this.add_category_permissions(categories_map.discussion, default_permissions); - this.add_category_permissions(categories_map.specialized, default_permissions); - this.add_category_permissions(categories_map.community, default_permissions); - this.add_category_permissions(categories_map.off_topic, off_topic_permissions); - this.add_category_permissions(categories_map.misc, default_permissions); - this.add_category_permissions(categories_map.bot_dev, default_permissions); - this.add_category_permissions(categories_map.voice, voice_permissions); - this.add_category_permissions(categories_map.archive, read_only_archive_channel); - this.add_category_permissions(categories_map.private_archive, mod_only_channel); - this.add_category_permissions(categories_map.challenges_archive, mod_only_channel); - this.add_category_permissions(categories_map.meta_archive, mod_only_channel); + this.add_category_permissions(categories_map.staff_logs.id, mod_only_channel); + this.add_category_permissions(categories_map.staff.id, mod_only_channel); + this.add_category_permissions(categories_map.meta.id, default_permissions); + this.add_category_permissions(categories_map.cpp_help.id, default_permissions); + this.add_category_permissions(categories_map.c_help.id, default_permissions); + this.add_category_permissions(categories_map.discussion.id, default_permissions); + this.add_category_permissions(categories_map.specialized.id, default_permissions); + this.add_category_permissions(categories_map.community.id, default_permissions); + this.add_category_permissions(categories_map.off_topic.id, off_topic_permissions); + this.add_category_permissions(categories_map.misc.id, default_permissions); + this.add_category_permissions(categories_map.bot_dev.id, default_permissions); + this.add_category_permissions(categories_map.voice.id, voice_permissions); + this.add_category_permissions(categories_map.archive.id, read_only_archive_channel); + this.add_category_permissions(categories_map.private_archive.id, mod_only_channel); + this.add_category_permissions(categories_map.challenges_archive.id, mod_only_channel); + this.add_category_permissions(categories_map.meta_archive.id, mod_only_channel); // staff overwrites - this.add_channel_overwrite(this.wheatley.channels.staff_only, { + this.add_channel_overwrite(this.wheatley.channels.staff_only.id, { [this.wheatley.guild.roles.everyone.id]: { deny: [Discord.PermissionsBitField.Flags.ViewChannel], }, @@ -203,7 +203,7 @@ export default class PermissionManager extends BotComponent { allow: [Discord.PermissionsBitField.Flags.ViewChannel], }, }); - this.add_channel_overwrite(this.wheatley.channels.voice_hotline, { + this.add_channel_overwrite(this.wheatley.channels.voice_hotline.id, { [this.wheatley.guild.roles.everyone.id]: { deny: [Discord.PermissionsBitField.Flags.ViewChannel], }, @@ -219,7 +219,7 @@ export default class PermissionManager extends BotComponent { }); // meta overwrites - this.add_channel_overwrite(this.wheatley.channels.rules, { + this.add_channel_overwrite(this.wheatley.channels.rules.id, { [this.wheatley.guild.roles.everyone.id]: { deny: [ Discord.PermissionsBitField.Flags.SendMessages, @@ -235,12 +235,12 @@ export default class PermissionManager extends BotComponent { ], }, }); - this.add_channel_overwrite(this.wheatley.channels.announcements, read_only_channel); - this.add_channel_overwrite(this.wheatley.channels.resources, read_only_channel); - this.add_channel_overwrite(this.wheatley.channels.old_resources, read_only_channel); - this.add_channel_overwrite(this.wheatley.channels.partners, read_only_channel); - this.add_channel_overwrite(this.wheatley.channels.articles, read_only_channel); - this.add_channel_overwrite(this.wheatley.channels.server_suggestions, { + this.add_channel_overwrite(this.wheatley.channels.announcements.id, read_only_channel); + this.add_channel_overwrite(this.wheatley.channels.resources.id, read_only_channel); + this.add_channel_overwrite(this.wheatley.channels.old_resources.id, read_only_channel); + this.add_channel_overwrite(this.wheatley.channels.partners.id, read_only_channel); + this.add_channel_overwrite(this.wheatley.channels.articles.id, read_only_channel); + this.add_channel_overwrite(this.wheatley.channels.server_suggestions.id, { ...default_permissions, [this.wheatley.guild.roles.everyone.id]: { deny: [ @@ -261,7 +261,7 @@ export default class PermissionManager extends BotComponent { deny: [Discord.PermissionsBitField.Flags.ViewChannel], }, }); - this.add_channel_overwrite(this.wheatley.channels.the_button, { + this.add_channel_overwrite(this.wheatley.channels.the_button.id, { ...default_permissions, [this.wheatley.guild.roles.everyone.id]: { deny: [ @@ -278,11 +278,11 @@ export default class PermissionManager extends BotComponent { allow: [Discord.PermissionsBitField.Flags.ViewChannel], }, }; - this.add_channel_overwrite(this.wheatley.channels.skill_roles_meta, jedi_council); - this.add_channel_overwrite(this.wheatley.channels.skill_role_suggestions, jedi_council); + this.add_channel_overwrite(this.wheatley.channels.skill_roles_meta.id, jedi_council); + this.add_channel_overwrite(this.wheatley.channels.skill_role_suggestions.id, jedi_council); // community overwrites - this.add_channel_overwrite(this.wheatley.channels.polls, { + this.add_channel_overwrite(this.wheatley.channels.polls.id, { ...read_only_channel_no_reactions, [this.wheatley.roles.moderators.id]: { allow: [ @@ -292,35 +292,35 @@ export default class PermissionManager extends BotComponent { ], }, }); - this.add_channel_overwrite(this.wheatley.channels.today_i_learned, { + this.add_channel_overwrite(this.wheatley.channels.today_i_learned.id, { ...default_permissions, [this.wheatley.roles.no_til.id]: muted_permissions, }); // off topic overwrites - this.add_channel_overwrite(this.wheatley.channels.memes, { + this.add_channel_overwrite(this.wheatley.channels.memes.id, { ...off_topic_permissions, [this.wheatley.roles.no_memes.id]: no_interaction_at_all, }); - this.add_channel_overwrite(this.wheatley.channels.starboard, { + this.add_channel_overwrite(this.wheatley.channels.starboard.id, { ...off_topic_permissions, [this.wheatley.roles.no_memes.id]: no_interaction_at_all, ...read_only_channel, }); - this.add_channel_overwrite(this.wheatley.channels.pin_archive, { + this.add_channel_overwrite(this.wheatley.channels.pin_archive.id, { ...off_topic_permissions, ...read_only_channel, }); - this.add_channel_overwrite(this.wheatley.channels.skill_role_log, { + this.add_channel_overwrite(this.wheatley.channels.skill_role_log.id, { ...read_only_channel, }); - this.add_channel_overwrite(this.wheatley.channels.public_action_log, { + this.add_channel_overwrite(this.wheatley.channels.public_action_log.id, { ...read_only_channel_no_reactions, }); - this.add_channel_overwrite(this.wheatley.channels.serious_off_topic, { + this.add_channel_overwrite(this.wheatley.channels.serious_off_topic.id, { ...off_topic_permissions, [this.wheatley.roles.no_serious_off_topic.id]: no_interaction_at_all, }); - this.add_channel_overwrite(this.wheatley.channels.room_of_requirement, { + this.add_channel_overwrite(this.wheatley.channels.room_of_requirement.id, { ...off_topic_permissions, [this.wheatley.roles.moderators.id]: { allow: [ @@ -330,7 +330,7 @@ export default class PermissionManager extends BotComponent { ], }, }); - this.add_channel_overwrite(this.wheatley.channels.boosters_only, { + this.add_channel_overwrite(this.wheatley.channels.boosters_only.id, { ...default_permissions, [this.wheatley.guild.roles.everyone.id]: { deny: [Discord.PermissionsBitField.Flags.ViewChannel], @@ -340,15 +340,15 @@ export default class PermissionManager extends BotComponent { }, }); // misc overwrites - this.add_channel_overwrite(this.wheatley.channels.days_since_last_incident, { + this.add_channel_overwrite(this.wheatley.channels.days_since_last_incident.id, { ...default_permissions, ...read_only_channel, }); - this.add_channel_overwrite(this.wheatley.channels.literally_1984, { + this.add_channel_overwrite(this.wheatley.channels.literally_1984.id, { ...default_permissions, ...read_only_channel, }); - this.add_channel_overwrite(this.wheatley.channels.lore, { + this.add_channel_overwrite(this.wheatley.channels.lore.id, { ...default_permissions, [this.wheatley.guild.roles.everyone.id]: { deny: [Discord.PermissionsBitField.Flags.ViewChannel], @@ -358,9 +358,9 @@ export default class PermissionManager extends BotComponent { }, }); // bot dev overwrites - this.add_channel_overwrite(this.wheatley.channels.bot_dev_internal, mod_only_channel); + this.add_channel_overwrite(this.wheatley.channels.bot_dev_internal.id, mod_only_channel); // voice overwrites - this.add_channel_overwrite(this.wheatley.channels.afk, { + this.add_channel_overwrite(this.wheatley.channels.afk.id, { [this.wheatley.guild.roles.everyone.id]: { deny: [ Discord.PermissionsBitField.Flags.Speak, @@ -375,7 +375,7 @@ export default class PermissionManager extends BotComponent { ], }, }); - this.add_channel_overwrite(this.wheatley.channels.deans_office, { + this.add_channel_overwrite(this.wheatley.channels.deans_office.id, { ...voice_permissions, [this.wheatley.guild.roles.everyone.id]: { deny: [ @@ -444,7 +444,7 @@ export default class PermissionManager extends BotComponent { async sync_permissions() { await Promise.all( Object.entries(this.category_permissions).map(async ([id, permissions]) => { - const category = await this.utilities.get_category(id); + const category = await this.utilities.get_category({ id }); await this.sync_category_permissions(category, unwrap(permissions)); }), ); diff --git a/src/modules/tccpp/components/roulette.ts b/src/modules/tccpp/components/roulette.ts index 26bfb71d..ce7683cf 100644 --- a/src/modules/tccpp/components/roulette.ts +++ b/src/modules/tccpp/components/roulette.ts @@ -94,8 +94,8 @@ export default class Roulette extends BotComponent { } async roulette(command: TextBasedCommand) { - if (command.channel_id != this.wheatley.channels.bot_spam) { - await command.reply(`Must be used in <#${this.wheatley.channels.bot_spam}>`, true); + if (command.channel_id != this.wheatley.channels.bot_spam.id) { + await command.reply(`Must be used in <#${this.wheatley.channels.bot_spam.id}>`, true); return; } if (this.disabled_users.has(command.user.id)) { diff --git a/src/modules/tccpp/components/server-suggestion-reactions.ts b/src/modules/tccpp/components/server-suggestion-reactions.ts index 43ec1bb5..9c11ca98 100644 --- a/src/modules/tccpp/components/server-suggestion-reactions.ts +++ b/src/modules/tccpp/components/server-suggestion-reactions.ts @@ -5,7 +5,7 @@ import { delay } from "../../../utils/misc.js"; import { file_exists } from "../../../utils/filesystem.js"; import { M } from "../../../utils/debugging-and-logging.js"; import { BotComponent } from "../../../bot-component.js"; -import { Wheatley } from "../../../wheatley.js"; +import { named_id, Wheatley } from "../../../wheatley.js"; import { forge_snowflake } from "../../../utils/discord.js"; import { set_timeout } from "../../../utils/node.js"; import { SERVER_SUGGESTION_TRACKER_START_TIME } from "./server-suggestion-tracker.js"; @@ -25,7 +25,7 @@ const root_only_reacts = new Set([ export default class ServerSuggestionReactions extends BotComponent { readonly monitored_channels = new Map(); stop = false; - monitored_channels_ids!: string[]; + monitored_channels_infos!: named_id[]; async handle_fetched_message(message: Discord.Message) { for (const [_, reaction] of message.reactions.cache) { @@ -59,7 +59,7 @@ export default class ServerSuggestionReactions extends BotComponent { // 100 messages every 3 minutes, avoid ratelimits // runs only on restart, no rush async hard_catch_up() { - const server_suggestions_channel = this.monitored_channels.get(this.wheatley.channels.server_suggestions); + const server_suggestions_channel = this.monitored_channels.get(this.wheatley.channels.server_suggestions.id); let oldest_seen = Date.now(); assert(server_suggestions_channel != undefined); while (true) { @@ -93,7 +93,7 @@ export default class ServerSuggestionReactions extends BotComponent { } override async on_ready() { - this.monitored_channels_ids = [ + this.monitored_channels_infos = [ this.wheatley.channels.server_suggestions, this.wheatley.channels.suggestion_dashboard, ]; @@ -101,10 +101,10 @@ export default class ServerSuggestionReactions extends BotComponent { const config = "../wheatley-private/config.js"; react_blacklist = (await import(config)).react_blacklist; } - for (const channel_id of this.monitored_channels_ids) { - const channel = await this.wheatley.client.channels.fetch(channel_id); + for (const channel_info of this.monitored_channels_infos) { + const channel = await this.wheatley.guild.channels.fetch(channel_info.id); assert(channel && (channel instanceof Discord.TextChannel || channel instanceof Discord.ThreadChannel)); - this.monitored_channels.set(channel_id, channel); + this.monitored_channels.set(channel_info.id, channel); } M.debug("server_suggestion reactions handler got channels"); // recover from down time: fetch last 100 messages (and add to cache) @@ -125,7 +125,10 @@ export default class ServerSuggestionReactions extends BotComponent { if (reaction.message.guildId !== this.wheatley.guild.id) { return; } - if (this.monitored_channels_ids.indexOf(reaction.message.channel.id) > -1) { + const is_channel_monitored = this.monitored_channels_infos.some( + channel_info => channel_info.id === reaction.message.channel.id, + ); + if (is_channel_monitored) { if (reaction.users.cache.some(user => react_blacklist.has(user.id))) { // Remove but not immediately M.debug("scheduling blacklisted user reaction removal"); diff --git a/src/modules/tccpp/components/server-suggestion-tracker.ts b/src/modules/tccpp/components/server-suggestion-tracker.ts index f39488f8..a51dc28b 100644 --- a/src/modules/tccpp/components/server-suggestion-tracker.ts +++ b/src/modules/tccpp/components/server-suggestion-tracker.ts @@ -64,6 +64,7 @@ export default class ServerSuggestionTracker extends BotComponent { this.wheatley.channels.suggestion_dashboard, ); this.server_suggestions = await this.utilities.get_channel(this.wheatley.channels.server_suggestions); + commands.add( new TextBasedCommandBuilder("suggestions-dashboard-count", EarlyReplyMode.visible) .set_category("Admin utilities") diff --git a/src/modules/tccpp/components/skill-roles.ts b/src/modules/tccpp/components/skill-roles.ts index 9496620e..24b67138 100644 --- a/src/modules/tccpp/components/skill-roles.ts +++ b/src/modules/tccpp/components/skill-roles.ts @@ -41,7 +41,8 @@ export default class SkillRoles extends BotComponent { override async on_ready() { for (const name in SkillLevel) { - const role = this.wheatley.get_role_by_name(capitalize(name)); + const role = this.utilities.get_role_by_name(capitalize(name)); + assert(role, `Could not find skill role: ${name}`); this.skill_roles.push(role); this.roles[name as skill_level] = role; } diff --git a/src/modules/tccpp/components/starboard.ts b/src/modules/tccpp/components/starboard.ts index b39e79bc..eaf9e14c 100644 --- a/src/modules/tccpp/components/starboard.ts +++ b/src/modules/tccpp/components/starboard.ts @@ -182,18 +182,18 @@ export default class Starboard extends BotComponent { this.repost_emojis = state?.repost_emojis ?? []; this.excluded_channels = new Set([ - this.wheatley.channels.rules, - this.wheatley.channels.announcements, - this.wheatley.channels.server_suggestions, - this.wheatley.channels.resources, - this.wheatley.channels.the_button, - this.wheatley.channels.introductions, - this.wheatley.channels.starboard, - this.wheatley.channels.goals2024, - this.wheatley.channels.goals2025, - this.wheatley.channels.goals2026, - this.wheatley.channels.skill_role_log, - this.wheatley.channels.polls, + this.wheatley.channels.rules.id, + this.wheatley.channels.announcements.id, + this.wheatley.channels.server_suggestions.id, + this.wheatley.channels.resources.id, + this.wheatley.channels.the_button.id, + this.wheatley.channels.introductions.id, + this.wheatley.channels.starboard.id, + this.wheatley.channels.goals2024.id, + this.wheatley.channels.goals2025.id, + this.wheatley.channels.goals2026.id, + this.wheatley.channels.skill_role_log.id, + this.wheatley.channels.polls.id, ]); } @@ -232,7 +232,7 @@ export default class Starboard extends BotComponent { } const parent_channel_id = this.get_parent_channel_id(reaction.message.channel); if (reaction.emoji.name == "⭐") { - if (parent_channel_id == this.wheatley.channels.memes) { + if (parent_channel_id == this.wheatley.channels.memes.id) { return reaction.count >= memes_star_threshold; } else { return reaction.count >= star_threshold; @@ -240,7 +240,7 @@ export default class Starboard extends BotComponent { } else if ( !(this.negative_emojis.includes(reaction.emoji.name) || this.ignored_emojis.includes(reaction.emoji.name)) ) { - if (parent_channel_id == this.wheatley.channels.memes) { + if (parent_channel_id == this.wheatley.channels.memes.id) { return reaction.count >= memes_other_threshold; } else { return reaction.count >= other_threshold; @@ -341,12 +341,6 @@ export default class Starboard extends BotComponent { try { const trigger_emoji = trigger_type === delete_trigger_type.delete_this ? "<:delet_this:669598943117836312>" : ":recycle:"; - const channel_context = - message.channel.id === this.wheatley.channels.memes - ? " in #memes" - : message.channel.id === this.wheatley.channels.cursed_code - ? " in #cursed-code" - : ""; const notification_embed = new Discord.EmbedBuilder() .setColor(colors.red) .setTitle(`Message Auto-Deleted in ${message.channel.url}`) @@ -385,7 +379,11 @@ export default class Starboard extends BotComponent { ); const max_non_negative = Math.max(...non_negative_reactions.map(([_, count]) => count)); // -inf if |a|=0 let do_delete = true; - if (![this.wheatley.channels.memes, this.wheatley.channels.cursed_code].includes(message.channel.id)) { + if ( + ![this.wheatley.channels.memes, this.wheatley.channels.cursed_code].some( + channel => channel.id === message.channel.id, + ) + ) { do_delete = false; } if (trigger_reaction.count <= max_non_negative) { @@ -509,7 +507,7 @@ export default class Starboard extends BotComponent { await message.delete(); assert(!(message.channel instanceof Discord.PartialGroupDMChannel)); if (trigger_type == delete_trigger_type.delete_this) { - if (message.channel.id == this.wheatley.channels.memes) { + if (message.channel.id == this.wheatley.channels.memes.id) { await message.channel.send( `<@${message.author.id}> A message of yours was automatically deleted because a threshold for` + " <:delet_this:669598943117836312> reactions (or similar) was reached.\n\n" + @@ -536,7 +534,7 @@ export default class Starboard extends BotComponent { // Check if the # of reactions is full, and there is no negative emojis reacted yet should_delete_reaction(reaction: Discord.MessageReaction) { return ( - reaction.message.channel.id === this.wheatley.channels.memes && + reaction.message.channel.id === this.wheatley.channels.memes.id && reaction.message.reactions.cache.size === 20 && reaction.message.reactions.cache.filter( reaction => diff --git a/src/modules/tccpp/components/thread-based-channels.ts b/src/modules/tccpp/components/thread-based-channels.ts index 8fff176e..a6e4c775 100644 --- a/src/modules/tccpp/components/thread-based-channels.ts +++ b/src/modules/tccpp/components/thread-based-channels.ts @@ -36,9 +36,9 @@ export default class ThreadBasedChannels extends BotComponent { override async on_ready() { this.thread_based_channel_ids = new Set([ - this.wheatley.channels.server_suggestions, - this.wheatley.channels.showcase, - this.wheatley.channels.today_i_learned, + this.wheatley.channels.server_suggestions.id, + this.wheatley.channels.showcase.id, + this.wheatley.channels.today_i_learned.id, ]); } @@ -93,7 +93,7 @@ export default class ThreadBasedChannels extends BotComponent { if (message.guildId !== this.wheatley.guild.id) { return; } - if (message.channelId === this.wheatley.channels.today_i_learned) { + if (message.channelId === this.wheatley.channels.today_i_learned.id) { if (message.hasThread) { await unwrap(message.thread).send( "This today I learned post was removed. If it was removed by a moderator it was likely due to it " + diff --git a/src/modules/wheatley/components/anti-crosspost.ts b/src/modules/wheatley/components/anti-crosspost.ts index 11f03150..06d4ed5b 100644 --- a/src/modules/wheatley/components/anti-crosspost.ts +++ b/src/modules/wheatley/components/anti-crosspost.ts @@ -53,7 +53,7 @@ export default class AntiCrosspost extends BotComponent { if (message.content.length < MIN_MESSAGE_LENGTH) { return; } - if (message.channelId === this.wheatley.channels.bot_spam) { + if (message.channelId === this.wheatley.channels.bot_spam.id) { return; } let user_messages = this.recent_messages.get(message.author.id) ?? []; diff --git a/src/modules/wheatley/components/code.ts b/src/modules/wheatley/components/code.ts index 3a1f3d43..e2a63e20 100644 --- a/src/modules/wheatley/components/code.ts +++ b/src/modules/wheatley/components/code.ts @@ -27,8 +27,8 @@ export default class Code extends BotComponent { } static make_code_formatting_embeds(wheatley: Wheatley, channel: Discord.TextBasedChannel): Discord.APIEmbedField[] { - const is_c = [wheatley.channels.c_help, wheatley.channels.c_help_text].includes( - wheatley.top_level_channel(channel), + const is_c = [wheatley.channels.c_help, wheatley.channels.c_help_text].some( + channel_info => channel_info.id == wheatley.top_level_channel(channel), ); return [ { diff --git a/src/modules/wheatley/components/moderation/rolepersist.ts b/src/modules/wheatley/components/moderation/rolepersist.ts index 13b708f9..4b4c74cd 100644 --- a/src/modules/wheatley/components/moderation/rolepersist.ts +++ b/src/modules/wheatley/components/moderation/rolepersist.ts @@ -122,7 +122,7 @@ export default class Rolepersist extends ModerationComponent { .set_category("Moderation") .set_alias_of("rolepersist add") .set_permissions(Discord.PermissionFlagsBits.ModerateMembers) - .set_description(`${capitalize(role).replace("_", " ")}`) + .set_description(`${capitalize(role as string).replace("_", " ")}`) .add_user_option({ title: "user", description: "User to rolepersist", diff --git a/src/modules/wheatley/components/redirect.ts b/src/modules/wheatley/components/redirect.ts index d6a13db6..50ef102f 100644 --- a/src/modules/wheatley/components/redirect.ts +++ b/src/modules/wheatley/components/redirect.ts @@ -29,8 +29,8 @@ export default class Redirect extends BotComponent { new TextBasedCommandBuilder("r", EarlyReplyMode.none) .set_category("Moderation Utilities") .set_description( - `Redirect a conversation from <#${this.wheatley.channels.c_cpp_discussion}> or ` + - `<#${this.wheatley.channels.general_discussion}> to a help channel`, + `Redirect a conversation from <#${this.wheatley.channels.c_cpp_discussion.id}> or ` + + `<#${this.wheatley.channels.general_discussion.id}> to a help channel`, ) .add_user_option({ title: "user", @@ -73,7 +73,7 @@ export default class Redirect extends BotComponent { this.wheatley.channels.c_help_text, this.wheatley.channels.tooling, this.wheatley.channels.algorithms_and_compsci, - ].includes(command.channel_id) + ].some(channel_info => channel_info.id === command.channel_id) ) { await command.reply("Can't be used in a help channel", true); return; @@ -83,13 +83,13 @@ export default class Redirect extends BotComponent { content: `Hello <@${user.id}>, welcome to Together C & C++! This is not a help channel, please ask your ` + `question in one of the help channels above (${format_list([ - `<#${this.wheatley.channels.cpp_help}>`, - `<#${this.wheatley.channels.cpp_help_text}>`, - `<#${this.wheatley.channels.c_help}>`, - `<#${this.wheatley.channels.c_help_text}>`, + `<#${this.wheatley.channels.cpp_help.id}>`, + `<#${this.wheatley.channels.cpp_help_text.id}>`, + `<#${this.wheatley.channels.c_help.id}>`, + `<#${this.wheatley.channels.c_help_text.id}>`, ])}), ` + - `or <#${this.wheatley.channels.tooling}> if your question is about tooling, ` + - `or <#${this.wheatley.channels.algorithms_and_compsci}> if your question pertains more to theory, ` + + `or <#${this.wheatley.channels.tooling.id}> if your question is about tooling, ` + + `or <#${this.wheatley.channels.algorithms_and_compsci.id}> if your question pertains more to theory, ` + `or any other help channel more suited for your question.`, should_text_reply: false, }); diff --git a/src/wheatley.ts b/src/wheatley.ts index d838ae66..18bf929d 100644 --- a/src/wheatley.ts +++ b/src/wheatley.ts @@ -69,107 +69,120 @@ export type wheatley_config = core_config & { [key: string]: any; }; +export type named_id = { + // channel id used in production + id: string; + + // fallback channel name (for development only) + name?: string; +}; + const channels = { // staff - staff_flag_log: "1026972603019169842", - staff_delet_log: "1462879414864838869", - staff_experimental_log: "1207899185790197760", - staff_action_log: "845290775692443699", - public_action_log: "1341611685223596103", - staff_clock_log: "1220882759862452284", - welcome: "778017793567490078", - staff_member_log: "875681819662622730", - staff_message_log: "467729928956411914", - staff_only: "342153262260289537", - mods: "847993258600038460", - voice_hotline: "1379456835634987098", + staff_flag_log: { id: "1026972603019169842", name: "🚩-flag-log" }, + staff_delet_log: { id: "1462879414864838869", name: "πŸ—‘οΈ-delet-log" }, + staff_experimental_log: { id: "1207899185790197760", name: "😱-experimental-log" }, + staff_action_log: { id: "845290775692443699", name: "πŸ”¨-action-log" }, + public_action_log: { id: "1341611685223596103", name: "moderation-log" }, + staff_clock_log: { id: "1220882759862452284", name: "πŸ‘€-clock-log" }, + welcome: { id: "778017793567490078", name: "πŸ“ˆ-join-boost-log" }, + staff_member_log: { id: "875681819662622730", name: "πŸ‘₯-member-log" }, + staff_message_log: { id: "467729928956411914", name: "πŸ’¬-message-log" }, + staff_only: { id: "342153262260289537", name: "staff-only" }, + mods: { id: "847993258600038460", name: "mods-🚲" }, + voice_hotline: { id: "1379456835634987098", name: "voice-hotline" }, + // meta - rules: "659868782877212723", - announcements: "331881381477089282", - server_suggestions: "802541516655951892", - skill_role_suggestions: "1211089633547526204", - skill_roles_meta: "1182536717056618557", - news: "1269506410530738267", - old_resources: "1124619767542718524", - resources: "1361574878561570926", - partners: "904790565000986745", - the_button: "1069678919667687455", - articles: "1130174377539940475", + rules: { id: "659868782877212723", name: "rules" }, + announcements: { id: "331881381477089282", name: "announcements" }, + server_suggestions: { id: "802541516655951892", name: "server-suggestions" }, + skill_role_suggestions: { id: "1211089633547526204", name: "skill-role-suggestions" }, + skill_roles_meta: { id: "1182536717056618557", name: "skill-roles-meta" }, + news: { id: "1269506410530738267", name: "news" }, + old_resources: { id: "1124619767542718524", name: "old-resources" }, + resources: { id: "1361574878561570926", name: "resources" }, + partners: { id: "904790565000986745", name: "partners" }, + the_button: { id: "1069678919667687455", name: "the-button" }, + articles: { id: "1130174377539940475", name: "archived-articles" }, + // language channels - cpp_help: "1013107104678162544", - c_help: "1013104018739974194", - cpp_help_text: "331718580070645760", - c_help_text: "331718539738087426", - c_cpp_discussion: "851121440425639956", - general_discussion: "855220264149057556", - code_review: "1078717238678409369", - showcase: "1014328785685979136", - tooling: "331913460080181258", - algorithms_and_compsci: "857668280012242944", + cpp_help: { id: "1013107104678162544", name: "cpp-help" }, + c_help: { id: "1013104018739974194", name: "c-help" }, + cpp_help_text: { id: "331718580070645760", name: "cpp-help-text" }, + c_help_text: { id: "331718539738087426", name: "c-help-text" }, + c_cpp_discussion: { id: "851121440425639956", name: "c-cpp-discussion" }, + general_discussion: { id: "855220264149057556", name: "general-technical" }, + code_review: { id: "1078717238678409369", name: "code-review" }, + showcase: { id: "1014328785685979136", name: "showcase" }, + tooling: { id: "331913460080181258", name: "tooling" }, + algorithms_and_compsci: { id: "857668280012242944", name: "algorithms-and-compsci" }, + // off-topic - starboard: "800509841424252968", - memes: "526518219549442071", - food: "1288515484513468436", - serious_off_topic: "921113903574958080", - room_of_requirement: "1082800064113672192", - boosters_only: "792183875241639977", + starboard: { id: "800509841424252968", name: "starboard" }, + memes: { id: "526518219549442071", name: "memes" }, + food: { id: "1288515484513468436", name: "food" }, + serious_off_topic: { id: "921113903574958080", name: "serious-off-topic" }, + room_of_requirement: { id: "1082800064113672192", name: "pets" }, + boosters_only: { id: "792183875241639977", name: "🩷pink🩷" }, + // other - bot_spam: "506274405500977153", - introductions: "933113495304679494", - cursed_code: "855220292736516128", - suggestion_dashboard: "908928083879415839", - suggestion_action_log: "909309608512880681", - today_i_learned: "873682069325217802", - goals2024: "1189255286364569640", - goals2025: "1323734788707848253", - goals2026: "1454237273712492615", - days_since_last_incident: "1195920462958575676", - literally_1984: "1097993854214488154", - lore: "890067781628866620", - bot_dev_internal: "1166517065763536977", - pin_archive: "1284234644396572714", - skill_role_log: "1315023714206617610", - polls: "1319336135213846568", - wiki_dev: "1350899338229846127", + bot_spam: { id: "506274405500977153", name: "bot-spam" }, + introductions: { id: "933113495304679494", name: "introductions" }, + cursed_code: { id: "855220292736516128", name: "cursed-code" }, + suggestion_dashboard: { id: "908928083879415839", name: "Suggestions Dashboard" }, + suggestion_action_log: { id: "909309608512880681", name: "Suggestion Action Log" }, + today_i_learned: { id: "873682069325217802", name: "did-you-know" }, + goals2024: { id: "1189255286364569640", name: "2024-goals" }, + goals2025: { id: "1323734788707848253", name: "2025-goals" }, + goals2026: { id: "1454237273712492615", name: "archived-2026-goals" }, + days_since_last_incident: { id: "1195920462958575676", name: "days-since-last-incident" }, + literally_1984: { id: "1097993854214488154", name: "literally-1984" }, + lore: { id: "890067781628866620", name: "lore" }, + bot_dev_internal: { id: "1166517065763536977", name: "wheatley-dev-internal" }, + pin_archive: { id: "1284234644396572714", name: "pin-archive" }, + skill_role_log: { id: "1315023714206617610", name: "skill-role-log" }, + polls: { id: "1319336135213846568", name: "polls" }, + wiki_dev: { id: "1350899338229846127", name: "wiki-dev" }, + // voice - chill: "1358502332941467879", - work_3: "1358502770575147230", - work_4: "1367735453112864838", - afk: "331732845523369985", - deans_office: "1379612678649155755", + chill: { id: "1358502332941467879", name: "Chill" }, + work_3: { id: "1358502770575147230", name: "Work 3" }, + work_4: { id: "1367735453112864838", name: "Work 4" }, + afk: { id: "331732845523369985", name: "AFK" }, + deans_office: { id: "1379612678649155755", name: "Dean's Office" }, // red telephone - red_telephone_alerts: "1140096352278290512", + red_telephone_alerts: { id: "1140096352278290512", name: "red-telephone-alerts" }, // error log - log: "1260777903700971581", -}; + log: { id: "1260777903700971581", name: "πŸ€–-wheatley-log" }, +} satisfies { [key: string]: named_id }; const roles_map = { - muted: "815987333094178825", - monke: "1139378060450332752", - no_off_topic: "879419994004422666", - no_suggestions: "831567015457980447", - no_suggestions_at_all: "895011256023535657", - no_reactions: "880152014036819968", - no_images: "1181029004866760854", - no_threads: "870181444742447135", - no_serious_off_topic: "921116034948272260", - no_til: "883474632370454588", - no_memes: "982307359370141756", - no_voice: "1371771250363465892", - voice_moderator: "1371706420730531870", - moderators: "847915341954154536", - root: "331719468440879105", - pink: "888158339878490132", - server_booster: "643013330616844333", - historian: "890067617069551646", - official_bot: "331886851784704001", - featured_bot: "995847409374605392", - jedi_council: "1138950835208990750", - herald: "1095555811536797787", - linked_github: "1080596526478397471", - wiki_core: "1354998426370314411", - voice: "1368073548983308328", -}; + muted: { id: "815987333094178825", name: "Muted" }, + monke: { id: "1139378060450332752", name: "Neuron Activation" }, + no_off_topic: { id: "879419994004422666", name: "No Off Topic" }, + no_suggestions: { id: "831567015457980447", name: "No Suggestions" }, + no_suggestions_at_all: { id: "895011256023535657", name: "No Suggestions At All" }, + no_reactions: { id: "880152014036819968", name: "No Reactions" }, + no_images: { id: "1181029004866760854", name: "No Images" }, + no_threads: { id: "870181444742447135", name: "No Threads" }, + no_serious_off_topic: { id: "921116034948272260", name: "No Serious Off Topic" }, + no_til: { id: "883474632370454588", name: "No TIL" }, + no_memes: { id: "982307359370141756", name: "No Memes" }, + no_voice: { id: "1371771250363465892", name: "No Voice" }, + voice_moderator: { id: "1371706420730531870", name: "Voice Moderator" }, + moderators: { id: "847915341954154536", name: "Moderator" }, + root: { id: "331719468440879105", name: "root" }, + pink: { id: "888158339878490132", name: "Pink" }, + server_booster: { id: "643013330616844333", name: "Server Booster" }, + historian: { id: "890067617069551646", name: "Historian" }, + official_bot: { id: "331886851784704001", name: "Official Bot" }, + featured_bot: { id: "995847409374605392", name: "Featured Bot" }, + jedi_council: { id: "1138950835208990750", name: "Jedi Council" }, + herald: { id: "1095555811536797787", name: "Herald" }, + linked_github: { id: "1080596526478397471", name: "Linked GitHub" }, + wiki_core: { id: "1354998426370314411", name: "core-wiki" }, + voice: { id: "1368073548983308328", name: "voice" }, +} satisfies { [key: string]: named_id }; type EventMap = { wheatley_ready: () => void; @@ -206,6 +219,10 @@ export class Wheatley { // True if freestanding mode is enabled. Defaults to false. readonly freestanding: boolean; + get devmode_enabled() { + return process.env.NODE_ENV !== "production"; + } + // Some emojis private emoji_map = { success: "βœ…", @@ -303,26 +320,41 @@ export class Wheatley { for (const [_, guild] of this.client.guilds.cache) { M.log(guild.id, guild.name); } - // Fetch the log channel immediately + + this.discord_application = (await this.client.application?.fetch()) ?? null; + this.discord_user = await this.client.users.fetch(config.id); + this.discord_guild = await this.client.guilds.fetch(config.guild); + + // Pre-fetch channels and roles so name resolution works in devmode + await this.guild.channels.fetch().catch(this.critical_error.bind(this)); + await this.guild.roles.fetch().catch(this.critical_error.bind(this)); + + // cache roles + for (const [key, info] of Object.entries(roles_map)) { + let role = this.guild.roles.cache.get(info.id); + + if (!role && info.name && this.devmode_enabled) { + role = this.guild.roles.cache.find(role => role.name === info.name); + } + + this.roles[key as keyof typeof roles_map] = unwrap(role); + } + if (!config.freestanding) { - const channel = await this.client.channels.fetch(channels.log); + const channel = this.client.channels.cache.get(channels.log.id); this.log_channel = channel && channel.isTextBased() ? channel : null; } this.info("Bot started"); - this.discord_application = (await this.client.application?.fetch()) ?? null; - this.discord_user = await this.client.users.fetch(config.id); - this.discord_guild = await this.client.guilds.fetch(config.guild); await this.fetch_emoji(); - await this.fetch_guild_info(); const command_set_builder = new CommandSetBuilder(this); for (const component of this.components.values()) { try { await component.setup(command_set_builder); } catch (e) { - this.critical_error(e); + this.critical_error(`Error setting up component ${component.constructor.name}:`, e as Error); } } const { text_commands, button_handlers, modal_handlers, other_commands } = @@ -366,42 +398,6 @@ export class Wheatley { } } - async fetch_guild_info() { - const wrap = async (fn: () => Promise): Promise => { - try { - return await fn(); - } catch (e) { - if (!this.freestanding) { - this.critical_error(e); - throw e; - } else { - // absorb error - return null; - } - } - }; - const fudged_unwrap = (value: T | null | undefined): T => { - if (!this.freestanding) { - // for the real bot actually check - return unwrap(value); - } else { - return value as T; - } - }; - // Roles - await Promise.all( - Object.entries(roles_map).map(async ([k, id]) => { - const role = await wrap(() => this.guild.roles.fetch(id)); - if (this.freestanding && role === null) { - return; - } - assert(role !== null, `Role ${k} ${id} not found`); - this.roles[k as keyof typeof roles_map] = role; - M.log(`Fetched role ${k}`); - }), - ); - } - async add_component(component: { new (w: Wheatley): T; get is_freestanding(): boolean }) { if (!this.freestanding || component.is_freestanding) { M.log(`Initializing ${component.name}`); @@ -539,7 +535,7 @@ export class Wheatley { } is_forum_help_channel(id: string) { - return [this.channels.cpp_help, this.channels.c_help].includes(id); + return [this.channels.cpp_help, this.channels.c_help].some(channel_info => channel_info.id === id); } is_forum_help_thread(thread: Discord.ThreadChannel) { @@ -620,11 +616,6 @@ export class Wheatley { ); } - // case-insensitive - get_role_by_name(name: string) { - return unwrap(this.guild.roles.cache.find(role => role.name.toLowerCase() === name.toLowerCase())); - } - async try_fetch_guild_member( options: Discord.GuildMember | Discord.User | Discord.UserResolvable | Discord.FetchMemberOptions, ): Promise {