From f333294fc583fe8e694e0e569e6d49724085efe1 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 31 Dec 2025 14:19:30 -0500 Subject: [PATCH 1/5] Adding in more changes --- README.md | 11 ++- src/commands/config/config.ts | 119 ++++++++++++++++++++++++ src/services/config/guildConfigStore.ts | 43 +++++++++ src/services/config/index.ts | 0 src/services/config/types.ts | 16 ++++ src/services/welcome/welcomeHandler.ts | 70 ++++++++------ 6 files changed, 226 insertions(+), 33 deletions(-) create mode 100644 src/commands/config/config.ts create mode 100644 src/services/config/guildConfigStore.ts create mode 100644 src/services/config/index.ts create mode 100644 src/services/config/types.ts diff --git a/README.md b/README.md index cc9973ee..94b50b7a 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,11 @@ OmegaBot is a modular Discord bot designed to support development projects with ## Current features -- Modular slash-command system (auto-loaded from dist/commands) +- Modular slash-command system (auto-loaded from `dist/commands`) - Centralized interaction routing with safe error handling - Structured logging (pino) - Welcome / onboarding system triggered on member join (`guildMemberAdd`) +- Per-guild configuration system backed by persistent storage ### Core commands @@ -151,13 +152,13 @@ OmegaBot is online │ │ └── OmegaBot.yml │ └── pull_request_template.md ├── .husky -│ ├── pre-commit │ └── pre-push ├── assets │ ├── banner.png │ └── omegabot.png ├── data │ ├── faqs.json +│ ├── guild-config.json │ ├── last-seen.json │ └── timezones.json ├── docs @@ -173,6 +174,8 @@ OmegaBot is online │ ├── commands │ │ ├── changelog │ │ │ └── changelog.ts +│ │ ├── config +│ │ │ └── config.ts │ │ ├── faq │ │ │ ├── subcommands │ │ │ │ ├── add.ts @@ -204,6 +207,10 @@ OmegaBot is online │ ├── config │ │ └── env.ts │ ├── services +│ │ ├── config +│ │ │ ├── guildConfigStore.ts +│ │ │ ├── index.ts +│ │ │ └── types.ts │ │ ├── discord │ │ │ ├── commandLoader.ts │ │ │ ├── fetchChannelMessages.ts diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts new file mode 100644 index 00000000..58e23f28 --- /dev/null +++ b/src/commands/config/config.ts @@ -0,0 +1,119 @@ +// src/commands/config/config.ts + +import { + SlashCommandBuilder, + PermissionFlagsBits, + ChannelType, + type ChatInputCommandInteraction, +} from "discord.js"; +import { setGuildConfig, getGuildConfig } from "../../services/config/guildConfigStore.js"; +import { logger } from "../../utils/logger.js"; + +/** + * Guild configuration command. + * + * This is intentionally small and focused: + * - Set/clear the welcome channel for onboarding messages + * + * Notes: + * - Requires "Manage Server" to prevent random users changing guild config + * - Only allows guild text channels (not DMs, not threads) + */ +export const data = new SlashCommandBuilder() + .setName("config") + .setDescription("Configure OmegaBot settings for this server") + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommandGroup((group) => + group + .setName("welcome-channel") + .setDescription("Configure where welcome messages are posted") + .addSubcommand((sub) => + sub + .setName("set") + .setDescription("Set the channel used for welcome messages") + .addChannelOption((opt) => + opt + .setName("channel") + .setDescription("Channel to post welcome messages in") + .setRequired(true) + // Only allow normal guild text channels. + .addChannelTypes(ChannelType.GuildText), + ), + ) + .addSubcommand((sub) => + sub.setName("clear").setDescription("Clear the configured welcome channel"), + ), + ); + +export async function execute(interaction: ChatInputCommandInteraction) { + // Only valid inside a guild. + if (!interaction.guildId) { + await interaction.reply({ + content: "This command can only be used in a server (not in DMs).", + ephemeral: true, + }); + return; + } + + const group = interaction.options.getSubcommandGroup(); + const sub = interaction.options.getSubcommand(); + + if (group !== "welcome-channel") { + await interaction.reply({ content: "Unknown config group.", ephemeral: true }); + return; + } + + if (sub === "set") { + const channel = interaction.options.getChannel("channel", true); + + // Extra safety: ensure it’s a guild text channel. + if (channel.type !== ChannelType.GuildText) { + await interaction.reply({ + content: "Please choose a normal text channel (not a thread or DM).", + ephemeral: true, + }); + return; + } + + const updated = setGuildConfig(interaction.guildId, { + welcomeChannelId: channel.id, + welcomeEnabled: true, + }); + + logger.info( + { guildId: interaction.guildId, welcomeChannelId: channel.id }, + "Updated guild welcome channel", + ); + + await interaction.reply({ + content: `✅ Welcome messages will be posted in <#${updated.welcomeChannelId}>.`, + ephemeral: true, + }); + return; + } + + if (sub === "clear") { + const current = getGuildConfig(interaction.guildId); + + // If nothing is set, still respond clearly. + if (!current.welcomeChannelId) { + await interaction.reply({ + content: "Welcome channel is already not set. I will use the system channel or first text channel as a fallback.", + ephemeral: true, + }); + return; + } + + const updated = setGuildConfig(interaction.guildId, { + welcomeChannelId: null, + }); + + logger.info({ guildId: interaction.guildId }, "Cleared guild welcome channel"); + + await interaction.reply({ + content: + "✅ Cleared the welcome channel. I will use the system channel or first text channel as a fallback.", + ephemeral: true, + }); + } +} \ No newline at end of file diff --git a/src/services/config/guildConfigStore.ts b/src/services/config/guildConfigStore.ts new file mode 100644 index 00000000..39f70a6c --- /dev/null +++ b/src/services/config/guildConfigStore.ts @@ -0,0 +1,43 @@ +// src/services/config/guildConfigStore.ts + +import fs from "fs"; +import path from "path"; +import { DEFAULT_GUILD_CONFIG, type GuildConfig, type GuildConfigPatch } from "./types.js"; + +const DATA_DIR = path.join(process.cwd(), "data"); +const FILE_PATH = path.join(DATA_DIR, "guild-config.json"); + +type StoreShape = Record>; + +function ensureDataDir() { + if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); +} + +function readStore(): StoreShape { + try { + const raw = fs.readFileSync(FILE_PATH, "utf8"); + return JSON.parse(raw) as StoreShape; + } catch { + return {}; + } +} + +function writeStore(store: StoreShape) { + ensureDataDir(); + fs.writeFileSync(FILE_PATH, JSON.stringify(store, null, 2), "utf8"); +} + +export function getGuildConfig(guildId: string): GuildConfig { + const store = readStore(); + const saved = store[guildId] ?? {}; + return { guildId, ...DEFAULT_GUILD_CONFIG, ...saved }; +} + +export function setGuildConfig(guildId: string, patch: GuildConfigPatch): GuildConfig { + const store = readStore(); + const current = store[guildId] ?? DEFAULT_GUILD_CONFIG; + const next = { ...current, ...patch }; + store[guildId] = next; + writeStore(store); + return { guildId, ...next }; +} \ No newline at end of file diff --git a/src/services/config/index.ts b/src/services/config/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/services/config/types.ts b/src/services/config/types.ts new file mode 100644 index 00000000..90db0a5c --- /dev/null +++ b/src/services/config/types.ts @@ -0,0 +1,16 @@ +// src/services/config/types.ts + +export type GuildConfig = { + guildId: string; + + // Onboarding / welcome flow + welcomeEnabled: boolean; + welcomeChannelId: string | null; +}; + +export type GuildConfigPatch = Partial>; + +export const DEFAULT_GUILD_CONFIG: Omit = { + welcomeEnabled: true, + welcomeChannelId: null, +}; \ No newline at end of file diff --git a/src/services/welcome/welcomeHandler.ts b/src/services/welcome/welcomeHandler.ts index f7752063..487fd3f0 100644 --- a/src/services/welcome/welcomeHandler.ts +++ b/src/services/welcome/welcomeHandler.ts @@ -1,58 +1,57 @@ // src/services/welcome/welcomeHandler.ts -import type { - Guild, - GuildMember, - TextChannel, - NewsChannel, - ThreadChannel, -} from "discord.js"; +import type { Guild, GuildMember, TextBasedChannel } from "discord.js"; import { logger } from "../../utils/logger.js"; import { buildWelcomeMessage } from "./welcomeMessage.js"; +import { getGuildConfig } from "../config/guildConfigStore.js"; /** - * Channels we can safely call `.send()` on in a guild context. + * Narrow a text-based channel into a "sendable" channel. * - * Note: - * `TextBasedChannel` is broader and can include DM-ish channel types where - * `.send()` is not guaranteed in the type system. + * discord.js typings include some text-based channel variants where `send` + * may not exist (ex: PartialGroupDMChannel). This guard keeps TypeScript happy + * and prevents runtime surprises. */ -type SendableGuildChannel = TextChannel | NewsChannel | ThreadChannel; +function isSendableTextChannel( + ch: TextBasedChannel, +): ch is TextBasedChannel & { send: (content: unknown) => Promise } { + return "send" in ch && typeof (ch as any).send === "function"; +} /** * Resolve the best channel to post onboarding messages. * * Order: - * 1) WELCOME_CHANNEL_ID (if set) + * 1) Guild config: welcomeEnabled/welcomeChannelId * 2) Guild system channel (if set) - * 3) First writable guild text channel we can find + * 3) First text-based channel we can find */ -async function resolveWelcomeChannel(guild: Guild): Promise { - const envChannelId = process.env.WELCOME_CHANNEL_ID; +async function resolveWelcomeChannel(guild: Guild): Promise { + // Pull per-guild configuration (defaults applied automatically). + const cfg = getGuildConfig(guild.id); - if (envChannelId) { - const ch = await guild.channels.fetch(envChannelId).catch(() => null); + // Allow guilds to disable welcome messages entirely. + if (!cfg.welcomeEnabled) return null; - if (ch && ch.isTextBased() && "send" in ch) { - return ch as SendableGuildChannel; - } + // If configured, try that channel first. + if (cfg.welcomeChannelId) { + const ch = await guild.channels.fetch(cfg.welcomeChannelId).catch(() => null); + if (ch && ch.isTextBased()) return ch; } - const system = guild.systemChannel; - if (system && system.isTextBased() && "send" in system) { - return system as SendableGuildChannel; + // Next best: guild "system channel" (if present). + if (guild.systemChannel && guild.systemChannel.isTextBased()) { + return guild.systemChannel; } + // Fallback: first text-based channel we can find. const channels = await guild.channels.fetch().catch(() => null); if (!channels) return null; for (const [, ch] of channels) { if (!ch) continue; - - // We only want channels that can accept .send() - if (ch.isTextBased() && "send" in ch) { - return ch as SendableGuildChannel; - } + if (!ch.isTextBased()) continue; + return ch; } return null; @@ -67,9 +66,18 @@ export async function onGuildMemberAdd(member: GuildMember): Promise { const channel = await resolveWelcomeChannel(member.guild); if (!channel) { + logger.info( + { guildId: member.guild.id }, + "Welcome handler skipped (no channel resolved or welcomes disabled)", + ); + return; + } + + // Extra runtime guard (also fixes TS `.send` issues). + if (!isSendableTextChannel(channel)) { logger.warn( { guildId: member.guild.id }, - "Welcome handler could not find a sendable channel", + "Welcome handler resolved a text-based channel that is not sendable", ); return; } @@ -81,4 +89,4 @@ export async function onGuildMemberAdd(member: GuildMember): Promise { "Welcome handler failed", ); } -} +} \ No newline at end of file From eacbc6bf0d9a9273708beecacca4599e548850f4 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 31 Dec 2025 14:21:36 -0500 Subject: [PATCH 2/5] Adding in more changes --- src/services/config/guildConfigStore.ts | 46 ++++++++++++++++++++++--- src/services/config/index.ts | 14 ++++++++ src/services/config/types.ts | 29 +++++++++++++++- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/services/config/guildConfigStore.ts b/src/services/config/guildConfigStore.ts index 39f70a6c..7dc9fd1d 100644 --- a/src/services/config/guildConfigStore.ts +++ b/src/services/config/guildConfigStore.ts @@ -4,40 +4,76 @@ import fs from "fs"; import path from "path"; import { DEFAULT_GUILD_CONFIG, type GuildConfig, type GuildConfigPatch } from "./types.js"; +/** + * Simple JSON-backed store for guild configuration. + * + * Design goals: + * - Extremely easy to run locally + * - No external dependencies (DB) + * - Durable across restarts + * + * Notes: + * - This implementation reads/writes the file per call. + * That’s fine for small bots. If you scale, add caching + periodic flush. + */ + const DATA_DIR = path.join(process.cwd(), "data"); const FILE_PATH = path.join(DATA_DIR, "guild-config.json"); +/** + * Stored JSON shape: + * { + * "": { welcomeEnabled: true, welcomeChannelId: "..." }, + * "": { ... } + * } + */ type StoreShape = Record>; -function ensureDataDir() { - if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); +function ensureDataDir(): void { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } } function readStore(): StoreShape { try { const raw = fs.readFileSync(FILE_PATH, "utf8"); - return JSON.parse(raw) as StoreShape; + const parsed = JSON.parse(raw) as StoreShape; + return parsed ?? {}; } catch { + // Missing file, invalid JSON, etc. return {}; } } -function writeStore(store: StoreShape) { +function writeStore(store: StoreShape): void { ensureDataDir(); fs.writeFileSync(FILE_PATH, JSON.stringify(store, null, 2), "utf8"); } +/** + * Get the effective config for a guild. + * Always returns a fully-populated config object (defaults applied). + */ export function getGuildConfig(guildId: string): GuildConfig { const store = readStore(); const saved = store[guildId] ?? {}; return { guildId, ...DEFAULT_GUILD_CONFIG, ...saved }; } +/** + * Update the stored config for a guild by patching values. + * Returns the updated effective config (defaults applied). + */ export function setGuildConfig(guildId: string, patch: GuildConfigPatch): GuildConfig { const store = readStore(); + + // Apply defaults for new guilds, then patch on top. const current = store[guildId] ?? DEFAULT_GUILD_CONFIG; - const next = { ...current, ...patch }; + const next: Omit = { ...current, ...patch }; + store[guildId] = next; writeStore(store); + return { guildId, ...next }; } \ No newline at end of file diff --git a/src/services/config/index.ts b/src/services/config/index.ts index e69de29b..fabcf97c 100644 --- a/src/services/config/index.ts +++ b/src/services/config/index.ts @@ -0,0 +1,14 @@ +// src/services/config/index.ts + +/** + * Public API barrel for config services. + * + * Keeps imports cleaner: + * import { getGuildConfig } from "../config/index.js"; + * instead of: + * import { getGuildConfig } from "../config/guildConfigStore.js"; + */ + +export { getGuildConfig, setGuildConfig } from "./guildConfigStore.js"; +export { DEFAULT_GUILD_CONFIG } from "./types.js"; +export type { GuildConfig, GuildConfigPatch } from "./types.js"; \ No newline at end of file diff --git a/src/services/config/types.ts b/src/services/config/types.ts index 90db0a5c..f0de410a 100644 --- a/src/services/config/types.ts +++ b/src/services/config/types.ts @@ -1,15 +1,42 @@ // src/services/config/types.ts +/** + * GuildConfig + * + * Per-guild configuration for OmegaBot. + * This is intentionally small and focused so we can expand safely over time. + */ export type GuildConfig = { + /** Discord guild (server) id */ guildId: string; - // Onboarding / welcome flow + /* ------------------------------------------------------------------ */ + /* Welcome / onboarding */ + /* ------------------------------------------------------------------ */ + + /** + * Master enable/disable for welcome messages in this guild. + * Defaults to true. + */ welcomeEnabled: boolean; + + /** + * Preferred channel for welcome messages. + * If null, we fall back to system channel, then first text channel. + */ welcomeChannelId: string | null; }; +/** + * Patch type used to update config values without requiring the full object. + * guildId is excluded intentionally (immutable key). + */ export type GuildConfigPatch = Partial>; +/** + * Default config for new guilds. + * Applied automatically when no saved config exists. + */ export const DEFAULT_GUILD_CONFIG: Omit = { welcomeEnabled: true, welcomeChannelId: null, From 6fe73f5dc0843b59ed43fdb389ed78376b364dca Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 31 Dec 2025 14:21:41 -0500 Subject: [PATCH 3/5] style: auto-format with Prettier [skip-precheck] --- src/commands/config/config.ts | 10 +++++++--- src/services/config/guildConfigStore.ts | 8 ++++++-- src/services/config/index.ts | 2 +- src/services/config/types.ts | 2 +- src/services/welcome/welcomeHandler.ts | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index 58e23f28..3de38d64 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -6,7 +6,10 @@ import { ChannelType, type ChatInputCommandInteraction, } from "discord.js"; -import { setGuildConfig, getGuildConfig } from "../../services/config/guildConfigStore.js"; +import { + setGuildConfig, + getGuildConfig, +} from "../../services/config/guildConfigStore.js"; import { logger } from "../../utils/logger.js"; /** @@ -98,7 +101,8 @@ export async function execute(interaction: ChatInputCommandInteraction) { // If nothing is set, still respond clearly. if (!current.welcomeChannelId) { await interaction.reply({ - content: "Welcome channel is already not set. I will use the system channel or first text channel as a fallback.", + content: + "Welcome channel is already not set. I will use the system channel or first text channel as a fallback.", ephemeral: true, }); return; @@ -116,4 +120,4 @@ export async function execute(interaction: ChatInputCommandInteraction) { ephemeral: true, }); } -} \ No newline at end of file +} diff --git a/src/services/config/guildConfigStore.ts b/src/services/config/guildConfigStore.ts index 7dc9fd1d..f8371327 100644 --- a/src/services/config/guildConfigStore.ts +++ b/src/services/config/guildConfigStore.ts @@ -2,7 +2,11 @@ import fs from "fs"; import path from "path"; -import { DEFAULT_GUILD_CONFIG, type GuildConfig, type GuildConfigPatch } from "./types.js"; +import { + DEFAULT_GUILD_CONFIG, + type GuildConfig, + type GuildConfigPatch, +} from "./types.js"; /** * Simple JSON-backed store for guild configuration. @@ -76,4 +80,4 @@ export function setGuildConfig(guildId: string, patch: GuildConfigPatch): GuildC writeStore(store); return { guildId, ...next }; -} \ No newline at end of file +} diff --git a/src/services/config/index.ts b/src/services/config/index.ts index fabcf97c..7df69e30 100644 --- a/src/services/config/index.ts +++ b/src/services/config/index.ts @@ -11,4 +11,4 @@ export { getGuildConfig, setGuildConfig } from "./guildConfigStore.js"; export { DEFAULT_GUILD_CONFIG } from "./types.js"; -export type { GuildConfig, GuildConfigPatch } from "./types.js"; \ No newline at end of file +export type { GuildConfig, GuildConfigPatch } from "./types.js"; diff --git a/src/services/config/types.ts b/src/services/config/types.ts index f0de410a..7c3ca528 100644 --- a/src/services/config/types.ts +++ b/src/services/config/types.ts @@ -40,4 +40,4 @@ export type GuildConfigPatch = Partial>; export const DEFAULT_GUILD_CONFIG: Omit = { welcomeEnabled: true, welcomeChannelId: null, -}; \ No newline at end of file +}; diff --git a/src/services/welcome/welcomeHandler.ts b/src/services/welcome/welcomeHandler.ts index 487fd3f0..d16ce376 100644 --- a/src/services/welcome/welcomeHandler.ts +++ b/src/services/welcome/welcomeHandler.ts @@ -89,4 +89,4 @@ export async function onGuildMemberAdd(member: GuildMember): Promise { "Welcome handler failed", ); } -} \ No newline at end of file +} From efe900af5498a0625dea47aaca19df9207843979 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 31 Dec 2025 14:26:23 -0500 Subject: [PATCH 4/5] Fixing some lint issues --- src/commands/config/config.ts | 10 +++--- src/services/welcome/welcomeHandler.ts | 48 +++++++++++--------------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index 3de38d64..ed1ab5b3 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -6,10 +6,7 @@ import { ChannelType, type ChatInputCommandInteraction, } from "discord.js"; -import { - setGuildConfig, - getGuildConfig, -} from "../../services/config/guildConfigStore.js"; +import { setGuildConfig, getGuildConfig } from "../../services/config/guildConfigStore.js"; import { logger } from "../../utils/logger.js"; /** @@ -108,7 +105,8 @@ export async function execute(interaction: ChatInputCommandInteraction) { return; } - const updated = setGuildConfig(interaction.guildId, { + // Clear configured channel and fall back to system/first text channel. + setGuildConfig(interaction.guildId, { welcomeChannelId: null, }); @@ -120,4 +118,4 @@ export async function execute(interaction: ChatInputCommandInteraction) { ephemeral: true, }); } -} +} \ No newline at end of file diff --git a/src/services/welcome/welcomeHandler.ts b/src/services/welcome/welcomeHandler.ts index d16ce376..03464a7e 100644 --- a/src/services/welcome/welcomeHandler.ts +++ b/src/services/welcome/welcomeHandler.ts @@ -3,55 +3,50 @@ import type { Guild, GuildMember, TextBasedChannel } from "discord.js"; import { logger } from "../../utils/logger.js"; import { buildWelcomeMessage } from "./welcomeMessage.js"; -import { getGuildConfig } from "../config/guildConfigStore.js"; +import { getGuildConfig } from "../config/index.js"; /** - * Narrow a text-based channel into a "sendable" channel. + * Type guard to ensure a text-based channel supports `.send()`. * - * discord.js typings include some text-based channel variants where `send` - * may not exist (ex: PartialGroupDMChannel). This guard keeps TypeScript happy - * and prevents runtime surprises. + * This avoids `any` and satisfies ESLint while keeping runtime safety. */ -function isSendableTextChannel( - ch: TextBasedChannel, -): ch is TextBasedChannel & { send: (content: unknown) => Promise } { - return "send" in ch && typeof (ch as any).send === "function"; +function isSendableChannel( + channel: TextBasedChannel, +): channel is TextBasedChannel & { send: (content: unknown) => Promise } { + return typeof (channel as { send?: unknown }).send === "function"; } /** * Resolve the best channel to post onboarding messages. * * Order: - * 1) Guild config: welcomeEnabled/welcomeChannelId - * 2) Guild system channel (if set) - * 3) First text-based channel we can find + * 1) Guild config (welcomeEnabled / welcomeChannelId) + * 2) Guild system channel + * 3) First available text-based channel */ async function resolveWelcomeChannel(guild: Guild): Promise { - // Pull per-guild configuration (defaults applied automatically). const cfg = getGuildConfig(guild.id); - // Allow guilds to disable welcome messages entirely. + // Allow per-guild disabling of welcome messages. if (!cfg.welcomeEnabled) return null; - // If configured, try that channel first. + // Prefer configured welcome channel. if (cfg.welcomeChannelId) { const ch = await guild.channels.fetch(cfg.welcomeChannelId).catch(() => null); - if (ch && ch.isTextBased()) return ch; + if (ch?.isTextBased()) return ch; } - // Next best: guild "system channel" (if present). - if (guild.systemChannel && guild.systemChannel.isTextBased()) { + // Fallback to system channel. + if (guild.systemChannel?.isTextBased()) { return guild.systemChannel; } - // Fallback: first text-based channel we can find. + // Final fallback: first text-based channel found. const channels = await guild.channels.fetch().catch(() => null); if (!channels) return null; for (const [, ch] of channels) { - if (!ch) continue; - if (!ch.isTextBased()) continue; - return ch; + if (ch?.isTextBased()) return ch; } return null; @@ -59,7 +54,7 @@ async function resolveWelcomeChannel(guild: Guild): Promise { try { @@ -68,16 +63,15 @@ export async function onGuildMemberAdd(member: GuildMember): Promise { if (!channel) { logger.info( { guildId: member.guild.id }, - "Welcome handler skipped (no channel resolved or welcomes disabled)", + "Welcome handler skipped (no channel resolved or disabled)", ); return; } - // Extra runtime guard (also fixes TS `.send` issues). - if (!isSendableTextChannel(channel)) { + if (!isSendableChannel(channel)) { logger.warn( { guildId: member.guild.id }, - "Welcome handler resolved a text-based channel that is not sendable", + "Resolved welcome channel is not sendable", ); return; } From 840ebabefb098d6cf109303eb2916181418e7914 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 31 Dec 2025 14:26:27 -0500 Subject: [PATCH 5/5] style: auto-format with Prettier [skip-precheck] --- src/commands/config/config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index ed1ab5b3..f65023e9 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -6,7 +6,10 @@ import { ChannelType, type ChatInputCommandInteraction, } from "discord.js"; -import { setGuildConfig, getGuildConfig } from "../../services/config/guildConfigStore.js"; +import { + setGuildConfig, + getGuildConfig, +} from "../../services/config/guildConfigStore.js"; import { logger } from "../../utils/logger.js"; /** @@ -118,4 +121,4 @@ export async function execute(interaction: ChatInputCommandInteraction) { ephemeral: true, }); } -} \ No newline at end of file +}