diff --git a/README.md b/README.md index af530c1..44df488 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ msgs, _ := c.Messages().Read(ctx, "", 20, "") // oldest-first | `Guilds()` | `List` `Sole` | | `Members()` | `List` `Get` `Kick` `Ban` | | `Roles()` | `List` `Create` `Update` `Delete` `Assign` `Unassign` | -| `Threads()` | `Start` `CreateForum` `ForumPost` | +| `Threads()` | `Start` `StartPrivate` `AddMember` `CreateForum` `ForumPost` | | `Reactions()` | `Add` `Remove` | | `Permissions()` | `Set` `Remove` | | `Webhooks()` | `Create` `List` `Delete` `Execute` | diff --git a/threads.go b/threads.go index b5a9745..0794b31 100644 --- a/threads.go +++ b/threads.go @@ -24,6 +24,31 @@ func (t *Threads) Start(ctx context.Context, channelID, messageID, name string) return &ch, nil } +// StartPrivate opens a private thread in channelID, with no parent message so +// the channel shows no trace of it. Only invited members and moderators holding +// MANAGE_THREADS can see it — invitable is false, so members cannot pull others +// in. The creating bot is a member; add the humans with AddMember. +func (t *Threads) StartPrivate(ctx context.Context, channelID, name string) (*Channel, error) { + var ch Channel + if err := t.rt.Do(ctx, http.MethodPost, "/channels/"+seg(channelID)+"/threads", + map[string]any{ + "name": name, + "type": ChannelPrivateThread, + "invitable": false, + "auto_archive_duration": autoArchive, + }, &ch); err != nil { + return nil, err + } + return &ch, nil +} + +// AddMember gives userID access to threadID. A private thread is invisible until +// its members are added, so this is what makes one usable by a person. +func (t *Threads) AddMember(ctx context.Context, threadID, userID string) error { + return t.rt.Do(ctx, http.MethodPut, + "/channels/"+seg(threadID)+"/thread-members/"+seg(userID), nil, nil) +} + // CreateForum creates a forum channel named name in guildID (or the sole guild). func (t *Threads) CreateForum(ctx context.Context, guildID, name string) (*Channel, error) { gid, err := t.def.resolveGuild(ctx, guildID) diff --git a/threads_test.go b/threads_test.go index c9807dd..951a3a5 100644 --- a/threads_test.go +++ b/threads_test.go @@ -33,6 +33,45 @@ func TestThreadsStart(t *testing.T) { } } +// A private thread is created off the CHANNEL, never off a message: a message +// would be the public trace the thread exists to avoid. +func TestThreadsStartPrivate(t *testing.T) { + s := transport.NewStub().Reply(`{"id":"t9","name":"job","type":12}`) + ch, err := thr(s).StartPrivate(context.Background(), "c1", "job") + if err != nil { + t.Fatal(err) + } + if ch.ID != "t9" || ch.Type != ChannelPrivateThread { + t.Fatalf("thread = %+v", ch) + } + c := s.Last() + if c.Method != "POST" || c.Path != "/channels/c1/threads" { + t.Fatalf("call = %s %s", c.Method, c.Path) + } + body := c.Body.(map[string]any) + if body["type"] != ChannelPrivateThread { + t.Errorf("type = %v, want a private thread", body["type"]) + } + // invitable would let any member drag others in, which defeats the point. + if body["invitable"] != false { + t.Errorf("invitable = %v, want false", body["invitable"]) + } + if body["auto_archive_duration"] != autoArchive { + t.Errorf("auto_archive_duration = %v", body["auto_archive_duration"]) + } +} + +func TestThreadsAddMember(t *testing.T) { + s := transport.NewStub().Reply(``) + if err := thr(s).AddMember(context.Background(), "t9", "u1"); err != nil { + t.Fatal(err) + } + c := s.Last() + if c.Method != "PUT" || c.Path != "/channels/t9/thread-members/u1" { + t.Fatalf("call = %s %s", c.Method, c.Path) + } +} + func TestThreadsCreateForum(t *testing.T) { s := transport.NewStub(). Reply(`[{"id":"g1"}]`).