Go Discord bot ("Vivy") providing multimodal AI chat (OpenAI), image generation/editing (Wavespeed), video creation, perceptual image hashing for duplicate detection, scheduled reminders, and a movie wheel betting game.
/usr/local/go/bin/go build -o voltgpt # build
./voltgpt # run (reads .env automatically)
/usr/local/go/bin/go vet ./... # static analysisSQLite database (voltgpt.db) is created automatically on first run with WAL mode enabled.
- Hook: PostToolUse runs
go build ./...after any.goedit; PreToolUse blocks edits to.env - Skill:
/go-check— runs vet + build and reports results - Subagent:
memory-reviewer— auto-triggered when modifyinginternal/memory/files - MCP:
sqliteserver connected tovoltgpt.dbfor direct DB queries during debugging
The backup/ directory holds legacy .gob snapshots from before the SQLite migration; safe to ignore.
Copy sample.env to .env and add missing tokens manually (sample.env is incomplete). Required:
DISCORD_TOKEN— bot authentication
Optional (features degrade gracefully without these):
OPENAI_TOKEN— OpenAI API (message handler chat)MEMORY_OPENAI_TOKEN— separate OpenAI key for the memory/embedding system (independent ofOPENAI_TOKEN)WAVESPEED_TOKEN— Wavespeed API (image generation, video creation)
main.go # Entry point, Discord session, handler registration
internal/
apis/
gemini/chat.go # Gemini streaming chat with tool use
openai/chat.go # OpenAI Responses API chat with stored response IDs
wavespeed/request.go # Wavespeed image/video generation
wavespeed/structs.go # Wavespeed API types
config/
commands.go # Discord slash command definitions
config.go # Constants, system prompts, admin IDs, types
db/db.go # SQLite init, schema, WAL config
discord/discord.go # Response helpers, error logging utilities
handler/
commands.go # Slash command handlers
components.go # Button/select menu handlers
messages.go # Message event handler (OpenAI chat)
modals.go # Modal submission handlers
gamble/gamble.go # Movie wheel game: rounds, bets, players
hasher/hasher.go # Perceptual image hashing, duplicate detection
memory/ # Vector-backed long-term memory (fact extraction, RAG via sqlite-vec)
consolidate.go # Deduplicates/merges similar facts via semantic similarity
extract.go # Extracts facts from Discord messages using OpenAI
memory.go # Init, embedding storage, sqlite-vec queries
retrieve.go # Retrieves relevant facts for RAG context injection
reminder/
reminder.go # Scheduled reminder management, SQLite-backed, in-memory timers
parse.go # Natural language reminder time parsing
utility/
discord.go # Discord message formatting, content extraction, admin
messages.go # Message splitting, sending, and retrieval
url.go # URL parsing, media type detection, downloading
image.go # Image processing, base64 encoding, PNG grids
video.go # FFmpeg video frame extraction
strings.go # Generic string helpers
init() in main.go loads .env, opens SQLite, then calls Init(db) on hasher, gamble, and memory packages, and reminder.Init(db.DB, dg) (also takes the Discord session). Each loads its data from SQLite into in-memory structures.
Handlers are registered as maps (handler.Commands, handler.Components, handler.Modals) mapping string keys to handler functions. All handlers are dispatched in goroutines from the main Discord event listener. Component and modal custom IDs are split on - to extract the handler key.
Data lives in memory (protected by sync.RWMutex) and is periodically written back to SQLite using INSERT OR REPLACE with JSON-serialized payloads. Tables: image_hashes, game_state, users, facts.
config.SystemMessage uses {TIME}, {CHANNEL}, and {BACKGROUND_FACTS} placeholders replaced at request time in gemini/chat.go. Always replace {BACKGROUND_FACTS} unconditionally (empty string when no facts) — never leave the placeholder literal in the prompt.
vec_factsvirtual table usesdistance_metric=cosine;distanceThreshold=0.35andretrievalDistanceThreshold=0.6are both cosine distance valuesconfig.MemoryBlacklistandconfig.MainServergate which channels/guilds get memory extraction- Facts are extracted in a 30s sliding buffer per user, then consolidated via Gemini before insert
sqlite-vecvec0 tables support full-table scans but ANN queries require theMATCH+k=syntax
config.RequestContent carries text, image URLs, video URLs, PDF URLs, and YouTube URLs through the processing pipeline. The utility package handles downloading, resizing, and format conversion (relies on FFmpeg for video).
go mod tidydrops unreferenced deps — a test-onlygo getwon't survive tidy until at least one_test.gofile imports it; add the import first, then tidy- Testing URL-downloading functions — use
httptest.NewServerserving in-memory bytes; suffix the URL path with the right extension (e.g.,/test.png) soURLToExtroutes correctly; no real network needed - Testing video functions —
getVideoDurationandextractVideoFrameAtTimetake file paths directly; generateinternal/utility/testdata/test.mp4viaffmpeg -f lavfi -i color=c=blue:size=64x64:rate=5 -t 1 -pix_fmt yuv420p -y testdata/test.mp4 - Testing Discord message functions —
*discordgo.Messagestructs can be constructed directly for tests that don't call the Discord API; onlyCleanMessage(readss.State.User.ID) needs a mock session - Testing
GetMessageMediaURLwith attachments — requiresWidth > 0 && Height > 0on each*discordgo.MessageAttachment; zero-value structs are silently skipped, returning no URLs - Testing
formatFactsXMLoutput — the<note>element contains literal<user>and<general>text; use</general>and<user name=as check strings to avoid false-positive substring matches - Testing hasher package —
hashStore.mis global; reset withhashStore.m = make(map[string]*discordgo.Message)between tests;writeHash(and any code path withStore: true) panics ifdatabaseis nil, so those tests need an in-memory DB viadb.Open(":memory:") - Testing memory package — white-box tests (
package memory) can setdatabase = db.DBdirectly afterdb.Open(":memory:"); OpenAI-backed tests skip cleanly whenMEMORY_OPENAI_TOKENis unset; load token for local runs viaexport $(grep MEMORY_OPENAI_TOKEN .env | xargs) - Run tests:
/usr/local/go/bin/go test ./... -timeout 60s(video tests need the timeout; they use ffmpeg)
- Package organization: one package per feature domain under
internal/ - No ORM: raw
database/sqlwithgo-sqlite3driver - Error handling:
log.Fatalfor startup failures; non-fatal errors logged and surfaced to Discord users viadiscord.ErrorResponse() - Concurrency:
sync.RWMutexon shared maps,sync.WaitGroupfor parallel operations - Naming: exported
CamelCase, unexportedcamelCase, struct fields tagged withjson:for serialization - Commands: defined as
[]*discordgo.ApplicationCommandinconfig/commands.go, auto-registered per guild on startup, stale commands auto-deleted
| Package | Purpose |
|---|---|
bwmarrin/discordgo |
Discord API |
google.golang.org/genai |
Google Gemini API |
mattn/go-sqlite3 |
SQLite driver |
asg017/sqlite-vec-go-bindings |
Vector search extension for SQLite (used by memory package) |
u2takey/ffmpeg-go |
FFmpeg media processing |
corona10/goimagehash |
Perceptual image hashing |
joho/godotenv |
.env file loading |
ewohltman/discordgo-mock |
Mock Discord sessions for unit tests (import as .../mocksession, .../mockstate, .../mockuser — no /pkg/ segment) |