1 Commits

Author SHA1 Message Date
be0a944665 Delete .github directory 2024-04-01 02:20:41 -06:00
37 changed files with 711 additions and 2834 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,10 +0,0 @@
# flyctl launch added from .gitignore
# Environment variables
**/.env
**/.env.local
**/.env.development.local
**/.env.test.local
**/.env.production.local
**/.env
**/himbot
fly.toml

View File

@ -1,31 +1,11 @@
# Discord Configuration
# Tokens
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
REPLICATE_API_TOKEN=""
# Comma separated
COOLDOWN_ALLOW_LIST=""
# S3
BUCKET_NAME=
AWS_ENDPOINT_URL_S3=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=

View File

@ -1,35 +0,0 @@
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

6
.gitignore vendored
View File

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

View File

@ -1,32 +1,14 @@
# Build stage
FROM golang:1.24.3 AS build
FROM golang:1.22.0 as build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /go/bin/app
RUN go mod download
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /go/bin/app
# Final stage
FROM ubuntu:22.04
FROM gcr.io/distroless/base-debian12
# 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 /
# 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/himbot"]
CMD [ "/app" ]

View File

@ -4,13 +4,14 @@ A discord bot written in Go.
## It's dangerous to go alone! Take this!
- Install Go 1.23.2 or higher (required)
- Install Go 1.21.5 or higher (required)
## Running Locally
- Copy .env.example and rename to .env
- Create a Discord Bot with all gateway permissions enabled
- Generate a token for this discord bot and paste it in the .env for DISCORD_TOKEN
- Generate and provide an Replicate token and paste it in the .env for REPLICATE_API_TOKEN
- Run `go run main.go` to run locally
## Adding the bot to a server

64
command/ask.go Normal file
View File

@ -0,0 +1,64 @@
package command
import (
"bytes"
"context"
"errors"
"fmt"
"himbot/lib"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/diamondburned/arikawa/v3/utils/sendpart"
)
func Ask(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
// Cooldown Logic
allowed, cooldownString := lib.CooldownHandler(*data.Event, "ask", time.Minute)
if !allowed {
return lib.ErrorResponse(errors.New(cooldownString))
}
// Command Logic
var options struct {
Prompt string `discord:"prompt"`
}
if err := data.Options.Unmarshal(&options); err != nil {
lib.CancelTimer(data.Event.Member.User.ID.String(), "ask")
return lib.ErrorResponse(err)
}
respString, err := lib.ReplicateTextGeneration(options.Prompt)
if err != nil {
fmt.Printf("ChatCompletion error: %v\n", err)
lib.CancelTimer(data.Event.Member.User.ID.String(), "ask")
return &api.InteractionResponseData{
Content: option.NewNullableString("ChatCompletion Error!"),
AllowedMentions: &api.AllowedMentions{},
}
}
if len(respString) > 1800 {
textFile := bytes.NewBuffer([]byte(respString))
file := sendpart.File{
Name: "himbot_response.md",
Reader: textFile,
}
return &api.InteractionResponseData{
Content: option.NewNullableString("Prompt: " + options.Prompt + "\n"),
AllowedMentions: &api.AllowedMentions{},
Files: []sendpart.File{file},
}
}
return &api.InteractionResponseData{
Content: option.NewNullableString("Prompt: " + options.Prompt + "\n--------------------\n" + respString),
AllowedMentions: &api.AllowedMentions{},
}
}

View File

@ -1,89 +0,0 @@
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
}

View File

@ -1,25 +1,26 @@
package command
import (
"fmt"
"context"
"himbot/lib"
"github.com/bwmarrin/discordgo"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
)
func HsCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
options := i.ApplicationCommandData().Options
if len(options) == 0 || options[0].Type != discordgo.ApplicationCommandOptionString {
return "", fmt.Errorf("please provide a nickname")
}
nickname := options[0].StringValue()
user, err := lib.GetUser(i)
if err != nil {
return "", fmt.Errorf("error processing command: %w", err)
func HS(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
var options struct {
Arg string `discord:"nickname"`
}
response := fmt.Sprintf("%s was %s's nickname in high school!", nickname, user.Username)
if err := data.Options.Unmarshal(&options); err != nil {
return lib.ErrorResponse(err)
}
return response, nil
user := lib.GetUserObject(*data.Event)
return &api.InteractionResponseData{
Content: option.NewNullableString(options.Arg + " was " + user.DisplayName() + "'s nickname in highschool!"),
}
}

File diff suppressed because it is too large Load Diff

56
command/pic.go Normal file
View File

@ -0,0 +1,56 @@
package command
import (
"context"
"errors"
"himbot/lib"
"strconv"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/diamondburned/arikawa/v3/utils/sendpart"
)
func Pic(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
// Cooldown Logic
allowed, cooldownString := lib.CooldownHandler(*data.Event, "pic", time.Minute*5)
if !allowed {
return lib.ErrorResponse(errors.New(cooldownString))
}
// Command Logic
var options struct {
Prompt string `discord:"prompt"`
}
if err := data.Options.Unmarshal(&options); err != nil {
lib.CancelTimer(data.Event.Member.User.ID.String(), "pic")
return lib.ErrorResponse(err)
}
// Get current epoch timestamp
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// Concatenate clean username and timestamp to form filename
filename := data.Event.Sender().Username + "_" + timestamp + ".jpg"
imageFile, err := lib.ReplicateImageGeneration(options.Prompt, filename)
if err != nil {
lib.CancelTimer(data.Event.Member.User.ID.String(), "pic")
return lib.ErrorResponse(err)
}
file := sendpart.File{
Name: filename,
Reader: imageFile,
}
return &api.InteractionResponseData{
Content: option.NewNullableString("Prompt: " + options.Prompt),
Files: []sendpart.File{file},
}
}

View File

@ -1,12 +1,16 @@
package command
import (
"github.com/bwmarrin/discordgo"
"context"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
)
func PingCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
// Customize the response based on whether it's a guild or DM
responseContent := "Pong!"
return responseContent, nil
func Ping(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
// Command Logic
return &api.InteractionResponseData{
Content: option.NewNullableString("Pong!"),
}
}

View File

@ -1,35 +0,0 @@
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,30 +1,15 @@
# fly.toml app configuration file generated for himbot on 2024-11-20T14:35:09-06:00
# fly.toml app configuration file generated for himbot on 2023-10-19T18:34:44-03:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'himbot'
primary_region = 'ord'
[build]
[[mounts]]
source = 'himbot_data'
destination = '/data'
app = "himbot"
primary_region = "sea"
swap_size_mb = 512
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_stop_machines = true
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'
processes = ["app"]

20
go.mod
View File

@ -1,18 +1,20 @@
module himbot
go 1.24
go 1.22.0
require github.com/diamondburned/arikawa/v3 v3.3.5
require (
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
github.com/jmespath/go-jmespath v0.4.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
)
require (
github.com/bwmarrin/discordgo v0.29.0
github.com/gorilla/websocket v1.5.3 // indirect
github.com/aws/aws-sdk-go v1.51.8
github.com/gorilla/schema v1.3.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/joho/godotenv v1.5.1
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e
github.com/replicate/replicate-go v0.18.1
golang.org/x/time v0.5.0 // indirect
)

100
go.sum
View File

@ -1,33 +1,83 @@
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/aws/aws-sdk-go v1.50.37 h1:gnAf6eYPSTb4QpVwugtWFqD07QXOoX7LewRrtLUx3lI=
github.com/aws/aws-sdk-go v1.50.37/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.51.3 h1:OqSyEXcJwf/XhZNVpMRgKlLA9nmbo5X8dwbll4RWxq8=
github.com/aws/aws-sdk-go v1.51.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.51.8 h1:tD7gQq5XKuKdhA6UMEH26ZNQH0s+HbL95rzv/ACz5TQ=
github.com/aws/aws-sdk-go v1.51.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/arikawa/v3 v3.3.5 h1:Z6BwetBMzPxTBLY2Ixxic2kdJJe0JhNvVrdbJ0gRcWg=
github.com/diamondburned/arikawa/v3 v3.3.5/go.mod h1:KPkkWr40xmEithhd15XD2dbkVY8A5+MCmZO0gRXk3qc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM=
github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/schema v1.3.0 h1:rbciOzXAx3IB8stEFnfTwO3sYa6EWlQk79XdyustPDA=
github.com/gorilla/schema v1.3.0/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
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.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=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/replicate/replicate-go v0.18.1 h1:4zduLVJxdQAoyl7zKj1e2nxwJVMcT6O/sXe6/eUEtns=
github.com/replicate/replicate-go v0.18.1/go.mod h1:D2x8SztjeUKcaYnSgVu3H2DechufLJWZJB4+TLA3Rag=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,59 +0,0 @@
package lib
import (
"time"
"github.com/bwmarrin/discordgo"
)
type CommandFunc func(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error)
func HandleCommand(commandName string, cooldownDuration time.Duration, handler CommandFunc) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !CheckAndApplyCooldown(s, i, commandName, cooldownDuration) {
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
interactErr := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
if interactErr != nil {
ThrowWithError(commandName, "Error deferring response: "+interactErr.Error())
return
}
// Execute the command handler
response, handlerErr := handler(s, i)
if handlerErr != nil {
RespondWithError(s, i, "Error processing command: "+handlerErr.Error())
return
}
// Send the follow-up message with the response
_, followErr := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: response,
})
if followErr != nil {
ThrowWithError(commandName, "Error sending follow-up message: "+followErr.Error())
}
}
}

View File

@ -1,96 +0,0 @@
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
}

View File

@ -1,66 +0,0 @@
package lib
import (
"fmt"
"sync"
"time"
"github.com/bwmarrin/discordgo"
)
var (
once sync.Once
instance *CooldownManager
)
type CooldownManager struct {
cooldowns map[string]time.Time
mu sync.Mutex
}
func GetCooldownManager() *CooldownManager {
once.Do(func() {
instance = &CooldownManager{
cooldowns: make(map[string]time.Time),
}
})
return instance
}
func (cm *CooldownManager) SetCooldown(userID, commandName string, duration time.Duration) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.cooldowns[userID+":"+commandName] = time.Now().Add(duration)
}
func (cm *CooldownManager) CheckCooldown(userID, commandName string) (bool, time.Duration) {
cm.mu.Lock()
defer cm.mu.Unlock()
key := userID + ":" + commandName
if cooldownEnd, exists := cm.cooldowns[key]; exists {
if time.Now().Before(cooldownEnd) {
return false, time.Until(cooldownEnd)
}
delete(cm.cooldowns, key)
}
return true, 0
}
func CheckAndApplyCooldown(s *discordgo.Session, i *discordgo.InteractionCreate, commandName string, duration time.Duration) bool {
cooldownManager := GetCooldownManager()
user, err := GetUser(i)
if err != nil {
RespondWithError(s, i, "Error processing command: "+err.Error())
return false
}
canUse, remaining := cooldownManager.CheckCooldown(user.ID, commandName)
if !canUse {
RespondWithError(s, i, fmt.Sprintf("You can use this command again in %v", remaining.Round(time.Second)))
return false
}
cooldownManager.SetCooldown(user.ID, commandName, duration)
return true
}

224
lib/db.go
View File

@ -1,224 +0,0 @@
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")
}

View File

@ -1,27 +1,28 @@
package lib
import (
"fmt"
"log"
"net"
"os"
"github.com/bwmarrin/discordgo"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
)
// respondWithError sends an error message as a response to the interaction
func RespondWithError(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
log.Printf("Responding with error: %s", message)
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
Flags: discordgo.MessageFlagsEphemeral,
},
})
if err != nil {
log.Printf("Error sending error response: %v", err)
func ErrorResponse(err error) *api.InteractionResponseData {
var content string
switch e := err.(type) {
case *net.OpError:
content = "**Network Error:** " + e.Error()
case *os.PathError:
content = "**File Error:** " + e.Error()
default:
content = "**Error:** " + err.Error()
}
return &api.InteractionResponseData{
Content: option.NewNullableString(content),
Flags: discord.EphemeralMessage,
AllowedMentions: &api.AllowedMentions{},
}
}
func ThrowWithError(command, message string) error {
return fmt.Errorf("error in command '%s': %s", command, message)
}

89
lib/helpers.go Normal file
View File

@ -0,0 +1,89 @@
package lib
import (
"fmt"
"os"
"strings"
"time"
"github.com/diamondburned/arikawa/v3/discord"
)
var manager = NewTimerManager()
// Userish is an interface that captures the common methods you may want to call
// on either a discord.Member or discord.User, including a display name.
type Userish interface {
ID() discord.UserID
Username() string
DisplayName() string
}
// memberUser adapts a discord.Member to the Userish interface.
type memberUser struct {
*discord.Member
}
func (mu memberUser) ID() discord.UserID {
return mu.User.ID
}
func (mu memberUser) Username() string {
return mu.User.Username
}
func (mu memberUser) DisplayName() string {
// If Nick is set, return it as the display name, otherwise return Username
if mu.Member.Nick != "" {
return mu.Member.Nick
}
return mu.User.Username
}
// directUser adapts a discord.User to the Userish interface.
type directUser struct {
*discord.User
}
func (du directUser) ID() discord.UserID {
return du.User.ID
}
func (du directUser) Username() string {
return du.User.Username
}
func (du directUser) DisplayName() string {
// For a direct user, the display name is just the username since no nickname is available.
return du.User.Username
}
// GetUserObject takes an interaction event and returns a Userish, which may be
// either a discord.Member or a discord.User, but exposes it through a consistent interface.
func GetUserObject(event discord.InteractionEvent) Userish {
if event.Member != nil {
return memberUser{event.Member}
} else {
return directUser{event.User}
}
}
func CooldownHandler(event discord.InteractionEvent, key string, duration time.Duration) (bool, string) {
user := GetUserObject(event)
allowList := strings.Split(os.Getenv("COOLDOWN_ALLOW_LIST"), ",")
// Check if the user ID is in the allowList
for _, id := range allowList {
if id == user.ID().String() {
return true, ""
}
}
isOnCooldown, remaining := manager.TimerRunning(user.ID().String(), key)
if isOnCooldown {
return false, fmt.Sprintf("You are on cooldown. Please wait for %v", remaining)
}
manager.StartTimer(user.ID().String(), key, duration)
return true, ""
}

View File

@ -1,181 +0,0 @@
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
}

View File

@ -1,47 +0,0 @@
package lib
import (
"github.com/bwmarrin/discordgo"
)
// InteractionUser represents a user from an interaction, abstracting away the differences
// between guild members and DM users.
type InteractionUser struct {
ID string
Username string
Bot bool
}
// GetUser extracts user information from an interaction, handling both guild and DM cases.
func GetUser(i *discordgo.InteractionCreate) (*InteractionUser, error) {
if i.Member != nil && i.Member.User != nil {
// Guild interaction
return &InteractionUser{
ID: i.Member.User.ID,
Username: i.Member.User.Username,
Bot: i.Member.User.Bot,
}, nil
} else if i.User != nil {
// DM interaction
return &InteractionUser{
ID: i.User.ID,
Username: i.User.Username,
Bot: i.User.Bot,
}, nil
}
return nil, ThrowWithError("GetUser", "Unable to extract user information from interaction")
}
// IsInGuild checks if the interaction occurred in a guild.
func IsInGuild(i *discordgo.InteractionCreate) bool {
return i.Member != nil
}
// GetGuildID safely retrieves the guild ID if the interaction is from a guild.
func GetGuildID(i *discordgo.InteractionCreate) string {
if i.GuildID != "" {
return i.GuildID
}
return ""
}

View File

@ -1,63 +0,0 @@
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())
}
}
}

143
lib/replicate.go Normal file
View File

@ -0,0 +1,143 @@
package lib
import (
"bytes"
"context"
"errors"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/replicate/replicate-go"
)
var ReplicatePromptPrefix = "Your designation is Himbot. You are an assistant bot designed to provide helpful responses with a touch of wit and sarcasm. Your responses should be natural and engaging, reflecting your unique personality. Avoid clichéd or overused expressions of sarcasm. Instead, focus on delivering information in a clever and subtly humorous way. If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information."
func ReplicateTextGeneration(prompt string) (string, error) {
client, clientError := replicate.NewClient(replicate.WithTokenFromEnv())
if clientError != nil {
return "", clientError
}
input := replicate.PredictionInput{
"prompt": prompt,
"system_prompt": ReplicatePromptPrefix,
"max_new_tokens": 4096,
}
webhook := replicate.Webhook{
URL: "https://example.com/webhook",
Events: []replicate.WebhookEventType{"start", "completed"},
}
prediction, predictionError := client.Run(context.Background(), "meta/llama-2-70b-chat:2d19859030ff705a87c746f7e96eea03aefb71f166725aee39692f1476566d48", input, &webhook)
if predictionError != nil {
return "", predictionError
}
if prediction == nil {
return "", errors.New("there was an error generating a response based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
test, ok := prediction.([]interface{})
if !ok {
return "", errors.New("there was an error generating a response based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
strs := make([]string, len(test))
for i, v := range test {
str, ok := v.(string)
if !ok {
return "", errors.New("element is not a string")
}
strs[i] = str
}
result := strings.Join(strs, "")
return result, nil
}
func ReplicateImageGeneration(prompt string, filename string) (*bytes.Buffer, error) {
client, clientError := replicate.NewClient(replicate.WithTokenFromEnv())
if clientError != nil {
return nil, clientError
}
input := replicate.PredictionInput{
"width": 1024,
"height": 1024,
"prompt": prompt,
"refine": "expert_ensemble_refiner",
"negative_prompt": "worst quality, normal quality, low quality, low res, blurry, text, watermark, logo, banner, extra digits, cropped, jpeg artifacts, signature, username, error, sketch ,duplicate, ugly, monochrome, horror, geometry, mutation, disgusting",
"num_outputs": 1,
"disable_safety_checker": true,
}
webhook := replicate.Webhook{
URL: "https://example.com/webhook",
Events: []replicate.WebhookEventType{"start", "completed"},
}
prediction, predictionError := client.Run(context.Background(), "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", input, &webhook)
if predictionError != nil {
return nil, predictionError
}
if prediction == nil {
return nil, errors.New("there was an error generating the image based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
test, ok := prediction.([]interface{})
if !ok {
return nil, errors.New("there was an error generating the image based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
imgUrl, ok := test[0].(string)
if !ok {
return nil, errors.New("there was an error generating the image based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
imageRes, imageGetErr := http.Get(imgUrl)
if imageGetErr != nil {
return nil, imageGetErr
}
defer imageRes.Body.Close()
imageBytes, imgReadErr := io.ReadAll(imageRes.Body)
if imgReadErr != nil {
return nil, imgReadErr
}
// Save image to a temporary file
tmpfile, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(imageBytes); err != nil {
log.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
// Upload the image to S3
_, uploadErr := UploadToS3(tmpfile.Name())
if uploadErr != nil {
log.Printf("Failed to upload image to S3: %v", uploadErr)
}
imageFile := bytes.NewBuffer(imageBytes)
return imageFile, nil
}

55
lib/s3.go Normal file
View File

@ -0,0 +1,55 @@
package lib
import (
"fmt"
"os"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
func UploadToS3(filePath string) (*s3manager.UploadOutput, error) {
bucket := os.Getenv("BUCKET_NAME")
if bucket == "" {
fmt.Println("No S3 bucket specified, skipping upload.")
return nil, nil
}
endpoint := os.Getenv("AWS_ENDPOINT_URL_S3")
accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
region := os.Getenv("AWS_REGION")
sess, err := session.NewSession(&aws.Config{
Region: &region,
Credentials: credentials.NewStaticCredentials(
accessKeyID,
secretAccessKey,
"",
),
Endpoint: aws.String(endpoint),
})
if err != nil {
return nil, fmt.Errorf("failed to create session, %v", err)
}
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file, %v", err)
}
defer file.Close()
uploader := s3manager.NewUploader(sess)
result, err := uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(bucket),
Key: aws.String(filePath),
Body: file,
})
if err != nil {
return nil, fmt.Errorf("failed to upload file, %v", err)
}
return result, nil
}

72
lib/timer.go Normal file
View File

@ -0,0 +1,72 @@
package lib
import (
"sync"
"time"
)
var (
mu sync.Mutex
instance *TimerManager
)
type TimerManager struct {
timers map[string]time.Time
mu sync.Mutex
}
func NewTimerManager() *TimerManager {
return &TimerManager{
timers: make(map[string]time.Time),
}
}
func GetInstance() *TimerManager {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &TimerManager{
timers: make(map[string]time.Time),
}
}
return instance
}
func (m *TimerManager) StartTimer(userID string, key string, duration time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.timers[userID+":"+key] = time.Now().Add(duration)
}
func (m *TimerManager) TimerRunning(userID string, key string) (bool, time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
timerEnd, exists := m.timers[userID+":"+key]
if !exists {
return false, 0
}
if time.Now().After(timerEnd) {
delete(m.timers, userID+":"+key)
return false, 0
}
return true, time.Until(timerEnd)
}
func CancelTimer(userID string, key string) {
manager := GetInstance()
// Handle non-existent keys gracefully
if _, exists := manager.timers[userID+":"+key]; !exists {
return
}
manager.mu.Lock()
defer manager.mu.Unlock()
delete(manager.timers, userID+":"+key)
}

View File

@ -1,43 +0,0 @@
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
}

310
main.go
View File

@ -1,264 +1,106 @@
package main
import (
"fmt"
"context"
"himbot/command"
"himbot/lib"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/arikawa/v3/state"
"github.com/joho/godotenv"
)
var (
commands []*discordgo.ApplicationCommand
commandHandlers map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate)
)
var commands = []api.CreateCommandData{
{
Name: "ping",
Description: "ping pong!",
},
{
Name: "ask",
Description: "Ask Himbot! Cooldown: 1 Minute.",
Options: []discord.CommandOption{
&discord.StringOption{
OptionName: "prompt",
Description: "The prompt to send to Himbot.",
Required: true,
},
},
},
{
Name: "pic",
Description: "Generate an image! Cooldown: 5 Minutes.",
Options: []discord.CommandOption{
&discord.StringOption{
OptionName: "prompt",
Description: "The prompt for the image generation.",
Required: true,
},
},
},
{
Name: "hs",
Description: "This command was your nickname in highschool!",
Options: []discord.CommandOption{
&discord.StringOption{
OptionName: "nickname",
Description: "Your nickname in highschool.",
Required: true,
},
},
},
}
func main() {
godotenv.Load(".env")
// Load configuration
config := lib.LoadConfig()
token := os.Getenv("DISCORD_TOKEN")
if token == "" {
godotenv.Load(".env")
// 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 token == "" {
log.Fatalln("No $DISCORD_TOKEN given.")
}
}
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)
h := newHandler(state.New("Bot " + token))
h.s.AddInteractionHandler(h)
h.s.AddIntents(gateway.IntentGuilds)
h.s.AddHandler(func(*gateway.ReadyEvent) {
me, _ := h.s.Me()
log.Println("connected to the gateway as", me.Tag())
})
dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages
err = dg.Open()
if err != nil {
log.Fatalf("Error opening connection: %v", err)
if err := cmdroute.OverwriteCommands(h.s, commands); err != nil {
log.Fatalln("cannot update commands:", err)
}
log.Println("Bot is now running. Press CTRL-C to exit.")
registerCommands(dg)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
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)
if err := h.s.Connect(ctx); err != nil {
log.Fatalln("cannot connect:", err)
}
}
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")
type handler struct {
*cmdroute.Router
s *state.State
}
// 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
}
func newHandler(s *state.State) *handler {
h := &handler{s: s}
// 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
}
}
h.Router = cmdroute.NewRouter()
// Automatically defer handles if they're slow.
h.Use(cmdroute.Deferrable(s, cmdroute.DeferOpts{}))
h.AddFunc("ping", command.Ping)
h.AddFunc("ask", command.Ask)
h.AddFunc("pic", command.Pic)
h.AddFunc("hs", command.HS)
return true
}
// initCommands initializes command definitions with configuration
func initCommands(config *lib.Config) {
commands = []*discordgo.ApplicationCommand{
{
Name: "ping",
Description: "ping pong!",
},
{
Name: "hs",
Description: "This command was your nickname in highschool!",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "nickname",
Description: "Your nickname in highschool.",
Required: true,
},
},
},
{
Name: "gen",
Description: "Generate a random message using markov chains based on channel history",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "messages",
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", 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),
}
return h
}

View File

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

View File

@ -1,6 +0,0 @@
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

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

View File

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

View File

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

View File

@ -1,14 +0,0 @@
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)
);