Compare commits
70 Commits
atridadl-p
...
main
Author | SHA1 | Date | |
---|---|---|---|
6955421fd7
|
|||
b306143db2
|
|||
0a730be6f7
|
|||
3af77b6bea
|
|||
025c829c68
|
|||
a3e406fb2f
|
|||
0a07bc3dbc
|
|||
d7edcad552
|
|||
33291b478f
|
|||
4cf9c3295f
|
|||
ba9bf88a1c
|
|||
1403a756f7
|
|||
7249e08f84
|
|||
b0e9372cd9
|
|||
041914c405
|
|||
be4f67218d
|
|||
1c46b28752
|
|||
997d2cb911
|
|||
5a1158048d
|
|||
82c62658bb
|
|||
69b76bd792
|
|||
c3e62155e2
|
|||
ce580e77fd
|
|||
46c24134d5
|
|||
0194c3d4fe
|
|||
d1e2d9bf4f
|
|||
fc8be94ee9
|
|||
e14051569c
|
|||
022686e14f
|
|||
9a1b3723bc
|
|||
6a55017624
|
|||
359520fe83
|
|||
b805f27d0e
|
|||
af3f3ab355
|
|||
2e05f946c5 | |||
a33c3185db | |||
13bf11abce
|
|||
0130ec538c
|
|||
26e931f44f
|
|||
7328a0139e
|
|||
b00028d8e3
|
|||
999605c2fc
|
|||
d2f94dec79
|
|||
8c2d7189c7
|
|||
0406e5cbf7
|
|||
44fc4e8dd1
|
|||
e76a886d5b
|
|||
18aa6846e9
|
|||
12e3414833
|
|||
e18d1c9aeb
|
|||
a615512ed7
|
|||
9385a31364 | |||
ba144dafa1 | |||
299cbcfcee | |||
b9e72fa537 | |||
605e40f16c | |||
5fa146dff9 | |||
d9d0c84135 | |||
a1fd9aec29 | |||
d3badf183c | |||
9ad6aa0c59 | |||
b317ebed86 | |||
3f908c4153 | |||
014dd57d93 | |||
db7b4e8e45 | |||
e89fdb4ac9 | |||
2038fa6df5 | |||
655409ea1a | |||
8a944a1e31 | |||
4eeeb39506 |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
# flyctl launch added from .gitignore
|
||||
# Environment variables
|
||||
**/.env
|
||||
**/.env.local
|
||||
**/.env.development.local
|
||||
**/.env.test.local
|
||||
**/.env.production.local
|
||||
**/.env
|
||||
**/himbot
|
||||
fly.toml
|
40
.env.example
40
.env.example
@ -1,11 +1,31 @@
|
||||
# Tokens
|
||||
# Discord Configuration
|
||||
DISCORD_TOKEN=""
|
||||
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=
|
||||
|
||||
# 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
35
.github/workflows/deploy.yml
vendored
Normal 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
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -5,4 +5,8 @@
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
himbot
|
||||
himbot
|
||||
*.db
|
||||
*.db-client_wal_index
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
30
Dockerfile
30
Dockerfile
@ -1,14 +1,32 @@
|
||||
FROM golang:1.22.0 as build
|
||||
# Build stage
|
||||
FROM golang:1.24.3 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /go/bin/app
|
||||
|
||||
FROM gcr.io/distroless/base-debian12
|
||||
COPY . .
|
||||
|
||||
COPY --from=build /go/bin/app /
|
||||
RUN go build -ldflags="-s -w" -o /go/bin/app
|
||||
|
||||
CMD [ "/app" ]
|
||||
# Final stage
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Install SSL certificates and required runtime libraries
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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"]
|
||||
|
@ -4,14 +4,13 @@ A discord bot written in Go.
|
||||
|
||||
## It's dangerous to go alone! Take this!
|
||||
|
||||
- Install Go 1.21.5 or higher (required)
|
||||
- Install Go 1.23.2 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
|
||||
|
@ -1,64 +0,0 @@
|
||||
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{},
|
||||
}
|
||||
}
|
89
command/himbucks.go
Normal file
89
command/himbucks.go
Normal 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
|
||||
}
|
@ -1,26 +1,25 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"himbot/lib"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/cmdroute"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func HS(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
var options struct {
|
||||
Arg string `discord:"nickname"`
|
||||
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)
|
||||
}
|
||||
|
||||
if err := data.Options.Unmarshal(&options); err != nil {
|
||||
return lib.ErrorResponse(err)
|
||||
}
|
||||
response := fmt.Sprintf("%s was %s's nickname in high school!", nickname, user.Username)
|
||||
|
||||
user := lib.GetUserObject(*data.Event)
|
||||
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString(options.Arg + " was " + user.DisplayName() + "'s nickname in highschool!"),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
1471
command/markov.go
Normal file
1471
command/markov.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,56 +0,0 @@
|
||||
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},
|
||||
}
|
||||
}
|
@ -1,16 +1,12 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/cmdroute"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func Ping(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
// Command Logic
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("Pong!"),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal 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
|
27
fly.toml
27
fly.toml
@ -1,15 +1,30 @@
|
||||
# fly.toml app configuration file generated for himbot on 2023-10-19T18:34:44-03: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.
|
||||
#
|
||||
|
||||
app = "himbot"
|
||||
primary_region = "sea"
|
||||
swap_size_mb = 512
|
||||
app = 'himbot'
|
||||
primary_region = 'ord'
|
||||
|
||||
[build]
|
||||
|
||||
[[mounts]]
|
||||
source = 'himbot_data'
|
||||
destination = '/data'
|
||||
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
auto_stop_machines = true
|
||||
force_https = true
|
||||
auto_stop_machines = 'stop'
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
processes = ["app"]
|
||||
processes = ['app']
|
||||
|
||||
[[services]]
|
||||
protocol = 'tcp'
|
||||
internal_port = 3000
|
||||
min_machines_running = 1
|
||||
ports = []
|
||||
|
||||
[[vm]]
|
||||
size = 'shared-cpu-1x'
|
||||
|
20
go.mod
20
go.mod
@ -1,20 +1,18 @@
|
||||
module himbot
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require github.com/diamondburned/arikawa/v3 v3.3.5
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
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
|
||||
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/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/bwmarrin/discordgo v0.29.0
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/replicate/replicate-go v0.18.1
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e
|
||||
)
|
||||
|
100
go.sum
100
go.sum
@ -1,83 +1,33 @@
|
||||
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/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.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/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/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=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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/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.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/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/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=
|
||||
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=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
|
59
lib/command.go
Normal file
59
lib/command.go
Normal file
@ -0,0 +1,59 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
96
lib/config.go
Normal file
96
lib/config.go
Normal 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
|
||||
}
|
66
lib/cooldown.go
Normal file
66
lib/cooldown.go
Normal file
@ -0,0 +1,66 @@
|
||||
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
Normal file
224
lib/db.go
Normal 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")
|
||||
}
|
@ -1,28 +1,27 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
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{},
|
||||
// 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 ThrowWithError(command, message string) error {
|
||||
return fmt.Errorf("error in command '%s': %s", command, message)
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
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, ""
|
||||
}
|
181
lib/himbucks.go
Normal file
181
lib/himbucks.go
Normal 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
|
||||
}
|
47
lib/member.go
Normal file
47
lib/member.go
Normal file
@ -0,0 +1,47 @@
|
||||
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 ""
|
||||
}
|
63
lib/processor.go
Normal file
63
lib/processor.go
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
143
lib/replicate.go
143
lib/replicate.go
@ -1,143 +0,0 @@
|
||||
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
55
lib/s3.go
@ -1,55 +0,0 @@
|
||||
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: ®ion,
|
||||
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
72
lib/timer.go
@ -1,72 +0,0 @@
|
||||
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)
|
||||
}
|
43
lib/users.go
Normal file
43
lib/users.go
Normal 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
|
||||
}
|
320
main.go
320
main.go
@ -1,106 +1,264 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"himbot/command"
|
||||
"himbot/lib"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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/bwmarrin/discordgo"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var (
|
||||
commands []*discordgo.ApplicationCommand
|
||||
commandHandlers map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate)
|
||||
)
|
||||
|
||||
func main() {
|
||||
godotenv.Load(".env")
|
||||
|
||||
token := os.Getenv("DISCORD_TOKEN")
|
||||
if token == "" {
|
||||
godotenv.Load(".env")
|
||||
// Load configuration
|
||||
config := lib.LoadConfig()
|
||||
|
||||
if token == "" {
|
||||
log.Fatalln("No $DISCORD_TOKEN given.")
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
if err := cmdroute.OverwriteCommands(h.s, commands); err != nil {
|
||||
log.Fatalln("cannot update commands:", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
log.Println("Command registration completed")
|
||||
}
|
||||
|
||||
if err := h.s.Connect(ctx); err != nil {
|
||||
log.Fatalln("cannot connect:", err)
|
||||
// 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",
|
||||
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],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
*cmdroute.Router
|
||||
s *state.State
|
||||
}
|
||||
|
||||
func newHandler(s *state.State) *handler {
|
||||
h := &handler{s: s}
|
||||
|
||||
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 h
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
1
migrations/000001_create_users_table.down.sql
Normal file
1
migrations/000001_create_users_table.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
6
migrations/000001_create_users_table.up.sql
Normal file
6
migrations/000001_create_users_table.up.sql
Normal 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
|
||||
);
|
1
migrations/000002_create_schema_migrations.down.sql
Normal file
1
migrations/000002_create_schema_migrations.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS schema_migrations;
|
4
migrations/000002_create_schema_migrations.up.sql
Normal file
4
migrations/000002_create_schema_migrations.up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
1
migrations/000003_create_guild_profiles_table.down.sql
Normal file
1
migrations/000003_create_guild_profiles_table.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS guild_profiles;
|
14
migrations/000003_create_guild_profiles_table.up.sql
Normal file
14
migrations/000003_create_guild_profiles_table.up.sql
Normal 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)
|
||||
);
|
Reference in New Issue
Block a user