diff --git a/api/client.go b/api/client.go index 1a2b7ef..332a82c 100644 --- a/api/client.go +++ b/api/client.go @@ -1,18 +1,17 @@ package api import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" + "errors" "time" + + "github.com/go-resty/resty/v2" + "github.com/spf13/viper" ) +var ErrRequestFailed = errors.New("request failed") + type Client struct { - baseURL string - httpClient *http.Client - token string + client *resty.Client } type Image struct { @@ -31,234 +30,12 @@ type AliasesResponse struct { Aliases []string `json:"aliases"` } -type GenLoginURLRequest struct { - UserID string `json:"userId"` -} +func NewClient() *Client { + 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{ - baseURL: baseURL, - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, + client: client, } } - -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 -} diff --git a/api/getImages.go b/api/getImages.go new file mode 100644 index 0000000..afd0e84 --- /dev/null +++ b/api/getImages.go @@ -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 +} diff --git a/api/postGenLoginUrl.go b/api/postGenLoginUrl.go new file mode 100644 index 0000000..053524b --- /dev/null +++ b/api/postGenLoginUrl.go @@ -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 +} diff --git a/bot/bot.go b/bot/bot.go index c0f8c67..9fd9d23 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -1,85 +1,81 @@ package bot import ( - "fmt" + "context" "log" "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/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 { session *discordgo.Session - config *Config apiClient *api.Client } -type Config struct { - Token string - APIClient *api.Client -} - -func New(cfg *config.Config) (*Bot, error) { +func New() (*Bot, error) { // Create Discord session - session, err := discordgo.New("Bot " + cfg.DiscordToken) + session, err := discordgo.New("Bot " + viper.GetString("discord-bot-token")) 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{ session: session, - apiClient: apiClient, - config: &Config{ - Token: cfg.DiscordToken, - APIClient: apiClient, - }, + apiClient: api.NewClient(), } // Register handlers bot.registerHandlers() // 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 } -func (b *Bot) registerSlashCommands(guildID string) 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", - }, - } - +func (b *Bot) registerSlashCommands(ctx context.Context) error { 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 { - 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) } -func (b *Bot) onReady(s *discordgo.Session, event *discordgo.Ready) { - log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator) +func (b *Bot) onReady( + 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 production: use "" for global commands @@ -120,18 +123,27 @@ func (b *Bot) onReady(s *discordgo.Session, event *discordgo.Ready) { } // Register slash commands - if err := b.registerSlashCommands(guildID); err != nil { - log.Printf("Error registering slash commands: %v", err) + if err := b.registerSlashCommands(ctx); err != nil { + tracing.Logger.Ctx(ctx). + Error("failed to register slash commands", + zap.Error(err)) + return } // Set bot status err := s.UpdateGameStatus(0, "/ping to check status") 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 { return } @@ -149,10 +161,7 @@ func (b *Bot) onInteractionCreate(s *discordgo.Session, i *discordgo.Interaction } func (b *Bot) Start() error { - if err := b.session.Open(); err != nil { - return fmt.Errorf("error opening connection: %w", err) - } - return nil + return b.session.Open() } func (b *Bot) Stop() { diff --git a/bot/commands.go b/bot/commands.go deleted file mode 100644 index 74d3912..0000000 --- a/bot/commands.go +++ /dev/null @@ -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, - }, - }) -} diff --git a/bot/handleSlashEcho.go b/bot/handleSlashEcho.go new file mode 100644 index 0000000..54d7767 --- /dev/null +++ b/bot/handleSlashEcho.go @@ -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, + }, + }) +} diff --git a/bot/handleSlashGreet.go b/bot/handleSlashGreet.go new file mode 100644 index 0000000..d8eccc3 --- /dev/null +++ b/bot/handleSlashGreet.go @@ -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), + }, + }) +} diff --git a/bot/handleSlashPing.go b/bot/handleSlashPing.go new file mode 100644 index 0000000..4a33643 --- /dev/null +++ b/bot/handleSlashPing.go @@ -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", + }, + }) +} diff --git a/bot/handleSlashWeb.go b/bot/handleSlashWeb.go new file mode 100644 index 0000000..98d14d3 --- /dev/null +++ b/bot/handleSlashWeb.go @@ -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, + }, + }) +} diff --git a/bot/onMessageCreate.go b/bot/onMessageCreate.go new file mode 100644 index 0000000..840d9ac --- /dev/null +++ b/bot/onMessageCreate.go @@ -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!") + } +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 54467b7..0000000 --- a/config/config.go +++ /dev/null @@ -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 -} diff --git a/go.mod b/go.mod index 547d194..77e1235 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,59 @@ module github.com/Penguin-71630/meme-bot-frontend-dc -go 1.21 +go 1.24.0 require ( 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 ( + 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 - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/sys v0.13.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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 ) diff --git a/go.sum b/go.sum index d666e98..5321c89 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,142 @@ 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/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.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 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/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +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.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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +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/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= +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= diff --git a/main.go b/main.go index 2380b34..b50d83f 100644 --- a/main.go +++ b/main.go @@ -2,47 +2,76 @@ package main import ( "fmt" - "log" "os" "os/signal" + "strings" "syscall" "github.com/Penguin-71630/meme-bot-frontend-dc/bot" - "github.com/Penguin-71630/meme-bot-frontend-dc/config" - "github.com/joho/godotenv" + "github.com/Penguin-71630/meme-bot-frontend-dc/tracing" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" ) -func main() { - // Load environment variables - if err := godotenv.Load(); err != nil { - log.Println("No .env file found, using system environment variables") - } +var rootCmd = &cobra.Command{ + Use: "dcbot", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + 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 - cfg, err := config.Load() - if err != nil { - log.Fatalf("Failed to load configuration: %v", err) - } + appname := "go2025-dcbot" + tracing.InitTracer(appname) + if viper.GetString("uptrace-dsn") != "" { + tracing.InitUptrace(appname) + defer tracing.DeferUptrace(ctx) + } - // Initialize bot - discordBot, err := bot.New(cfg) - if err != nil { - log.Fatalf("Failed to create bot: %v", err) - } + // Initialize bot + discordBot, err := bot.New() + if err != nil { + tracing.Logger.Ctx(ctx). + Panic("failed to create bot", + zap.Error(err)) + panic(err) + } - // Start bot - if err := discordBot.Start(); err != nil { - log.Fatalf("Failed to start bot: %v", err) - } + // Start bot + if err := discordBot.Start(); err != nil { + 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) - signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) - <-sc - - // Cleanup - discordBot.Stop() - fmt.Println("Bot stopped gracefully.") + // Wait for interrupt signal + sc := make(chan os.Signal, 1) + signal.Notify(sc, + syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-sc + }, +} + +func init() { + 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() } diff --git a/tracing/tracer.go b/tracing/tracer.go new file mode 100644 index 0000000..b58ed5f --- /dev/null +++ b/tracing/tracer.go @@ -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) +}