Skip to content

VEKAgg/DiscordBot

Repository files navigation

VEKA Discord Bot

A professional networking and community-development Discord bot built with nextcord and backed by PostgreSQL. It provides profile/networking tools, a marketplace, curated RSS resource feeds, and operational tooling designed to keep running in a degraded state when its database or any single feature is unavailable.


Table of Contents


Tech Stack

  • Python >=3.13 (nextcord 3.x supports 3.12–3.14)
  • nextcord 3.2.0 — Discord API wrapper
  • asyncpg 0.31.0 — async PostgreSQL driver (the only datastore; no MongoDB, no Redis)
  • aiohttp / feedparser / beautifulsoup4 — RSS resource feeds
  • python-dotenv / validators — config and input validation

Package management is done via uvuv.lock is the lockfile. pyproject.toml is the source of truth for dependencies. A requirements.txt is kept for Docker images that don't have uv.


Features

Loaded modules (see EXTENSIONS in src/core/app.py):

  • 🤝 Networking — professional profiles, connection requests, and connections (/profile, /connect, plus ! prefix equivalents)
  • 🛒 Marketplace — listings, reviews, and enhanced marketplace features
  • 📚 Resource Feeds — curated RSS (tech news, jobs, dev blogs) refreshed every 15 minutes
  • 🎮 RPG / Leveling — XP from messages, voice time, and commands; level-up system; activity role evaluation; background tasks for leaderboard auto-update, inactivity monitoring, and activity role assessment
  • 📊 Leaderboard & Stats — auto-updating leaderboard embed (/setupleaderboard), /leaderboard [stat], /level [@user], /most streamed|played|listened|coded activity leaderboards
  • 🔍 Activity Tracking — streaming, gaming, listening, and coding activity duration tracking with detailed activity logging (game names, songs, coding apps)
  • 🛠️ Admin / Health — administrative commands, help, health/diagnostics, moderation, notifications, and honeypot anti-spam
  • 📋 Mentorship — mentorship matching and management
  • 💼 Portfolio — portfolio management and display
  • 📻 Radio — voice channel radio/audio features
  • 🔗 External — info and export commands
  • 📡 Status — bot status and system information

Additional cogs exist in src/cogs/ (quiz, gamification, workshops) but are not loaded until added to EXTENSIONS. Gamification is an intentional disabled stub.


Architecture

The entrypoint chain is main.pysrc/core/app.py:run_bot(), which loads .env, configures logging, builds the bot, registers events, and starts it.

Degraded-mode design is the central theme — the bot is built to keep running when the database, a cog, or a background task fails, rather than crashing:

  • src/core/runtime_state.py — a single global runtime_state dataclass (attached as bot.runtime_state) tracking db_available, loaded_cogs, failed_cogs, degraded_features, startup-check results, and an alert cache. This is the source of truth for health/observability.
  • DB failures don't abort bootinitialize_database() catches errors, sets db_available = False, and marks database degraded.
  • A 1-minute db_health_check loop pings the DB, flips db_available, and fires admin alerts on lost/recovered transitions.
  • Cog load failures are isolated per-extension and recorded in failed_cogs; other cogs still load.

Layering: cogs/ (thin Discord command handlers) → services/ (business logic) → database/ (data access). Keep logic in services.

Safety wrappers (src/utils/safety.py) carry the degraded-mode contract into command code:

  • @safe_command(requires_db=True) / @safe_slash_command(requires_db=True) — short-circuit with DatabaseUnavailableError when the DB is down, catch unexpected exceptions, log with context, and reply with an error embed.
  • safe_send(target, ...) — unified reply helper for both commands.Context and nextcord.Interaction. Prefer it over raw ctx.send / interaction.response.
  • @safe_background_task(name=...) — wrap @tasks.loop bodies; tracks consecutive failures and alerts admins after 3, with recovery notices.
  • Domain exceptions (DatabaseUnavailableError, ValidationError, ExternalRequestError) map to user-facing messages centrally in app.py's error handlers.

Embeds (src/utils/embeds.py) — all user-facing output uses veka_embed, success_embed, error_embed, info_embed, alert_embed. Pass contributor_source=__name__ from a cog so attribution resolves.


Project Structure

main.py                     → entrypoint (delegates to src.core.app:run_bot)
src/
├── core/
│   ├── app.py              → bot bootstrap, EXTENSIONS allowlist, event handlers
│   ├── runtime_state.py    → global runtime_state (health/observability)
│   └── checks.py           → StartupChecks run on on_ready
├── config/config.py        → all env vars, constants, RSS feeds, rate limits
├── database/
│   ├── database.py         → asyncpg pool, global `db` singleton, run_migrations()
│   └── migrations.py       → migration file discovery (schema_migrations table)
├── cogs/
│   ├── admin/              → basic.py, help.py, health.py
│   ├── networking/         → networking.py (profiles, connections)
│   ├── marketplace/        → marketplace.py, reviews.py
│   └── resources/feeds.py  → RSS (@tasks.loop every 15 min)
├── services/               → networking, mentorship, quiz, rss
└── utils/
    ├── safety.py           → safe_command / safe_send / safe_background_task
    ├── embeds.py           → VEKA embed builders
    ├── security/           → rbac.py, rate_limiter.py, audit.py, validation.py
    └── marketplace/        → fraud_detection.py
migrations/                 → SQL files, auto-applied on startup

Database

  • PostgreSQL via asyncpg. Access goes through the global singleton: from src.database.database import db. Use the $1, $2 (asyncpg) parameter style.
  • All db methods (fetch, fetch_one/fetchrow, fetchval, execute, execute_many) raise DatabaseUnavailableError on failure and flip runtime_state.db_available = False. Never assume a query succeeded.
  • Migrations are plain .sql files in migrations/, applied automatically on connect by db.run_migrations() and tracked in the schema_migrations table. Add a new one as migrations/00N_name.sql; it runs on next startup.

Key tables: users, profiles, connections, connection_requests, mentorships, quizzes, quiz_attempts, workshops, portfolios, resources, rss_cache, guild_config, marketplace_listings, marketplace_offers, marketplace_reviews, marketplace_transactions, and the security tables (audit_logs, user_security, security_events).


Security

Security utilities live in src/utils/security/ and are exported from the package:

from src.utils.security import (
    rate_limit, rate_limiter,            # rate limiting
    InputValidator, sanitize, is_safe, validate_id,   # input validation
    audit_log, audit_action,             # audit logging
    rbac, Role, require_mod, require_admin, require_verified,  # RBAC
)

Rate limiting@rate_limit('marketplace') on a command; named buckets (default, quiz, marketplace, mentorship, admin). Manual check via await rate_limiter.is_rate_limited(user_id, 'quiz').

Input validationsanitize(text, max_length=...), is_safe(text), InputValidator.validate_discord_id/validate_url/validate_marketplace_item(...).

Audit logging@audit_action('quiz_completed') decorator, or await audit_log.record(user_id=..., action=..., details={...}, severity=...). Persisted to audit_logs; user_security and security_events track warnings/blocks. Suspicious-activity detection: await audit_log.detect_suspicious_activity(user_id).

RBAC — hierarchy USER < VERIFIED < MODERATOR < ADMIN < OWNER, mapped from Discord role names (everyone, verified, mod/moderator, admin/administrator, and guild owner). Gate commands with @require_mod(), @require_admin(), @require_verified(), or check rbac.get_user_role(ctx).

Note: safety.py also exposes a simpler admin_only() check keyed off ADMIN_IDS/OWNER_IDS from config — two parallel auth mechanisms exist.

Checklist for new commands: apply rate limiting where resource-intensive, validate and length-limit all user input, add an RBAC gate if restricted, and audit-log security-relevant actions.


Configuration

Create a .env in the project root (see .env.example). Variables actually read by src/config/config.py:

Variable Required Default Notes
DISCORD_TOKEN Bot token
DATABASE_URL ✅* composed from POSTGRES_* Full PostgreSQL DSN
POSTGRES_HOST/PORT/DB/USER/PASSWORD ✅* localhost/5432/veka_bot/veka_bot_user/example Used to compose DATABASE_URL if it isn't set
ADMIN_IDS empty Comma-separated Discord user IDs
OWNER_IDS empty Comma-separated Discord user IDs
ADMIN_ALERT_CHANNEL_ID none Channel for operational alerts
MASSUNBAN_LOG_CHANNEL_ID none Dedicated channel for per-user mass-unban audit logs (separate from admin alert channel)
BOT_VERSION 1.0.0
LOG_LEVEL INFO
ENVIRONMENT development

* Provide either DATABASE_URL directly, or the individual POSTGRES_* vars. The command prefix is hardcoded to ! in config.py and is not read from the environment.


Setup

Local

git clone <repo> && cd discord-bot
cp .env.example .env          # then fill in DISCORD_TOKEN and DATABASE_URL

# Install all deps (runtime + dev) via uv
uv sync --extra dev

# Activate pre-commit hooks (once after clone)
pre-commit install

python main.py                # needs a reachable PostgreSQL

Docker

docker-compose.dev.yml is a fully self-contained local stack (bot + a bundled PostgreSQL). docker-compose.yml (production) runs only the bot and expects an external/managed PostgreSQL reachable via DATABASE_URL.

# Local: bot + bundled postgres
docker compose -f docker-compose.dev.yml up -d --build

# Production: bot only (set DATABASE_URL to your managed DB)
docker compose up -d --build

docker logs veka-discord-bot

The bot does not require Docker — python main.py runs it directly against any reachable PostgreSQL. Docker is simply how it is packaged and deployed.


Commands

Both slash commands and !-prefix commands are provided, often side by side for the same feature — keep them in sync when changing behavior.

Command Description
/profile [@user] / !profile View a professional profile
!setupprofile Set up your profile
/connect @user / !connect @user [message] Send a connection request
/leaderboard [stat] / !leaderboard View leaderboard (xp, messages, voice, streaming, gaming, listening, coded)
/level [@user] / !level View your or another user's level and XP
/most streamed|played|listened|coded Activity-specific leaderboards
/setupleaderboard (Admin) Configure the auto-updating leaderboard channel
/massunban (Admin) Bulk-unban users within a date range with double confirmation, resumable job tracking, and per-user audit logs
/massunban status <job_id> (Admin) Check status of a running or completed mass-unban job
/massunban cancel <job_id> (Admin) Cancel a pending or running mass-unban job
/massunban recent (Admin) List recent mass-unban jobs for this server
/help / !help [command] List commands / command detail

Development

Lint, Format, Typecheck

Run these before every push:

ruff check .                  # lint (no auto-fixes)
ruff check --fix . && ruff format .  # auto-fix then format
mypy src/ main.py --explicit-package-bases  # static type check
pre-commit run --all-files    # all checks (ruff + mypy + misc)

Pre-commit hooks run automatically on git commit. Config in .pre-commit-config.yaml. Install them once with pre-commit install.

All tool config lives in pyproject.toml ([tool.ruff], [tool.mypy]).

Guidelines

  • Every module logs via logging.getLogger('VEKA.<area>'), configured by setup_logging() in src/utils/logger.py.
  • To enable an unloaded cog, add its module path to EXTENSIONS in src/core/app.py. Each cog module needs a top-level setup(bot).
  • Startup health checks (src/core/checks.py) run from on_ready; results are stored in runtime_state.startup_check_results. Admin alerts and startup summaries go through src/services/admin_notifier.py (bot.notifier).
  • CLAUDE.md contains condensed architecture guidance for AI coding agents.

CI/CD

.github/workflows/deploy-discord-bot.yml deploys on push to main/production, but only when the commit message starts with Merge pull request (i.e. merged PRs). It runs on a self-hosted runner (self-hosted, X64, Linux, Veka) using Docker (docker compose up -d --build), writes a .env from GitHub secrets/variables, and gates success on the log line "is ready. DB available" appearing within 60s.

Configure these in the repository's GitHub settings:

  • Secrets (sensitive): DISCORD_TOKEN, DATABASE_URL
  • Variables (non-sensitive): ADMIN_IDS, OWNER_IDS, ADMIN_ALERT_CHANNEL_ID, LOG_LEVEL (optional)

BOT_VERSION is set automatically to the commit SHA and ENVIRONMENT to production.

Production uses an external/managed database — ensure the DATABASE_URL secret points at a reachable host (not @postgres:5432).


Known Issues

  1. CRITICAL — Activity tracking broken. FIXED. Listener renamed from on_member_update to on_presence_update. Activity tracking now fires on the correct Discord event.
  2. HIGH — Leaderboard auto-update requires setup. LEADERBOARD_CHANNEL_ID defaults to None. Must set the env var or run /setupleaderboard (admin-only).
  3. MEDIUM — joined_at column missing. FIXED. Migration 015_add_joined_at.sql adds the column. inactivity_service.py uses COALESCE(joined_at, created_at) as fallback.
  4. NOTE — /massunban banned_by filter only works for bans logged by the bot after its moderation audit system was in place. Historical bans placed before the bot was running cannot be filtered by moderator — Discord's audit log API is ephemeral and not stored retroactively.

License

MIT — see the LICENSE file.

About

First iteration of Discord bot made by amateur TwistedVortek from VEKA team. Contributions are greatly appreciated

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors