diff --git a/src/boost/boost_button_reactions.go b/src/boost/boost_button_reactions.go index 343d0dc2..ac246fc4 100644 --- a/src/boost/boost_button_reactions.go +++ b/src/boost/boost_button_reactions.go @@ -394,6 +394,7 @@ func buttonReactionRunChickens(client dc.Client, contract *Contract, cUserID str go func() { client := client + sendSuccess := false for _, location := range contract.Location { contract.mutex.Lock() components, _ := buildCRMessageComponents(contract, location.RoleMention) @@ -418,6 +419,7 @@ func buttonReactionRunChickens(client dc.Client, contract *Contract, cUserID str continue } + sendSuccess = true contract.mutex.Lock() setChickenRunMessageID(contract, location.ChannelID, newMsg.ID) contract.CRNoticeCount++ @@ -438,6 +440,17 @@ func buttonReactionRunChickens(client dc.Client, contract *Contract, cUserID str } } } + + if !sendSuccess { + // Rollback RunChickensTime so the user isn't permanently locked out of retrying + contract.mutex.Lock() + if booster := contract.Boosters[userID]; booster != nil { + booster.RunChickensTime = time.Time{} + } + contract.mutex.Unlock() + } else { + saveData(contract.ContractHash) + } }() str = "You've asked for Chicken Runs, now what...\n...\nMaybe.. check on your habs and gusset?\nI'm sure you've already forced a game sync so no need to remind about that." return true, str diff --git a/src/boost/chicken_run_test.go b/src/boost/chicken_run_test.go index ff23319b..54d71333 100644 --- a/src/boost/chicken_run_test.go +++ b/src/boost/chicken_run_test.go @@ -1,12 +1,14 @@ package boost import ( + "errors" "slices" "strings" "testing" "time" "github.com/mkmccarty/TokenTimeBoostBot/src/dc" + "github.com/mkmccarty/TokenTimeBoostBot/src/dc/dctest" ) func TestRanCoopAndBuildChickenRunLists(t *testing.T) { @@ -199,3 +201,49 @@ func TestBuildCRMessageComponentsCompleted(t *testing.T) { t.Errorf("expected completion message with Player1, got %q", textDisplay.Content) } } + +func TestButtonReactionRunChickensSendFailureRollback(t *testing.T) { + c := &Contract{ + ContractHash: "test-hash-rollback", + Order: []string{"user1", "user2"}, + Location: []*LocationData{ + { + GuildID: "guild1", + ChannelID: "channel1", + }, + }, + CRMessageIDs: make(map[string]string), + Boosters: map[string]*Booster{ + "user1": { + UserID: "user1", + Nick: "Player1", + BoostState: BoostStateBoosted, + }, + "user2": { + UserID: "user2", + Nick: "Player2", + BoostState: BoostStateBoosted, + }, + }, + } + + client := dctest.New() + client.SendErr = errors.New("failed to send CR message") + + // Trigger buttonReactionRunChickens + ok, _ := buttonReactionRunChickens(client, c, "user1") + if !ok { + t.Fatalf("expected buttonReactionRunChickens to return true") + } + + // Wait for goroutine to finish + time.Sleep(50 * time.Millisecond) + + c.mutex.Lock() + runTime := c.Boosters["user1"].RunChickensTime + c.mutex.Unlock() + + if !runTime.IsZero() { + t.Errorf("expected RunChickensTime to be rolled back to zero on send error, got %v", runTime) + } +} diff --git a/src/dc/client_disgo.go b/src/dc/client_disgo.go index c620d367..e0ba26b5 100644 --- a/src/dc/client_disgo.go +++ b/src/dc/client_disgo.go @@ -2,10 +2,12 @@ package dc import ( "context" + "errors" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/disgo/rest" "github.com/disgoorg/snowflake/v2" ) @@ -52,7 +54,16 @@ func (c *disgoClient) SendMessage(channelID string, m Message) (*MessageRef, err } msg, err := c.bot.Rest.CreateMessage(ids[0], m.toMessageCreate()) if err != nil { - return nil, wrapAPIError(err) + var restErr *rest.Error + if errors.As(err, &restErr) && restErr.Code == ErrCodeThreadArchived { + unarchived := false + if _, updateErr := c.bot.Rest.UpdateChannel(ids[0], discord.GuildThreadUpdate{Archived: &unarchived}); updateErr == nil { + msg, err = c.bot.Rest.CreateMessage(ids[0], m.toMessageCreate()) + } + } + if err != nil { + return nil, wrapAPIError(err) + } } return messageRefFrom(msg), nil } diff --git a/src/dc/errors.go b/src/dc/errors.go index 04fcea77..0b4298b3 100644 --- a/src/dc/errors.go +++ b/src/dc/errors.go @@ -19,6 +19,9 @@ const ( // ErrCodeMissingPermissions means the bot can see the channel but is not // allowed the action it attempted. ErrCodeMissingPermissions = 50013 + // ErrCodeThreadArchived means the thread is archived and must be unarchived + // before messages can be sent to it. + ErrCodeThreadArchived = 50083 ) // APIError is a rejected Discord REST call. Code is Discord's own error code @@ -96,3 +99,10 @@ func IsUnknownChannel(err error) bool { } return apiErr.Code == ErrCodeUnknownChannel || apiErr.StatusCode == 404 } + +// IsThreadArchived reports whether err is Discord refusing a message send +// because the target thread is archived. +func IsThreadArchived(err error) bool { + apiErr, ok := AsAPIError(err) + return ok && apiErr.Code == ErrCodeThreadArchived +} diff --git a/src/dc/errors_test.go b/src/dc/errors_test.go index bf1e97d5..605ee03e 100644 --- a/src/dc/errors_test.go +++ b/src/dc/errors_test.go @@ -84,3 +84,15 @@ func TestIsUnknownChannel(t *testing.T) { t.Fatal("missing permissions is not unknown channel") } } + +func TestIsThreadArchived(t *testing.T) { + if !IsThreadArchived(restError(400, ErrCodeThreadArchived, "Thread is archived")) { + t.Fatal("expected thread is archived") + } + if IsThreadArchived(restError(404, ErrCodeUnknownChannel, "Unknown Channel")) { + t.Fatal("unknown channel is not thread archived") + } + if IsThreadArchived(errors.New("nope")) { + t.Fatal("a plain error is not thread archived") + } +}