From dc6c01b28cd44deb96eb733d29cd8abeda83492b Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 16 Dec 2025 19:08:32 -0500 Subject: [PATCH 1/5] Adding in changes --- .gitignore | 2 +- README.md | 5 +- src/config/env.ts | 94 +++++++++++++++++++++++---- src/services/github/githubApi.ts | 96 +++++++++++----------------- src/services/github/lastSeenStore.ts | 91 ++++++++++++++++++++++++++ src/services/github/prFormatter.ts | 26 ++++++++ src/services/github/prPoller.ts | 70 ++++++++++++++++++++ src/services/github/types.ts | 31 +++------ 8 files changed, 317 insertions(+), 98 deletions(-) create mode 100644 src/services/github/lastSeenStore.ts create mode 100644 src/services/github/prFormatter.ts create mode 100644 src/services/github/prPoller.ts diff --git a/.gitignore b/.gitignore index b4dcd2ba..0a2450c5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ Thumbs.db *.zip # Runtime data -data/timezones.json \ No newline at end of file +*.json \ No newline at end of file diff --git a/README.md b/README.md index 79d3c3d4..c7e5831c 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ OmegaBot is online ``` . -├── .env.example +├── .env ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug.yml @@ -166,6 +166,9 @@ OmegaBot is online │ ├── github │ │ ├── githubApi.ts │ │ ├── githubClient.ts +│ │ ├── lastSeenStore.ts +│ │ ├── prFormatter.ts +│ │ ├── prPoller.ts │ │ └── types.ts │ ├── summary │ │ ├── llmSummary.ts diff --git a/src/config/env.ts b/src/config/env.ts index 7ff4a33d..0c284ac0 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -3,6 +3,7 @@ dotenv.config(); /** * Helper to enforce that required environment variables are present. + * * If a variable is missing, the bot fails fast at startup instead of * crashing later at runtime in unpredictable ways. */ @@ -17,30 +18,95 @@ function requireEnv(name: string): string { /** * Centralized environment configuration for OmegaBot. * - * Required fields: - * - DISCORD_TOKEN Bot authentication token for connecting to Discord. - * - DISCORD_APP_ID Application ID needed for command registration. - * - DISCORD_GUILD_ID Guild where development commands are registered. + * Keeping this configuration in one place: + * - Prevents scattered process.env lookups + * - Makes configuration explicit and auditable + * - Allows optional features to be gated cleanly + * + * ------------------------------------------------------------------ + * Required (core bot functionality) + * ------------------------------------------------------------------ + * + * - DISCORD_TOKEN + * Bot authentication token used to connect to Discord. + * + * - DISCORD_APP_ID + * Application ID required for slash command registration. + * + * - DISCORD_GUILD_ID + * Guild where development commands are registered. + * + * ------------------------------------------------------------------ + * Optional (feature flags / enhancements) + * ------------------------------------------------------------------ + * + * - SUMMARY_MODE + * "local" (default) or "llm" + * Determines which summarizer implementation is used. + * + * - OPENAI_API_KEY + * Required only when SUMMARY_MODE === "llm". + * + * ------------------------------------------------------------------ + * GitHub integration + * ------------------------------------------------------------------ + * + * - GITHUB_TOKEN + * Personal Access Token used for authenticated GitHub REST calls. + * Required for: + * - Issue lookup + * - PR lookup + * - PR announcements * - * Optional fields: - * - SUMMARY_MODE "local" (default) or "llm" - * Determines which summarizer implementation is used. - * - OPENAI_API_KEY Only required when using LLM summary mode. + * - GITHUB_OWNER + * Default repository owner for polling PRs (optional). * - * Keeping this configuration in one place prevents scattered env lookups - * and makes the bot easier to configure, test, and deploy. + * - GITHUB_REPO + * Default repository name for polling PRs (optional). + * + * - GITHUB_ANNOUNCE_CHANNEL_ID + * Discord channel ID where PR announcements are posted (optional). + * + * - GITHUB_POLL_INTERVAL_MS + * Polling interval for PR announcements. + * Defaults to 60 seconds if not provided. + * + * All GitHub announcement fields are optional so the bot can run + * without polling enabled. */ export const env = { + /* ---------------------------------------------------------------- */ + /* Discord (required) */ + /* ---------------------------------------------------------------- */ + token: requireEnv("DISCORD_TOKEN"), appId: requireEnv("DISCORD_APP_ID"), guildId: requireEnv("DISCORD_GUILD_ID"), - // Summary mode defaults to local so the bot can run without AI keys. + /* ---------------------------------------------------------------- */ + /* Summaries */ + /* ---------------------------------------------------------------- */ + + // Defaults to local so the bot runs without AI keys. summaryMode: process.env.SUMMARY_MODE ?? "local", - // Optional, only needed when user chooses LLM summarization. + // Only required when summaryMode === "llm" openAIKey: process.env.OPENAI_API_KEY ?? null, - // GitHub + /* ---------------------------------------------------------------- */ + /* GitHub */ + /* ---------------------------------------------------------------- */ + + // Auth token for GitHub REST API githubToken: process.env.GITHUB_TOKEN ?? null, -}; + + // Default repo configuration for PR polling (optional) + githubOwner: process.env.GITHUB_OWNER ?? null, + githubRepo: process.env.GITHUB_REPO ?? null, + + // Where PR announcements should be posted + githubAnnounceChannelId: process.env.GITHUB_ANNOUNCE_CHANNEL_ID ?? null, + + // Polling interval (ms). Defaults to 60s. + githubPollIntervalMs: Number(process.env.GITHUB_POLL_INTERVAL_MS ?? "60000"), +}; \ No newline at end of file diff --git a/src/services/github/githubApi.ts b/src/services/github/githubApi.ts index a87da5db..9d3a9259 100644 --- a/src/services/github/githubApi.ts +++ b/src/services/github/githubApi.ts @@ -1,12 +1,7 @@ // src/services/github/githubApi.ts import { githubRequest, GitHubApiError } from "./githubClient.js"; -import type { - GitHubIssue, - GitHubPullRequest, - GitHubIssueSummary, - GitHubPrSummary, -} from "./types.js"; +import type { GitHubIssue, GitHubPullRequest, GitHubIssueSummary, GitHubPrSummary } from "./types.js"; /* ------------------------------------------------------------------ */ /* Path helpers */ @@ -24,15 +19,13 @@ function prPath(owner: string, repo: string, number: number): string { return `${repoBase(owner, repo)}/pulls/${number}`; } -function listIssuesPath( - owner: string, - repo: string, - options?: { - state?: "open" | "closed" | "all"; - limit?: number; - labels?: string[]; - }, -): string { +export type ListIssuesOptions = { + state?: "open" | "closed" | "all"; + limit?: number; + labels?: string[]; +}; + +function listIssuesPath(owner: string, repo: string, options?: ListIssuesOptions): string { const params = new URLSearchParams(); if (options?.state) params.set("state", options.state); @@ -46,24 +39,21 @@ function listIssuesPath( return `${repoBase(owner, repo)}/issues${query ? `?${query}` : ""}`; } -function listPullRequestsPath( - owner: string, - repo: string, - options?: { - state?: "open" | "closed" | "all"; - limit?: number; - }, -): string { - const params = new URLSearchParams(); +export type ListPrOptions = { + state?: "open" | "closed" | "all"; + limit?: number; +}; - if (options?.state) params.set("state", options.state); - if (options?.limit) params.set("per_page", String(options.limit)); +function listPullRequestsPath(owner: string, repo: string, options?: ListPrOptions): string { + const params = new URLSearchParams(); + params.set("state", options?.state ?? "open"); + params.set("per_page", String(options?.limit ?? 20)); + // For pulls: GitHub supports sort=updated params.set("sort", "updated"); params.set("direction", "desc"); - const query = params.toString(); - return `${repoBase(owner, repo)}/pulls${query ? `?${query}` : ""}`; + return `${repoBase(owner, repo)}/pulls?${params.toString()}`; } /* ------------------------------------------------------------------ */ @@ -79,22 +69,14 @@ function is404(err: unknown): err is GitHubApiError { } /* ------------------------------------------------------------------ */ -/* Single-item fetchers */ +/* Single item fetchers */ /* ------------------------------------------------------------------ */ -export async function getIssue( - owner: string, - repo: string, - number: number, -): Promise { +export async function getIssue(owner: string, repo: string, number: number): Promise { return githubRequest(issuePath(owner, repo, number)); } -export async function getPullRequest( - owner: string, - repo: string, - number: number, -): Promise { +export async function getPullRequest(owner: string, repo: string, number: number): Promise { return githubRequest(prPath(owner, repo, number)); } @@ -117,16 +99,12 @@ export async function getIssueOrPr( /** * List issues (issues-only; PRs filtered out) + * + * Note: + * GitHub's /issues endpoint can include PRs. + * PRs contain `pull_request` marker. We filter those out here. */ -export async function listIssues( - owner: string, - repo: string, - options?: { - state?: "open" | "closed" | "all"; - limit?: number; - labels?: string[]; - }, -): Promise { +export async function listIssues(owner: string, repo: string, options?: ListIssuesOptions): Promise { const data = await githubRequest(listIssuesPath(owner, repo, options)); return data @@ -136,25 +114,24 @@ export async function listIssues( title: i.title, html_url: i.html_url, state: i.state, - user: i.user, + user: { login: i.user.login }, comments: i.comments, })); } /** - * List pull requests + * List pull requests (summary view) + * + * This is what the poller should consume because it includes: + * - number, title, url, author + * - updated_at for last-seen comparisons */ export async function listPullRequests( owner: string, repo: string, - options?: { - state?: "open" | "closed" | "all"; - limit?: number; - }, + options?: ListPrOptions, ): Promise { - const data = await githubRequest( - listPullRequestsPath(owner, repo, options), - ); + const data = await githubRequest(listPullRequestsPath(owner, repo, options)); return data.map((pr) => ({ number: pr.number, @@ -162,8 +139,9 @@ export async function listPullRequests( html_url: pr.html_url, state: pr.state, merged_at: pr.merged_at, - user: pr.user, + updated_at: pr.updated_at, + user: { login: pr.user.login }, comments: pr.comments, review_comments: pr.review_comments, })); -} +} \ No newline at end of file diff --git a/src/services/github/lastSeenStore.ts b/src/services/github/lastSeenStore.ts new file mode 100644 index 00000000..cd659f02 --- /dev/null +++ b/src/services/github/lastSeenStore.ts @@ -0,0 +1,91 @@ +import fs from "fs"; +import path from "path"; + +/** + * Absolute path to the persistent store for GitHub announcement state. + * + * This file is intentionally: + * - JSON (human-readable, debuggable) + * - Stored outside src/ so it survives rebuilds + * + * Example contents: + * { + * "owner/repo": 1713291823000 + * } + */ +const STORE_PATH = path.join(process.cwd(), "data", "last-seen.json"); + +type Store = Record; + +/** + * Get the last stored timestamp for a repo. + * + * Returns: + * - unix millis number if present + * - null if the repo has never been seen + */ +export function getLastSeen(owner: string, repo: string): number | null { + const store = loadStore(); + const key = makeRepoKey(owner, repo); + return key in store ? store[key] : null; +} + +/** + * Save the "last seen" timestamp for PR announcements. + * + * We store unix millis (Number) to make comparisons cheap and timezone-agnostic. + */ +export function setLastSeenPr(owner: string, repo: string, updatedAtIso: string): void { + const store = loadStore(); + const key = makeRepoKey(owner, repo); + + const ms = Date.parse(updatedAtIso); + if (Number.isNaN(ms)) { + throw new Error(`Invalid updatedAt timestamp: ${updatedAtIso}`); + } + + store[key] = ms; + saveStore(store); +} + +/** + * Remove any stored "last seen" value for a repo. + * Safe even if key does not exist. + */ +export function clearLastSeen(owner: string, repo: string): void { + const store = loadStore(); + const key = makeRepoKey(owner, repo); + + if (key in store) { + delete store[key]; + saveStore(store); + } +} + +function makeRepoKey(owner: string, repo: string): string { + return `${owner}/${repo}`; +} + +function loadStore(): Store { + if (!fs.existsSync(STORE_PATH)) { + return {}; + } + + const raw = fs.readFileSync(STORE_PATH, "utf8"); + try { + return JSON.parse(raw) as Store; + } catch { + // If the JSON is corrupted, fail safe to empty. + return {}; + } +} + +function saveStore(store: Store): void { + const dir = path.dirname(STORE_PATH); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); +} \ No newline at end of file diff --git a/src/services/github/prFormatter.ts b/src/services/github/prFormatter.ts new file mode 100644 index 00000000..895a7a2b --- /dev/null +++ b/src/services/github/prFormatter.ts @@ -0,0 +1,26 @@ +// src/services/github/prFormatter.ts + +import type { GitHubPrSummary } from "./types.js"; + +/** + * PR formatting helpers for Discord output. + * + * Design goals: + * - Keep output readable in a busy channel + * - Use only fields we already have in the PR summary + * - Make it easy to swap formatting later without touching poller logic + */ + +/** + * 2-line PR output (readable, compact): + * Line 1: "#123 Title (by author)" + * Line 2: URL + */ +export function formatPullRequest(pr: GitHubPrSummary): string { + const author = pr.user?.login ?? "unknown"; + + return [ + `#${pr.number} ${pr.title} (by ${author})`, + pr.html_url, + ].join("\n"); +} \ No newline at end of file diff --git a/src/services/github/prPoller.ts b/src/services/github/prPoller.ts new file mode 100644 index 00000000..2954bf84 --- /dev/null +++ b/src/services/github/prPoller.ts @@ -0,0 +1,70 @@ +// src/services/github/prPoller.ts + +import type { Client, TextBasedChannel } from "discord.js"; +import { listPullRequests } from "./githubApi.js"; +import { getLastSeen, setLastSeenPr } from "./lastSeenStore.js"; +import { formatPullRequest } from "./prFormatter.js"; + +/** + * Minimal shape we need from the GitHub PR list for polling announcements. + * Your githubApi.listPullRequests MUST return objects that include `updated_at`. + */ +type PrForPolling = { + updated_at: string; // ISO timestamp from GitHub +}; + +/** + * Poll GitHub for new or updated PRs and announce them in a Discord channel. + * + * Policy: + * - We track "last seen" using PR.updated_at + * - We announce PRs where updated_at is newer than last seen + * - After announcing, we update last seen to the newest updated_at we announced + */ +export async function pollPullRequestsOnce(args: { + client: Client; + owner: string; + repo: string; + announceChannelId: string; + limit?: number; +}): Promise { + const { client, owner, repo, announceChannelId, limit = 20 } = args; + + // Pull newest-updated first (based on your githubApi query params). + const prs = (await listPullRequests(owner, repo, { + state: "open", + limit, + })) as unknown as (PrForPolling & Parameters[0])[]; + + if (prs.length === 0) return; + + const lastSeen = getLastSeen(owner, repo) ?? 0; + + // Keep only PRs updated after our stored last-seen timestamp. + const fresh = prs.filter((pr) => { + const ms = Date.parse(pr.updated_at); + return !Number.isNaN(ms) && ms > lastSeen; + }); + + if (fresh.length === 0) return; + + // Fetch the announce channel and safely narrow it to something we can `.send()` to. + const fetched = await client.channels.fetch(announceChannelId); + if (!fetched || !fetched.isTextBased()) return; + + // Post oldest-first so announcements read naturally. + const oldestFirst = [...fresh].sort( + (a, b) => Date.parse(a.updated_at) - Date.parse(b.updated_at), + ); + +// Extra TS guard: only proceed if this thing actually has a send() function. +if (!("send" in fetched) || typeof fetched.send !== "function") return; + +for (const pr of oldestFirst) { + await fetched.send(formatPullRequest(pr)); +} + + // Update last-seen to the newest updated_at we just announced. + const newest = oldestFirst[oldestFirst.length - 1]; + setLastSeenPr(owner, repo, newest.updated_at); +} \ No newline at end of file diff --git a/src/services/github/types.ts b/src/services/github/types.ts index 2b215ed8..7a651083 100644 --- a/src/services/github/types.ts +++ b/src/services/github/types.ts @@ -11,20 +11,14 @@ */ /* ------------------------------------------------------------------ */ -/* Shared base types */ +/* Shared base types */ /* ------------------------------------------------------------------ */ -/** - * GitHub user object - */ export type GitHubUser = { login: string; html_url: string; }; -/** - * GitHub issue/PR label - */ export type GitHubLabel = { name: string; color: string; @@ -32,7 +26,6 @@ export type GitHubLabel = { /* ------------------------------------------------------------------ */ /* Full GitHub API response types */ -/* These match what the GitHub REST API actually returns */ /* ------------------------------------------------------------------ */ /** @@ -59,10 +52,6 @@ export type GitHubIssue = { updated_at: string; closed_at: string | null; - /** - * Present only if this issue is actually a Pull Request - * (when fetched from the /issues API) - */ pull_request?: { html_url: string; }; @@ -100,10 +89,6 @@ export type GitHubPullRequest = { merged_at: string | null; }; -/** - * Lightweight repository info - * Useful for validation, previews, or summaries later - */ export type GitHubRepo = { full_name: string; description: string | null; @@ -114,13 +99,9 @@ export type GitHubRepo = { }; /* ------------------------------------------------------------------ */ -/* Command-friendly / normalized response types */ -/* These are what slash commands should actually consume */ +/* Command-friendly / normalized response types */ /* ------------------------------------------------------------------ */ -/** - * Minimal Issue view used by commands - */ export type GitHubIssueSummary = { number: number; title: string; @@ -131,7 +112,10 @@ export type GitHubIssueSummary = { }; /** - * Minimal PR view used by commands + * PR summary used by commands and the poller. + * + * Important: + * - includes `updated_at` so we can do "last seen" comparisons */ export type GitHubPrSummary = { number: number; @@ -139,7 +123,8 @@ export type GitHubPrSummary = { html_url: string; state: "open" | "closed"; merged_at: string | null; + updated_at: string; user?: { login: string }; comments?: number; review_comments?: number; -}; +}; \ No newline at end of file From 24d5c7d92d3cb2fdbf7b8d4d735810fb0daeb913 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 16 Dec 2025 19:08:37 -0500 Subject: [PATCH 2/5] style: auto-format with Prettier [skip-precheck] --- src/config/env.ts | 2 +- src/services/github/githubApi.ts | 43 ++++++++++++++++++++++------ src/services/github/lastSeenStore.ts | 2 +- src/services/github/prFormatter.ts | 7 ++--- src/services/github/prPoller.ts | 12 ++++---- src/services/github/types.ts | 2 +- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index 0c284ac0..c665f9f6 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -109,4 +109,4 @@ export const env = { // Polling interval (ms). Defaults to 60s. githubPollIntervalMs: Number(process.env.GITHUB_POLL_INTERVAL_MS ?? "60000"), -}; \ No newline at end of file +}; diff --git a/src/services/github/githubApi.ts b/src/services/github/githubApi.ts index 9d3a9259..230385a5 100644 --- a/src/services/github/githubApi.ts +++ b/src/services/github/githubApi.ts @@ -1,7 +1,12 @@ // src/services/github/githubApi.ts import { githubRequest, GitHubApiError } from "./githubClient.js"; -import type { GitHubIssue, GitHubPullRequest, GitHubIssueSummary, GitHubPrSummary } from "./types.js"; +import type { + GitHubIssue, + GitHubPullRequest, + GitHubIssueSummary, + GitHubPrSummary, +} from "./types.js"; /* ------------------------------------------------------------------ */ /* Path helpers */ @@ -25,7 +30,11 @@ export type ListIssuesOptions = { labels?: string[]; }; -function listIssuesPath(owner: string, repo: string, options?: ListIssuesOptions): string { +function listIssuesPath( + owner: string, + repo: string, + options?: ListIssuesOptions, +): string { const params = new URLSearchParams(); if (options?.state) params.set("state", options.state); @@ -44,7 +53,11 @@ export type ListPrOptions = { limit?: number; }; -function listPullRequestsPath(owner: string, repo: string, options?: ListPrOptions): string { +function listPullRequestsPath( + owner: string, + repo: string, + options?: ListPrOptions, +): string { const params = new URLSearchParams(); params.set("state", options?.state ?? "open"); params.set("per_page", String(options?.limit ?? 20)); @@ -72,11 +85,19 @@ function is404(err: unknown): err is GitHubApiError { /* Single item fetchers */ /* ------------------------------------------------------------------ */ -export async function getIssue(owner: string, repo: string, number: number): Promise { +export async function getIssue( + owner: string, + repo: string, + number: number, +): Promise { return githubRequest(issuePath(owner, repo, number)); } -export async function getPullRequest(owner: string, repo: string, number: number): Promise { +export async function getPullRequest( + owner: string, + repo: string, + number: number, +): Promise { return githubRequest(prPath(owner, repo, number)); } @@ -104,7 +125,11 @@ export async function getIssueOrPr( * GitHub's /issues endpoint can include PRs. * PRs contain `pull_request` marker. We filter those out here. */ -export async function listIssues(owner: string, repo: string, options?: ListIssuesOptions): Promise { +export async function listIssues( + owner: string, + repo: string, + options?: ListIssuesOptions, +): Promise { const data = await githubRequest(listIssuesPath(owner, repo, options)); return data @@ -131,7 +156,9 @@ export async function listPullRequests( repo: string, options?: ListPrOptions, ): Promise { - const data = await githubRequest(listPullRequestsPath(owner, repo, options)); + const data = await githubRequest( + listPullRequestsPath(owner, repo, options), + ); return data.map((pr) => ({ number: pr.number, @@ -144,4 +171,4 @@ export async function listPullRequests( comments: pr.comments, review_comments: pr.review_comments, })); -} \ No newline at end of file +} diff --git a/src/services/github/lastSeenStore.ts b/src/services/github/lastSeenStore.ts index cd659f02..c9f3b3e2 100644 --- a/src/services/github/lastSeenStore.ts +++ b/src/services/github/lastSeenStore.ts @@ -88,4 +88,4 @@ function saveStore(store: Store): void { } fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); -} \ No newline at end of file +} diff --git a/src/services/github/prFormatter.ts b/src/services/github/prFormatter.ts index 895a7a2b..5ceb2d3c 100644 --- a/src/services/github/prFormatter.ts +++ b/src/services/github/prFormatter.ts @@ -19,8 +19,5 @@ import type { GitHubPrSummary } from "./types.js"; export function formatPullRequest(pr: GitHubPrSummary): string { const author = pr.user?.login ?? "unknown"; - return [ - `#${pr.number} ${pr.title} (by ${author})`, - pr.html_url, - ].join("\n"); -} \ No newline at end of file + return [`#${pr.number} ${pr.title} (by ${author})`, pr.html_url].join("\n"); +} diff --git a/src/services/github/prPoller.ts b/src/services/github/prPoller.ts index 2954bf84..0b8634cc 100644 --- a/src/services/github/prPoller.ts +++ b/src/services/github/prPoller.ts @@ -57,14 +57,14 @@ export async function pollPullRequestsOnce(args: { (a, b) => Date.parse(a.updated_at) - Date.parse(b.updated_at), ); -// Extra TS guard: only proceed if this thing actually has a send() function. -if (!("send" in fetched) || typeof fetched.send !== "function") return; + // Extra TS guard: only proceed if this thing actually has a send() function. + if (!("send" in fetched) || typeof fetched.send !== "function") return; -for (const pr of oldestFirst) { - await fetched.send(formatPullRequest(pr)); -} + for (const pr of oldestFirst) { + await fetched.send(formatPullRequest(pr)); + } // Update last-seen to the newest updated_at we just announced. const newest = oldestFirst[oldestFirst.length - 1]; setLastSeenPr(owner, repo, newest.updated_at); -} \ No newline at end of file +} diff --git a/src/services/github/types.ts b/src/services/github/types.ts index 7a651083..b34ee99e 100644 --- a/src/services/github/types.ts +++ b/src/services/github/types.ts @@ -127,4 +127,4 @@ export type GitHubPrSummary = { user?: { login: string }; comments?: number; review_comments?: number; -}; \ No newline at end of file +}; From a82de302b230bd43971521405c3b67bf798e9f82 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 16 Dec 2025 19:23:05 -0500 Subject: [PATCH 3/5] Fixing this up --- .env.example | 89 ++++++++++++++++++++++++++++++--- src/bot.ts | 40 ++++++++++----- src/config/env.ts | 2 +- src/services/github/prPoller.ts | 2 +- 4 files changed, 112 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 5bf7297e..128b8c9b 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,87 @@ -DISCORD_TOKEN=your-bot-token-here -DISCORD_APP_ID=your-application-id-here -DISCORD_GUILD_ID=your-test-guild-id-here +# ============================================================ +# OmegaBot Environment Configuration +# ============================================================ +# +# Copy this file to `.env` and fill in real values. +# NEVER commit `.env` with secrets. +# +# ============================================================ +# Discord (required) +# ============================================================ -# Summary mode: use "llm" to enable OpenAI, or anything else for local summary +# Bot authentication token used to connect to Discord +DISCORD_TOKEN=your-discord-bot-token-here + +# Application ID required for slash command registration +DISCORD_APP_ID=your-discord-application-id + +# Guild where development slash commands are registered +# (guild commands update instantly) +DISCORD_GUILD_ID=your-discord-guild-id + + +# ============================================================ +# Summaries +# ============================================================ + +# Summary mode: +# - "local" → simple local summarizer (default) +# - "llm" → OpenAI-powered summaries SUMMARY_MODE=local -# Only required if SUMMARY_MODE=llm +# OpenAI API key +# Required ONLY when SUMMARY_MODE=llm OPENAI_API_KEY=your-openai-key-if-needed -# GitHub token -GITHUB_TOKEN=your-token-here \ No newline at end of file + +# ============================================================ +# GitHub Integration +# ============================================================ + +# Personal Access Token (PAT) for GitHub REST API +# +# Required for: +# - Issue lookup +# - Pull request lookup +# - PR announcements +# +# Fine-grained token recommended. +# Minimum permissions: +# - Contents: Read +# - Pull requests: Read +# - Issues: Read +GITHUB_TOKEN=your-github-pat-here + + +# ============================================================ +# GitHub PR Announcements (optional) +# ============================================================ + +# Default GitHub repo owner/org for PR polling +# Example: NickTheDevOpsGuy +GITHUB_OWNER=your-github-owner + +# Repository name for PR polling +# Example: OmegaBot +GITHUB_REPO=your-repo-name + +# Discord channel ID where PR announcements will be posted +# +# IMPORTANT: +# This must be a numeric channel ID, not a channel name. +# Enable Developer Mode in Discord → right-click channel → Copy ID +GITHUB_ANNOUNCE_CHANNEL_ID=your-discord-channel-id + + +# ============================================================ +# GitHub PR Polling Interval +# ============================================================ + +# Polling interval in milliseconds +# +# Examples: +# 60000 → every 1 minute +# 300000 → every 5 minutes +# +# Defaults to 60000 (1 minute) if not set +GITHUB_POLL_INTERVAL_MS=60000 \ No newline at end of file diff --git a/src/bot.ts b/src/bot.ts index 81de3a10..0af40ff0 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,39 +1,55 @@ import { Client, GatewayIntentBits } from "discord.js"; import { loadCommands, type CommandClient } from "./services/discord/commandLoader.js"; import { handleInteraction } from "./services/discord/interactionHandler.js"; +import { pollPullRequestsOnce } from "./services/github/prPoller.js"; import { env } from "./config/env.js"; -/* + +/** * Create the Discord client with only the intents required for slash-command handling. */ const client = new Client({ intents: [GatewayIntentBits.Guilds], }) as CommandClient; -/* - * Initialize the command registry where loaded slash commands will be stored. +/** + * Initialize the command registry. */ client.commands = new Map(); -/* - * Load and register all compiled command modules before the bot starts handling interactions. +/** + * Load and register slash commands. */ await loadCommands(client); -/* - * Forward every incoming interaction to the central interaction handler. +/** + * Route all interactions through the central handler. */ client.on("interactionCreate", async (interaction) => { await handleInteraction(interaction, client); }); -/* - * Log a confirmation once the bot successfully connects. +/** + * Start background services once the bot is fully ready. */ client.once("clientReady", () => { console.log("OmegaBot is online"); + + // Poll GitHub every 5 minutes + setInterval(async () => { + try { + await pollPullRequestsOnce({ + client, + owner: "NickTheDevOpsGuy", + repo: "OmegaBot", + announceChannelId: "1450641533509439538", + }); + } catch (err) { + console.error("[prPoller] failed", err); + } + }, 5 * 60 * 1000); }); -/* - * Start the bot session using the configured token. +/** + * Connect to Discord. */ -client.login(env.token); +client.login(env.token); \ No newline at end of file diff --git a/src/config/env.ts b/src/config/env.ts index c665f9f6..0c284ac0 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -109,4 +109,4 @@ export const env = { // Polling interval (ms). Defaults to 60s. githubPollIntervalMs: Number(process.env.GITHUB_POLL_INTERVAL_MS ?? "60000"), -}; +}; \ No newline at end of file diff --git a/src/services/github/prPoller.ts b/src/services/github/prPoller.ts index 0b8634cc..05925740 100644 --- a/src/services/github/prPoller.ts +++ b/src/services/github/prPoller.ts @@ -1,6 +1,6 @@ // src/services/github/prPoller.ts -import type { Client, TextBasedChannel } from "discord.js"; +import type { Client } from "discord.js"; import { listPullRequests } from "./githubApi.js"; import { getLastSeen, setLastSeenPr } from "./lastSeenStore.js"; import { formatPullRequest } from "./prFormatter.js"; From 196e9d2634df0b99505e98c0e75690ed4d222c89 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 16 Dec 2025 19:23:09 -0500 Subject: [PATCH 4/5] style: auto-format with Prettier [skip-precheck] --- src/bot.ts | 29 ++++++++++++++++------------- src/config/env.ts | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 0af40ff0..0ae829f9 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -35,21 +35,24 @@ client.once("clientReady", () => { console.log("OmegaBot is online"); // Poll GitHub every 5 minutes - setInterval(async () => { - try { - await pollPullRequestsOnce({ - client, - owner: "NickTheDevOpsGuy", - repo: "OmegaBot", - announceChannelId: "1450641533509439538", - }); - } catch (err) { - console.error("[prPoller] failed", err); - } - }, 5 * 60 * 1000); + setInterval( + async () => { + try { + await pollPullRequestsOnce({ + client, + owner: "NickTheDevOpsGuy", + repo: "OmegaBot", + announceChannelId: "1450641533509439538", + }); + } catch (err) { + console.error("[prPoller] failed", err); + } + }, + 5 * 60 * 1000, + ); }); /** * Connect to Discord. */ -client.login(env.token); \ No newline at end of file +client.login(env.token); diff --git a/src/config/env.ts b/src/config/env.ts index 0c284ac0..c665f9f6 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -109,4 +109,4 @@ export const env = { // Polling interval (ms). Defaults to 60s. githubPollIntervalMs: Number(process.env.GITHUB_POLL_INTERVAL_MS ?? "60000"), -}; \ No newline at end of file +}; From c99b0048a185d04164b7979bcfeda5a24c514838 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 16 Dec 2025 19:25:53 -0500 Subject: [PATCH 5/5] Adding in mroe files --- src/bot.ts | 63 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 0ae829f9..b9cf1d01 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,3 +1,5 @@ +// src/bot.ts + import { Client, GatewayIntentBits } from "discord.js"; import { loadCommands, type CommandClient } from "./services/discord/commandLoader.js"; import { handleInteraction } from "./services/discord/interactionHandler.js"; @@ -5,54 +7,71 @@ import { pollPullRequestsOnce } from "./services/github/prPoller.js"; import { env } from "./config/env.js"; /** - * Create the Discord client with only the intents required for slash-command handling. + * Create the Discord client with only the intents required for slash commands. */ const client = new Client({ intents: [GatewayIntentBits.Guilds], }) as CommandClient; /** - * Initialize the command registry. + * Command registry populated by the command loader. */ client.commands = new Map(); /** - * Load and register slash commands. + * Load compiled command modules and attach them to client.commands. */ await loadCommands(client); /** - * Route all interactions through the central handler. + * Forward every incoming interaction to the central handler. */ client.on("interactionCreate", async (interaction) => { await handleInteraction(interaction, client); }); /** - * Start background services once the bot is fully ready. + * GitHub PR polling is optional. + * We only enable it when all required env vars are present. + */ +const githubPollingEnabled = + !!env.githubToken && + !!env.githubOwner && + !!env.githubRepo && + !!env.githubAnnounceChannelId; + +/** + * Log a confirmation once the bot successfully connects. */ client.once("clientReady", () => { console.log("OmegaBot is online"); - // Poll GitHub every 5 minutes - setInterval( - async () => { - try { - await pollPullRequestsOnce({ - client, - owner: "NickTheDevOpsGuy", - repo: "OmegaBot", - announceChannelId: "1450641533509439538", - }); - } catch (err) { - console.error("[prPoller] failed", err); - } - }, - 5 * 60 * 1000, - ); + if (githubPollingEnabled) { + console.log( + `GitHub PR polling enabled for ${env.githubOwner}/${env.githubRepo} -> channel ${env.githubAnnounceChannelId}`, + ); + console.log(`GitHub PR polling interval: ${env.githubPollIntervalMs}ms`); + } else { + console.log("GitHub PR polling disabled (missing env config)"); + } }); /** - * Connect to Discord. + * Start the bot session using the configured token. */ client.login(env.token); + +/** + * Schedule PR polling (if enabled). + * Uses void to avoid unhandled promise warnings. + */ +if (githubPollingEnabled) { + setInterval(() => { + void pollPullRequestsOnce({ + client, + owner: env.githubOwner!, + repo: env.githubRepo!, + announceChannelId: env.githubAnnounceChannelId!, + }); + }, env.githubPollIntervalMs); +}