Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7bab291
WIP: Add Channel and Role Names and validate them before booting up t…
xLuxy Jan 28, 2026
2b8a9e0
fix: Fixed validateChannelsAndRoles always returning true and reverte…
xLuxy Jan 28, 2026
76cb277
chore: removed old TODOS
xLuxy Jan 28, 2026
4f8da5f
fix: Fix invisible unicode characters in channel and role names
xLuxy Jan 28, 2026
48cb7b8
chore: more fixes
xLuxy Jan 28, 2026
a1e1b97
chore: remove unused variable + fix CI issues
xLuxy Jan 28, 2026
a07720d
chore: made validateChannelsAndRoles more readable
xLuxy Jan 28, 2026
7ba9ec4
fix: Fixed PermissionManager component
xLuxy Jan 28, 2026
27c1494
fix: Fix Wheatley.log_channel assignment
xLuxy Jan 28, 2026
f9f3032
fix: more channel id fixes
xLuxy Jan 28, 2026
2988f30
rename ch and renamed validateChannelAndRoles
xLuxy Feb 5, 2026
c6265db
refactor get_* in bot-utilities
xLuxy Feb 5, 2026
8dd299a
add case_insensitive parameter to get_channel_by_name
xLuxy Feb 5, 2026
e783b76
fixed an error
xLuxy Feb 5, 2026
19b69bb
type fixes
xLuxy Feb 5, 2026
21d5eee
fixed server-suggestion-reactions
xLuxy Feb 5, 2026
7370555
Merge branch 'main' into improvements
xLuxy Feb 5, 2026
729f81d
fix voice get_channel call
xLuxy Feb 6, 2026
2c16bc1
de-duplicate get_*_channel utilities
xLuxy Feb 6, 2026
289d128
renamed ch -> channel_info
xLuxy Feb 6, 2026
891a0b5
reverted is_forum_help_channel type to be string
xLuxy Feb 6, 2026
1fa338a
refactor get_*_channel usage by taking in only named_id
xLuxy Feb 6, 2026
5731f18
move get_channel_by_name and get_role_by_name into bot-utilities
xLuxy Feb 6, 2026
5ea36e5
make sonar happy
xLuxy Feb 6, 2026
6504d04
remove introduced new lines
xLuxy Feb 6, 2026
6510794
cleanup expected_types
xLuxy Feb 6, 2026
07bb982
remove unncecesary asserts
xLuxy Feb 6, 2026
72d32d6
got rid of left-over snowflake types for now
xLuxy Feb 6, 2026
0f363fa
add satisfies and remove roles_map
xLuxy Feb 6, 2026
bbc0632
add missing satisfies
xLuxy Feb 6, 2026
aa83956
partially revert get_category
xLuxy Feb 6, 2026
41db2cc
revert discord.ts changes
xLuxy Feb 6, 2026
9ac7c40
more changes
xLuxy Feb 6, 2026
1b1a7fb
more stuff
xLuxy Feb 6, 2026
3947f68
remove unnecesary unwrapping
xLuxy Feb 6, 2026
fd4ccb7
stuff
xLuxy Feb 6, 2026
20c539f
more stuff
xLuxy Feb 6, 2026
3118241
minor cleanup
xLuxy Feb 6, 2026
91bfa9e
Merge branch 'main' of https://github.com/TCCPP/wheatley into improve…
xLuxy Feb 6, 2026
231124b
Remove a `: { [key: string]: named_id }`
jeremy-rifkin Feb 7, 2026
387c7ac
Update a name
jeremy-rifkin Feb 7, 2026
4ce17f4
Switch a `#` to `private`
jeremy-rifkin Feb 7, 2026
85f11f9
Update bot-utilities.ts
jeremy-rifkin Feb 7, 2026
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
94 changes: 70 additions & 24 deletions src/bot-utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`);
Comment thread
xLuxy marked this conversation as resolved.

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;
}

Expand All @@ -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());
}
}
6 changes: 5 additions & 1 deletion src/modules/tccpp/components/anti-self-star.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/modules/tccpp/components/auto-reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down
17 changes: 7 additions & 10 deletions src/modules/tccpp/components/autoreact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -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)) {
Expand All @@ -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)) {
Expand Down
24 changes: 12 additions & 12 deletions src/modules/tccpp/components/c-help-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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}>?`,
);
}
}
Expand All @@ -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;
}

Expand All @@ -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}>?`,
);
}
}
Expand All @@ -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}>?`,
);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/modules/tccpp/components/forum-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
);
}

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/modules/tccpp/components/memes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
Expand Down
Loading