Skip to content

Repository files navigation

Bernice

Discord K-Pop photocard collection bot powered by FastAPI and PostgreSQL.

Bernice is a Discord Interactions bot for collecting gacha-style K-Pop idol photocards. A FastAPI webhook receives Discord interaction payloads, verifies Discord signatures, performs card and inventory work in PostgreSQL, then edits the deferred Discord response through the Discord webhook API.

About | Features | Built With | Getting Started | Usage | Deployment

About

Bernice gives a Discord server a lightweight photocard collection loop: users roll cards, build an inventory, page through their collection, and view individual cards by public code. The app is webhook-first, so it can run as a normal HTTP service without maintaining a long-lived Discord gateway connection.

The project includes a local Docker Compose setup, PostgreSQL schema and seed data, slash command registration, and a Render-oriented healthcheck workflow.

Features

  • Discord slash commands handled through Interaction webhooks.
  • /drop rolls a random photocard, increments the global print count, and adds the card to the user's inventory.
  • /inventory shows a paginated inventory with Discord button components for previous/next navigation.
  • /view code:<public_code> displays card metadata, image, global print count, and the requester's copy count.
  • /status returns a creator-only status embed with a button-driven status display.
  • PostgreSQL schema and seed data for users, artists, idols, card sets, cards, inventories, and rarity rates.
  • /health endpoint for uptime checks and Render warmup pings.

Built With

Layer Tools
App runtime Python, FastAPI, Uvicorn
Database PostgreSQL, asyncpg
Discord integration Discord Interactions webhooks, httpx, requests
Local development Docker Compose
Automation GitHub Actions, UptimeRobot

Architecture

Discord slash command
        |
        v
POST /interactions
        |
        v
Discord signature verification
        |
        v
FastAPI router dispatches command/component
        |
        v
Background handler calls app service
        |
        v
PostgresRepository performs database work
        |
        v
Discord webhook PATCH edits original deferred response

The FastAPI lifespan hook in src/server/main.py creates a shared PostgresRepository, initializes an asyncpg connection pool, loads rarity rates at startup, and closes the pool on shutdown.

Note

Bernice retries database startup up to 5 times. Discord webhook PATCH requests currently log rate-limit responses, but they do not retry failed or rate-limited Discord responses.

Project Structure

Path Purpose
src/server/ FastAPI app, routers, Discord signature verification, and Discord response handlers
src/app/ Domain models and service functions for drop, inventory, and card view workflows
src/infra/ PostgreSQL repository and Discord webhook client adapters
init/ Database schema and seed data used by the Postgres container
tests/ Current manual/unit test files
register_commands.py Registers Discord slash commands to the development guild
.github/workflows/ Scheduled Render healthcheck workflow

Getting Started

Prerequisites

  • Python 3.10+ recommended. The Docker image uses Python 3.12, and setup.py allows Python 3.8+.
  • Docker and Docker Compose for the easiest local database setup.
  • A Discord application with a bot token and public key.
  • An HTTPS tunnel such as ngrok for local Discord interaction testing.

Install Locally

From the repository root:

py -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
pip install -e .

pip install -e . makes the server, app, and infra packages importable from the src/ layout.

Run With Docker Compose

docker compose up --build

This starts:

Service URL/Port Notes
app http://localhost:8000 FastAPI app with /docs, /health, and /interactions
db localhost:5433 Postgres container mapped to internal port 5432

Useful local endpoints:

Endpoint Purpose
GET /docs FastAPI OpenAPI documentation
GET /health Healthcheck for local checks and Render warmup
HEAD /health Lightweight healthcheck variant
POST /interactions Discord Interactions webhook endpoint

Run the App Without Docker

Use this when you already have a configured PostgreSQL database and DATABASE_URL points to it:

python -m uvicorn server.main:app --reload --port 8000

Configure Discord for Local Webhooks

Start an HTTPS tunnel to the local FastAPI app:

ngrok http 8000

In the Discord Developer Portal, set the Interactions Endpoint URL to:

https://YOUR-TUNNEL-HOST/interactions

Then register slash commands:

python register_commands.py

register_commands.py currently registers guild-scoped commands for faster development updates.

Configuration

Create a .env file at the repository root.

Variable Used by Description
DATABASE_URL App, repository, tests PostgreSQL connection string read by PostgresRepository
DATABASE_URL_DOCKER Docker Compose Container-facing database URL assigned to DATABASE_URL inside the app container
POSTGRES_USER Postgres container Local database username
POSTGRES_PASSWORD Postgres container Local database password
POSTGRES_DB Postgres container Local database name
DISCORD_APPLICATION_ID App and command registration Discord application id
DISCORD_TOKEN register_commands.py Bot token used to register slash commands
DISCORD_PUBLIC_KEY App Public key used to verify Discord interaction signatures

Example local shape:

DATABASE_URL=postgresql://postgres:postgres@localhost:5433/bernice
DATABASE_URL_DOCKER=postgresql://postgres:postgres@db:5432/bernice
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=bernice
DISCORD_APPLICATION_ID=your_application_id
DISCORD_TOKEN=your_bot_token
DISCORD_PUBLIC_KEY=your_public_key

Usage

After the app is running and the Interactions Endpoint URL points to /interactions, users can interact with Bernice through Discord slash commands.

Command Behavior
/drop Rolls a random card, creates the user if needed, increments print count, and adds/increments inventory quantity
/inventory Shows up to 10 distinct card lines per page with previous/next buttons
/view code:<public_code> Shows a specific card by public code, including image and copy counts
/status Creator-only status embed with a status button

Database

init/1_init_tables.sql creates the schema, and init/2_init_data.sql seeds:

  • Rarity rates for rarities 1 through 5.
  • Artists including NewJeans, LE SSERAFIM, ITZY, aespa, and BIGBANG.
  • 23 idols.
  • A Regular card set.
  • 50 seeded card catalog rows with Cloudinary image URLs.

Note

The repository currently falls back to rarities 1 through 3 when a sampled rarity is above 3, because the catalog is not fully populated for higher rarities yet.

Testing

There is no configured CI test runner for the project yet, but the repository includes two test files:

python tests\test_postgres_repository.py
python -m unittest tests\test_discord_client.py

Current caveats:

  • tests/test_postgres_repository.py is a manual async smoke test and needs a real DATABASE_URL.
  • tests/test_discord_client.py appears stale relative to src/infra/discord/client.py; it imports retry helpers that are not present in the current implementation.

Deployment

The Dockerfile installs dependencies, installs the package in editable mode, exposes port 8000, and runs:

python -m uvicorn server.main:app --host 0.0.0.0 --port 8000

For Render-style deployment, use a port provided by the platform:

uvicorn server.main:app --host 0.0.0.0 --port $PORT

The workflow in .github/workflows/keep-render-warm.yml can ping a deployed healthcheck URL every five minutes when the RENDER_HEALTHCHECK_URL repository secret is configured.

Troubleshooting

ModuleNotFoundError: No module named 'server'

Install the package from the repository root:

pip install -e .

Then run Uvicorn with the package import path:

python -m uvicorn server.main:app --reload --port 8000

No DATABASE_URL in .env

PostgresRepository reads DATABASE_URL when the module imports. Add DATABASE_URL to .env or export it in the shell before starting the app.

Discord rejects the endpoint URL

Confirm that:

  • The URL is HTTPS and ends in /interactions.
  • DISCORD_PUBLIC_KEY is set correctly.
  • The local app is reachable through the tunnel.
  • The app can connect to PostgreSQL during startup.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages