Files
banner/main.go

228 lines
7.2 KiB
Go

package main
import (
"context"
"fmt"
"net/http"
"net/http/cookiejar"
"os"
"os/signal"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
"github.com/cespare/xxhash/v2"
"github.com/cnf/structhash"
"github.com/joho/godotenv"
"github.com/redis/go-redis/v9"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
)
var (
ctx context.Context
kv *redis.Client
session *discordgo.Session
client http.Client
cookies http.CookieJar
isDevelopment bool
baseURL string // Base URL for all requests to the banner system
environment string
)
func init() {
ctx = context.Background()
// Try to grab the environment variable, or default to development
environment = GetFirstEnv("ENVIRONMENT", "RAILWAY_ENVIRONMENT")
if environment == "" {
environment = "development"
}
log.Debug().Str("environment", environment).Msg("")
// Use the custom console writer if we're in development
isDevelopment = environment == "development"
if isDevelopment {
log.Logger = zerolog.New(logOut{}).With().Timestamp().Logger()
}
// Set discordgo's logger to use zerolog
discordgo.Logger = DiscordGoLogger
// Load environment variables
if err := godotenv.Load(); err != nil {
log.Debug().Err(err).Msg("Error loading .env file")
}
baseURL = os.Getenv("BANNER_BASE_URL")
}
func main() {
// Setup redis
redisUrl := GetFirstEnv("REDIS_URL", "REDIS_PRIVATE_URL")
if redisUrl == "" {
log.Fatal().Msg("REDIS_URL/REDIS_PRIVATE_URL not set")
}
// Parse URL and create client
options, err := redis.ParseURL(redisUrl)
if err != nil {
log.Fatal().Err(err).Msg("Cannot parse redis url")
}
kv = redis.NewClient(options)
var lastPingErr error
pingCount := 0 // Nth ping being attempted
totalPings := 5 // Total pings to attempt
// Wait for private networking to kick in (production only)
if !isDevelopment {
time.Sleep(250 * time.Millisecond)
}
// Test the redis instance, try to ping every 2 seconds 5 times, otherwise panic
for {
pingCount++
if pingCount > totalPings {
log.Fatal().Err(lastPingErr).Msg("Reached ping limit while trying to connect")
}
pong, err := kv.Ping(ctx).Result()
if err != nil {
lastPingErr = err
log.Warn().Err(err).Int("pings", pingCount).Int("remaining", totalPings-pingCount).Msg("Cannot ping redis")
time.Sleep(2 * time.Second)
continue
}
log.Debug().Str("ping", pong).Msg("Redis connection successful")
break
}
// Create cookie jar
cookies, err = cookiejar.New(nil)
if err != nil {
log.Err(err).Msg("Cannot create cookie jar")
}
// Create client, setup session (acquire cookies)
client = http.Client{Jar: cookies}
setup()
// Create discord session
session, err = discordgo.New("Bot " + os.Getenv("BOT_TOKEN"))
if err != nil {
log.Err(err).Msg("Invalid bot parameters")
}
// Open discord session
session.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
log.Info().Str("username", r.User.Username).Str("discriminator", r.User.Discriminator).Str("id", r.User.ID).Str("session", s.State.SessionID).Msg("Bot is logged in")
})
err = session.Open()
if err != nil {
log.Fatal().Err(err).Msg("Cannot open the session")
}
// Setup command handlers
session.AddHandler(func(internalSession *discordgo.Session, interaction *discordgo.InteractionCreate) {
name := interaction.ApplicationCommandData().Name
if handler, ok := commandHandlers[name]; ok {
handler(internalSession, interaction)
} else {
log.Warn().Str("commandName", name).Msg("Unknown command")
}
})
// Register commands with discord
arr := zerolog.Arr()
lo.ForEach(commandDefinitions, func(cmd *discordgo.ApplicationCommand, _ int) {
arr.Str(cmd.Name)
})
log.Info().Array("commands", arr).Msg("Registering commands")
// Register commands
registeredCommands := make([]*discordgo.ApplicationCommand, len(commandDefinitions))
for i, cmdDefinition := range commandDefinitions {
// Create a hash of the command definition
hash := xxhash.Sum64(structhash.Dump(cmdDefinition, 1))
key := fmt.Sprintf("%s:command:%s:xxhash", environment, cmdDefinition.Name)
// Get the stored hash
storedHash, err := kv.Get(ctx, key).Uint64()
if err != nil {
if err != redis.Nil {
log.Err(err).Msg("Cannot get command hash from redis")
} else {
log.Debug().Str("command", cmdDefinition.Name).Str("key", key).Msg("Command hash not found in redis")
}
}
// If the hash is the same, skip registering the command
if hash == storedHash {
log.Debug().Str("command", cmdDefinition.Name).Str("key", key).Uint64("hash", hash).Msg("Command hash matches, skipping registration")
continue
}
log.Info().Str("command", cmdDefinition.Name).Str("key", key).Uint64("hash", hash).Uint64("storedHash", storedHash).Msg("Command hash mismatch, registering command")
// Unregister the old command first (retrieve the ID from redis)
oldCommandId, err := kv.Get(ctx, fmt.Sprintf("%s:command:%s:id", environment, cmdDefinition.Name)).Result()
if err != nil {
if err != redis.Nil {
// Not really sure what to do here, something failed with redis. Best to just keep the old commad in place.
log.Err(err).Str("name", cmdDefinition.Name).Str("key", key).Msg("Cannot get old command ID from redis (skipping registration/deletion)")
continue
} else {
// It's an unlikely case, but if required, we could get the old command ID from discord to unregisstter it.
log.Debug().Str("name", cmdDefinition.Name).Str("key", key).Msg("Old command ID not found in redis")
}
} else {
err = session.ApplicationCommandDelete(session.State.User.ID, "", oldCommandId)
if err != nil {
// No panic - the command is probably still registered.
log.Err(err).Str("name", cmdDefinition.Name).Str("key", key).Msg("Cannot unregister old command")
continue
} else {
log.Info().Str("name", cmdDefinition.Name).Str("key", key).Msg("Unregistered old command")
}
}
// Register the command
cmdInstance, err := session.ApplicationCommandCreate(session.State.User.ID, "", cmdDefinition)
if err != nil {
log.Panic().Err(err).Str("name", cmdDefinition.Name).Str("key", key).Msg("Cannot register command")
}
registeredCommands[i] = cmdInstance
log.Info().Str("name", cmdDefinition.Name).Str("key", key).Msg("Registered command")
// Store the hash for the new registered command
err = kv.Set(ctx, key, hash, 0).Err()
if err != nil {
// No panic - the command is still registered, hash is only to prevent unnecessary registrations
log.Err(err).Str("name", cmdDefinition.Name).Str("key", key).Msg("Cannot set command hash in redis")
continue
}
// Store the command ID to unregister later
err = kv.Set(ctx, fmt.Sprintf("%s:command:%s:id", environment, cmdDefinition.Name), cmdInstance.ID, 0).Err()
if err != nil {
// No
log.Err(err).Str("name", cmdDefinition.Name).Str("key", key).Msg("Cannot set command ID in redis")
}
}
// Cloes session, ensure http client closes idle connections
defer session.Close()
defer client.CloseIdleConnections()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt) // Ctrl+C signal
signal.Notify(stop, syscall.SIGTERM) // Container stop signal
<-stop
// Defers are called after this
log.Warn().Msg("Gracefully shutting down")
}