From 03ca2bc4e6b7a402fab23381874fb05a628f2edd Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sat, 10 Jan 2026 18:31:05 -0500 Subject: [PATCH 1/2] Adding in files --- .env.example | 94 +++++++++++++-------------- README.md | 21 ++++++ docs/dev-notes.md | 25 +++++++ docs/setup-env.md | 16 +++-- src/bot.ts | 6 +- src/config/env.ts | 13 ++++ src/services/roles/autoRoleHandler.ts | 63 ++++++++++++++++++ 7 files changed, 184 insertions(+), 54 deletions(-) create mode 100644 src/services/roles/autoRoleHandler.ts diff --git a/.env.example b/.env.example index b35a1295..c0e68789 100644 --- a/.env.example +++ b/.env.example @@ -9,88 +9,84 @@ # Discord (required) # ============================================================ -# Bot authentication token used to connect to Discord DISCORD_TOKEN= - -# Application ID required for slash command registration DISCORD_APP_ID= - -# Guild where development slash commands are registered -# (guild commands update instantly; optional in production) DISCORD_GUILD_ID= +# ============================================================ +# Discord Role Automation (optional) +# ============================================================ + +# Auto-assign role ID for new members when they join the server. +# +# If set, the bot will attempt to assign this role on guildMemberAdd. +# If unset, auto-role assignment is skipped. +# +# Notes: +# - Must be a numeric role ID (not a role name) +# - Bot requires "Manage Roles" permission +# - Bot's role must be ABOVE this role in the role hierarchy +DISCORD_AUTO_ROLE_ID= + # ============================================================ # Summaries # ============================================================ -# Summary mode: -# - "local" → simple local summarizer (default) -# - "llm" → OpenAI-powered summaries SUMMARY_MODE=local - -# OpenAI API key -# Required ONLY when SUMMARY_MODE=llm OPENAI_API_KEY= # ============================================================ # 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 +# Fine-grained PAT recommended. +# Minimum repository permissions: # - Issues: Read +# - Pull requests: Read +# +# NOTE: +# For org-owned repositories, this token may require approval by an org admin. GITHUB_TOKEN= -# ============================================================ -# GitHub PR Announcements (optional) -# ============================================================ - -# Default GitHub repo owner/org for PR polling -# Example: NickTheDevOpsGuy GITHUB_OWNER= - -# Repository name for PR polling -# Example: OmegaBot GITHUB_REPO= -# Discord channel ID where PR announcements will be posted +# ============================================================ +# GitHub Announcement Channels (optional) +# ============================================================ # -# IMPORTANT: -# This must be a numeric channel ID, not a channel name. -# Enable Developer Mode in Discord → right-click channel → Copy ID +# Legacy (fallback) channel: +# If set, it will be used as the default for GitHub announcements +# unless a more specific channel is provided below. GITHUB_ANNOUNCE_CHANNEL_ID= + +# PR announcements channel (preferred): +# Used for: +# - New PR creation +# - PR update announcements +# +# If unset, falls back to GITHUB_ANNOUNCE_CHANNEL_ID. +GITHUB_PR_ANNOUNCE_CHANNEL_ID= + +# Assignee + closure activity channel: +# Used for: +# - Assignee added / removed +# - Self-assignment / unassignment +# - Issue / PR closed events +# +# If unset, falls back to GITHUB_ANNOUNCE_CHANNEL_ID. +# If neither is set, assignee polling is disabled. GITHUB_ASSIGNEE_ANNOUNCE_CHANNEL_ID= # ============================================================ -# GitHub PR Polling Interval +# GitHub 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 # ============================================================ # Logging # ============================================================ -# Log level: -# debug | info | warn | error LOG_LEVEL=info - -# Pretty-print logs in development (requires pino-pretty) -# Set to false in production for JSON logs LOG_PRETTY=true \ No newline at end of file diff --git a/README.md b/README.md index 70f1c4f1..f1086bbd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ OmegaBot is a modular Discord bot designed to support development projects with - 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) ### Core commands @@ -162,6 +163,24 @@ OmegaBot is online │ │ └── OmegaBot.yml │ └── pull_request_template.md ├── .husky +│ ├── _ +│ │ ├── .gitignore +│ │ ├── applypatch-msg +│ │ ├── commit-msg +│ │ ├── h +│ │ ├── husky.sh +│ │ ├── post-applypatch +│ │ ├── post-checkout +│ │ ├── post-commit +│ │ ├── post-merge +│ │ ├── post-rewrite +│ │ ├── pre-applypatch +│ │ ├── pre-auto-gc +│ │ ├── pre-commit +│ │ ├── pre-merge-commit +│ │ ├── pre-push +│ │ ├── pre-rebase +│ │ └── prepare-commit-msg │ ├── pre-commit │ └── pre-push ├── assets @@ -250,6 +269,8 @@ OmegaBot is online │ │ │ ├── prFormatter.ts │ │ │ ├── prPoller.ts │ │ │ └── types.ts +│ │ ├── roles +│ │ │ └── autoRoleHandler.ts │ │ ├── summary │ │ │ ├── llmSummary.ts │ │ │ ├── localSummary.ts diff --git a/docs/dev-notes.md b/docs/dev-notes.md index 50f96920..4274e217 100644 --- a/docs/dev-notes.md +++ b/docs/dev-notes.md @@ -34,6 +34,31 @@ environment variables. --- +## Configuration and Feature Gating + +OmegaBot uses environment variables not only for secrets, but also +to enable or disable optional features at runtime. + +Design principles: + +- Required variables are validated at startup and fail fast +- Optional features are gated by the presence of their related env vars +- The bot must be able to start and run safely with optional features disabled +- Feature-specific code should never assume configuration exists + +Examples: + +- GitHub polling is enabled only when all required GitHub env vars are present +- Auto-role assignment is enabled only when DISCORD_AUTO_ROLE_ID is set +- LLM summaries are enabled only when SUMMARY_MODE=llm and OPENAI_API_KEY is present + +This allows: +- Safe local development without external services +- Gradual feature rollout via configuration +- Clear operational behavior without code changes + +--- + ## Error Handling - Commands must catch errors and reply gracefully diff --git a/docs/setup-env.md b/docs/setup-env.md index f7e80367..a22426c6 100644 --- a/docs/setup-env.md +++ b/docs/setup-env.md @@ -34,12 +34,20 @@ GITHUB_TOKEN=your-github-pat GITHUB_OWNER=org-or-user GITHUB_REPO=repo-name -# Channel for PR creation / PR updates +# Legacy (fallback) channel: +# If set, it will be used as the default for GitHub announcements +# unless a more specific channel is provided below. GITHUB_ANNOUNCE_CHANNEL_ID=channel-id -# Channel for issue + PR assignee changes and closures -# (can be the same as above if you want) -GITHUB_ASSIGNEE_CHANNEL_ID=channel-id +# Assignee + closure activity channel: +# Used for: +# - Assignee added / removed +# - Self-assignment / unassignment +# - Issue / PR closed events +# +# If unset, falls back to GITHUB_ANNOUNCE_CHANNEL_ID. +# If neither is set, assignee polling is disabled. +GITHUB_ASSIGNEE_ANNOUNCE_CHANNEL_ID= # Polling interval (milliseconds) GITHUB_POLL_INTERVAL_MS=60000 diff --git a/src/bot.ts b/src/bot.ts index 924e477e..51d7ab8a 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -5,6 +5,7 @@ import { loadCommands, type CommandClient } from "./services/discord/commandLoad import { handleInteraction } from "./services/discord/interactionHandler.js"; import { pollPullRequestsOnce } from "./services/github/prPoller.js"; import { pollIssueAssigneesOnce } from "./services/github/issueAssigneePoller.js"; +import { handleAutoRole } from "./services/roles/autoRoleHandler.js"; import { onGuildMemberAdd } from "./services/welcome/welcomeHandler.js"; import { env } from "./config/env.js"; import { logger } from "./utils/logger.js"; @@ -54,6 +55,9 @@ client.on("guildMemberAdd", async (member) => { "guildMemberAdd event fired", ); + // Auto-assign a default role on join (if configured) + await handleAutoRole(member); + await onGuildMemberAdd(member); }); @@ -133,4 +137,4 @@ if (githubPrPollingEnabled || githubAssigneePollingEnabled) { }); } }, env.githubPollIntervalMs); -} +} \ No newline at end of file diff --git a/src/config/env.ts b/src/config/env.ts index 21439692..9876cc78 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -130,6 +130,19 @@ export const env = { */ guildId: process.env.DISCORD_GUILD_ID ?? null, + /** + * Optional: + * Discord role ID automatically assigned to new members on join. + * + * If unset, auto-role assignment is disabled. + * + * IMPORTANT: + * - This must be a ROLE ID, not a role name + * - The bot must have permission to manage this role + * - The role must be lower than the bot’s highest role + */ + discordAutoRoleId: process.env.DISCORD_AUTO_ROLE_ID ?? null, + /* ---------------------------------------------------------------- */ /* Summaries */ /* ---------------------------------------------------------------- */ diff --git a/src/services/roles/autoRoleHandler.ts b/src/services/roles/autoRoleHandler.ts new file mode 100644 index 00000000..c212b958 --- /dev/null +++ b/src/services/roles/autoRoleHandler.ts @@ -0,0 +1,63 @@ +import type { GuildMember } from "discord.js"; +import { PermissionFlagsBits } from "discord.js"; +import { env } from "../../config/env.js"; +import { logger } from "../../utils/logger.js"; + +export async function handleAutoRole(member: GuildMember): Promise { + if (!env.discordAutoRoleId) { + logger.warn("discordAutoRoleId not set; auto-role disabled"); + return; + } + + if (member.user.bot) { + logger.debug("auto-role skipped: bot user"); + return; + } + + const role = member.guild.roles.cache.get(env.discordAutoRoleId); + + if (!role) { + logger.warn( + { roleId: env.discordAutoRoleId, guildId: member.guild.id }, + "auto-role skipped: role not found in guild", + ); + return; + } + + const hasAutoRole = member.roles.cache.has(role.id); + if (hasAutoRole) { + logger.debug( + { userId: member.user.id, roleId: role.id }, + "auto-role skipped: member already has role", + ); + return; + } + + const botMember = member.guild.members.me; + if (!botMember) { + logger.warn("auto-role failed: could not resolve bot member in guild"); + return; + } + + if (!botMember.permissions.has(PermissionFlagsBits.ManageRoles)) { + logger.warn("auto-role failed: missing Manage Roles permission"); + return; + } + + // 4) Role hierarchy guard + if (role.position >= botMember.roles.highest.position) { + logger.warn("auto-role failed: role is higher than bot's highest role"); + return; + } + + // 5) Assign + try { + await member.roles.add(role); + logger.info({ userId: member.user.id, roleId: role.id }, "auto-role assigned"); + } catch (err) { + logger.error( + { err, userId: member.user.id, roleId: role.id }, + "auto-role failed during assignment", + ); + } +} From 582a23b7631bbab9ac1adb1b442d68f428c00bda Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sat, 10 Jan 2026 18:31:10 -0500 Subject: [PATCH 2/2] style: auto-format with Prettier [skip-precheck] --- docs/dev-notes.md | 1 + src/bot.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev-notes.md b/docs/dev-notes.md index 4274e217..3d740b57 100644 --- a/docs/dev-notes.md +++ b/docs/dev-notes.md @@ -53,6 +53,7 @@ Examples: - LLM summaries are enabled only when SUMMARY_MODE=llm and OPENAI_API_KEY is present This allows: + - Safe local development without external services - Gradual feature rollout via configuration - Clear operational behavior without code changes diff --git a/src/bot.ts b/src/bot.ts index 51d7ab8a..a5c5c51c 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -137,4 +137,4 @@ if (githubPrPollingEnabled || githubAssigneePollingEnabled) { }); } }, env.githubPollIntervalMs); -} \ No newline at end of file +}