Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
25 changes: 25 additions & 0 deletions threads.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions threads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]`).
Expand Down
Loading