Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 45 additions & 49 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -250,6 +269,8 @@ OmegaBot is online
│ │ │ ├── prFormatter.ts
│ │ │ ├── prPoller.ts
│ │ │ └── types.ts
│ │ ├── roles
│ │ │ └── autoRoleHandler.ts
│ │ ├── summary
│ │ │ ├── llmSummary.ts
│ │ │ ├── localSummary.ts
Expand Down
26 changes: 26 additions & 0 deletions docs/dev-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ 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
Expand Down
16 changes: 12 additions & 4 deletions docs/setup-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
});

Expand Down
13 changes: 13 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
/* ---------------------------------------------------------------- */
Expand Down
63 changes: 63 additions & 0 deletions src/services/roles/autoRoleHandler.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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",
);
}
}