diff --git a/src/boost/boost.go b/src/boost/boost.go index 96651283..036841d1 100644 --- a/src/boost/boost.go +++ b/src/boost/boost.go @@ -893,8 +893,13 @@ func AddFarmerToContract(client dc.Client, contract *Contract, guildID string, c } for _, el := range contract.Location { - if el.GuildID == guildID && b.UserID != b.Name && el.GuildContractRole.ID != "" { - _ = client.AddGuildMemberRole(guildID, b.UserID, el.GuildContractRole.ID) + if (guildID == "" || el.GuildID == guildID) && dc.IsSnowflake(b.UserID) { + if el.GuildContractRole.ID != "" { + _ = client.AddGuildMemberRole(el.GuildID, b.UserID, el.GuildContractRole.ID) + } + if el.ChannelID != "" { + _ = client.AddThreadMember(el.ChannelID, b.UserID) + } } } @@ -1424,6 +1429,13 @@ func JoinContract(client dc.Client, guildID string, channelID string, userID str // test if userID in Boosters if contract.Boosters[userID] != nil { contract.Boosters[userID].Ping = bell + for _, el := range contract.Location { + if (guildID == "" || el.GuildID == guildID) && dc.IsSnowflake(userID) { + if el.ChannelID != "" { + _ = client.AddThreadMember(el.ChannelID, userID) + } + } + } } if bell { diff --git a/src/boost/contract_test.go b/src/boost/contract_test.go index 81f316d9..426944e4 100644 --- a/src/boost/contract_test.go +++ b/src/boost/contract_test.go @@ -807,3 +807,100 @@ func TestBoostMenuNextBoosterTokens(t *testing.T) { t.Errorf("Expected next2:user2 emoji to match ultra_gg name %q, got %q", ultraGGEmoji.Name, emoji) } } + +func TestAddFarmerToContract_AddsThreadMember(t *testing.T) { + client := dctest.New(). + WithGuild("guild1", "Guild 1"). + WithChannel("thread1", "guild1", "contract-thread"). + WithUser("123456789012345678", "farmer1", "Farmer One") + + contract := &Contract{ + ContractHash: "test-hash-thread", + ContractID: "test-contract", + CoopID: "test-coop", + CoopSize: 10, + State: ContractStateFastrun, + CreatorID: []string{"creator1"}, + Order: make([]string, 0), + Boosters: make(map[string]*Booster), + Location: []*LocationData{{GuildID: "guild1", ChannelID: "thread1"}}, + } + Contracts[contract.ContractHash] = contract + defer delete(Contracts, contract.ContractHash) + + b, err := AddFarmerToContract(client, contract, "guild1", "thread1", "123456789012345678", ContractOrderSignup, false, false) + if err != nil { + t.Fatalf("unexpected error adding farmer: %v", err) + } + if b == nil { + t.Fatalf("expected booster to be created, got nil") + } + + // Verify AddThreadMember call was recorded for snowflake user + found := false + for _, call := range client.Calls { + if call.Method == "AddThreadMember" { + if len(call.Args) >= 2 && call.Args[0] == "thread1" && call.Args[1] == "123456789012345678" { + found = true + break + } + } + } + if !found { + t.Errorf("expected AddThreadMember(thread1, 123456789012345678) to be called, recorded calls: %v", client.Calls) + } + + // Adding a non-snowflake guest should NOT call AddThreadMember + client.Calls = nil + _, err = AddFarmerToContract(client, contract, "guild1", "thread1", "guest-farmer", ContractOrderSignup, false, false) + if err != nil { + t.Fatalf("unexpected error adding guest farmer: %v", err) + } + for _, call := range client.Calls { + if call.Method == "AddThreadMember" { + t.Errorf("unexpected AddThreadMember call for guest: %v", call) + } + } +} + +func TestJoinRunningContract_AddsThreadMember(t *testing.T) { + client := dctest.New(). + WithGuild("guild1", "Guild 1"). + WithChannel("thread2", "guild1", "running-contract-thread"). + WithUser("234567890123456789", "farmer2", "Farmer Two") + + contract := &Contract{ + ContractHash: "test-hash-running-join", + ContractID: "test-contract-running", + CoopID: "test-coop-running", + CoopSize: 5, + State: ContractStateFastrun, + CreatorID: []string{"creator1"}, + Order: []string{"creator1"}, + Boosters: map[string]*Booster{ + "creator1": {UserID: "creator1", Name: "Creator", Nick: "Creator"}, + }, + Location: []*LocationData{{GuildID: "guild1", ChannelID: "thread2"}}, + } + Contracts[contract.ContractHash] = contract + defer delete(Contracts, contract.ContractHash) + + err := JoinContract(client, "guild1", "thread2", "234567890123456789", false) + if err != nil { + t.Fatalf("unexpected error joining contract: %v", err) + } + + // Verify AddThreadMember call was recorded + found := false + for _, call := range client.Calls { + if call.Method == "AddThreadMember" { + if len(call.Args) >= 2 && call.Args[0] == "thread2" && call.Args[1] == "234567890123456789" { + found = true + break + } + } + } + if !found { + t.Errorf("expected AddThreadMember(thread2, 234567890123456789) to be called on join, recorded calls: %v", client.Calls) + } +} diff --git a/src/dc/client.go b/src/dc/client.go index 6a65e35d..463ff5e0 100644 --- a/src/dc/client.go +++ b/src/dc/client.go @@ -71,6 +71,8 @@ type Client interface { StartThread(channelID, name string, archiveDurationMinutes int) (*Channel, error) // JoinThread adds the bot to a thread. JoinThread(channelID string) error + // AddThreadMember adds a member to a thread. + AddThreadMember(threadID, userID string) error // ActiveThreads lists the active threads in a guild. Discord's // active-threads endpoint is guild-scoped, so callers that want the // threads under one channel filter the result on ParentID. diff --git a/src/dc/client_disgo.go b/src/dc/client_disgo.go index 047c6f15..c620d367 100644 --- a/src/dc/client_disgo.go +++ b/src/dc/client_disgo.go @@ -25,6 +25,12 @@ func newDisgoClient(b *bot.Client) *disgoClient { return &disgoClient{bot: b} } +// IsSnowflake reports whether s is a valid Discord snowflake ID. +func IsSnowflake(s string) bool { + id, err := snowflake.Parse(s) + return err == nil && id != 0 +} + // parseIDs parses a list of facade IDs, returning the first failure. func parseIDs(ids ...string) ([]snowflake.ID, error) { out := make([]snowflake.ID, 0, len(ids)) @@ -405,6 +411,15 @@ func (c *disgoClient) JoinThread(channelID string) error { return wrapAPIError(c.bot.Rest.JoinThread(ids[0])) } +// AddThreadMember adds a member to a thread. +func (c *disgoClient) AddThreadMember(threadID, userID string) error { + ids, err := parseIDs(threadID, userID) + if err != nil { + return err + } + return wrapAPIError(c.bot.Rest.AddThreadMember(ids[0], ids[1])) +} + // ActiveThreads lists the active threads in a guild. The argument is a guild // ID: Discord's active-threads endpoint is guild-scoped. func (c *disgoClient) ActiveThreads(guildID string) ([]Channel, error) { diff --git a/src/dc/dctest/fake.go b/src/dc/dctest/fake.go index 61fb57ff..e1bb8616 100644 --- a/src/dc/dctest/fake.go +++ b/src/dc/dctest/fake.go @@ -306,3 +306,9 @@ func (f *FakeClient) ActiveThreads(guildID string) ([]dc.Channel, error) { } return threads, nil } + +// AddThreadMember records adding a member to a thread. +func (f *FakeClient) AddThreadMember(threadID, userID string) error { + f.record("AddThreadMember", threadID, userID) + return nil +}