From 8722509344d6a0ffe5ce56d47b7470bbd602632a Mon Sep 17 00:00:00 2001 From: Michael McCarty Date: Mon, 14 Sep 2026 11:32:06 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feat:=20add=20thread=20rename=20cooldo?= =?UTF-8?q?wn=20and=20skip=20unchanged=20edits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/boost/boost_reactions.go | 4 ++-- src/boost/boost_slashcmd.go | 30 +++++++++++++++------------ src/boost/boost_speedrun.go | 4 ++-- src/boost/contract_test.go | 39 ++++++++++++++++++++++++++++++++++++ src/boost/thread.go | 4 ++++ src/dc/dctest/fake.go | 31 +++++++++++++++++++++------- 6 files changed, 88 insertions(+), 24 deletions(-) diff --git a/src/boost/boost_reactions.go b/src/boost/boost_reactions.go index 087a3833..d9fcc36c 100644 --- a/src/boost/boost_reactions.go +++ b/src/boost/boost_reactions.go @@ -135,8 +135,8 @@ func ReactionAdd(client dc.Client, e *dc.ReactionEvent) string { // Anyone can use these reactions switch e.EmojiName() { case "🌊": - if time.Since(contract.ThreadRenameTime) < 30*time.Second { - msg, err := client.SendMessage(e.ChannelID(), dc.Message{Content: fmt.Sprintf("🌊 thread renaming is on cooldown, try again ", contract.ThreadRenameTime.Add(30*time.Second).Unix())}) + if time.Since(contract.ThreadRenameTime) < ThreadRenameCooldown { + msg, err := client.SendMessage(e.ChannelID(), dc.Message{Content: fmt.Sprintf("🌊 thread renaming is on cooldown, try again ", contract.ThreadRenameTime.Add(ThreadRenameCooldown).Unix())}) if err == nil { time.AfterFunc(10*time.Second, func() { err := client.DeleteMessage(msg.ChannelID, msg.ID) diff --git a/src/boost/boost_slashcmd.go b/src/boost/boost_slashcmd.go index 8765a409..ecacebfc 100644 --- a/src/boost/boost_slashcmd.go +++ b/src/boost/boost_slashcmd.go @@ -30,27 +30,31 @@ func tokenSerialToInt(serial string) int32 { return 0 } +// ThreadRenameCooldown is the cooldown between Discord thread/channel renames (Discord limits PATCH /channels to 2 per 10m). +const ThreadRenameCooldown = 3 * time.Minute + // UpdateThreadName will update a threads name to the current contract state func UpdateThreadName(client dc.Client, contract *Contract) { - if contract == nil { + if contract == nil || client == nil { return } - contract.ThreadRenameTime = time.Now() - - var builder strings.Builder - builder.WriteString(generateThreadName(contract)) - contract.ThreadRenameTime = time.Now() - + desiredName := generateThreadName(contract) for _, loc := range contract.Location { + if loc == nil || loc.ChannelID == "" { + continue + } ch, err := client.Channel(loc.ChannelID) - if err == nil { + if err == nil && ch != nil && ch.IsThread { + // Skip editing if the thread already has the desired name + if ch.Name == desiredName { + continue + } - if ch.IsThread { - _, err := client.EditChannel(loc.ChannelID, builder.String()) - if err != nil { - log.Println("Error updating thread name", err) - } + contract.ThreadRenameTime = time.Now() + _, err := client.EditChannel(loc.ChannelID, desiredName) + if err != nil { + log.Println("Error updating thread name", err) } } } diff --git a/src/boost/boost_speedrun.go b/src/boost/boost_speedrun.go index 96fd0b20..35242f33 100644 --- a/src/boost/boost_speedrun.go +++ b/src/boost/boost_speedrun.go @@ -282,8 +282,8 @@ func speedrunReactions(client dc.Client, e *dc.ReactionEvent, contract *Contract } if e.EmojiName() == "🌊" { - if time.Since(contract.ThreadRenameTime) < 3*time.Minute { - msg, err := client.SendMessage(e.ChannelID(), dc.Message{Content: fmt.Sprintf("🌊 thread renaming is on cooldown, try again ", contract.ThreadRenameTime.Add(3*time.Minute).Unix())}) + if time.Since(contract.ThreadRenameTime) < ThreadRenameCooldown { + msg, err := client.SendMessage(e.ChannelID(), dc.Message{Content: fmt.Sprintf("🌊 thread renaming is on cooldown, try again ", contract.ThreadRenameTime.Add(ThreadRenameCooldown).Unix())}) if err == nil { time.AfterFunc(10*time.Second, func() { err := client.DeleteMessage(msg.ChannelID, msg.ID) diff --git a/src/boost/contract_test.go b/src/boost/contract_test.go index 711c71b1..6cf839ce 100644 --- a/src/boost/contract_test.go +++ b/src/boost/contract_test.go @@ -935,3 +935,42 @@ func TestRefreshBoostListMessage_EmptyMsgIDs(t *testing.T) { t.Errorf("expected 0 client calls when ListMsgID and ReactionID are empty, got %d calls: %v", len(client.Calls), client.Calls) } } + +func TestUpdateThreadName_SkipIfUnchanged(t *testing.T) { + contract := &Contract{ + ContractHash: "test-contract-skip-rename", + ContractID: "test-contract", + CoopID: "test-coop", + CoopSize: 5, + State: ContractStateSignup, + Location: []*LocationData{{ + GuildID: "guild1", + ChannelID: "thread1", + }}, + } + expectedName := generateThreadName(contract) + + // Case 1: Thread already has desired name -> should not call EditChannel + client := dctest.New(). + WithGuild("guild1", "Guild 1"). + WithThread("thread1", "guild1", "parent1", expectedName) + + UpdateThreadName(client, contract) + if client.Called("EditChannel") { + t.Errorf("expected EditChannel NOT to be called when name is already matching, calls: %v", client.Calls) + } + + // Case 2: Thread has a different name -> should call EditChannel + client2 := dctest.New(). + WithGuild("guild1", "Guild 1"). + WithThread("thread1", "guild1", "parent1", "Old Thread Name") + + UpdateThreadName(client2, contract) + if !client2.Called("EditChannel") { + t.Errorf("expected EditChannel to be called when thread name changed") + } + edits := client2.CallsTo("EditChannel") + if len(edits) != 1 || edits[0].Args[1] != expectedName { + t.Errorf("expected EditChannel with name %q, got: %v", expectedName, edits) + } +} diff --git a/src/boost/thread.go b/src/boost/thread.go index 0eff0c1b..8a523ed0 100644 --- a/src/boost/thread.go +++ b/src/boost/thread.go @@ -78,6 +78,10 @@ func HandleRenameThreadCommand(client dc.Client, e *dc.CommandEvent) { if c.ThreadName != "" { fmt.Fprintf(&builder, "\nThe thread name is currently set to:\n> %s", c.ThreadName) } + + if time.Since(c.ThreadRenameTime) < ThreadRenameCooldown { + fmt.Fprintf(&builder, "\n\nāš ļø Thread renaming is on cooldown until .", c.ThreadRenameTime.Add(ThreadRenameCooldown).Unix()) + } } _ = e.Respond(dc.Message{ diff --git a/src/dc/dctest/fake.go b/src/dc/dctest/fake.go index e1bb8616..b4362e27 100644 --- a/src/dc/dctest/fake.go +++ b/src/dc/dctest/fake.go @@ -51,10 +51,11 @@ type FakeClient struct { // ErrNotFound. Threads map[string][]dc.Channel - // SendErr, EditErr and DeleteErr are returned by the message methods. - SendErr error - EditErr error - DeleteErr error + // SendErr, EditErr, DeleteErr and ChannelEditErr are returned by the respective client methods. + SendErr error + EditErr error + DeleteErr error + ChannelEditErr error // NextMessageID is the ID handed back by SendMessage. It is suffixed with // the call count so repeated sends do not collide. @@ -88,15 +89,17 @@ func (f *FakeClient) WithChannel(id, guildID, name string) *FakeClient { } // WithThread registers an active thread the fake will return from -// ActiveThreads for the thread's guild. +// ActiveThreads for the thread's guild and Channel. func (f *FakeClient) WithThread(id, guildID, parentID, name string) *FakeClient { - f.Threads[guildID] = append(f.Threads[guildID], dc.Channel{ + th := dc.Channel{ ID: id, GuildID: guildID, ParentID: parentID, Name: name, IsThread: true, - }) + } + f.Channels[id] = &th + f.Threads[guildID] = append(f.Threads[guildID], th) return f } @@ -248,6 +251,20 @@ func (f *FakeClient) EditMessage(channelID, messageID string, m dc.Message) (*dc return &dc.MessageRef{ID: messageID, ChannelID: channelID, Content: m.Content}, nil } +// EditChannel records the channel edit and updates the channel's name in the fake cache. +func (f *FakeClient) EditChannel(channelID, name string) (*dc.Channel, error) { + f.record("EditChannel", channelID, name) + if f.ChannelEditErr != nil { + return nil, f.ChannelEditErr + } + ch, ok := f.Channels[channelID] + if !ok { + return nil, ErrNotFound + } + ch.Name = name + return ch, nil +} + // DeleteMessage records the delete. func (f *FakeClient) DeleteMessage(channelID, messageID string) error { f.record("DeleteMessage", channelID, messageID)