From c6fba2f8630b6907b8b2449bcf1c029bb8fd7045 Mon Sep 17 00:00:00 2001 From: Elisiei Yehorov Date: Wed, 1 Oct 2025 00:54:13 +0200 Subject: [PATCH] ratelimit --- cmd/bot/register.go | 1 + go.mod | 3 +- go.sum | 2 + internal/bot/commands.go | 14 +++++-- internal/bot/events.go | 7 +++- internal/commands/register/register.go | 51 ++++++++++++++++++++++++++ internal/lastfm/api/api.go | 24 ++++++++---- 7 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 internal/commands/register/register.go diff --git a/cmd/bot/register.go b/cmd/bot/register.go index 1b13432..7ad1c49 100644 --- a/cmd/bot/register.go +++ b/cmd/bot/register.go @@ -3,5 +3,6 @@ package main import ( _ "first.fm/internal/commands/fm" _ "first.fm/internal/commands/profile" + _ "first.fm/internal/commands/register" _ "first.fm/internal/commands/stats" ) diff --git a/go.mod b/go.mod index a78f27d..42f2130 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.25.0 require ( github.com/disgoorg/disgo v0.19.0-rc.6.0.20250924005456-3274c76733fc github.com/disgoorg/snowflake/v2 v2.0.3 + github.com/mattn/go-sqlite3 v1.14.32 + golang.org/x/time v0.13.0 ) require ( github.com/disgoorg/json/v2 v2.0.0 // indirect github.com/disgoorg/omit v1.0.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 6de9bea..0864e5e 100644 --- a/go.sum +++ b/go.sum @@ -22,5 +22,7 @@ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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/internal/bot/commands.go b/internal/bot/commands.go index 53854dc..01fa453 100644 --- a/internal/bot/commands.go +++ b/internal/bot/commands.go @@ -3,8 +3,10 @@ package bot import ( "context" "strings" + "sync" "time" + "first.fm/internal/emojis" "first.fm/internal/lastfm" "first.fm/internal/logger" "github.com/disgoorg/disgo/discord" @@ -23,9 +25,15 @@ type CommandHandler func(*CommandContext) error var ( allCommands []discord.ApplicationCommandCreate registry = map[string]CommandHandler{} + initOnce sync.Once ) func Register(meta discord.ApplicationCommandCreate, handler CommandHandler) { + initOnce.Do(func() { + allCommands = []discord.ApplicationCommandCreate{} + registry = map[string]CommandHandler{} + }) + logger.Infow("registered command", logger.F{"name": meta.CommandName()}) allCommands = append(allCommands, meta) registry[meta.CommandName()] = handler @@ -58,9 +66,9 @@ func Dispatcher(bot *Bot) func(*events.ApplicationCommandInteractionCreate) { } if err := handler(ctx); err != nil { - logger.Errorw("command failed", logger.F{"name": data.CommandName(), "err": err.Error()}) - _ = event.CreateMessage(discord.NewMessageCreateBuilder(). - SetContent("error: " + err.Error()). + logger.Warnw("command failed", logger.F{"name": data.CommandName(), "err": err.Error()}) + _ = ctx.CreateMessage(discord.NewMessageCreateBuilder(). + SetContentf("%s %v", emojis.EmojiCross, err). SetEphemeral(true). Build()) } diff --git a/internal/bot/events.go b/internal/bot/events.go index 25edf5e..834f8cc 100644 --- a/internal/bot/events.go +++ b/internal/bot/events.go @@ -10,5 +10,10 @@ import ( func onReady(event *events.Ready) { logger.Info("started client") - event.Client().SetPresence(context.Background(), gateway.WithCustomActivity("gwa gwa")) + event. + Client(). + SetPresence( + context.Background(), + gateway.WithCustomActivity("listen to crystal castles!"), + ) } diff --git a/internal/commands/register/register.go b/internal/commands/register/register.go new file mode 100644 index 0000000..d368c74 --- /dev/null +++ b/internal/commands/register/register.go @@ -0,0 +1,51 @@ +package register + +import ( + "errors" + "strings" + + "first.fm/internal/bot" + "first.fm/internal/persistence/sqlc" + "github.com/disgoorg/disgo/discord" +) + +func init() { + bot.Register(data, handle) +} + +var data = discord.SlashCommandCreate{ + Name: "register", + Description: "link your last.fm username", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "username", + Description: "your last.fm username", + Required: true, + }, + }, +} + +func handle(ctx *bot.CommandContext) error { + username := ctx.SlashCommandInteractionData().String("username") + + _, err := ctx.LastFM.User.Info(username) + if err != nil { + return errors.New("last.fm user not found") + } + + err = ctx.Queries.UpsertUser(ctx.Ctx, sqlc.UpsertUserParams{ + UserID: ctx.User().ID, + LastfmUsername: username, + }) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + return errors.New("another discord user already uses this username") + } + + return err + } + + return ctx.CreateMessage(discord.NewMessageCreateBuilder(). + SetContentf("successfully linked your account to **%s**", username). + Build()) +} diff --git a/internal/lastfm/api/api.go b/internal/lastfm/api/api.go index c7e4cbd..20c53d0 100644 --- a/internal/lastfm/api/api.go +++ b/internal/lastfm/api/api.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/xml" "errors" "fmt" @@ -11,6 +12,7 @@ import ( "time" "first.fm/internal/lastfm" + "golang.org/x/time/rate" ) var ( @@ -37,10 +39,11 @@ type HTTPClient interface { } type API struct { - APIKey string - UserAgent string - Retries uint - Client HTTPClient + APIKey string + UserAgent string + Retries uint + Client HTTPClient + rateLimiter *rate.Limiter } func New(apiKey string) *API { @@ -50,10 +53,11 @@ func New(apiKey string) *API { func NewWithTimeout(apiKey string, timeout int) *API { t := time.Duration(timeout) * time.Second return &API{ - APIKey: apiKey, - UserAgent: DefaultUserAgent, - Retries: DefaultRetries, - Client: &http.Client{Timeout: t}, + APIKey: apiKey, + UserAgent: DefaultUserAgent, + Retries: DefaultRetries, + Client: &http.Client{Timeout: t}, + rateLimiter: rate.NewLimiter(rate.Every(time.Second), 5), } } @@ -110,6 +114,10 @@ func (a API) PostBody(dest any, url, body string) error { } func (a API) tryRequest(dest any, method, url, body string) error { + if err := a.rateLimiter.Wait(context.Background()); err != nil { + return err + } + var ( res *http.Response lfm LFMWrapper