Threads reloaded (#1058)

* feat(endpoints): bumped discord version to 9

* feat: threads barebones

* feat(threads): documentation

* feat(threads): membership caching

* feat(threads): added type to StartThread method

* fix: replaced missing Timestamp definitions with time.Time

* chore: removed debug logs

* chore: removed thread alias for channel type

* feat(webhooks): separated thread option into method

* fix(state): ThreadMembersUpdate member duplication bug

* fix: golint

* feat(threads): pr fixes and BeforeUpdate in ThreadUpdate

* feat: removed unnecessary todo

* feat(state): removed thread last message update in MessageAdd

* Revert "feat(state): removed thread last message update in MessageAdd"

This reverts commit 4ca359fd2cc304e5d0ec2937e25c0c487a1f2096.

* feat(state): update only last message id for thread update

Implements updating message id in MESSAGE_CREATE and MESSAGE_DELETE events. Refer to https://discord.com/developers/docs/topics/gateway#thread-update for more info.

* fix(restapi): passing threadID in WebhookThreadExecute

* feat(state): dropped last_message_id updates for threads

* fix: gofmt

* feat(events#ThreadCreate): added newly_created field

* feat(restapi)!: corrected names of thread functions
This commit is contained in:
Fedor Lapshin 2022-02-17 22:50:42 +03:00 committed by GitHub
parent 6015eed933
commit 992358e106
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 899 additions and 121 deletions

View file

@ -14,7 +14,7 @@ package discordgo
import "strconv"
// APIVersion is the Discord API version used for the REST and Websocket API.
var APIVersion = "8"
var APIVersion = "9"
// Known Discord API Endpoints.
var (
@ -53,12 +53,15 @@ var (
uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator)
return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png"
}
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildThreads = func(gID string) string { return EndpointGuild(gID) + "/threads" }
EndpointGuildActiveThreads = func(gID string) string { return EndpointGuildThreads(gID) + "/active" }
EndpointGuildPreview = func(gID string) string { return EndpointGuilds + gID + "/preview" }
EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" }
@ -86,17 +89,25 @@ var (
EndpointGuildSticker = func(gID, sID string) string { return EndpointGuilds + gID + "/stickers/" + sID }
EndpointChannel = func(cID string) string { return EndpointChannels + cID }
EndpointChannelThreads = func(cID string) string { return EndpointChannel(cID) + "/threads" }
EndpointChannelActiveThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/active" }
EndpointChannelPublicArchivedThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/public" }
EndpointChannelPrivateArchivedThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/private" }
EndpointChannelJoinedPrivateArchivedThreads = func(cID string) string { return EndpointChannel(cID) + "/users/@me/threads/archived/private" }
EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" }
EndpointChannelPermission = func(cID, tID string) string { return EndpointChannelPermissions(cID) + "/" + tID }
EndpointChannelPermission = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID }
EndpointChannelInvites = func(cID string) string { return EndpointChannels + cID + "/invites" }
EndpointChannelTyping = func(cID string) string { return EndpointChannels + cID + "/typing" }
EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" }
EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
EndpointChannelMessageThread = func(cID, mID string) string { return EndpointChannelMessage(cID, mID) + "/threads" }
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
EndpointChannelMessageCrosspost = func(cID, mID string) string { return EndpointChannel(cID) + "/messages/" + mID + "/crosspost" }
EndpointChannelFollow = func(cID string) string { return EndpointChannel(cID) + "/followers" }
EndpointThreadMembers = func(tID string) string { return EndpointChannel(tID) + "/thread-members" }
EndpointThreadMember = func(tID, mID string) string { return EndpointThreadMembers(tID) + "/" + mID }
EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" }

View file

@ -44,6 +44,12 @@ const (
relationshipAddEventType = "RELATIONSHIP_ADD"
relationshipRemoveEventType = "RELATIONSHIP_REMOVE"
resumedEventType = "RESUMED"
threadCreateEventType = "THREAD_CREATE"
threadDeleteEventType = "THREAD_DELETE"
threadListSyncEventType = "THREAD_LIST_SYNC"
threadMemberUpdateEventType = "THREAD_MEMBER_UPDATE"
threadMembersUpdateEventType = "THREAD_MEMBERS_UPDATE"
threadUpdateEventType = "THREAD_UPDATE"
typingStartEventType = "TYPING_START"
userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE"
userNoteUpdateEventType = "USER_NOTE_UPDATE"
@ -774,6 +780,126 @@ func (eh resumedEventHandler) Handle(s *Session, i interface{}) {
}
}
// threadCreateEventHandler is an event handler for ThreadCreate events.
type threadCreateEventHandler func(*Session, *ThreadCreate)
// Type returns the event type for ThreadCreate events.
func (eh threadCreateEventHandler) Type() string {
return threadCreateEventType
}
// New returns a new instance of ThreadCreate.
func (eh threadCreateEventHandler) New() interface{} {
return &ThreadCreate{}
}
// Handle is the handler for ThreadCreate events.
func (eh threadCreateEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*ThreadCreate); ok {
eh(s, t)
}
}
// threadDeleteEventHandler is an event handler for ThreadDelete events.
type threadDeleteEventHandler func(*Session, *ThreadDelete)
// Type returns the event type for ThreadDelete events.
func (eh threadDeleteEventHandler) Type() string {
return threadDeleteEventType
}
// New returns a new instance of ThreadDelete.
func (eh threadDeleteEventHandler) New() interface{} {
return &ThreadDelete{}
}
// Handle is the handler for ThreadDelete events.
func (eh threadDeleteEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*ThreadDelete); ok {
eh(s, t)
}
}
// threadListSyncEventHandler is an event handler for ThreadListSync events.
type threadListSyncEventHandler func(*Session, *ThreadListSync)
// Type returns the event type for ThreadListSync events.
func (eh threadListSyncEventHandler) Type() string {
return threadListSyncEventType
}
// New returns a new instance of ThreadListSync.
func (eh threadListSyncEventHandler) New() interface{} {
return &ThreadListSync{}
}
// Handle is the handler for ThreadListSync events.
func (eh threadListSyncEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*ThreadListSync); ok {
eh(s, t)
}
}
// threadMemberUpdateEventHandler is an event handler for ThreadMemberUpdate events.
type threadMemberUpdateEventHandler func(*Session, *ThreadMemberUpdate)
// Type returns the event type for ThreadMemberUpdate events.
func (eh threadMemberUpdateEventHandler) Type() string {
return threadMemberUpdateEventType
}
// New returns a new instance of ThreadMemberUpdate.
func (eh threadMemberUpdateEventHandler) New() interface{} {
return &ThreadMemberUpdate{}
}
// Handle is the handler for ThreadMemberUpdate events.
func (eh threadMemberUpdateEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*ThreadMemberUpdate); ok {
eh(s, t)
}
}
// threadMembersUpdateEventHandler is an event handler for ThreadMembersUpdate events.
type threadMembersUpdateEventHandler func(*Session, *ThreadMembersUpdate)
// Type returns the event type for ThreadMembersUpdate events.
func (eh threadMembersUpdateEventHandler) Type() string {
return threadMembersUpdateEventType
}
// New returns a new instance of ThreadMembersUpdate.
func (eh threadMembersUpdateEventHandler) New() interface{} {
return &ThreadMembersUpdate{}
}
// Handle is the handler for ThreadMembersUpdate events.
func (eh threadMembersUpdateEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*ThreadMembersUpdate); ok {
eh(s, t)
}
}
// threadUpdateEventHandler is an event handler for ThreadUpdate events.
type threadUpdateEventHandler func(*Session, *ThreadUpdate)
// Type returns the event type for ThreadUpdate events.
func (eh threadUpdateEventHandler) Type() string {
return threadUpdateEventType
}
// New returns a new instance of ThreadUpdate.
func (eh threadUpdateEventHandler) New() interface{} {
return &ThreadUpdate{}
}
// Handle is the handler for ThreadUpdate events.
func (eh threadUpdateEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*ThreadUpdate); ok {
eh(s, t)
}
}
// typingStartEventHandler is an event handler for TypingStart events.
type typingStartEventHandler func(*Session, *TypingStart)
@ -1012,6 +1138,18 @@ func handlerForInterface(handler interface{}) EventHandler {
return relationshipRemoveEventHandler(v)
case func(*Session, *Resumed):
return resumedEventHandler(v)
case func(*Session, *ThreadCreate):
return threadCreateEventHandler(v)
case func(*Session, *ThreadDelete):
return threadDeleteEventHandler(v)
case func(*Session, *ThreadListSync):
return threadListSyncEventHandler(v)
case func(*Session, *ThreadMemberUpdate):
return threadMemberUpdateEventHandler(v)
case func(*Session, *ThreadMembersUpdate):
return threadMembersUpdateEventHandler(v)
case func(*Session, *ThreadUpdate):
return threadUpdateEventHandler(v)
case func(*Session, *TypingStart):
return typingStartEventHandler(v)
case func(*Session, *UserGuildSettingsUpdate):
@ -1067,6 +1205,12 @@ func init() {
registerInterfaceProvider(relationshipAddEventHandler(nil))
registerInterfaceProvider(relationshipRemoveEventHandler(nil))
registerInterfaceProvider(resumedEventHandler(nil))
registerInterfaceProvider(threadCreateEventHandler(nil))
registerInterfaceProvider(threadDeleteEventHandler(nil))
registerInterfaceProvider(threadListSyncEventHandler(nil))
registerInterfaceProvider(threadMemberUpdateEventHandler(nil))
registerInterfaceProvider(threadMembersUpdateEventHandler(nil))
registerInterfaceProvider(threadUpdateEventHandler(nil))
registerInterfaceProvider(typingStartEventHandler(nil))
registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil))
registerInterfaceProvider(userNoteUpdateEventHandler(nil))

View file

@ -73,6 +73,53 @@ type ChannelPinsUpdate struct {
GuildID string `json:"guild_id,omitempty"`
}
// ThreadCreate is the data for a ThreadCreate event.
type ThreadCreate struct {
*Channel
NewlyCreated bool `json:"newly_created"`
}
// ThreadUpdate is the data for a ThreadUpdate event.
type ThreadUpdate struct {
*Channel
BeforeUpdate *Channel `json:"-"`
}
// ThreadDelete is the data for a ThreadDelete event.
type ThreadDelete struct {
*Channel
}
// ThreadListSync is the data for a ThreadListSync event.
type ThreadListSync struct {
// The id of the guild
GuildID string `json:"guild_id"`
// The parent channel ids whose threads are being synced.
// If omitted, then threads were synced for the entire guild.
// This array may contain channel_ids that have no active threads as well, so you know to clear that data.
ChannelIDs []string `json:"channel_ids"`
// All active threads in the given channels that the current user can access
Threads []*Channel `json:"threads"`
// All thread member objects from the synced threads for the current user,
// indicating which threads the current user has been added to
Members []*ThreadMember `json:"members"`
}
// ThreadMemberUpdate is the data for a ThreadMemberUpdate event.
type ThreadMemberUpdate struct {
*ThreadMember
GuildID string `json:"guild_id"`
}
// ThreadMembersUpdate is the data for a ThreadMembersUpdate event.
type ThreadMembersUpdate struct {
ID string `json:"id"`
GuildID string `json:"guild_id"`
MemberCount int `json:"member_count"`
AddedMembers []AddedThreadMember `json:"added_members"`
RemovedMembers []string `json:"removed_member_ids"`
}
// GuildCreate is the data for a GuildCreate event.
type GuildCreate struct {
*Guild

73
examples/threads/main.go Normal file
View file

@ -0,0 +1,73 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
// Flags
var (
BotToken = flag.String("token", "", "Bot token")
)
const timeout time.Duration = time.Second * 10
var games map[string]time.Time = make(map[string]time.Time)
func main() {
flag.Parse()
s, _ := discordgo.New("Bot " + *BotToken)
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
fmt.Println("Bot is ready")
})
s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
if strings.Contains(m.Content, "ping") {
if ch, err := s.State.Channel(m.ChannelID); err != nil || !ch.IsThread() {
thread, err := s.MessageThreadStartComplex(m.ChannelID, m.ID, &discordgo.ThreadStart{
Name: "Pong game with " + m.Author.Username,
AutoArchiveDuration: 60,
Invitable: false,
RateLimitPerUser: 10,
})
if err != nil {
panic(err)
}
_, _ = s.ChannelMessageSend(thread.ID, "pong")
m.ChannelID = thread.ID
} else {
_, _ = s.ChannelMessageSendReply(m.ChannelID, "pong", m.Reference())
}
games[m.ChannelID] = time.Now()
<-time.After(timeout)
if time.Since(games[m.ChannelID]) >= timeout {
_, err := s.ChannelEditComplex(m.ChannelID, &discordgo.ChannelEdit{
Archived: true,
Locked: true,
})
if err != nil {
panic(err)
}
}
}
})
s.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
err := s.Open()
if err != nil {
log.Fatalf("Cannot open the session: %v", err)
}
defer s.Close()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<-stop
log.Println("Graceful shutdown")
}

View file

@ -38,8 +38,10 @@ const (
MessageTypeChannelFollowAdd MessageType = 12
MessageTypeGuildDiscoveryDisqualified MessageType = 14
MessageTypeGuildDiscoveryRequalified MessageType = 15
MessageTypeThreadCreated MessageType = 18
MessageTypeReply MessageType = 19
MessageTypeChatInputCommand MessageType = 20
MessageTypeThreadStarterMessage MessageType = 21
MessageTypeContextMenuCommand MessageType = 23
)
@ -126,11 +128,21 @@ type Message struct {
// To generate a reference to this message, use (*Message).Reference().
MessageReference *MessageReference `json:"message_reference"`
// The message associated with the message_reference
// NOTE: This field is only returned for messages with a type of 19 (REPLY) or 21 (THREAD_STARTER_MESSAGE).
// If the message is a reply but the referenced_message field is not present,
// the backend did not attempt to fetch the message that was being replied to, so its state is unknown.
// If the field exists but is null, the referenced message was deleted.
ReferencedMessage *Message `json:"referenced_message"`
// The flags of the message, which describe extra features of a message.
// This is a combination of bit masks; the presence of a certain permission can
// be checked by performing a bitwise AND between this int and the flag.
Flags MessageFlags `json:"flags"`
// The thread that was started from this message, includes thread member object
Thread *Channel `json:"thread,omitempty"`
// An array of Sticker objects, if any were sent.
StickerItems []*Sticker `json:"sticker_items"`
}

View file

@ -1988,15 +1988,19 @@ func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook,
return
}
// WebhookExecute executes a webhook.
// webhookID: The ID of a webhook.
// token : The auth token for the webhook
// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (st *Message, err error) {
func (s *Session) webhookExecute(webhookID, token string, wait bool, threadID string, data *WebhookParams) (st *Message, err error) {
uri := EndpointWebhookToken(webhookID, token)
v := url.Values{}
if wait {
uri += "?wait=true"
v.Set("wait", "true")
}
if threadID != "" {
v.Set("thread_id", threadID)
}
if len(v) != 0 {
uri += "?" + v.Encode()
}
var response []byte
@ -2018,6 +2022,23 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho
return
}
// WebhookExecute executes a webhook.
// webhookID: The ID of a webhook.
// token : The auth token for the webhook
// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (st *Message, err error) {
return s.webhookExecute(webhookID, token, wait, "", data)
}
// WebhookThreadExecute executes a webhook in a thread.
// webhookID: The ID of a webhook.
// token : The auth token for the webhook
// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
// threadID : Sends a message to the specified thread within a webhook's channel. The thread will automatically be unarchived.
func (s *Session) WebhookThreadExecute(webhookID, token string, wait bool, threadID string, data *WebhookParams) (st *Message, err error) {
return s.webhookExecute(webhookID, token, wait, threadID, data)
}
// WebhookMessage gets a webhook message.
// webhookID : The ID of a webhook
// token : The auth token for the webhook
@ -2164,6 +2185,226 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i
return
}
// ------------------------------------------------------------------------------------------------
// Functions specific to threads
// ------------------------------------------------------------------------------------------------
// MessageThreadStartComplex creates a new thread from an existing message.
// channelID : Channel to create thread in
// messageID : Message to start thread from
// data : Parameters of the thread
func (s *Session) MessageThreadStartComplex(channelID, messageID string, data *ThreadStart) (ch *Channel, err error) {
endpoint := EndpointChannelMessageThread(channelID, messageID)
var body []byte
body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint)
if err != nil {
return
}
err = unmarshal(body, &ch)
return
}
// MessageThreadStart creates a new thread from an existing message.
// channelID : Channel to create thread in
// messageID : Message to start thread from
// name : Name of the thread
// archiveDuration : Auto archive duration (in minutes)
func (s *Session) MessageThreadStart(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) {
return s.MessageThreadStartComplex(channelID, messageID, &ThreadStart{
Name: name,
AutoArchiveDuration: archiveDuration,
})
}
// ThreadStartComplex creates a new thread.
// channelID : Channel to create thread in
// data : Parameters of the thread
func (s *Session) ThreadStartComplex(channelID string, data *ThreadStart) (ch *Channel, err error) {
endpoint := EndpointChannelThreads(channelID)
var body []byte
body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint)
if err != nil {
return
}
err = unmarshal(body, &ch)
return
}
// ThreadStart creates a new thread.
// channelID : Channel to create thread in
// name : Name of the thread
// archiveDuration : Auto archive duration (in minutes)
func (s *Session) ThreadStart(channelID, name string, typ ChannelType, archiveDuration int) (ch *Channel, err error) {
return s.ThreadStartComplex(channelID, &ThreadStart{
Name: name,
Type: typ,
AutoArchiveDuration: archiveDuration,
})
}
// ThreadJoin adds current user to a thread
func (s *Session) ThreadJoin(id string) error {
endpoint := EndpointThreadMember(id, "@me")
_, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint)
return err
}
// ThreadLeave removes current user to a thread
func (s *Session) ThreadLeave(id string) error {
endpoint := EndpointThreadMember(id, "@me")
_, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint)
return err
}
// ThreadMemberAdd adds another member to a thread
func (s *Session) ThreadMemberAdd(threadID, memberID string) error {
endpoint := EndpointThreadMember(threadID, memberID)
_, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint)
return err
}
// ThreadMemberRemove removes another member from a thread
func (s *Session) ThreadMemberRemove(threadID, memberID string) error {
endpoint := EndpointThreadMember(threadID, memberID)
_, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint)
return err
}
// ThreadMember returns thread member object for the specified member of a thread
func (s *Session) ThreadMember(threadID, memberID string) (member *ThreadMember, err error) {
endpoint := EndpointThreadMember(threadID, memberID)
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
return
}
err = unmarshal(body, &member)
return
}
// ThreadMembers returns all members of specified thread.
func (s *Session) ThreadMembers(threadID string) (members []*ThreadMember, err error) {
var body []byte
body, err = s.RequestWithBucketID("GET", EndpointThreadMembers(threadID), nil, EndpointThreadMembers(threadID))
if err != nil {
return
}
err = unmarshal(body, &members)
return
}
// ThreadsActive returns all active threads for specified channel.
func (s *Session) ThreadsActive(channelID string) (threads *ThreadsList, err error) {
var body []byte
body, err = s.RequestWithBucketID("GET", EndpointChannelActiveThreads(channelID), nil, EndpointChannelActiveThreads(channelID))
if err != nil {
return
}
err = unmarshal(body, &threads)
return
}
// GuildThreadsActive returns all active threads for specified guild.
func (s *Session) GuildThreadsActive(guildID string) (threads *ThreadsList, err error) {
var body []byte
body, err = s.RequestWithBucketID("GET", EndpointGuildActiveThreads(guildID), nil, EndpointGuildActiveThreads(guildID))
if err != nil {
return
}
err = unmarshal(body, &threads)
return
}
// ThreadsArchived returns archived threads for specified channel.
// before : If specified returns only threads before the timestamp
// limit : Optional maximum amount of threads to return.
func (s *Session) ThreadsArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) {
endpoint := EndpointChannelPublicArchivedThreads(channelID)
v := url.Values{}
if before != nil {
v.Set("before", before.Format(time.RFC3339))
}
if limit > 0 {
v.Set("limit", strconv.Itoa(limit))
}
if len(v) > 0 {
endpoint += "?" + v.Encode()
}
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
return
}
err = unmarshal(body, &threads)
return
}
// ThreadsPrivateArchived returns archived private threads for specified channel.
// before : If specified returns only threads before the timestamp
// limit : Optional maximum amount of threads to return.
func (s *Session) ThreadsPrivateArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) {
endpoint := EndpointChannelPrivateArchivedThreads(channelID)
v := url.Values{}
if before != nil {
v.Set("before", before.Format(time.RFC3339))
}
if limit > 0 {
v.Set("limit", strconv.Itoa(limit))
}
if len(v) > 0 {
endpoint += "?" + v.Encode()
}
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
return
}
err = unmarshal(body, &threads)
return
}
// ThreadsPrivateJoinedArchived returns archived joined private threads for specified channel.
// before : If specified returns only threads before the timestamp
// limit : Optional maximum amount of threads to return.
func (s *Session) ThreadsPrivateJoinedArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) {
endpoint := EndpointChannelJoinedPrivateArchivedThreads(channelID)
v := url.Values{}
if before != nil {
v.Set("before", before.Format(time.RFC3339))
}
if limit > 0 {
v.Set("limit", strconv.Itoa(limit))
}
if len(v) > 0 {
endpoint += "?" + v.Encode()
}
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
return
}
err = unmarshal(body, &threads)
return
}
// ------------------------------------------------------------------------------------------------
// Functions specific to application (slash) commands
// ------------------------------------------------------------------------------------------------

221
state.go
View file

@ -40,8 +40,10 @@ type State struct {
// MaxMessageCount represents how many messages per channel the state will store.
MaxMessageCount int
TrackChannels bool
TrackThreads bool
TrackEmojis bool
TrackMembers bool
TrackThreadMembers bool
TrackRoles bool
TrackVoice bool
TrackPresences bool
@ -59,8 +61,10 @@ func NewState() *State {
Guilds: []*Guild{},
},
TrackChannels: true,
TrackThreads: true,
TrackEmojis: true,
TrackMembers: true,
TrackThreadMembers: true,
TrackRoles: true,
TrackVoice: true,
TrackPresences: true,
@ -93,6 +97,11 @@ func (s *State) GuildAdd(guild *Guild) error {
s.channelMap[c.ID] = c
}
// Add all the threads to the state in case of thread sync list.
for _, t := range guild.Threads {
s.channelMap[t.ID] = t
}
// If this guild contains a new member slice, we must regenerate the member map so the pointers stay valid
if guild.Members != nil {
s.createMemberMap(guild)
@ -122,6 +131,9 @@ func (s *State) GuildAdd(guild *Guild) error {
if guild.Channels == nil {
guild.Channels = g.Channels
}
if guild.Threads == nil {
guild.Threads = g.Threads
}
if guild.VoiceStates == nil {
guild.VoiceStates = g.VoiceStates
}
@ -180,21 +192,12 @@ func (s *State) Guild(guildID string) (*Guild, error) {
return nil, ErrStateNotFound
}
// PresenceAdd adds a presence to the current world state, or
// updates it if it already exists.
func (s *State) PresenceAdd(guildID string, presence *Presence) error {
if s == nil {
return ErrNilState
func (s *State) presenceAdd(guildID string, presence *Presence) error {
guild, ok := s.guildMap[guildID]
if !ok {
return ErrStateNotFound
}
guild, err := s.Guild(guildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, p := range guild.Presences {
if p.User.ID == presence.User.ID {
//guild.Presences[i] = presence
@ -233,6 +236,19 @@ func (s *State) PresenceAdd(guildID string, presence *Presence) error {
return nil
}
// PresenceAdd adds a presence to the current world state, or
// updates it if it already exists.
func (s *State) PresenceAdd(guildID string, presence *Presence) error {
if s == nil {
return ErrNilState
}
s.Lock()
defer s.Unlock()
return s.presenceAdd(guildID, presence)
}
// PresenceRemove removes a presence from the current world state.
func (s *State) PresenceRemove(guildID string, presence *Presence) error {
if s == nil {
@ -279,21 +295,12 @@ func (s *State) Presence(guildID, userID string) (*Presence, error) {
// TODO: Consider moving Guild state update methods onto *Guild.
// MemberAdd adds a member to the current world state, or
// updates it if it already exists.
func (s *State) MemberAdd(member *Member) error {
if s == nil {
return ErrNilState
func (s *State) memberAdd(member *Member) error {
guild, ok := s.guildMap[member.GuildID]
if !ok {
return ErrStateNotFound
}
guild, err := s.Guild(member.GuildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
members, ok := s.memberMap[member.GuildID]
if !ok {
return ErrStateNotFound
@ -311,10 +318,22 @@ func (s *State) MemberAdd(member *Member) error {
}
*m = *member
}
return nil
}
// MemberAdd adds a member to the current world state, or
// updates it if it already exists.
func (s *State) MemberAdd(member *Member) error {
if s == nil {
return ErrNilState
}
s.Lock()
defer s.Unlock()
return s.memberAdd(member)
}
// MemberRemove removes a member from current world state.
func (s *State) MemberRemove(member *Member) error {
if s == nil {
@ -465,6 +484,9 @@ func (s *State) ChannelAdd(channel *Channel) error {
if channel.PermissionOverwrites == nil {
channel.PermissionOverwrites = c.PermissionOverwrites
}
if channel.ThreadMetadata == nil {
channel.ThreadMetadata = c.ThreadMetadata
}
*c = *channel
return nil
@ -472,12 +494,18 @@ func (s *State) ChannelAdd(channel *Channel) error {
if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM {
s.PrivateChannels = append(s.PrivateChannels, channel)
} else {
s.channelMap[channel.ID] = channel
return nil
}
guild, ok := s.guildMap[channel.GuildID]
if !ok {
return ErrStateNotFound
}
if channel.IsThread() {
guild.Threads = append(guild.Threads, channel)
} else {
guild.Channels = append(guild.Channels, channel)
}
@ -507,7 +535,10 @@ func (s *State) ChannelRemove(channel *Channel) error {
break
}
}
} else {
delete(s.channelMap, channel.ID)
return nil
}
guild, err := s.Guild(channel.GuildID)
if err != nil {
return err
@ -516,6 +547,14 @@ func (s *State) ChannelRemove(channel *Channel) error {
s.Lock()
defer s.Unlock()
if channel.IsThread() {
for i, t := range guild.Threads {
if t.ID == channel.ID {
guild.Threads = append(guild.Threads[:i], guild.Threads[i+1:]...)
break
}
}
} else {
for i, c := range guild.Channels {
if c.ID == channel.ID {
guild.Channels = append(guild.Channels[:i], guild.Channels[i+1:]...)
@ -529,6 +568,99 @@ func (s *State) ChannelRemove(channel *Channel) error {
return nil
}
// ThreadListSync syncs guild threads with provided ones.
func (s *State) ThreadListSync(tls *ThreadListSync) error {
guild, err := s.Guild(tls.GuildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
// This algorithm filters out archived or
// threads which are children of channels in channelIDs
// and then it adds all synced threads to guild threads and cache
index := 0
outer:
for _, t := range guild.Threads {
if !t.ThreadMetadata.Archived && tls.ChannelIDs != nil {
for _, v := range tls.ChannelIDs {
if t.ParentID == v {
delete(s.channelMap, t.ID)
continue outer
}
}
guild.Threads[index] = t
index++
} else {
delete(s.channelMap, t.ID)
}
}
guild.Threads = guild.Threads[:index]
for _, t := range tls.Threads {
s.channelMap[t.ID] = t
guild.Threads = append(guild.Threads, t)
}
for _, m := range tls.Members {
if c, ok := s.channelMap[m.ID]; ok {
c.Member = m
}
}
return nil
}
// ThreadMembersUpdate updates thread members list
func (s *State) ThreadMembersUpdate(tmu *ThreadMembersUpdate) error {
thread, err := s.Channel(tmu.ID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for idx, member := range thread.Members {
for _, removedMember := range tmu.RemovedMembers {
if member.ID == removedMember {
thread.Members = append(thread.Members[:idx], thread.Members[idx+1:]...)
break
}
}
}
for _, addedMember := range tmu.AddedMembers {
thread.Members = append(thread.Members, addedMember.ThreadMember)
if addedMember.Member != nil {
err = s.memberAdd(addedMember.Member)
if err != nil {
return err
}
}
if addedMember.Presence != nil {
err = s.presenceAdd(tmu.GuildID, addedMember.Presence)
if err != nil {
return err
}
}
}
thread.MemberCount = tmu.MemberCount
return nil
}
// ThreadMemberUpdate sets or updates member data for the current user.
func (s *State) ThreadMemberUpdate(mu *ThreadMemberUpdate) error {
thread, err := s.Channel(mu.ID)
if err != nil {
return err
}
thread.Member = mu.ThreadMember
return nil
}
// GuildChannel gets a channel by ID from a guild.
// This method is Deprecated, use Channel(channelID)
func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) {
@ -668,6 +800,7 @@ func (s *State) MessageAdd(message *Message) error {
if len(c.Messages) > s.MaxMessageCount {
c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:]
}
return nil
}
@ -693,6 +826,7 @@ func (s *State) messageRemoveByID(channelID, messageID string) error {
for i, m := range c.Messages {
if m.ID == messageID {
c.Messages = append(c.Messages[:i], c.Messages[i+1:]...)
return nil
}
}
@ -913,6 +1047,35 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
if s.TrackChannels {
err = s.ChannelRemove(t.Channel)
}
case *ThreadCreate:
if s.TrackThreads {
err = s.ChannelAdd(t.Channel)
}
case *ThreadUpdate:
if s.TrackThreads {
old, err := s.Channel(t.ID)
if err == nil {
oldCopy := *old
t.BeforeUpdate = &oldCopy
}
err = s.ChannelAdd(t.Channel)
}
case *ThreadDelete:
if s.TrackThreads {
err = s.ChannelRemove(t.Channel)
}
case *ThreadMemberUpdate:
if s.TrackThreads {
err = s.ThreadMemberUpdate(t)
}
case *ThreadMembersUpdate:
if s.TrackThreadMembers {
err = s.ThreadMembersUpdate(t)
}
case *ThreadListSync:
if s.TrackThreads {
err = s.ThreadListSync(t)
}
case *MessageCreate:
if s.MaxMessageCount != 0 {
err = s.MessageAdd(t.Message)

View file

@ -233,6 +233,9 @@ const (
ChannelTypeGuildCategory ChannelType = 4
ChannelTypeGuildNews ChannelType = 5
ChannelTypeGuildStore ChannelType = 6
ChannelTypeGuildNewsThread ChannelType = 10
ChannelTypeGuildPublicThread ChannelType = 11
ChannelTypeGuildPrivateThread ChannelType = 12
)
// A Channel holds all data related to an individual Discord channel.
@ -261,6 +264,11 @@ type Channel struct {
// nil if the channel has no pinned messages.
LastPinTimestamp *time.Time `json:"last_pin_timestamp"`
// An approximate count of messages in a thread, stops counting at 50
MessageCount int `json:"message_count"`
// An approximate count of users in a thread, stops counting at 50
MemberCount int `json:"member_count"`
// Whether the channel is marked as NSFW.
NSFW bool `json:"nsfw"`
@ -286,18 +294,26 @@ type Channel struct {
// The user limit of the voice channel.
UserLimit int `json:"user_limit"`
// The ID of the parent channel, if the channel is under a category
// The ID of the parent channel, if the channel is under a category. For threads - id of the channel thread was created in.
ParentID string `json:"parent_id"`
// Amount of seconds a user has to wait before sending another message (0-21600)
// Amount of seconds a user has to wait before sending another message or creating another thread (0-21600)
// bots, as well as users with the permission manage_messages or manage_channel, are unaffected
RateLimitPerUser int `json:"rate_limit_per_user"`
// ID of the DM creator Zeroed if guild channel
// ID of the creator of the group DM or thread
OwnerID string `json:"owner_id"`
// ApplicationID of the DM creator Zeroed if guild channel or not a bot user
ApplicationID string `json:"application_id"`
// Thread-specific fields not needed by other channels
ThreadMetadata *ThreadMetadata `json:"thread_metadata,omitempty"`
// Thread member object for the current user, if they have joined the thread, only included on certain API endpoints
Member *ThreadMember `json:"thread_member"`
// All thread members. State channels only.
Members []*ThreadMember `json:"-"`
}
// Mention returns a string which mentions the channel
@ -305,6 +321,11 @@ func (c *Channel) Mention() string {
return fmt.Sprintf("<#%s>", c.ID)
}
// IsThread is a helper function to determine if channel is a thread or not
func (c *Channel) IsThread() bool {
return c.Type == ChannelTypeGuildPublicThread || c.Type == ChannelTypeGuildPrivateThread || c.Type == ChannelTypeGuildNewsThread
}
// A ChannelEdit holds Channel Field data for a channel edit.
type ChannelEdit struct {
Name string `json:"name,omitempty"`
@ -316,6 +337,13 @@ type ChannelEdit struct {
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
ParentID string `json:"parent_id,omitempty"`
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`
// NOTE: threads only
Archived bool `json:"archived,omitempty"`
AutoArchiveDuration int `json:"auto_archive_duration,omitempty"`
Locked bool `json:"locked,bool"`
Invitable bool `json:"invitable,omitempty"`
}
// A ChannelFollow holds data returned after following a news channel
@ -342,6 +370,56 @@ type PermissionOverwrite struct {
Allow int64 `json:"allow,string"`
}
// ThreadStart stores all parameters you can use with MessageThreadStartComplex or ThreadStartComplex
type ThreadStart struct {
Name string `json:"name"`
AutoArchiveDuration int `json:"auto_archive_duration,omitempty"`
Type ChannelType `json:"type,omitempty"`
Invitable bool `json:"invitable"`
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`
}
// ThreadMetadata contains a number of thread-specific channel fields that are not needed by other channel types.
type ThreadMetadata struct {
// Whether the thread is archived
Archived bool `json:"archived"`
// Duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080
AutoArchiveDuration int `json:"auto_archive_duration"`
// Timestamp when the thread's archive status was last changed, used for calculating recent activity
ArchiveTimestamp time.Time `json:"archive_timestamp"`
// Whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it
Locked bool `json:"locked"`
// Whether non-moderators can add other non-moderators to a thread; only available on private threads
Invitable bool `json:"invitable"`
}
// ThreadMember is used to indicate whether a user has joined a thread or not.
// NOTE: ID and UserID are empty (omitted) on the member sent within each thread in the GUILD_CREATE event.
type ThreadMember struct {
// The id of the thread
ID string `json:"id,omitempty"`
// The id of the user
UserID string `json:"user_id,omitempty"`
// The time the current user last joined the thread
JoinTimestamp time.Time `json:"join_timestamp"`
// Any user-thread settings, currently only used for notifications
Flags int
}
// ThreadsList represents a list of threads alongisde with thread member objects for the current user.
type ThreadsList struct {
Threads []*Channel `json:"threads"`
Members []*ThreadMember `json:"members"`
HasMore bool `json:"has_more"`
}
// AddedThreadMember holds information about the user who was added to the thread
type AddedThreadMember struct {
*ThreadMember
Member *Member `json:"member"`
Presence *Presence `json:"presence"`
}
// Emoji struct holds data related to Emoji's
type Emoji struct {
ID string `json:"id"`
@ -507,6 +585,11 @@ type Guild struct {
// update events, and thus is only present in state-cached guilds.
Channels []*Channel `json:"channels"`
// A list of all active threads in the guild that current user has permission to view
// This field is only present in GUILD_CREATE events and websocket
// update events and thus is only present in state-cached guilds.
Threads []*Channel `json:"threads"`
// A list of voice states for the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
@ -1377,6 +1460,10 @@ const (
PermissionMentionEveryone = 0x0000000000020000
PermissionUseExternalEmojis = 0x0000000000040000
PermissionUseSlashCommands = 0x0000000080000000
PermissionManageThreads = 0x0000000400000000
PermissionCreatePublicThreads = 0x0000000800000000
PermissionCreatePrivateThreads = 0x0000001000000000
PermissionSendMessagesInThreads = 0x0000004000000000
)
// Constants for the different bit offsets of voice permissions