35 Commits

Author SHA1 Message Date
6955421fd7 Markov updates 2025-05-27 00:09:48 -06:00
b306143db2 ???? 2025-05-26 21:13:30 -06:00
0a730be6f7 many update 2025-05-26 20:35:50 -06:00
3af77b6bea no 2024-11-22 17:34:41 -06:00
025c829c68 no 2024-11-22 17:24:49 -06:00
a3e406fb2f no 2024-11-22 17:24:17 -06:00
0a07bc3dbc no 2024-11-22 17:23:49 -06:00
d7edcad552 no 2024-11-22 17:19:49 -06:00
33291b478f Wat 2024-11-22 17:14:58 -06:00
4cf9c3295f HIMBOT RETURNS 2024-11-22 17:04:33 -06:00
ba9bf88a1c Whoopsie daisy 2024-11-22 16:53:59 -06:00
1403a756f7 Dockerify 2024-11-22 16:52:23 -06:00
7249e08f84 Pls 2024-11-22 01:14:04 -06:00
b0e9372cd9 updated workflow 2024-11-20 14:53:45 -06:00
041914c405 Fly changes 2024-11-20 14:44:43 -06:00
be4f67218d Added sending himbucks 2024-11-07 02:56:56 -06:00
1c46b28752 Deps 2024-11-07 02:40:08 -06:00
997d2cb911 Deps 2024-11-07 02:38:38 -06:00
5a1158048d Spending lots of time on useless discord bots makes to make the pain go away 2024-11-05 18:01:58 -06:00
82c62658bb Added a purge for old commands and re-adding every start 2024-11-05 16:42:44 -06:00
69b76bd792 Fixed again 2024-11-05 16:37:43 -06:00
c3e62155e2 Fix 2024-11-05 16:36:50 -06:00
ce580e77fd Fix 2024-11-05 13:03:06 -06:00
46c24134d5 Better syncing 2024-11-05 12:49:10 -06:00
0194c3d4fe Sadge 2024-11-05 11:05:11 -06:00
d1e2d9bf4f Using transactions 2024-11-05 00:42:25 -06:00
fc8be94ee9 Fixed himbucks 2024-11-05 00:39:35 -06:00
e14051569c Fixed himbucks 2024-11-05 00:39:09 -06:00
022686e14f Fixed himbucks 2024-11-05 00:28:33 -06:00
9a1b3723bc Added himbucks 2024-11-05 00:19:36 -06:00
6a55017624 Embedded Replicas 2024-11-04 11:41:29 -06:00
359520fe83 Turso 2024-11-04 11:14:43 -06:00
b805f27d0e turso 2024-11-04 01:23:57 -06:00
af3f3ab355 Deps cleanup 2024-10-27 22:31:22 -06:00
2e05f946c5 Merge pull request #5 from atridadl/atridadl-patch-2
Update README.md
2024-10-22 21:55:41 -06:00
25 changed files with 2524 additions and 164 deletions

View File

@ -1,2 +1,31 @@
# Tokens
# Discord Configuration
DISCORD_TOKEN=""
# Container configuration
IMAGE=""
ROOT_DIR=""
# Himbucks System Configuration
HIMBUCKS_PER_REWARD=10
MESSAGE_COUNT_THRESHOLD=5
HIMBUCKS_COOLDOWN_MINUTES=1
# Markov Chain Configuration
MARKOV_DEFAULT_MESSAGES=500
MARKOV_MAX_MESSAGES=1000
MARKOV_CACHE_SIZE=50
MARKOV_MAX_NGRAM=5
MARKOV_MEMORY_LIMIT_MB=100
# Database Configuration
DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=5
DB_CONN_MAX_LIFETIME_MINUTES=5
# Command Cooldowns (in seconds)
PING_COOLDOWN_SECONDS=5
HS_COOLDOWN_SECONDS=10
MARKOV_COOLDOWN_SECONDS=30
HIMBUCKS_COOLDOWN_SECONDS=5
HIMBOARD_COOLDOWN_SECONDS=5
SENDBUCKS_COOLDOWN_SECONDS=1800

35
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Docker Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.REPO_HOST }}
username: ${{ github.repository_owner }}
password: ${{ secrets.DEPLOY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64
push: true
tags: |
${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.sha }}
${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest

View File

@ -1,18 +0,0 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

6
.gitignore vendored
View File

@ -5,4 +5,8 @@
.env.test.local
.env.production.local
.env
himbot
himbot
*.db
*.db-client_wal_index
*.db-shm
*.db-wal

View File

@ -1,4 +1,5 @@
FROM golang:1.23.2-alpine AS build
# Build stage
FROM golang:1.24.3 AS build
WORKDIR /app
@ -8,13 +9,24 @@ RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /go/bin/app
RUN go build -ldflags="-s -w" -o /go/bin/app
FROM gcr.io/distroless/static-debian11
# Final stage
FROM ubuntu:22.04
COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Install SSL certificates and required runtime libraries
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /go/bin/app /app
# Create app directory
WORKDIR /app
# Copy the binary
COPY --from=build /go/bin/app /app/himbot
# Copy migrations directory
COPY --from=build /app/migrations /app/migrations
# Set the entrypoint
ENTRYPOINT ["/app"]
ENTRYPOINT ["/app/himbot"]

89
command/himbucks.go Normal file
View File

@ -0,0 +1,89 @@
package command
import (
"fmt"
"himbot/lib"
"strings"
"github.com/bwmarrin/discordgo"
)
func BalanceGetCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
user, err := lib.GetUser(i)
if err != nil {
return "", err
}
balance, err := lib.GetBalance(user.ID, i.GuildID)
if err != nil {
return "", err
}
return fmt.Sprintf("💸 You have %d Himbucks! 💸", balance), nil
}
func LeaderboardCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
entries, err := lib.GetLeaderboard(i.GuildID, 10)
if err != nil {
return "", err
}
if len(entries) == 0 {
return "No himbucks earned yet!", nil
}
var sb strings.Builder
sb.WriteString("🏆 Himbucks Leaderboard 🏆\n\n")
for idx, entry := range entries {
sb.WriteString(fmt.Sprintf("%d. %s: %d himbucks\n", idx+1, entry.Username, entry.Balance))
}
return sb.String(), nil
}
func BalanceSendCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
options := i.ApplicationCommandData().Options
// Discord handles the user mention/tag and provides the correct user ID
var recipientID string
var amount int
for _, opt := range options {
switch opt.Name {
case "user":
recipientID = opt.UserValue(nil).ID
case "amount":
amount = int(opt.IntValue())
}
}
// Validate amount
if amount <= 0 {
return "", fmt.Errorf("amount must be positive")
}
// Get sender's info
sender, err := lib.GetUser(i)
if err != nil {
return "", fmt.Errorf("failed to get sender info: %w", err)
}
// Don't allow sending to self
if sender.ID == recipientID {
return "", fmt.Errorf("you cannot send himbucks to yourself")
}
// Get recipient's info
recipient, err := s.User(recipientID)
if err != nil {
return "", fmt.Errorf("failed to get recipient info: %w", err)
}
// Send the himbucks
err = lib.SendBalance(sender.ID, recipientID, i.GuildID, amount)
if err != nil {
return "", fmt.Errorf("failed to send himbucks: %w", err)
}
return fmt.Sprintf("💸 Successfully sent %d Himbucks to %s! 💸", amount, recipient.Username), nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: your-app-image:latest
command: ["/app"]
pull_policy: build
environment:
- DISCORD_TOKEN=$DISCORD_TOKEN
- COOLDOWN_ALLOW_LIST=$COOLDOWN_ALLOW_LIST

35
docker-compose.yml Normal file
View File

@ -0,0 +1,35 @@
services:
server:
image: ${IMAGE}
ports:
- "3117:3000"
environment:
# Discord Configuration
- DISCORD_TOKEN=${DISCORD_TOKEN}
# Himbucks System Configuration
- HIMBUCKS_PER_REWARD=${HIMBUCKS_PER_REWARD:-10}
- MESSAGE_COUNT_THRESHOLD=${MESSAGE_COUNT_THRESHOLD:-5}
- HIMBUCKS_COOLDOWN_MINUTES=${HIMBUCKS_COOLDOWN_MINUTES:-1}
# Markov Chain Configuration
- MARKOV_DEFAULT_MESSAGES=${MARKOV_DEFAULT_MESSAGES:-100}
- MARKOV_MAX_MESSAGES=${MARKOV_MAX_MESSAGES:-1000}
- MARKOV_CACHE_SIZE=${MARKOV_CACHE_SIZE:-10}
- MARKOV_MAX_NGRAM=${MARKOV_MAX_NGRAM:-5}
- MARKOV_MEMORY_LIMIT_MB=${MARKOV_MEMORY_LIMIT_MB:-100}
# Database Configuration
- DB_MAX_OPEN_CONNS=${DB_MAX_OPEN_CONNS:-25}
- DB_MAX_IDLE_CONNS=${DB_MAX_IDLE_CONNS:-5}
- DB_CONN_MAX_LIFETIME_MINUTES=${DB_CONN_MAX_LIFETIME_MINUTES:-5}
# Command Cooldowns (in seconds)
- PING_COOLDOWN_SECONDS=${PING_COOLDOWN_SECONDS:-5}
- HS_COOLDOWN_SECONDS=${HS_COOLDOWN_SECONDS:-10}
- MARKOV_COOLDOWN_SECONDS=${MARKOV_COOLDOWN_SECONDS:-30}
- HIMBUCKS_COOLDOWN_SECONDS=${HIMBUCKS_COOLDOWN_SECONDS:-5}
- HIMBOARD_COOLDOWN_SECONDS=${HIMBOARD_COOLDOWN_SECONDS:-5}
- SENDBUCKS_COOLDOWN_SECONDS=${SENDBUCKS_COOLDOWN_SECONDS:-1800}
volumes:
- ${ROOT_DIR}:/data

View File

@ -1,4 +1,4 @@
# fly.toml app configuration file generated for himbot on 2024-10-20T22:41:20-06:00
# fly.toml app configuration file generated for himbot on 2024-11-20T14:35:09-06:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
@ -8,10 +8,23 @@ primary_region = 'ord'
[build]
[[services]]
[[mounts]]
source = 'himbot_data'
destination = '/data'
[http_service]
internal_port = 3000
protocol = "tcp"
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 1
processes = ['app']
[[services]]
protocol = 'tcp'
internal_port = 3000
min_machines_running = 1
ports = []
[[vm]]
size = 'shared-cpu-1x'

15
go.mod
View File

@ -1,17 +1,18 @@
module himbot
go 1.23
require github.com/diamondburned/arikawa/v3 v3.4.0
go 1.24
require (
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/sys v0.33.0 // indirect
)
require (
github.com/bwmarrin/discordgo v0.28.1
github.com/gorilla/schema v1.4.1 // indirect
github.com/bwmarrin/discordgo v0.29.0
github.com/gorilla/websocket v1.5.3 // indirect
github.com/joho/godotenv v1.5.1
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e
)

32
go.sum
View File

@ -1,21 +1,33 @@
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/diamondburned/arikawa/v3 v3.4.0 h1:wI3Qv8h2E2dkeddF1I35nv4T6OQ3RtA21rbghW/fnd0=
github.com/diamondburned/arikawa/v3 v3.4.0/go.mod h1:WVkbdenUfsCCkptIlqSglF4eo2/HSXv74eCqGnOZaYY=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c h1:WsJ6G+hkDXIMfQE8FIxnnziT26WmsRgZhdWQ0IQGlcc=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c/go.mod h1:gIcFddvsvPcRCO6QDmWH9/zcFd5U26QWWRMgZh4ddyo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e h1:DUEcD8ukLWxIlcRWWJSuAX6IbEQln2bc7t9HOT45FFk=
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

View File

@ -14,30 +14,46 @@ func HandleCommand(commandName string, cooldownDuration time.Duration, handler C
return
}
// Get user information (handle both guild and DM contexts)
user, userErr := GetUser(i)
if userErr != nil {
RespondWithError(s, i, "Error getting user information: "+userErr.Error())
return
}
// Get or create user and guild profile
_, createUserError := GetOrCreateUserWithGuild(user.ID, user.Username, i.GuildID)
if createUserError != nil {
RespondWithError(s, i, "Error creating user profile: "+createUserError.Error())
return
}
// Acknowledge the interaction immediately
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
interactErr := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
if err != nil {
ThrowWithError(commandName, "Error deferring response: "+err.Error())
if interactErr != nil {
ThrowWithError(commandName, "Error deferring response: "+interactErr.Error())
return
}
// Execute the command handler
response, err := handler(s, i)
response, handlerErr := handler(s, i)
if err != nil {
RespondWithError(s, i, "Error processing command: "+err.Error())
if handlerErr != nil {
RespondWithError(s, i, "Error processing command: "+handlerErr.Error())
return
}
// Send the follow-up message with the response
_, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
_, followErr := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: response,
})
if err != nil {
ThrowWithError(commandName, "Error sending follow-up message: "+err.Error())
if followErr != nil {
ThrowWithError(commandName, "Error sending follow-up message: "+followErr.Error())
}
}
}

96
lib/config.go Normal file
View File

@ -0,0 +1,96 @@
package lib
import (
"os"
"strconv"
"time"
)
// Config holds all configuration values
type Config struct {
// Discord settings
DiscordToken string
// Himbucks settings
HimbucksPerReward int
MessageCountThreshold int
CooldownPeriod time.Duration
// Markov settings
MarkovDefaultMessages int
MarkovMaxMessages int
MarkovCacheSize int
MarkovMaxNGram int // Maximum n-gram level (3, 4, 5, etc.)
MarkovMemoryLimit int // Memory limit in MB for n-gram chains
// Database settings
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
// Command cooldowns (in seconds)
PingCooldown int
HsCooldown int
MarkovCooldown int
MarkovAskCooldown int
HimbucksCooldown int
HimboardCooldown int
SendbucksCooldown int
}
var AppConfig *Config
// LoadConfig loads configuration from environment variables
func LoadConfig() *Config {
config := &Config{
// Discord settings
DiscordToken: getEnv("DISCORD_TOKEN", ""),
// Himbucks settings
HimbucksPerReward: getEnvInt("HIMBUCKS_PER_REWARD", 10),
MessageCountThreshold: getEnvInt("MESSAGE_COUNT_THRESHOLD", 5),
CooldownPeriod: time.Duration(getEnvInt("HIMBUCKS_COOLDOWN_MINUTES", 1)) * time.Minute,
// Markov settings
MarkovDefaultMessages: getEnvInt("MARKOV_DEFAULT_MESSAGES", 100),
MarkovMaxMessages: getEnvInt("MARKOV_MAX_MESSAGES", 1000),
MarkovCacheSize: getEnvInt("MARKOV_CACHE_SIZE", 10),
MarkovMaxNGram: getEnvInt("MARKOV_MAX_NGRAM", 5),
MarkovMemoryLimit: getEnvInt("MARKOV_MEMORY_LIMIT_MB", 100),
// Database settings
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
ConnMaxLifetime: time.Duration(getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 5)) * time.Minute,
// Command cooldowns (in seconds)
PingCooldown: getEnvInt("PING_COOLDOWN_SECONDS", 5),
HsCooldown: getEnvInt("HS_COOLDOWN_SECONDS", 10),
MarkovCooldown: getEnvInt("MARKOV_COOLDOWN_SECONDS", 30),
MarkovAskCooldown: getEnvInt("MARKOV_ASK_COOLDOWN_SECONDS", 30),
HimbucksCooldown: getEnvInt("HIMBUCKS_COOLDOWN_SECONDS", 5),
HimboardCooldown: getEnvInt("HIMBOARD_COOLDOWN_SECONDS", 5),
SendbucksCooldown: getEnvInt("SENDBUCKS_COOLDOWN_SECONDS", 1800),
}
AppConfig = config
return config
}
// getEnv gets an environment variable with a default value
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// getEnvInt gets an environment variable as an integer with a default value
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}

224
lib/db.go Normal file
View File

@ -0,0 +1,224 @@
package lib
import (
"database/sql"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/tursodatabase/go-libsql"
)
var DBClient *sql.DB
var DBConnector *libsql.Connector
// Prepared statements
var (
stmtGetBalance *sql.Stmt
stmtUpdateBalance *sql.Stmt
stmtGetLeaderboard *sql.Stmt
stmtGetUserProfile *sql.Stmt
stmtUpdateProfile *sql.Stmt
)
func InitDB() error {
// Determine DB path based on /data directory existence
var dbPath string
if _, err := os.Stat("/data"); os.IsNotExist(err) {
dbPath = "file:./himbot.db"
} else {
dbPath = "file:/data/himbot.db"
}
db, err := sql.Open("libsql", dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open db %s: %v", dbPath, err)
os.Exit(1)
}
// Configure connection pool using config values
db.SetMaxOpenConns(AppConfig.MaxOpenConns)
db.SetMaxIdleConns(AppConfig.MaxIdleConns)
db.SetConnMaxLifetime(AppConfig.ConnMaxLifetime)
// Test the connection
if err := db.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
DBClient = db
if err := runMigrations(); err != nil {
return err
}
// Prepare frequently used statements
return prepareStatements()
}
type Migration struct {
Version int
Up string
Down string
}
func loadMigrations() ([]Migration, error) {
var migrations []Migration
migrationFiles, err := filepath.Glob("migrations/*.up.sql")
if err != nil {
return nil, fmt.Errorf("failed to read migration files: %w", err)
}
for _, upFile := range migrationFiles {
// Extract version from filename (000001_create_users_table.up.sql -> 1)
baseName := filepath.Base(upFile)
version := 0
fmt.Sscanf(baseName, "%d_", &version)
downFile := strings.Replace(upFile, ".up.sql", ".down.sql", 1)
upSQL, err := ioutil.ReadFile(upFile)
if err != nil {
return nil, fmt.Errorf("failed to read migration file %s: %w", upFile, err)
}
downSQL, err := ioutil.ReadFile(downFile)
if err != nil {
return nil, fmt.Errorf("failed to read migration file %s: %w", downFile, err)
}
migrations = append(migrations, Migration{
Version: version,
Up: string(upSQL),
Down: string(downSQL),
})
}
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version < migrations[j].Version
})
return migrations, nil
}
func runMigrations() error {
// Create migrations table if it doesn't exist
_, err := DBClient.Exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return fmt.Errorf("failed to create schema_migrations table: %w", err)
}
migrations, err := loadMigrations()
if err != nil {
return err
}
for _, migration := range migrations {
var exists bool
err := DBClient.QueryRow(
"SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = ?)",
migration.Version).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check migration status: %w", err)
}
if !exists {
tx, err := DBClient.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
// Run migration
_, err = tx.Exec(migration.Up)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to apply migration %d: %w", migration.Version, err)
}
// Record migration
_, err = tx.Exec(
"INSERT INTO schema_migrations (version) VALUES (?)",
migration.Version)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to record migration %d: %w", migration.Version, err)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("failed to commit migration %d: %w", migration.Version, err)
}
log.Printf("Applied migration %d", migration.Version)
}
}
log.Println("Database migrations completed successfully")
return nil
}
func prepareStatements() error {
var err error
// Prepare balance query
stmtGetBalance, err = DBClient.Prepare(`
SELECT gp.currency_balance
FROM guild_profiles gp
JOIN users u ON gp.user_id = u.id
WHERE u.discord_id = ? AND gp.guild_id = ?`)
if err != nil {
return fmt.Errorf("failed to prepare balance query: %w", err)
}
// Prepare leaderboard query
stmtGetLeaderboard, err = DBClient.Prepare(`
SELECT u.username, gp.currency_balance, gp.message_count
FROM guild_profiles gp
JOIN users u ON gp.user_id = u.id
WHERE gp.guild_id = ?
ORDER BY gp.currency_balance DESC
LIMIT ?`)
if err != nil {
return fmt.Errorf("failed to prepare leaderboard query: %w", err)
}
// Prepare user profile query
stmtGetUserProfile, err = DBClient.Prepare(`
SELECT message_count, last_reward_at
FROM guild_profiles
WHERE user_id = ? AND guild_id = ?`)
if err != nil {
return fmt.Errorf("failed to prepare user profile query: %w", err)
}
log.Println("Prepared statements initialized successfully")
return nil
}
// CleanupDB closes all prepared statements
func CleanupDB() {
if stmtGetBalance != nil {
stmtGetBalance.Close()
}
if stmtUpdateBalance != nil {
stmtUpdateBalance.Close()
}
if stmtGetLeaderboard != nil {
stmtGetLeaderboard.Close()
}
if stmtGetUserProfile != nil {
stmtGetUserProfile.Close()
}
if stmtUpdateProfile != nil {
stmtUpdateProfile.Close()
}
log.Println("Database cleanup completed")
}

181
lib/himbucks.go Normal file
View File

@ -0,0 +1,181 @@
package lib
import (
"database/sql"
"fmt"
"time"
"github.com/bwmarrin/discordgo"
)
type HimbucksEntry struct {
Username string
Balance int
MessageCount int
}
func ProcessHimbucks(s *discordgo.Session, m *discordgo.MessageCreate, ctx *ProcessContext) error {
tx, err := DBClient.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer tx.Rollback()
// Get current state
var messageCount int
var lastRewardAt sql.NullTime
err = tx.QueryRow(`
SELECT message_count, last_reward_at
FROM guild_profiles
WHERE user_id = ? AND guild_id = ?`,
ctx.UserID, ctx.GuildID).Scan(&messageCount, &lastRewardAt)
if err != nil {
return fmt.Errorf("failed to get message count: %w", err)
}
messageCount++
shouldReward := messageCount >= AppConfig.MessageCountThreshold &&
(!lastRewardAt.Valid || time.Since(lastRewardAt.Time) >= AppConfig.CooldownPeriod)
if shouldReward {
_, err = tx.Exec(`
UPDATE guild_profiles
SET currency_balance = currency_balance + ?,
message_count = 0,
last_reward_at = CURRENT_TIMESTAMP
WHERE user_id = ? AND guild_id = ?`,
AppConfig.HimbucksPerReward, ctx.UserID, ctx.GuildID)
} else {
_, err = tx.Exec(`
UPDATE guild_profiles
SET message_count = ?
WHERE user_id = ? AND guild_id = ?`,
messageCount, ctx.UserID, ctx.GuildID)
}
if err != nil {
return fmt.Errorf("failed to update profile: %w", err)
}
return tx.Commit()
}
func GetBalance(discordID, guildID string) (int, error) {
var balance int
err := stmtGetBalance.QueryRow(discordID, guildID).Scan(&balance)
if err == sql.ErrNoRows {
return 0, nil
}
if err != nil {
return 0, fmt.Errorf("failed to get balance: %w", err)
}
return balance, nil
}
func SendBalance(fromDiscordID, toDiscordID, guildID string, amount int) error {
// Start database transaction
tx, err := DBClient.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer tx.Rollback()
// Get sender's user ID
var fromUserID string
err = tx.QueryRow(`
SELECT id
FROM users
WHERE discord_id = ?`, fromDiscordID).Scan(&fromUserID)
if err != nil {
return fmt.Errorf("sender not found: %w", err)
}
// Get recipient's user ID
var toUserID string
err = tx.QueryRow(`
SELECT id
FROM users
WHERE discord_id = ?`, toDiscordID).Scan(&toUserID)
if err != nil {
return fmt.Errorf("recipient not found: %w", err)
}
// Check if sender has sufficient balance
var senderBalance int
err = tx.QueryRow(`
SELECT currency_balance
FROM guild_profiles
WHERE user_id = ? AND guild_id = ?`,
fromUserID, guildID).Scan(&senderBalance)
if err != nil {
return fmt.Errorf("failed to get sender balance: %w", err)
}
if senderBalance < amount {
return fmt.Errorf("insufficient balance: have %d, trying to send %d", senderBalance, amount)
}
// Deduct from sender
_, err = tx.Exec(`
UPDATE guild_profiles
SET currency_balance = currency_balance - ?
WHERE user_id = ? AND guild_id = ?`,
amount, fromUserID, guildID)
if err != nil {
return fmt.Errorf("failed to deduct from sender: %w", err)
}
// Add to recipient
result, err := tx.Exec(`
UPDATE guild_profiles
SET currency_balance = currency_balance + ?
WHERE user_id = ? AND guild_id = ?`,
amount, toUserID, guildID)
if err != nil {
return fmt.Errorf("failed to add to recipient: %w", err)
}
// Check if recipient exists in guild_profiles
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to check rows affected: %w", err)
}
// If recipient doesn't have a profile in this guild, create one
if rowsAffected == 0 {
_, err = tx.Exec(`
INSERT INTO guild_profiles (user_id, guild_id, currency_balance, message_count)
VALUES (?, ?, ?, 0)`,
toUserID, guildID, amount)
if err != nil {
return fmt.Errorf("failed to create recipient profile: %w", err)
}
}
// Commit transaction
if err = tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func GetLeaderboard(guildID string, limit int) ([]HimbucksEntry, error) {
rows, err := stmtGetLeaderboard.Query(guildID, limit)
if err != nil {
return nil, fmt.Errorf("failed to get leaderboard: %w", err)
}
defer rows.Close()
var entries []HimbucksEntry
for rows.Next() {
var entry HimbucksEntry
err := rows.Scan(&entry.Username, &entry.Balance, &entry.MessageCount)
if err != nil {
return nil, fmt.Errorf("failed to scan leaderboard entry: %w", err)
}
entries = append(entries, entry)
}
return entries, nil
}

63
lib/processor.go Normal file
View File

@ -0,0 +1,63 @@
package lib
import (
"fmt"
"github.com/bwmarrin/discordgo"
)
// Represents a function that processes a message
type MessageProcessor func(s *discordgo.Session, m *discordgo.MessageCreate, ctx *ProcessContext) error
// Holds shared data between processors
type ProcessContext struct {
UserID int
GuildID string
DiscordUser *discordgo.User
}
// Handles the registration and execution of message processors
type MessageProcessorManager struct {
processors []MessageProcessor
}
// Creates a new MessageProcessorManager
func NewMessageProcessorManager() *MessageProcessorManager {
return &MessageProcessorManager{
processors: make([]MessageProcessor, 0),
}
}
// Adds a new message processor to the manager
func (pm *MessageProcessorManager) RegisterProcessor(processor MessageProcessor) {
pm.processors = append(pm.processors, processor)
}
// Runs all registered processors on a message
func (pm *MessageProcessorManager) ProcessMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.Author.Bot {
return
}
// Create processing context
ctx := &ProcessContext{
GuildID: m.GuildID,
DiscordUser: m.Author,
}
// Get or create user and guild profile together
userID, err := GetOrCreateUserWithGuild(m.Author.ID, m.Author.Username, m.GuildID)
if err != nil {
ThrowWithError("MessageProcessor", fmt.Sprintf("failed to handle user and guild profile: %v", err))
return
}
ctx.UserID = userID
// Run each processor with the context
for _, processor := range pm.processors {
if err := processor(s, m, ctx); err != nil {
ThrowWithError("MessageProcessor", err.Error())
}
}
}

43
lib/users.go Normal file
View File

@ -0,0 +1,43 @@
package lib
import "fmt"
func GetOrCreateUserWithGuild(discordID string, username string, guildID string) (int, error) {
var userID int
tx, err := DBClient.Begin()
if err != nil {
return 0, fmt.Errorf("failed to start transaction: %w", err)
}
defer tx.Rollback()
// First get or create the user
err = tx.QueryRow(`
INSERT INTO users (discord_id, username)
VALUES (?, ?)
ON CONFLICT (discord_id)
DO UPDATE SET username = excluded.username
RETURNING id`,
discordID, username).Scan(&userID)
if err != nil {
return 0, fmt.Errorf("failed to get or create user: %w", err)
}
// Then ensure guild profile exists for this user
_, err = tx.Exec(`
INSERT INTO guild_profiles (user_id, guild_id, currency_balance, message_count)
VALUES (?, ?, 0, 0)
ON CONFLICT (user_id, guild_id) DO NOTHING`,
userID, guildID)
if err != nil {
return 0, fmt.Errorf("failed to create guild profile: %w", err)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("failed to commit transaction: %w", err)
}
return userID, nil
}

279
main.go
View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"himbot/command"
"himbot/lib"
"log"
@ -14,6 +15,164 @@ import (
)
var (
commands []*discordgo.ApplicationCommand
commandHandlers map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate)
)
func main() {
godotenv.Load(".env")
// Load configuration
config := lib.LoadConfig()
// Initialize commands and handlers with config
initCommands(config)
initCommandHandlers(config)
err := lib.InitDB()
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
if config.DiscordToken == "" {
log.Fatalln("No $DISCORD_TOKEN given.")
}
dg, err := discordgo.New("Bot " + config.DiscordToken)
if err != nil {
log.Fatalf("Error creating Discord session: %v", err)
}
dg.AddHandler(ready)
dg.AddHandler(interactionCreate)
processorManager := lib.NewMessageProcessorManager()
// Register processors
processorManager.RegisterProcessor(lib.ProcessHimbucks)
dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
processorManager.ProcessMessage(s, m)
})
dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages
err = dg.Open()
if err != nil {
log.Fatalf("Error opening connection: %v", err)
}
log.Println("Bot is now running. Press CTRL-C to exit.")
registerCommands(dg)
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
log.Println("Shutting down gracefully...")
if lib.DBClient != nil {
// Close prepared statements
lib.CleanupDB()
lib.DBClient.Close()
}
dg.Close()
}
func ready(s *discordgo.Session, event *discordgo.Ready) {
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
}
func interactionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
}
func registerCommands(s *discordgo.Session) {
log.Println("Checking command registration...")
existingCommands, err := s.ApplicationCommands(s.State.User.ID, "")
if err != nil {
log.Printf("Error fetching existing commands: %v", err)
return
}
// Create maps for easier comparison
existingMap := make(map[string]*discordgo.ApplicationCommand)
for _, cmd := range existingCommands {
existingMap[cmd.Name] = cmd
}
desiredMap := make(map[string]*discordgo.ApplicationCommand)
for _, cmd := range commands {
desiredMap[cmd.Name] = cmd
}
// Delete commands that no longer exist
for name, existingCmd := range existingMap {
if _, exists := desiredMap[name]; !exists {
log.Printf("Deleting removed command: %s", name)
err := s.ApplicationCommandDelete(s.State.User.ID, "", existingCmd.ID)
if err != nil {
log.Printf("Error deleting command %s: %v", name, err)
}
}
}
// Update or create commands
for _, desiredCmd := range commands {
if existingCmd, exists := existingMap[desiredCmd.Name]; exists {
// Check if command needs updating (simple comparison)
if !commandsEqual(existingCmd, desiredCmd) {
log.Printf("Updating command: %s", desiredCmd.Name)
_, err := s.ApplicationCommandEdit(s.State.User.ID, "", existingCmd.ID, desiredCmd)
if err != nil {
log.Printf("Error updating command %s: %v", desiredCmd.Name, err)
}
} else {
log.Printf("Command %s is up to date", desiredCmd.Name)
}
} else {
log.Printf("Creating new command: %s", desiredCmd.Name)
_, err := s.ApplicationCommandCreate(s.State.User.ID, "", desiredCmd)
if err != nil {
log.Printf("Error creating command %s: %v", desiredCmd.Name, err)
}
}
}
log.Println("Command registration completed")
}
// commandsEqual performs a basic comparison between two commands
func commandsEqual(existing, desired *discordgo.ApplicationCommand) bool {
if existing.Name != desired.Name ||
existing.Description != desired.Description ||
len(existing.Options) != len(desired.Options) {
return false
}
// Compare options (basic comparison)
for i, existingOpt := range existing.Options {
if i >= len(desired.Options) {
return false
}
desiredOpt := desired.Options[i]
if existingOpt.Name != desiredOpt.Name ||
existingOpt.Description != desiredOpt.Description ||
existingOpt.Type != desiredOpt.Type ||
existingOpt.Required != desiredOpt.Required {
return false
}
}
return true
}
// initCommands initializes command definitions with configuration
func initCommands(config *lib.Config) {
commands = []*discordgo.ApplicationCommand{
{
Name: "ping",
@ -32,78 +191,74 @@ var (
},
},
{
Name: "markov",
Description: "Why did the Markov chain break up with its partner? Because it couldn't handle the past!",
Name: "gen",
Description: "Generate a random message using markov chains based on channel history",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "messages",
Description: "Number of messages to use (default: 100, max: 1000)",
Description: fmt.Sprintf("Number of messages to use (default: %d, max: %d)", config.MarkovDefaultMessages, config.MarkovMaxMessages),
Required: false,
},
},
},
{
Name: "ask",
Description: "Ask a question and get a markov chain answer based on channel contents (like ChatGPT but dumb!)",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "question",
Description: "The question you want to ask",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "messages",
Description: fmt.Sprintf("Number of messages to use (default: %d, max: %d)", config.MarkovDefaultMessages, config.MarkovMaxMessages),
Required: false,
},
},
},
{
Name: "himbucks",
Description: "Check your himbucks balance",
},
{
Name: "himboard",
Description: "View the himbucks leaderboard",
},
{
Name: "sendbucks",
Description: "Send himbucks to another user",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "The user to send himbucks to",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "amount",
Description: "Amount of himbucks to send",
Required: true,
MinValue: &[]float64{1}[0],
},
},
},
}
}
// initCommandHandlers initializes command handlers with configuration
func initCommandHandlers(config *lib.Config) {
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"ping": lib.HandleCommand("ping", 5*time.Second, command.PingCommand),
"hs": lib.HandleCommand("hs", 10*time.Second, command.HsCommand),
"markov": lib.HandleCommand("markov", 30*time.Second, command.MarkovCommand),
}
)
func main() {
godotenv.Load(".env")
token := os.Getenv("DISCORD_TOKEN")
if token == "" {
log.Fatalln("No $DISCORD_TOKEN given.")
}
dg, err := discordgo.New("Bot " + token)
if err != nil {
log.Fatalf("Error creating Discord session: %v", err)
}
dg.AddHandler(ready)
dg.AddHandler(interactionCreate)
dg.Identify.Intents = discordgo.IntentsGuilds
err = dg.Open()
if err != nil {
log.Fatalf("Error opening connection: %v", err)
}
log.Println("Bot is now running. Press CTRL-C to exit.")
registerCommands(dg)
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
dg.Close()
}
func ready(s *discordgo.Session, event *discordgo.Ready) {
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
}
func interactionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
}
func registerCommands(s *discordgo.Session) {
log.Println("Registering commands...")
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
for i, v := range commands {
cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", v)
if err != nil {
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
}
registeredCommands[i] = cmd
"ping": lib.HandleCommand("ping", time.Duration(config.PingCooldown)*time.Second, command.PingCommand),
"hs": lib.HandleCommand("hs", time.Duration(config.HsCooldown)*time.Second, command.HsCommand),
"gen": lib.HandleCommand("gen", time.Duration(config.MarkovCooldown)*time.Second, command.MarkovCommand),
"ask": lib.HandleCommand("ask", time.Duration(config.MarkovAskCooldown)*time.Second, command.MarkovQuestionCommand),
"himbucks": lib.HandleCommand("himbucks", time.Duration(config.HimbucksCooldown)*time.Second, command.BalanceGetCommand),
"himboard": lib.HandleCommand("himboard", time.Duration(config.HimboardCooldown)*time.Second, command.LeaderboardCommand),
"sendbucks": lib.HandleCommand("sendbucks", time.Duration(config.SendbucksCooldown)*time.Second, command.BalanceSendCommand),
}
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS users;

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
discord_id TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS schema_migrations;

View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS guild_profiles;

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS guild_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
guild_id TEXT NOT NULL,
-- Himbucks-related fields
currency_balance INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
last_reward_at DATETIME,
-- Add other profile-related fields here as needed
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, guild_id)
);