Refactor: cleanup

- Introduce tracing
- Introduce cobra / viper framework
- Introduce resty client
- Seperate files in api/ and bot/
- Trim unused functions
This commit is contained in:
2025-12-12 23:51:48 +08:00
parent 344176063b
commit cb11672817
15 changed files with 575 additions and 466 deletions

View File

@@ -1,18 +1,17 @@
package api package api
import ( import (
"bytes" "errors"
"encoding/json"
"fmt"
"io"
"net/http"
"time" "time"
"github.com/go-resty/resty/v2"
"github.com/spf13/viper"
) )
var ErrRequestFailed = errors.New("request failed")
type Client struct { type Client struct {
baseURL string client *resty.Client
httpClient *http.Client
token string
} }
type Image struct { type Image struct {
@@ -31,234 +30,12 @@ type AliasesResponse struct {
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
} }
type GenLoginURLRequest struct { func NewClient() *Client {
UserID string `json:"userId"` client := resty.New()
} client.SetBaseURL(viper.GetString("api-endpoint"))
client.SetAuthToken(viper.GetString("preshared-key"))
type GenLoginURLResponse struct {
LoginURL string `json:"loginUrl"`
}
func NewClient(baseURL string) *Client {
return &Client{ return &Client{
baseURL: baseURL, client: client,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
} }
} }
func (c *Client) SetToken(token string) {
c.token = token
}
func (c *Client) GetImages(search string, limit, page int) (*ImagesResponse, error) {
url := fmt.Sprintf("%s/api/images?search=%s&limit=%d&page=%d", c.baseURL, search, limit, page)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
var result ImagesResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
func (c *Client) GetImage(id string) (*Image, error) {
url := fmt.Sprintf("%s/api/images/%s", c.baseURL, id)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
var image Image
if err := json.NewDecoder(resp.Body).Decode(&image); err != nil {
return nil, err
}
return &image, nil
}
func (c *Client) GetAliases() (*AliasesResponse, error) {
url := fmt.Sprintf("%s/api/aliases", c.baseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
var result AliasesResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
func (c *Client) UploadImage(imageData []byte, aliases []string) (*Image, error) {
// TODO: Implement multipart form upload
return nil, fmt.Errorf("not implemented")
}
func (c *Client) DeleteImage(id string) error {
url := fmt.Sprintf("%s/api/images/%s", c.baseURL, id)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("API returned status %d", resp.StatusCode)
}
return nil
}
func (c *Client) AddAlias(imageID, alias string) (*Image, error) {
url := fmt.Sprintf("%s/api/images/%s/aliases", c.baseURL, imageID)
body := map[string]string{"alias": alias}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
var image Image
if err := json.NewDecoder(resp.Body).Decode(&image); err != nil {
return nil, err
}
return &image, nil
}
func (c *Client) GetImageFile(id string) ([]byte, error) {
url := fmt.Sprintf("%s/api/images/%s/file", c.baseURL, id)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
func (c *Client) GenerateLoginURL(userID string) (string, error) {
url := fmt.Sprintf("%s/auth/gen-login-url", c.baseURL)
reqBody := GenLoginURLRequest{UserID: userID}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
// TODO: Set preshared key authorization
req.Header.Set("Authorization", "Bearer poop")
resp, err := c.httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var result GenLoginURLResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
return result.LoginURL, nil
}

33
api/getImages.go Normal file
View File

@@ -0,0 +1,33 @@
package api
import (
"net/http"
"strconv"
"github.com/Penguin-71630/meme-bot-frontend-dc/tracing"
"go.uber.org/zap"
)
func (c *Client) GetImages(
search string,
limit, page int,
) (ImagesResponse, error) {
var res ImagesResponse
resp, err := c.client.R().
SetResult(&res).
SetQueryParam("search", search).
SetQueryParam("limit", strconv.Itoa(limit)).
SetQueryParam("page", strconv.Itoa(page)).
Get("/api/images")
if err != nil || resp.StatusCode() != http.StatusOK {
tracing.Logger.Ctx(resp.Request.Context()).
Error("failed to get api images",
zap.String("search", search),
zap.Int("limit", limit),
zap.Int("page", page),
zap.Error(err))
return ImagesResponse{}, err
}
return res, nil
}

32
api/postGenLoginUrl.go Normal file
View File

@@ -0,0 +1,32 @@
package api
import (
"net/http"
"github.com/Penguin-71630/meme-bot-frontend-dc/tracing"
"go.uber.org/zap"
)
type GenLoginURLRequest struct {
UserId string `json:"userId"`
}
type GenLoginURLResponse struct {
LoginURL string `json:"loginUrl"`
}
func (c *Client) PostGenLoginURL(
userId string,
) (string, error) {
var res GenLoginURLResponse
resp, err := c.client.R().
SetBody(GenLoginURLRequest{UserId: userId}).
Post("/auth/gen-login-url")
if err != nil || resp.StatusCode() != http.StatusOK {
tracing.Logger.Ctx(resp.Request.Context()).
Error("failed to post gen-login-url",
zap.Error(err))
return "", ErrRequestFailed
}
return res.LoginURL, nil
}

View File

@@ -1,85 +1,81 @@
package bot package bot
import ( import (
"fmt" "context"
"log" "log"
"github.com/Penguin-71630/meme-bot-frontend-dc/api" "github.com/Penguin-71630/meme-bot-frontend-dc/api"
"github.com/Penguin-71630/meme-bot-frontend-dc/config" "github.com/Penguin-71630/meme-bot-frontend-dc/tracing"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/spf13/viper"
"go.uber.org/zap"
) )
var commands = []*discordgo.ApplicationCommand{
{
Name: "ping",
Description: "Check if bot is responsive",
},
{
Name: "greet",
Description: "Get a friendly greeting",
},
{
Name: "echo",
Description: "Bot repeats what you say",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Message to echo",
Required: true,
},
},
},
{
Name: "web",
Description: "Get a login link to the web interface",
},
}
type Bot struct { type Bot struct {
session *discordgo.Session session *discordgo.Session
config *Config
apiClient *api.Client apiClient *api.Client
} }
type Config struct { func New() (*Bot, error) {
Token string
APIClient *api.Client
}
func New(cfg *config.Config) (*Bot, error) {
// Create Discord session // Create Discord session
session, err := discordgo.New("Bot " + cfg.DiscordToken) session, err := discordgo.New("Bot " + viper.GetString("discord-bot-token"))
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating Discord session: %w", err) return nil, err
} }
// Create API client
apiClient := api.NewClient(cfg.APIBaseURL)
bot := &Bot{ bot := &Bot{
session: session, session: session,
apiClient: apiClient, apiClient: api.NewClient(),
config: &Config{
Token: cfg.DiscordToken,
APIClient: apiClient,
},
} }
// Register handlers // Register handlers
bot.registerHandlers() bot.registerHandlers()
// Set intents - only need guild messages for the ciallo listener // Set intents - only need guild messages for the ciallo listener
session.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsDirectMessages | discordgo.IntentsMessageContent session.Identify.Intents = discordgo.IntentsGuildMessages |
discordgo.IntentsDirectMessages |
discordgo.IntentsMessageContent
return bot, nil return bot, nil
} }
func (b *Bot) registerSlashCommands(guildID string) error { func (b *Bot) registerSlashCommands(ctx context.Context) error {
commands := []*discordgo.ApplicationCommand{
{
Name: "ping",
Description: "Check if bot is responsive",
},
{
Name: "greet",
Description: "Get a friendly greeting",
},
{
Name: "echo",
Description: "Bot repeats what you say",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Message to echo",
Required: true,
},
},
},
{
Name: "web",
Description: "Get a login link to the web interface",
},
}
for _, cmd := range commands { for _, cmd := range commands {
_, err := b.session.ApplicationCommandCreate(b.session.State.User.ID, guildID, cmd) _, err := b.session.ApplicationCommandCreate(
b.session.State.User.ID, "", cmd)
if err != nil { if err != nil {
return fmt.Errorf("cannot create command %s: %w", cmd.Name, err) tracing.Logger.Ctx(ctx).
Error("failed to create command",
zap.String("command", cmd.Name),
zap.Error(err))
return err
} }
} }
@@ -107,8 +103,15 @@ func (b *Bot) registerHandlers() {
b.session.AddHandler(b.onInteractionCreate) b.session.AddHandler(b.onInteractionCreate)
} }
func (b *Bot) onReady(s *discordgo.Session, event *discordgo.Ready) { func (b *Bot) onReady(
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator) s *discordgo.Session,
event *discordgo.Ready,
) {
ctx := context.Background()
tracing.Logger.Ctx(ctx).
Info("logged in",
zap.String("username", s.State.User.Username),
zap.String("discriminator", s.State.User.Discriminator))
// For development: set your guild ID here for instant updates // For development: set your guild ID here for instant updates
// For production: use "" for global commands // For production: use "" for global commands
@@ -120,18 +123,27 @@ func (b *Bot) onReady(s *discordgo.Session, event *discordgo.Ready) {
} }
// Register slash commands // Register slash commands
if err := b.registerSlashCommands(guildID); err != nil { if err := b.registerSlashCommands(ctx); err != nil {
log.Printf("Error registering slash commands: %v", err) tracing.Logger.Ctx(ctx).
Error("failed to register slash commands",
zap.Error(err))
return
} }
// Set bot status // Set bot status
err := s.UpdateGameStatus(0, "/ping to check status") err := s.UpdateGameStatus(0, "/ping to check status")
if err != nil { if err != nil {
log.Printf("Error setting status: %v", err) tracing.Logger.Ctx(ctx).
Error("failed to set status",
zap.Error(err))
return
} }
} }
func (b *Bot) onInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { func (b *Bot) onInteractionCreate(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
if i.Type != discordgo.InteractionApplicationCommand { if i.Type != discordgo.InteractionApplicationCommand {
return return
} }
@@ -149,10 +161,7 @@ func (b *Bot) onInteractionCreate(s *discordgo.Session, i *discordgo.Interaction
} }
func (b *Bot) Start() error { func (b *Bot) Start() error {
if err := b.session.Open(); err != nil { return b.session.Open()
return fmt.Errorf("error opening connection: %w", err)
}
return nil
} }
func (b *Bot) Stop() { func (b *Bot) Stop() {

View File

@@ -1,101 +0,0 @@
package bot
import (
"fmt"
"strings"
"github.com/bwmarrin/discordgo"
)
// Message listener for "ciallo"
func (b *Bot) onMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore messages from the bot itself
if m.Author.ID == s.State.User.ID {
return
}
// Check if message is "ciallo" (case insensitive)
if strings.ToLower(strings.TrimSpace(m.Content)) == "ciallo" {
s.ChannelMessageSend(m.ChannelID, "Ciallo!")
}
}
// Slash command handlers
func (b *Bot) handleSlashPing(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "pong",
},
})
}
func (b *Bot) handleSlashGreet(s *discordgo.Session, i *discordgo.InteractionCreate) {
var username string
if i.Member != nil {
username = i.Member.User.Username
} else if i.User != nil {
username = i.User.Username
} else {
username = "Unknown"
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Ciallo, %s!", username),
},
})
}
func (b *Bot) handleSlashEcho(s *discordgo.Session, i *discordgo.InteractionCreate) {
options := i.ApplicationCommandData().Options
if len(options) == 0 {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "No message provided!",
},
})
return
}
message := options[0].StringValue()
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
},
})
}
func (b *Bot) handleSlashWeb(s *discordgo.Session, i *discordgo.InteractionCreate) {
var userID string
if i.Member != nil {
userID = i.Member.User.ID
} else if i.User != nil {
userID = i.User.ID
}
// Call backend API
loginURL, err := b.apiClient.GenerateLoginURL(userID)
if err != nil {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Failed to generate login URL: " + err.Error(),
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("🔗 **Click here to access the web page:**\n%s\n\n", loginURL),
Flags: discordgo.MessageFlagsEphemeral,
},
})
}

28
bot/handleSlashEcho.go Normal file
View File

@@ -0,0 +1,28 @@
package bot
import "github.com/bwmarrin/discordgo"
func (b *Bot) handleSlashEcho(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
options := i.ApplicationCommandData().Options
if len(options) == 0 {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "No message provided!",
},
})
return
}
message := options[0].StringValue()
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
},
})
}

28
bot/handleSlashGreet.go Normal file
View File

@@ -0,0 +1,28 @@
package bot
import (
"fmt"
"github.com/bwmarrin/discordgo"
)
func (b *Bot) handleSlashGreet(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
var username string
if i.Member != nil {
username = i.Member.User.Username
} else if i.User != nil {
username = i.User.Username
} else {
username = "Unknown"
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Ciallo, %s!", username),
},
})
}

15
bot/handleSlashPing.go Normal file
View File

@@ -0,0 +1,15 @@
package bot
import "github.com/bwmarrin/discordgo"
func (b *Bot) handleSlashPing(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "pong",
},
})
}

47
bot/handleSlashWeb.go Normal file
View File

@@ -0,0 +1,47 @@
package bot
import (
"context"
"github.com/Penguin-71630/meme-bot-frontend-dc/tracing"
"github.com/bwmarrin/discordgo"
"go.uber.org/zap"
)
func (b *Bot) handleSlashWeb(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
var userID string
if i.Member != nil {
userID = i.Member.User.ID
} else if i.User != nil {
userID = i.User.ID
}
// Call backend API
loginURL, err := b.apiClient.PostGenLoginURL(userID)
if err != nil {
tracing.Logger.Ctx(context.Background()).
Error("failed to generate login url",
zap.Error(err))
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Failed to generate login URL",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
content := "🔗 **Click here to access the web page:**\n"+
loginURL + "\n\n"
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: content,
Flags: discordgo.MessageFlagsEphemeral,
},
})
}

23
bot/onMessageCreate.go Normal file
View File

@@ -0,0 +1,23 @@
package bot
import (
"strings"
"github.com/bwmarrin/discordgo"
)
// Message listener for "ciallo"
func (b *Bot) onMessageCreate(
s *discordgo.Session,
m *discordgo.MessageCreate,
) {
// Ignore messages from the bot itself
if m.Author.ID == s.State.User.ID {
return
}
// Check if message is "ciallo" (case insensitive)
if strings.ToLower(strings.TrimSpace(m.Content)) == "ciallo" {
s.ChannelMessageSend(m.ChannelID, "Ciallo!")
}
}

View File

@@ -1,28 +0,0 @@
package config
import (
"errors"
"os"
)
type Config struct {
DiscordToken string
APIBaseURL string
}
func Load() (*Config, error) {
token := os.Getenv("DISCORD_BOT_TOKEN")
if token == "" {
return nil, errors.New("DISCORD_BOT_TOKEN is required")
}
apiURL := os.Getenv("API_BASE_URL")
if apiURL == "" {
apiURL = "http://localhost:8080" // Default
}
return &Config{
DiscordToken: token,
APIBaseURL: apiURL,
}, nil
}

53
go.mod
View File

@@ -1,14 +1,59 @@
module github.com/Penguin-71630/meme-bot-frontend-dc module github.com/Penguin-71630/meme-bot-frontend-dc
go 1.21 go 1.24.0
require ( require (
github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/discordgo v0.27.1
github.com/joho/godotenv v1.5.1 github.com/go-resty/resty/v2 v2.17.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2
github.com/uptrace/uptrace-go v1.39.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
go.uber.org/zap v1.27.1
) )
require ( require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
golang.org/x/sys v0.13.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
) )

137
go.sum
View File

@@ -1,17 +1,142 @@
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0=
github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0=
github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw=
github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 h1:cj/Z6FKTTYBnstI0Lni9PA+k2foounKIPUmj1LBwNiQ=
github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2/go.mod h1:LDaXk90gKEC2nC7JH3Lpnhfu+2V7o/TsqomJJmqA39o=
github.com/uptrace/uptrace-go v1.39.0 h1:MszuE3eX/z86xzYywN2JBtYcmsS4ofdo1VMDhRvkWrI=
github.com/uptrace/uptrace-go v1.39.0/go.mod h1:FquipEqgTMXPbhdhenjbiLHG1R5WYdxVH6zgwHeMzzA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 h1:/+/+UjlXjFcdDlXxKL1PouzX8Z2Vl0OxolRKeBEgYDw=
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0/go.mod h1:Ldm/PDuzY2DP7IypudopCR3OCOW42NJlN9+mNEroevo=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

93
main.go
View File

@@ -2,47 +2,76 @@ package main
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"github.com/Penguin-71630/meme-bot-frontend-dc/bot" "github.com/Penguin-71630/meme-bot-frontend-dc/bot"
"github.com/Penguin-71630/meme-bot-frontend-dc/config" "github.com/Penguin-71630/meme-bot-frontend-dc/tracing"
"github.com/joho/godotenv" "github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
) )
func main() { var rootCmd = &cobra.Command{
// Load environment variables Use: "dcbot",
if err := godotenv.Load(); err != nil { PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.Println("No .env file found, using system environment variables") viper.AutomaticEnv()
} viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.BindPFlags(cmd.PersistentFlags())
viper.BindPFlags(cmd.Flags())
},
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
// Load configuration appname := "go2025-dcbot"
cfg, err := config.Load() tracing.InitTracer(appname)
if err != nil { if viper.GetString("uptrace-dsn") != "" {
log.Fatalf("Failed to load configuration: %v", err) tracing.InitUptrace(appname)
} defer tracing.DeferUptrace(ctx)
}
// Initialize bot // Initialize bot
discordBot, err := bot.New(cfg) discordBot, err := bot.New()
if err != nil { if err != nil {
log.Fatalf("Failed to create bot: %v", err) tracing.Logger.Ctx(ctx).
} Panic("failed to create bot",
zap.Error(err))
panic(err)
}
// Start bot // Start bot
if err := discordBot.Start(); err != nil { if err := discordBot.Start(); err != nil {
log.Fatalf("Failed to start bot: %v", err) tracing.Logger.Ctx(ctx).
} Panic("failed to start bot",
zap.Error(err))
panic(err)
}
fmt.Println("Bot is now running. Press CTRL-C to exit.")
defer discordBot.Stop()
fmt.Println("Bot is now running. Press CTRL-C to exit.") // Wait for interrupt signal
sc := make(chan os.Signal, 1)
// Wait for interrupt signal signal.Notify(sc,
sc := make(chan os.Signal, 1) syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-sc
<-sc },
}
// Cleanup
discordBot.Stop() func init() {
fmt.Println("Bot stopped gracefully.") cobra.EnableTraverseRunHooks = true
rootCmd.Flags().
String("discord-bot-token", "", "discord bot token")
rootCmd.Flags().
String("api-endpoint", "http://localhost:8080", "api endpoint")
rootCmd.Flags().
Bool("zap-production", true, "Toggle production log format")
rootCmd.Flags().
String("uptrace-dsn", "", "Uptrace DSN (disabled by default)")
}
func main() {
rootCmd.Execute()
} }

47
tracing/tracer.go Normal file
View File

@@ -0,0 +1,47 @@
package tracing
import (
"context"
"github.com/spf13/viper"
"github.com/uptrace/opentelemetry-go-extra/otelzap"
"github.com/uptrace/uptrace-go/uptrace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
var (
Tracer trace.Tracer
Logger *otelzap.Logger
version string = "v0.0.1"
)
func InitTracer(appname string) {
Tracer = otel.Tracer(appname)
var l *zap.Logger
var err error
if viper.GetBool("zap-production") {
l, err = zap.NewProduction()
} else {
l, err = zap.NewDevelopment()
}
if err != nil {
panic(err)
}
Logger = otelzap.New(l)
}
func InitUptrace(appname string) {
uptrace.ConfigureOpentelemetry(
uptrace.WithDSN(viper.GetString("uptrace-dsn")),
uptrace.WithServiceName(appname),
uptrace.WithServiceVersion(version),
)
}
func DeferUptrace(ctx context.Context) {
uptrace.Shutdown(ctx)
}