turso
This commit is contained in:
parent
af3f3ab355
commit
b805f27d0e
10 changed files with 246 additions and 10 deletions
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
|
||||
|
|
5
go.mod
5
go.mod
|
@ -2,8 +2,13 @@ module himbot
|
|||
|
||||
go 1.23
|
||||
|
||||
require github.com/tursodatabase/go-libsql v0.0.0-20241011135853-3effbb6dea5c
|
||||
|
||||
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.28.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
)
|
||||
|
||||
|
|
16
go.sum
16
go.sum
|
@ -1,17 +1,33 @@
|
|||
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.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c h1:WsJ6G+hkDXIMfQE8FIxnnziT26WmsRgZhdWQ0IQGlcc=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c/go.mod h1:gIcFddvsvPcRCO6QDmWH9/zcFd5U26QWWRMgZh4ddyo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20241011135853-3effbb6dea5c h1:a8TrFzP+zK+uYcMWuLQoNOR78SG/yISSnHwMIcyWa2Q=
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20241011135853-3effbb6dea5c/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
@ -10,34 +11,54 @@ type CommandFunc func(s *discordgo.Session, i *discordgo.InteractionCreate) (str
|
|||
|
||||
func HandleCommand(commandName string, cooldownDuration time.Duration, handler CommandFunc) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
// Get user information first
|
||||
user, userErr := GetUser(i)
|
||||
if userErr != nil {
|
||||
RespondWithError(s, i, "Error processing command: "+userErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Store user in database
|
||||
dbErr := StoreUser(user.ID, user.Username)
|
||||
if dbErr != nil {
|
||||
// Log the error but don't stop command execution
|
||||
log.Printf("Error storing user: %v", dbErr)
|
||||
}
|
||||
|
||||
// Rest of your existing HandleCommand logic...
|
||||
if !CheckAndApplyCooldown(s, i, commandName, cooldownDuration) {
|
||||
return
|
||||
}
|
||||
|
||||
if !CheckAndApplyCooldown(s, i, commandName, cooldownDuration) {
|
||||
return
|
||||
}
|
||||
|
||||
// Acknowledge the interaction immediately
|
||||
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
interactErr := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
})
|
||||
if err != nil {
|
||||
ThrowWithError(commandName, "Error deferring response: "+err.Error())
|
||||
|
||||
if interactErr != nil {
|
||||
ThrowWithError(commandName, "Error deferring response: "+interactErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the command handler
|
||||
response, err := handler(s, i)
|
||||
response, handlerErr := handler(s, i)
|
||||
|
||||
if err != nil {
|
||||
RespondWithError(s, i, "Error processing command: "+err.Error())
|
||||
if handlerErr != nil {
|
||||
RespondWithError(s, i, "Error processing command: "+handlerErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Send the follow-up message with the response
|
||||
_, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
|
||||
_, followErr := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
|
||||
Content: response,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ThrowWithError(commandName, "Error sending follow-up message: "+err.Error())
|
||||
if followErr != nil {
|
||||
ThrowWithError(commandName, "Error sending follow-up message: "+followErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
169
lib/db.go
Normal file
169
lib/db.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tursodatabase/go-libsql"
|
||||
)
|
||||
|
||||
var DBClient *sql.DB
|
||||
|
||||
func InitDB() error {
|
||||
dbUrl := os.Getenv("DATABASE_URL")
|
||||
dbToken := os.Getenv("DATABASE_AUTH_TOKEN")
|
||||
|
||||
if dbUrl == "" || dbToken == "" {
|
||||
return fmt.Errorf("database configuration missing")
|
||||
}
|
||||
|
||||
connector, err := libsql.NewEmbeddedReplicaConnector(
|
||||
"./himbot.db",
|
||||
dbUrl,
|
||||
libsql.WithAuthToken(dbToken),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
client := sql.OpenDB(connector)
|
||||
DBClient = client
|
||||
|
||||
return runMigrations()
|
||||
}
|
||||
|
||||
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 StoreUser(discordID, username string) error {
|
||||
// Check if user exists
|
||||
var exists bool
|
||||
err := DBClient.QueryRow(
|
||||
"SELECT EXISTS(SELECT 1 FROM users WHERE discord_id = ?)",
|
||||
discordID).Scan(&exists)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check user existence: %w", err)
|
||||
}
|
||||
|
||||
// If user doesn't exist, insert them
|
||||
if !exists {
|
||||
_, err = DBClient.Exec(
|
||||
"INSERT INTO users (discord_id, username) VALUES (?, ?)",
|
||||
discordID, username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store user: %w", err)
|
||||
}
|
||||
log.Printf("New user stored: %s (%s)", username, discordID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
9
main.go
9
main.go
|
@ -55,6 +55,11 @@ var (
|
|||
func main() {
|
||||
godotenv.Load(".env")
|
||||
|
||||
err := lib.InitDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
|
||||
token := os.Getenv("DISCORD_TOKEN")
|
||||
|
||||
if token == "" {
|
||||
|
@ -83,6 +88,10 @@ func main() {
|
|||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
<-sc
|
||||
|
||||
if lib.DBClient != nil {
|
||||
lib.DBClient.Close()
|
||||
}
|
||||
|
||||
dg.Close()
|
||||
}
|
||||
|
||||
|
|
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
|
||||
);
|
Loading…
Add table
Reference in a new issue