From 10ae462bd67242019a315f571a56112ac1df748e Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 17:42:44 -0500 Subject: [PATCH 1/4] Trying this again --- README.md | 427 +++--------------------------------- docs/setup-discord.md | 218 ++++-------------- src/commands/admin/admin.ts | 426 ++++++++++++++++------------------- 3 files changed, 258 insertions(+), 813 deletions(-) diff --git a/README.md b/README.md index 1d75b1e0..98c02862 100644 --- a/README.md +++ b/README.md @@ -1,431 +1,58 @@ -

- OmegaBot Banner -

- -

- - - - - -

- # OmegaBot -OmegaBot is a modular Discord bot designed to support development projects with quick summaries, FAQs, GitHub lookups, and automated notifications. The structure is clean and fully service based which makes it easy to extend. - ---- - -## Table of Contents - -- [Core Features](#core-features) -- [Documentation](#documentation) -- [Getting Started](#getting-started) -- [Project Structure](#project-structure) -- [Extending OmegaBot](#extending-omegabot) -- [Contributors](#contributors) -- [License](#license) +OmegaBot is a modular, community-driven Discord bot focused on fun commands, utilities, and learning by building together. --- -## Core Features - -**Discord Integration:** - -- Modular slash-command system with auto-loading -- Welcome messages and auto-role assignment for new members -- Structured logging (pino) and safe error handling - -**GitHub Integration:** - -- Issue and PR lookups with smart caching (80-90% fewer API calls) -- Automated announcements for PRs, issue activity, and closures -- Health checks and status monitoring - -**Community Features:** - -- User-submitted jokes with 13 categories and moderation -- Coin flips, dice rolls, weather, and polls -- FAQ system for server knowledge base -- Timezone management for coordination across time zones - -**Data & Configuration:** - -- SQLite database for persistent storage -- Environment-based configuration with feature gating -- Per-guild settings via admin commands - -### Core commands - -- /help – command discovery and getting started guide -- /ping – health check -- /summary – conversation summaries (local + LLM mode) -- /history – conversation history (DM + file fallback) -- /playback – transcript playback with button pagination -- /pagination – reusable inline paging helper -- /timezone – per-user IANA timezone support -- /changelog – ephemeral release preview - -### FAQ system - -- /faq add – create persistent FAQ entries -- /faq get – retrieve FAQs by key -- /faq list – list FAQs with sorting and filtering -- /faq remove – remove FAQs with confirmation flow -- SQLite-backed persistent storage -- Usage tracking for FAQs -- Permission guardrails for destructive actions - -### Fun / utility commands - -All fun commands are available under `/fun`: +## 🚀 Quick Start -- `/fun chucknorris` – Chuck Norris facts (random, category, or search) -- `/fun dadjoke` – Random or searched dad jokes -- `/fun joke` – Community-submitted jokes organized by generation - - Categories: boomer, genx, millennial, genz, genalpha, random - - Add jokes, browse by category, track usage - - Moderator tools for content management -- `/fun coinflip` – Heads or tails (results tracked for stats) -- `/fun coinstats` – Coin flip statistics and leaderboards - - Track your heads vs. tails record - - View personal stats with avatar display - - See top flippers leaderboard -- `/fun dice` – Custom dice rolls -- `/fun weather` – Daily weather -- `/fun weather7` – 7-day forecast -- `/fun leaderboard` – Track fun command usage and top users - -## Planned features - -- `/docs` command for documentation lookups -- Expanded GitHub automation (labels, reviews, merge events) -- Enhanced AI-powered summaries with Claude API -- Admin dashboard for bot statistics and monitoring - ---- - -## Documentation - -- 🤖 [Discord Bot Setup Guide](docs/setup-discord.md) -- 📘 [Command Reference](docs/commands.md) -- ❓ [FAQ Storage Design](docs/faq.md) -- 🧠 [Transcript & Summary Design](docs/transcripts.md) -- 🛠️ [Development Notes](docs/dev-notes.md) - ---- - -## Getting Started - -### Requirements - -- Node 18 or newer -- A Discord bot token -- A development server where you have Manage Server permissions - -### Setup - -> Need help creating a Discord bot and token? -> See the [Discord Bot Setup Guide](docs/setup-discord.md). - -1. Clone the repository: - -```bash -git clone https://github.com/NickTheDevOpsGuy/OmegaBot.git -cd OmegaBot -``` - -2. Install dependencies: +1. Clone the repo +2. Copy `.env.example` to `.env` +3. Fill in required environment variables +4. Install deps and run ```bash npm install -``` - -3. Create a .env file based on the example configuration: - -- [.env.example](.env.example) - -4. Register slash commands with your development guild: - -```bash -npm run register -``` - -**Command registration mode** - -OmegaBot supports two registration modes: - -- Guild registration (recommended for development) - If DISCORD_GUILD_ID is set, commands are registered to that guild and appear immediately. - -- Global registration - If DISCORD_GUILD_ID is not set, commands are registered globally and may take up to 1 hour to appear. - -5. Build and run the bot: - -```bash npm run build +npm run register npm start ``` -For development with auto-reload: - -```bash -npm run dev -``` - -You should see: - -``` -OmegaBot is online -``` - --- -## Technical Stack - -- **Runtime**: Node.js 18+ -- **Language**: TypeScript 5.x -- **Discord Library**: discord.js v14 -- **Database**: SQLite (better-sqlite3) -- **Logging**: pino -- **Code Quality**: ESLint, Prettier -- **Git Hooks**: Husky - -### Discord.js v14 Features +## 📚 Documentation -OmegaBot uses discord.js v14 which includes: - -- Improved TypeScript support -- Better slash command handling -- Enhanced permission system -- Modern Discord API features - -### Performance Optimizations - -- **GitHub API Caching**: In-memory cache with 5-minute TTL - - 80-90% reduction in API calls - - Sub-millisecond response times on cache hits - - Automatic expiration and cleanup - - Rate limit protection -- **SQLite Storage**: Fast, reliable persistent data storage - - Zero-configuration database - - ACID transactions - - Efficient indexing for quick lookups +- [Discord Setup Guide](./setup-discord.md) +- [Commands Reference](./commands.md) +- [FAQ](./faq.md) +- [Transcripts & Examples](./transcripts.md) +- [Developer Notes](./dev-notes.md) --- -## Project Structure +## 🔐 Admin & Moderation Commands -
-🗂 Click to expand file structure +Some admin commands (timeout, kick, ban, health, stats) require: -``` -. -├── .github -│ ├── ISSUE_TEMPLATE -│ │ ├── bug.yml -│ │ ├── config.yml -│ │ ├── documentation.yml -│ │ ├── enhancement_refactor.yml -│ │ ├── feature_request.yml -│ │ └── question_discussion.yml -│ ├── workflows -│ │ └── OmegaBot.yml -│ └── pull_request_template.md -├── .husky -│ ├── pre-commit -│ └── pre-push -├── assets -│ ├── banner.png -│ └── omegabot.png -├── data -│ ├── faqs.json -│ ├── fun-usage.json -│ ├── github-assignees.json -│ ├── guild-config.json -│ ├── last-seen.json -│ └── omegabot.db -├── docs -│ ├── Claude.dmg -│ ├── commands.md -│ ├── dev-notes.md -│ ├── faq.md -│ ├── setup-discord.md -│ ├── setup-env.md -│ └── transcripts.md -├── scripts -│ └── precheck.sh -├── src -│ ├── commands -│ │ ├── admin -│ │ │ └── admin.ts -│ │ ├── changelog -│ │ │ └── changelog.ts -│ │ ├── config -│ │ │ └── config.ts -│ │ ├── faq -│ │ │ ├── subcommands -│ │ │ │ ├── add.ts -│ │ │ │ ├── get.ts -│ │ │ │ ├── list.ts -│ │ │ │ └── remove.ts -│ │ │ └── faq.ts -│ │ ├── fun -│ │ │ ├── subcommands -│ │ │ │ ├── joke -│ │ │ │ │ ├── add.ts -│ │ │ │ │ ├── index.ts -│ │ │ │ │ ├── list.ts -│ │ │ │ │ ├── random.ts -│ │ │ │ │ └── remove.ts -│ │ │ │ ├── coinflip.ts -│ │ │ │ ├── dice.ts -│ │ │ │ ├── leaderboard.ts -│ │ │ │ ├── poll.ts -│ │ │ │ └── weather.ts -│ │ │ └── fun.ts -│ │ ├── general -│ │ │ └── ping.ts -│ │ ├── github -│ │ │ ├── gh.ts -│ │ │ ├── pr.ts -│ │ │ └── status.ts -│ │ ├── help -│ │ │ ├── help.ts -│ │ │ └── helpText.ts -│ │ ├── history -│ │ │ └── history.ts -│ │ ├── pagination -│ │ │ └── pagination.ts -│ │ ├── playback -│ │ │ └── playback.ts -│ │ ├── summary -│ │ │ └── summary.ts -│ │ └── timezone -│ │ └── timezone.ts -│ ├── config -│ │ └── env.ts -│ ├── services -│ │ ├── ai -│ │ │ └── claudeService.ts -│ │ ├── cache -│ │ │ └── simpleCache.ts -│ │ ├── config -│ │ │ ├── guildConfigStore.ts -│ │ │ ├── index.ts -│ │ │ └── types.ts -│ │ ├── database -│ │ │ ├── db-joke-schema.sql -│ │ │ └── db.ts -│ │ ├── discord -│ │ │ ├── commandLoader.ts -│ │ │ ├── commandMeta.ts -│ │ │ ├── cooldowns.ts -│ │ │ ├── fetchChannelMessages.ts -│ │ │ ├── interactionHandler.ts -│ │ │ └── safeReply.ts -│ │ ├── faq -│ │ │ ├── _shared.ts -│ │ │ ├── faqService.ts -│ │ │ ├── permissions.ts -│ │ │ ├── services.test.ts -│ │ │ ├── services.ts -│ │ │ ├── store.test.ts -│ │ │ ├── store.test.ts.disabled -│ │ │ ├── store.ts -│ │ │ └── types.ts -│ │ ├── fun -│ │ │ ├── coinStore.ts -│ │ │ ├── funUsageStore.ts -│ │ │ └── pollStore.ts -│ │ ├── github -│ │ │ ├── githubApi.ts -│ │ │ ├── githubApi.ts.backup -│ │ │ ├── githubCache.ts -│ │ │ ├── githubClient.ts -│ │ │ ├── githubErrorMessage.ts -│ │ │ ├── issueAssigneePoller.ts -│ │ │ ├── issueAssigneePoller.ts.backup -│ │ │ ├── lastSeenStore.ts -│ │ │ ├── prFormatter.ts -│ │ │ ├── prPoller.ts -│ │ │ └── types.ts -│ │ ├── joke -│ │ │ └── jokeStore.ts -│ │ ├── roles -│ │ │ └── autoRoleHandler.ts -│ │ ├── summary -│ │ │ ├── llmSummary.ts -│ │ │ ├── localSummary.ts -│ │ │ └── summarizer.ts -│ │ ├── time -│ │ │ ├── formatTimestamp.ts -│ │ │ └── validateTimezone.ts -│ │ ├── timezone -│ │ │ └── timezoneStore.ts -│ │ ├── transcript -│ │ │ ├── buildTranscript.ts -│ │ │ └── defaults.ts -│ │ ├── weather -│ │ │ ├── forecast.ts -│ │ │ └── types.ts -│ │ └── welcome -│ │ ├── welcomeHandler.ts -│ │ └── welcomeMessage.ts -│ ├── utils -│ │ ├── colors.ts -│ │ ├── interactions.ts -│ │ └── logger.ts -│ ├── bot.ts -│ └── registerCommands.ts -├── .env.example -├── .gitignore -├── .prettierignore -├── .prettierrc.yml -├── CHANGELOG.md -├── CONTRIBUTORS.md -├── eslint.config.ts -├── install-claude-admin.sh -├── LICENSE -├── package-lock.json -├── package.json -├── README.md -├── tsconfig.json -└── vitest.config.ts +- Proper bot permissions in the server +- The bot role to be **above** the target user’s role +- Required gateway intents enabled +- Correct OAuth2 scopes on invite -``` - -
+See the [Discord Setup Guide](./setup-discord.md) for full details. --- -## Extending OmegaBot - -OmegaBot is designed for small, focused modules. To add new features: - -1. Create a new command file under `src/commands//` -2. Add any logic needed inside `src/services//` -3. Run `npm run register` to publish new slash commands - ---- - -## Contributors - -Thanks to everyone who has helped build or improve OmegaBot. - - - Contributors - - -Generated using https://contrib.rocks +## 🤝 Contributing -To learn how to contribute, read the [CONTRIBUTOR.md](CONTRIBUTOR.md) file. +Pull requests are welcome. -If you would like to contribute, please open an issue or submit a pull request. +If you want to contribute, help shape the bot, or just hang out and build together, +see [CONTRIBUTORS.md](./CONTRIBUTORS.md) or join the Discord. --- -## License +## 📄 License -MIT License. Use and modify freely. +This project is licensed under the MIT License. +See the [LICENSE](./LICENSE) file for full details. diff --git a/docs/setup-discord.md b/docs/setup-discord.md index 6c69c4b4..c14f9d44 100644 --- a/docs/setup-discord.md +++ b/docs/setup-discord.md @@ -1,223 +1,95 @@ # Discord Bot Setup Guide (OmegaBot) -This guide walks you through creating and configuring a Discord bot for **OmegaBot**, including required gateway intents, installation settings, and common pitfalls. +This guide walks you through creating and configuring a Discord bot for **OmegaBot**, including +required scopes, permissions, gateway intents, and common moderation pitfalls. --- -## 1. Create a Discord Application +## Required OAuth Scopes -1. Go to [Applications](https://discord.com/developers/applications) -2. Click **New Application** -3. Name it (e.g. `OmegaBot`) -4. Open the application +When inviting the bot, you **must** include: ---- - -## 2. Create a Bot User (Required) - -1. In the left sidebar, click **Bot** -2. Click **Add Bot** -3. Confirm - -> Without a bot user, gateway intents and bot tokens will not behave correctly. - ---- - -## 3. Copy Required Credentials - -### Bot Token - -- Location: **Bot → Token** -- Click **Reset Token** or **Copy** -- Store securely in `.env`: - -```env -DISCORD_TOKEN=your_bot_token_here -``` - -### Application ID - -- Location: **General Information → Application ID** -- Store in `.env`: - -```env -DISCORD_APP_ID=your_application_id_here -``` - -> The Application ID is **not secret**. -> The Bot Token **must be kept private**. - ---- - -## 4. Select Installation Type (Important) - -Go to **Installation** (sometimes labeled Integration Type). - -### Enable: - -- ✅ **Guild Install** - -### Do NOT rely on: - -- ❌ User Install (OAuth-only apps, no gateway events) - -Guild Install is required for: +- bot +- applications.commands -- Gateway bots -- Slash commands -- Member join events -- Welcome messages - -Save changes. +If you change scopes later, you must re-invite the bot. --- -## 5. Enable Privileged Gateway Intents - -Go to **Bot → Privileged Gateway Intents**. - -Enable: - -- ✅ **Server Members Intent** - -This is required for: - -- `guildMemberAdd` -- welcome / onboarding messages - -Optional (enable only if needed later): +## Required Bot Permissions -- Message Content Intent -- Presence Intent +For full functionality, especially **admin/moderation commands**, the bot role needs: -Click **Save Changes**. - -> Both the **portal toggle** and the **code intent** must be enabled. - ---- - -## 6. Invite the Bot to Your Server - -Go to **OAuth2 → URL Generator**. - -### Scopes - -- ✅ `bot` -- ✅ `applications.commands` - -### Bot Permissions (minimum) +### Core - View Channels - Send Messages - Read Message History +- Embed Links -Copy the generated URL and open it in your browser to invite the bot. +### Moderation (Admin commands) -> If you change permissions later, you must **re-invite** the bot. +- Moderate Members (timeouts) +- Kick Members +- Ban Members ---- - -## 7. Enable Developer Mode (Local Setup) - -In Discord: - -1. User Settings → Advanced -2. Enable **Developer Mode** - -This allows copying IDs. +⚠️ The bot’s role **must be higher** than the roles it is moderating. --- -## 8. Get IDs +## Gateway Intents -- **Guild ID**: Right-click server → Copy ID -- **Channel ID**: Right-click channel → Copy ID +Enable in **Developer Portal → Bot → Privileged Gateway Intents**: -Add to `.env` as needed: +- Server Members Intent (required) -```env -DISCORD_GUILD_ID=your_guild_id_here -WELCOME_CHANNEL_ID=your_channel_id_here -``` +Your code must also request the same intent. --- -## 9. Verify Gateway Intents in Code - -Your bot client **must request the same intents** you enabled in the portal. - -Example: +## Why Admin Commands Might Fail -```ts -const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], -}); -``` +If `/admin timeout`, `/admin kick`, or `/admin ban` do nothing: -Missing either side causes: - -- Gateway disconnects -- “Used disallowed intents” errors +- User is not an Administrator or approved moderator +- Bot role is below target user +- Bot lacks permission (Kick/Ban/Moderate) +- Bot was not re-invited after permission changes --- -## Testing Welcome Messages - -The `guildMemberAdd` event **only fires when a real join happens**. +## Moderator Roles (SQLite-backed) -Valid test methods: +OmegaBot supports moderator roles stored in SQLite. -- Join with an alt account -- Ask an admin to kick you once and rejoin -- Create a private test server and join there +Admins must configure these roles using the config command. +Only users with: +- Administrator permission, OR +- A configured moderator role -There is no “fake join” or manual trigger in Discord. +can run moderation commands. --- -## Common Issues - -### Bot logs in but welcome message never fires - -- Server Members Intent not enabled in portal -- `GatewayIntentBits.GuildMembers` missing in code -- Bot was not restarted after enabling intent +## Re-inviting the Bot -### Slash commands not showing +You MUST re-invite the bot if you change: +- Permissions +- Scopes +- Installation type -- Run the command registration script -- Ensure `applications.commands` scope was used on invite - -### Bot cannot send welcome message - -- Missing **View Channel** or **Send Messages** permission -- Wrong `WELCOME_CHANNEL_ID` -- Channel overrides blocking the bot role +Old invites do not update permissions. --- -## Official Discord Documentation - -- Developer Portal +## Helpful Links - [Applications](https://discord.com/developers/applications) - -- Getting Started - - [Getting-Started](https://discord.com/developers/docs/getting-started) - -- OAuth2 & Inviting Bots - - [Oauth2](https://discord.com/developers/docs/topics/oauth2) +- Discord Developer Portal + https://discord.com/developers/applications - Bot Permissions Reference + https://discord.com/developers/docs/topics/permissions - [Permissions](https://discord.com/developers/docs/topics/permissions) - -- Gateway Intents - - [Gateway](https://discord.com/developers/docs/topics/gateway#gateway-intents) - -- discord.js Slash Commands +- OAuth2 Scopes + https://discord.com/developers/docs/topics/oauth2 - [Slash Commands](https://discordjs.guide/interactions/slash-commands.html) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index e1829758..18197a23 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -1,5 +1,4 @@ // src/commands/admin/admin.ts - import { SlashCommandBuilder, EmbedBuilder, @@ -13,14 +12,6 @@ import { getDb } from "../../services/database/db.js"; import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; import { EmbedColors } from "../../utils/colors.js"; -/** - * NOTE - * This file intentionally centralizes reply behavior: - * - We deferReply once per execution path - * - We prefer editReply after deferring - * - We fall back to reply if not deferred/replied - */ - export const data = new SlashCommandBuilder() .setName("admin") .setDescription("Admin and moderation commands") @@ -86,112 +77,82 @@ export const data = new SlashCommandBuilder() ) .setDMPermission(true); -async function safeEphemeralMessage( +/* -------------------------------------------------------------------------- */ +/* Reply helpers */ +/* -------------------------------------------------------------------------- */ + +async function safeReply( interaction: ChatInputCommandInteraction, - content: string, + payload: { content?: string; embeds?: EmbedBuilder[]; flags?: MessageFlags }, ): Promise { try { if (interaction.deferred || interaction.replied) { - await interaction.editReply({ content }); + await interaction.editReply(payload); return; } - await interaction.reply({ content, flags: MessageFlags.Ephemeral }); + await interaction.reply(payload); } catch (err) { - logger.error({ err }, "[admin] failed to send safe reply"); + logger.error({ err }, "[admin] failed to reply/editReply"); } } -async function ensureDeferred( - interaction: ChatInputCommandInteraction, - ephemeral: boolean, -): Promise { - if (interaction.deferred || interaction.replied) return; - - // discord.js supports either { ephemeral: true } or flags - // Using flags keeps compatibility with the rest of this repo style. - await interaction.deferReply({ - flags: ephemeral ? MessageFlags.Ephemeral : undefined, - }); -} - -function isModeratorOrManager(interaction: ChatInputCommandInteraction): boolean { - return Boolean(interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)); -} - -async function checkModeratorRole( - interaction: ChatInputCommandInteraction, -): Promise { - // Hard guard - if (!interaction.inGuild() || !interaction.guildId || !interaction.member) return false; +function userFacingError(err: unknown): string { + const msg = err instanceof Error ? err.message : "Unknown error"; - // Manage Guild always allowed - if (isModeratorOrManager(interaction)) return true; - - try { - const db = getDb(); - const roles = db - .prepare(`SELECT role_id FROM moderator_roles WHERE guild_id = ?`) - .all(interaction.guildId) as Array<{ role_id: string }>; - - if (!roles.length) return false; - - const member = interaction.member as GuildMember; - const memberRoles = member.roles.cache; - return roles.some((r) => memberRoles.has(r.role_id)); - } catch (err) { - logger.error( - { err, guildId: interaction.guildId }, - "[admin] failed to check moderator role", - ); - return false; + // Common Discord permission failure messages are not super friendly + if (msg.toLowerCase().includes("missing permissions")) { + return "❌ I am missing required permissions. Check my role permissions, and that my role is above the target user."; + } + if (msg.toLowerCase().includes("unknown interaction")) { + return "❌ That took too long and Discord expired the command. Try again."; } + return "❌ Command failed. Check my permissions and role position."; } -export async function execute(interaction: ChatInputCommandInteraction): Promise { - const subcommand = interaction.options.getSubcommand(); +/* -------------------------------------------------------------------------- */ +/* Execute */ +/* -------------------------------------------------------------------------- */ - // Route 1: stats and health can run anywhere - if (subcommand === "stats" || subcommand === "health") { - await ensureDeferred(interaction, false); +export async function execute(interaction: ChatInputCommandInteraction): Promise { + const subcommand = interaction.options.getSubcommand(true); - try { - if (subcommand === "stats") { - await handleStats(interaction); - } else { - await handleHealth(interaction); - } + try { + // Always defer so we never hit the 3s interaction timeout. + // Stats/Health can be non-ephemeral if you prefer, but ephemeral keeps channels clean. + const ephemeral = subcommand !== "stats" && subcommand !== "health"; + await interaction.deferReply({ flags: ephemeral ? MessageFlags.Ephemeral : undefined }); + + // Stats + health do not need mod role checks + if (subcommand === "stats") { + await handleStats(interaction); return; - } catch (err) { - logger.error( - { err, command: "admin", subcommand, userId: interaction.user.id }, - "[admin] stats/health failed", - ); - await safeEphemeralMessage( - interaction, - "Something went wrong. Please try again later.", - ); + } + if (subcommand === "health") { + await handleHealth(interaction); return; } - } - - // Route 2: moderation commands must be in guild - if (!interaction.inGuild() || !interaction.guildId) { - await safeEphemeralMessage(interaction, "This command can only be used in a server."); - return; - } - // Moderation commands should be ephemeral to reduce channel noise - await ensureDeferred(interaction, true); + // Moderation must be in a guild + if (!interaction.inGuild()) { + await safeReply(interaction, { + content: "This command can only be used in a server.", + flags: MessageFlags.Ephemeral, + }); + return; + } - try { const hasPermission = await checkModeratorRole(interaction); if (!hasPermission) { - await safeEphemeralMessage( - interaction, - [ - "You do not have permission to use moderation commands.", + await safeReply(interaction, { + content: + "❌ You don't have permission to use moderation commands.\n" + "Ask an admin to set up moderator roles with `/config moderator-role`.", - ].join("\n"), + flags: MessageFlags.Ephemeral, + }); + + logger.info( + { userId: interaction.user.id, guildId: interaction.guildId, subcommand }, + "[admin] moderation blocked, missing permission", ); return; } @@ -207,34 +168,71 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await handleBan(interaction); return; default: - await safeEphemeralMessage(interaction, "Unknown subcommand."); + await safeReply(interaction, { + content: "Unknown subcommand.", + flags: MessageFlags.Ephemeral, + }); return; } } catch (err) { logger.error( - { - err, - command: "admin", - subcommand, - userId: interaction.user.id, - guildId: interaction.guildId, - }, - "[admin] moderation command failed", - ); - await safeEphemeralMessage( - interaction, - "Something went wrong. Please try again later.", + { err, command: "admin", userId: interaction.user.id, subcommand }, + "[admin] command failed", ); + + await safeReply(interaction, { + content: "Something went wrong. Please try again later.", + flags: MessageFlags.Ephemeral, + }); } } +/* -------------------------------------------------------------------------- */ +/* Permission check */ +/* -------------------------------------------------------------------------- */ + +async function checkModeratorRole(interaction: ChatInputCommandInteraction): Promise { + if (!interaction.inGuild() || !interaction.member) return false; + + // Admins/mods should be allowed even if DB roles aren't configured yet. + if (interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)) return true; + if (interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return true; + if (interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers)) return true; + + try { + const db = getDb(); + const guildId = interaction.guildId!; + + const roles = db + .prepare(`SELECT role_id FROM moderator_roles WHERE guild_id = ?`) + .all(guildId) as Array<{ role_id: string }>; + + if (!roles.length) return false; + + const member = interaction.member as GuildMember; + const memberRoles = member.roles.cache; + + return roles.some((r) => memberRoles.has(r.role_id)); + } catch (err) { + logger.error({ err }, "[admin] failed to check moderator role"); + return false; + } +} + +/* -------------------------------------------------------------------------- */ +/* Moderation handlers */ +/* -------------------------------------------------------------------------- */ + async function handleTimeout(interaction: ChatInputCommandInteraction): Promise { const targetUser = interaction.options.getUser("user", true); const duration = interaction.options.getInteger("duration", true); const reason = interaction.options.getString("reason") ?? "No reason provided"; if (!interaction.guild) { - await safeEphemeralMessage(interaction, "This command can only be used in a server."); + await safeReply(interaction, { + content: "This command can only be used in a server.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -242,50 +240,37 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< const member = await interaction.guild.members.fetch(targetUser.id); if (member.user.bot) { - await safeEphemeralMessage(interaction, "Cannot timeout bots."); + await safeReply(interaction, { content: "❌ Cannot timeout bots.", flags: MessageFlags.Ephemeral }); return; } - if (member.id === interaction.user.id) { - await safeEphemeralMessage(interaction, "You cannot timeout yourself."); + await safeReply(interaction, { content: "❌ You cannot timeout yourself.", flags: MessageFlags.Ephemeral }); return; } const executor = interaction.member as GuildMember; if (member.roles.highest.position >= executor.roles.highest.position) { - await safeEphemeralMessage( - interaction, - "You cannot timeout someone with an equal or higher role.", - ); + await safeReply(interaction, { + content: "❌ You cannot timeout someone with an equal or higher role.", + flags: MessageFlags.Ephemeral, + }); return; } const durationMs = duration * 60 * 1000; await member.timeout(durationMs, reason); - await interaction.editReply( - `Timed out **${targetUser.tag}** for **${duration}** minute(s).\nReason: ${reason}`, - ); + await safeReply(interaction, { + content: `✅ ${targetUser.tag} has been timed out for ${duration} minute(s).\nReason: ${reason}`, + }); logger.info( - { - moderator: interaction.user.tag, - target: targetUser.tag, - duration, - reason, - guildId: interaction.guildId, - }, + { moderator: interaction.user.tag, target: targetUser.tag, duration, reason }, "[admin] user timed out", ); } catch (err) { - logger.error( - { err, targetUser: targetUser.id, guildId: interaction.guildId }, - "[admin] timeout failed", - ); - await safeEphemeralMessage( - interaction, - "Failed to timeout user. Check my permissions and role position.", - ); + logger.error({ err, targetUser: targetUser.id }, "[admin] timeout failed"); + await safeReply(interaction, { content: userFacingError(err), flags: MessageFlags.Ephemeral }); } } @@ -294,7 +279,10 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { - await safeEphemeralMessage( - interaction, - "You cannot kick someone with an equal or higher role.", - ); + await safeReply(interaction, { + content: "❌ You cannot kick someone with an equal or higher role.", + flags: MessageFlags.Ephemeral, + }); return; } if (!member.kickable) { - await safeEphemeralMessage( - interaction, - "I do not have permission to kick this user.", - ); + await safeReply(interaction, { + content: "❌ I don't have permission to kick this user. Check my role position and Kick Members permission.", + flags: MessageFlags.Ephemeral, + }); return; } await member.kick(reason); - await interaction.editReply(`Kicked **${targetUser.tag}**.\nReason: ${reason}`); + await safeReply(interaction, { content: `✅ ${targetUser.tag} has been kicked.\nReason: ${reason}` }); logger.info( - { - moderator: interaction.user.tag, - target: targetUser.tag, - reason, - guildId: interaction.guildId, - }, + { moderator: interaction.user.tag, target: targetUser.tag, reason }, "[admin] user kicked", ); } catch (err) { - logger.error( - { err, targetUser: targetUser.id, guildId: interaction.guildId }, - "[admin] kick failed", - ); - await safeEphemeralMessage( - interaction, - "Failed to kick user. Check my permissions and role position.", - ); + logger.error({ err, targetUser: targetUser.id }, "[admin] kick failed"); + await safeReply(interaction, { content: userFacingError(err), flags: MessageFlags.Ephemeral }); } } @@ -359,38 +335,41 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise null); + if (targetUser.id === interaction.user.id) { - await safeEphemeralMessage(interaction, "You cannot ban yourself."); + await safeReply(interaction, { content: "❌ You cannot ban yourself.", flags: MessageFlags.Ephemeral }); return; } - const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); - if (member) { if (member.user.bot) { - await safeEphemeralMessage(interaction, "Cannot ban bots."); + await safeReply(interaction, { content: "❌ Cannot ban bots.", flags: MessageFlags.Ephemeral }); return; } const executor = interaction.member as GuildMember; if (member.roles.highest.position >= executor.roles.highest.position) { - await safeEphemeralMessage( - interaction, - "You cannot ban someone with an equal or higher role.", - ); + await safeReply(interaction, { + content: "❌ You cannot ban someone with an equal or higher role.", + flags: MessageFlags.Ephemeral, + }); return; } if (!member.bannable) { - await safeEphemeralMessage( - interaction, - "I do not have permission to ban this user.", - ); + await safeReply(interaction, { + content: "❌ I don't have permission to ban this user. Check my role position and Ban Members permission.", + flags: MessageFlags.Ephemeral, + }); return; } } @@ -400,52 +379,36 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise 0 ? `\nMessages deleted: last ${deleteDays} day(s)` : ""; - await interaction.editReply( - `Banned **${targetUser.tag}**.\nReason: ${reason}${deletedText}`, - ); + await safeReply(interaction, { + content: + `✅ ${targetUser.tag} has been banned.\nReason: ${reason}` + + (deleteDays > 0 ? `\nMessages deleted: last ${deleteDays} day(s)` : ""), + }); logger.info( - { - moderator: interaction.user.tag, - target: targetUser.tag, - reason, - deleteDays, - guildId: interaction.guildId, - }, + { moderator: interaction.user.tag, target: targetUser.tag, reason, deleteDays }, "[admin] user banned", ); } catch (err) { - logger.error( - { err, targetUser: targetUser.id, guildId: interaction.guildId }, - "[admin] ban failed", - ); - await safeEphemeralMessage( - interaction, - "Failed to ban user. Check my permissions and role position.", - ); + logger.error({ err, targetUser: targetUser.id }, "[admin] ban failed"); + await safeReply(interaction, { content: userFacingError(err), flags: MessageFlags.Ephemeral }); } } +/* -------------------------------------------------------------------------- */ +/* Stats + Health */ +/* -------------------------------------------------------------------------- */ + async function handleStats(interaction: ChatInputCommandInteraction): Promise { try { const db = getDb(); - const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { - count: number; - }; - const coinFlipCount = db - .prepare("SELECT COUNT(*) as count FROM coin_flips") - .get() as { count: number }; + const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { count: number }; + const coinFlipCount = db.prepare("SELECT COUNT(*) as count FROM coin_flips").get() as { count: number }; const funUsage = await getFunUsageSnapshot(); - - const totalsByCommand = (funUsage?.totalsByCommand ?? {}) as Record; - const totalCommands = Object.values(totalsByCommand).reduce( - (sum, count) => sum + (count ?? 0), - 0, - ); + const totals = (funUsage?.totalsByCommand ?? {}) as Record; + const totalCommands = Object.values(totals).reduce((sum, count) => sum + (Number.isFinite(count) ? count : 0), 0); const uptimeSeconds = process.uptime(); const uptimeDays = Math.floor(uptimeSeconds / 86400); @@ -460,30 +423,22 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise !process.env[v]?.trim()); + const requiredEnvVars = ["DISCORD_TOKEN", "DISCORD_APP_ID"]; + const missingVars = requiredEnvVars.filter((v) => !process.env[v]); - if (missingRequired.length === 0) { - checks.push({ name: "Required env", status: "All set" }); + if (missingVars.length === 0) { + checks.push({ name: "Environment", status: "✅ Required vars set" }); } else { - checks.push({ - name: "Required env", - status: "Missing", - details: missingRequired.join(", "), - }); + checks.push({ name: "Environment", status: "⚠️ Missing vars", details: missingVars.join(", ") }); } - const optionalEnv = [ + const optionalKeys = [ { name: "OpenAI", key: "OPENAI_API_KEY" }, { name: "GitHub", key: "GITHUB_TOKEN" }, - { name: "WeatherAPI", key: "WEATHERAPI_KEY" }, + { name: "Weather API", key: "WEATHERAPI_KEY" }, ]; - for (const item of optionalEnv) { - checks.push({ - name: item.name, - status: process.env[item.key]?.trim() ? "Configured" : "Not configured", - }); + for (const { name, key } of optionalKeys) { + checks.push({ name, status: process.env[key] ? "✅ Configured" : "⚠️ Not configured" }); } const embed = new EmbedBuilder() @@ -535,19 +483,17 @@ async function handleHealth(interaction: ChatInputCommandInteraction): Promise - c.details - ? `**${c.name}:** ${c.status}\n${c.details}` - : `**${c.name}:** ${c.status}`, + c.details ? `**${c.name}:** ${c.status}\n${c.details}` : `**${c.name}:** ${c.status}`, ) .join("\n\n"), ) .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); + await safeReply(interaction, { embeds: [embed] }); - logger.info({ userId: interaction.user.id }, "[admin] viewed health check"); + logger.info({ userId: interaction.user.id }, "[admin] viewed health"); } catch (error) { logger.error({ error }, "[admin] health check failed"); - await safeEphemeralMessage(interaction, "Failed to run health check."); + await safeReply(interaction, { content: "❌ Failed to run health check", flags: MessageFlags.Ephemeral }); } -} +} \ No newline at end of file From 73d59d271b2e7c87ef429b406266d46d0af43016 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 17:42:51 -0500 Subject: [PATCH 2/4] style: auto-format with Prettier [skip-precheck] --- docs/setup-discord.md | 3 +- src/commands/admin/admin.ts | 118 ++++++++++++++++++++++++++++-------- 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/docs/setup-discord.md b/docs/setup-discord.md index c14f9d44..e5c0d9f9 100644 --- a/docs/setup-discord.md +++ b/docs/setup-discord.md @@ -64,6 +64,7 @@ OmegaBot supports moderator roles stored in SQLite. Admins must configure these roles using the config command. Only users with: + - Administrator permission, OR - A configured moderator role @@ -74,6 +75,7 @@ can run moderation commands. ## Re-inviting the Bot You MUST re-invite the bot if you change: + - Permissions - Scopes - Installation type @@ -92,4 +94,3 @@ Old invites do not update permissions. - OAuth2 Scopes https://discord.com/developers/docs/topics/oauth2 - diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 18197a23..55ab2cda 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -120,7 +120,9 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise // Always defer so we never hit the 3s interaction timeout. // Stats/Health can be non-ephemeral if you prefer, but ephemeral keeps channels clean. const ephemeral = subcommand !== "stats" && subcommand !== "health"; - await interaction.deferReply({ flags: ephemeral ? MessageFlags.Ephemeral : undefined }); + await interaction.deferReply({ + flags: ephemeral ? MessageFlags.Ephemeral : undefined, + }); // Stats + health do not need mod role checks if (subcommand === "stats") { @@ -191,13 +193,16 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise /* Permission check */ /* -------------------------------------------------------------------------- */ -async function checkModeratorRole(interaction: ChatInputCommandInteraction): Promise { +async function checkModeratorRole( + interaction: ChatInputCommandInteraction, +): Promise { if (!interaction.inGuild() || !interaction.member) return false; // Admins/mods should be allowed even if DB roles aren't configured yet. if (interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)) return true; if (interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return true; - if (interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers)) return true; + if (interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers)) + return true; try { const db = getDb(); @@ -240,11 +245,17 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< const member = await interaction.guild.members.fetch(targetUser.id); if (member.user.bot) { - await safeReply(interaction, { content: "❌ Cannot timeout bots.", flags: MessageFlags.Ephemeral }); + await safeReply(interaction, { + content: "❌ Cannot timeout bots.", + flags: MessageFlags.Ephemeral, + }); return; } if (member.id === interaction.user.id) { - await safeReply(interaction, { content: "❌ You cannot timeout yourself.", flags: MessageFlags.Ephemeral }); + await safeReply(interaction, { + content: "❌ You cannot timeout yourself.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -270,7 +281,10 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< ); } catch (err) { logger.error({ err, targetUser: targetUser.id }, "[admin] timeout failed"); - await safeReply(interaction, { content: userFacingError(err), flags: MessageFlags.Ephemeral }); + await safeReply(interaction, { + content: userFacingError(err), + flags: MessageFlags.Ephemeral, + }); } } @@ -290,11 +304,17 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise null); if (targetUser.id === interaction.user.id) { - await safeReply(interaction, { content: "❌ You cannot ban yourself.", flags: MessageFlags.Ephemeral }); + await safeReply(interaction, { + content: "❌ You cannot ban yourself.", + flags: MessageFlags.Ephemeral, + }); return; } if (member) { if (member.user.bot) { - await safeReply(interaction, { content: "❌ Cannot ban bots.", flags: MessageFlags.Ephemeral }); + await safeReply(interaction, { + content: "❌ Cannot ban bots.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -367,7 +399,8 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise; - const totalCommands = Object.values(totals).reduce((sum, count) => sum + (Number.isFinite(count) ? count : 0), 0); + const totalCommands = Object.values(totals).reduce( + (sum, count) => sum + (Number.isFinite(count) ? count : 0), + 0, + ); const uptimeSeconds = process.uptime(); const uptimeDays = Math.floor(uptimeSeconds / 86400); @@ -423,12 +466,20 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise - c.details ? `**${c.name}:** ${c.status}\n${c.details}` : `**${c.name}:** ${c.status}`, + c.details + ? `**${c.name}:** ${c.status}\n${c.details}` + : `**${c.name}:** ${c.status}`, ) .join("\n\n"), ) @@ -494,6 +557,9 @@ async function handleHealth(interaction: ChatInputCommandInteraction): Promise Date: Sun, 18 Jan 2026 17:43:27 -0500 Subject: [PATCH 3/4] Trying this again --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 98c02862..94f420b8 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,10 @@ see [CONTRIBUTORS.md](./CONTRIBUTORS.md) or join the Discord. This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for full details. + +--- + +## Contributing + +Want to help build OmegaBot? +See [CONTRIBUTORS.md](./CONTRIBUTORS.md) or open a PR. From 065b60a5531bee1d7030e004dea1640c4c0f32f1 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 17:55:04 -0500 Subject: [PATCH 4/4] Fixing this up: --- src/commands/admin/admin.ts | 131 ++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 55ab2cda..1736ed9a 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -5,7 +5,6 @@ import { PermissionFlagsBits, type ChatInputCommandInteraction, type GuildMember, - MessageFlags, } from "discord.js"; import { logger } from "../../utils/logger.js"; import { getDb } from "../../services/database/db.js"; @@ -16,6 +15,7 @@ export const data = new SlashCommandBuilder() .setName("admin") .setDescription("Admin and moderation commands") .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + // Moderation subcommands .addSubcommand((sub) => sub @@ -66,6 +66,7 @@ export const data = new SlashCommandBuilder() .setMaxValue(7), ), ) + // Bot stats subcommands .addSubcommand((sub) => sub @@ -81,16 +82,29 @@ export const data = new SlashCommandBuilder() /* Reply helpers */ /* -------------------------------------------------------------------------- */ +type ReplyPayload = { + content?: string; + embeds?: EmbedBuilder[]; + ephemeral?: boolean; +}; + async function safeReply( interaction: ChatInputCommandInteraction, - payload: { content?: string; embeds?: EmbedBuilder[]; flags?: MessageFlags }, + payload: ReplyPayload, ): Promise { try { if (interaction.deferred || interaction.replied) { - await interaction.editReply(payload); + // editReply cannot set ephemeral or flags + const { content, embeds } = payload; + await interaction.editReply({ content, embeds }); return; } - await interaction.reply(payload); + + await interaction.reply({ + content: payload.content, + embeds: payload.embeds, + ephemeral: payload.ephemeral ?? true, + }); } catch (err) { logger.error({ err }, "[admin] failed to reply/editReply"); } @@ -98,19 +112,24 @@ async function safeReply( function userFacingError(err: unknown): string { const msg = err instanceof Error ? err.message : "Unknown error"; + const low = msg.toLowerCase(); - // Common Discord permission failure messages are not super friendly - if (msg.toLowerCase().includes("missing permissions")) { - return "❌ I am missing required permissions. Check my role permissions, and that my role is above the target user."; + if (low.includes("missing permissions")) { + return ( + "❌ I am missing required permissions.\n" + + "Check my role permissions, and make sure my role is above the target user's role." + ); } - if (msg.toLowerCase().includes("unknown interaction")) { + + if (low.includes("unknown interaction")) { return "❌ That took too long and Discord expired the command. Try again."; } + return "❌ Command failed. Check my permissions and role position."; } /* -------------------------------------------------------------------------- */ -/* Execute */ +/* Execute */ /* -------------------------------------------------------------------------- */ export async function execute(interaction: ChatInputCommandInteraction): Promise { @@ -118,11 +137,9 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise try { // Always defer so we never hit the 3s interaction timeout. - // Stats/Health can be non-ephemeral if you prefer, but ephemeral keeps channels clean. + // Moderation defaults to ephemeral to keep channels clean. const ephemeral = subcommand !== "stats" && subcommand !== "health"; - await interaction.deferReply({ - flags: ephemeral ? MessageFlags.Ephemeral : undefined, - }); + await interaction.deferReply({ ephemeral }); // Stats + health do not need mod role checks if (subcommand === "stats") { @@ -138,7 +155,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise if (!interaction.inGuild()) { await safeReply(interaction, { content: "This command can only be used in a server.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } @@ -149,7 +166,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise content: "❌ You don't have permission to use moderation commands.\n" + "Ask an admin to set up moderator roles with `/config moderator-role`.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); logger.info( @@ -170,10 +187,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await handleBan(interaction); return; default: - await safeReply(interaction, { - content: "Unknown subcommand.", - flags: MessageFlags.Ephemeral, - }); + await safeReply(interaction, { content: "Unknown subcommand.", ephemeral: true }); return; } } catch (err) { @@ -184,13 +198,13 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await safeReply(interaction, { content: "Something went wrong. Please try again later.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); } } /* -------------------------------------------------------------------------- */ -/* Permission check */ +/* Permission check */ /* -------------------------------------------------------------------------- */ async function checkModeratorRole( @@ -198,7 +212,7 @@ async function checkModeratorRole( ): Promise { if (!interaction.inGuild() || !interaction.member) return false; - // Admins/mods should be allowed even if DB roles aren't configured yet. + // Allow built-in Discord perms first if (interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)) return true; if (interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) return true; if (interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers)) @@ -225,7 +239,7 @@ async function checkModeratorRole( } /* -------------------------------------------------------------------------- */ -/* Moderation handlers */ +/* Moderation handlers */ /* -------------------------------------------------------------------------- */ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise { @@ -236,7 +250,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< if (!interaction.guild) { await safeReply(interaction, { content: "This command can only be used in a server.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } @@ -247,14 +261,14 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< if (member.user.bot) { await safeReply(interaction, { content: "❌ Cannot timeout bots.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } if (member.id === interaction.user.id) { await safeReply(interaction, { content: "❌ You cannot timeout yourself.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } @@ -263,7 +277,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< if (member.roles.highest.position >= executor.roles.highest.position) { await safeReply(interaction, { content: "❌ You cannot timeout someone with an equal or higher role.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } @@ -273,6 +287,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< await safeReply(interaction, { content: `✅ ${targetUser.tag} has been timed out for ${duration} minute(s).\nReason: ${reason}`, + ephemeral: false, }); logger.info( @@ -281,10 +296,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< ); } catch (err) { logger.error({ err, targetUser: targetUser.id }, "[admin] timeout failed"); - await safeReply(interaction, { - content: userFacingError(err), - flags: MessageFlags.Ephemeral, - }); + await safeReply(interaction, { content: userFacingError(err), ephemeral: true }); } } @@ -295,7 +307,7 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { await safeReply(interaction, { content: "❌ You cannot kick someone with an equal or higher role.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } @@ -330,8 +339,8 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { await safeReply(interaction, { content: "❌ You cannot ban someone with an equal or higher role.", - flags: MessageFlags.Ephemeral, + ephemeral: true, }); return; } @@ -400,8 +404,8 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise 0 ? `\nMessages deleted: last ${deleteDays} day(s)` : ""), + ephemeral: false, }); logger.info( @@ -424,15 +429,12 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise { @@ -442,6 +444,7 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise