diff --git a/README.md b/README.md index f1086bbd..07183036 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,15 @@ OmegaBot is a modular Discord bot designed to support development projects with - 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 and admin slash commands -- Auto-role assignment on member join (optional via DISCORD_AUTO_ROLE_ID) +- Welcome and onboarding flows triggered on member join (`guildMemberAdd`) +- Optional auto-role assignment for new members (`DISCORD_AUTO_ROLE_ID`) +- GitHub integration: issue and PR lookups plus polling-based announcements + - Check if Github is working + - New PR announcements + - Issue and PR assignee change announcements + - Issue and PR closed announcements +- Configuration and feature gating via environment variables (optional features run only when configured) +- Per-guild configuration backed by persistent storage and admin slash commands ### Core commands @@ -188,6 +194,7 @@ OmegaBot is online │ └── omegabot.png ├── data │ ├── faqs.json +│ ├── github-assignees.json │ ├── guild-config.json │ ├── last-seen.json │ └── timezones.json @@ -225,7 +232,8 @@ OmegaBot is online │ │ │ └── ping.ts │ │ ├── github │ │ │ ├── gh.ts -│ │ │ └── pr.ts +│ │ │ ├── pr.ts +│ │ │ └── status.ts │ │ ├── help │ │ │ ├── help.ts │ │ │ └── helpText.ts @@ -308,6 +316,7 @@ OmegaBot is online ├── README.md ├── tsconfig.json └── vitest.config.ts + ``` diff --git a/docs/commands.md b/docs/commands.md index d233e3e0..4e4654a0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -16,7 +16,8 @@ - `/gh issue` — Fetch a GitHub issue by number - `/gh issues` — List open GitHub issues - `/gh prs` — List open pull requests -- `/pr` — Fetch a single pull request by number +- `/gh status` — Show GitHub integration status (configuration, polling, and channels) +- `/pr` — Fetch a single pull request by number (legacy shortcut) ## Timezone diff --git a/src/commands/github/gh.ts b/src/commands/github/gh.ts index 8ed1acb7..356d70af 100644 --- a/src/commands/github/gh.ts +++ b/src/commands/github/gh.ts @@ -5,6 +5,7 @@ import { SlashCommandBuilder, type ChatInputCommandInteraction, } from "discord.js"; +import { env } from "../../config/env.js"; import { getIssue, listIssues, @@ -21,6 +22,18 @@ export const data = new SlashCommandBuilder() .setName("gh") .setDescription("GitHub helpers") + .addSubcommand((s) => + s + .setName("status") + .setDescription("Show GitHub integration status for this bot") + .addStringOption((o) => + o.setName("owner").setDescription("Org/user (optional; defaults to env)"), + ) + .addStringOption((o) => + o.setName("repo").setDescription("Repo (optional; defaults to env)"), + ), + ) + .addSubcommand((s) => s .setName("issue") @@ -73,10 +86,46 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const owner = interaction.options.getString("owner", true); - const repo = interaction.options.getString("repo", true); - try { + if (sub === "status") { + const owner = + interaction.options.getString("owner") ?? env.githubOwner ?? "(unset)"; + const repo = interaction.options.getString("repo") ?? env.githubRepo ?? "(unset)"; + + const lines: string[] = []; + lines.push("**GitHub Status**"); + lines.push(""); + + // Config (never print secrets) + lines.push(`Token: ${env.githubToken ? "configured" : "missing"}`); + lines.push(`Default repo: ${owner}/${repo}`); + lines.push(`Poll interval: ${env.githubPollIntervalMs}ms`); + lines.push(""); + + // Polling streams + lines.push("**Polling streams**"); + lines.push( + `- PR announcements: ${env.githubPrPollingEnabled ? "enabled" : "disabled"}`, + ); + lines.push(` - Channel: ${env.githubPrAnnounceChannelId ?? "(unset)"}`); + lines.push( + `- Assignee activity: ${env.githubAssigneePollingEnabled ? "enabled" : "disabled"}`, + ); + lines.push(` - Channel: ${env.githubAssigneeAnnounceChannelId ?? "(unset)"}`); + + lines.push(""); + lines.push( + "_Tip: If polling is disabled, set GITHUB_TOKEN + GITHUB_OWNER + GITHUB_REPO + the channel env var(s)._", + ); + + await interaction.editReply(lines.join("\n")); + return; + } + + // The remaining subcommands require explicit owner/repo options + const owner = interaction.options.getString("owner", true); + const repo = interaction.options.getString("repo", true); + if (sub === "issue") { const number = interaction.options.getInteger("number", true); const issue = await getIssue(owner, repo, number); @@ -138,7 +187,6 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise } await interaction.editReply("Unknown subcommand."); - return; } catch (err) { const msg = getGitHubUserMessage(err); if (msg) { @@ -146,8 +194,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise return; } - logger.warn({ err, owner, repo, sub }, "[gh] GitHub request failed"); + logger.warn({ err, sub }, "[gh] GitHub request failed"); await interaction.editReply("GitHub request failed. Please try again in a bit."); - return; } } diff --git a/src/commands/github/status.ts b/src/commands/github/status.ts new file mode 100644 index 00000000..0f3d8774 --- /dev/null +++ b/src/commands/github/status.ts @@ -0,0 +1,51 @@ +// src/commands/github/status.ts + +import { SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { env } from "../../config/env.js"; +import { logger } from "../../utils/logger.js"; + +/** + * /github status command + * + * Reports current GitHub-related configuration and which GitHub pollers are enabled. + * This command does not call the GitHub API. + */ +export const data = new SlashCommandBuilder() + .setName("status") + .setDescription("Show GitHub integration status for this bot"); + +export async function execute(interaction: ChatInputCommandInteraction): Promise { + try { + const lines: string[] = []; + + // High level config (do not print secrets) + lines.push("**GitHub Status**"); + lines.push(`Repo: ${env.githubOwner ?? "(unset)"}/${env.githubRepo ?? "(unset)"}`); + lines.push(`Token: ${env.githubToken ? "present" : "missing"}`); + lines.push(`Poll interval: ${env.githubPollIntervalMs} ms`); + lines.push(""); + + // PR polling + lines.push("**PR announcements**"); + lines.push(`Enabled: ${env.githubPrPollingEnabled ? "yes" : "no"}`); + lines.push(`Channel: ${env.githubPrAnnounceChannelId ?? "(unset)"}`); + lines.push(""); + + // Assignee/issue activity polling + lines.push("**Assignee + issue activity**"); + lines.push(`Enabled: ${env.githubAssigneePollingEnabled ? "yes" : "no"}`); + lines.push(`Channel: ${env.githubAssigneeAnnounceChannelId ?? "(unset)"}`); + + await interaction.reply({ content: lines.join("\n"), ephemeral: true }); + } catch (err) { + logger.error({ err }, "[/status] failed"); + + // Best-effort reply (avoid throwing twice) + const msg = "Something went wrong while building GitHub status."; + if (interaction.deferred || interaction.replied) { + await interaction.editReply({ content: msg }); + } else { + await interaction.reply({ content: msg, ephemeral: true }); + } + } +}