From 32aac90b1352fbb37b40c7408382800adb08dc6b Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 15 Dec 2021 23:54:26 +0300 Subject: [PATCH 01/21] feat(endpoints): bumped discord version to 9 --- endpoints.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints.go b/endpoints.go index e5d7d9de5..d3b897d24 100644 --- a/endpoints.go +++ b/endpoints.go @@ -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 ( From 9dfbd8b48f1ae47e04b90029f06a05e8fa111631 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sun, 19 Dec 2021 01:51:31 +0300 Subject: [PATCH 02/21] feat: threads barebones --- endpoints.go | 36 +++++--- eventhandlers.go | 144 +++++++++++++++++++++++++++++++ events.go | 46 ++++++++++ examples/threads/main.go | 67 +++++++++++++++ message.go | 12 +++ restapi.go | 179 +++++++++++++++++++++++++++++++++++++++ state.go | 118 +++++++++++++++++++++++--- structs.go | 123 ++++++++++++++++++++++----- 8 files changed, 679 insertions(+), 46 deletions(-) create mode 100644 examples/threads/main.go diff --git a/endpoints.go b/endpoints.go index d3b897d24..a8a0bf1b1 100644 --- a/endpoints.go +++ b/endpoints.go @@ -78,6 +78,8 @@ var ( EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } 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" } @@ -103,19 +105,27 @@ var ( EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID } EndpointGuildBanner = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".png" } - EndpointChannel = func(cID string) string { return EndpointChannels + cID } - EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } - 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 } - EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } - 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" } + 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 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" } + EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } + 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" } diff --git a/eventhandlers.go b/eventhandlers.go index 4f5c4d92e..29565a7a0 100644 --- a/eventhandlers.go +++ b/eventhandlers.go @@ -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)) diff --git a/events.go b/events.go index be358ca31..c601674dd 100644 --- a/events.go +++ b/events.go @@ -73,6 +73,52 @@ type ChannelPinsUpdate struct { GuildID string `json:"guild_id,omitempty"` } +// ThreadCreate is the data for a ThreadCreate event. +type ThreadCreate struct { + *Channel +} + +// ThreadUpdate is the data for a ThreadUpdate event. +type ThreadUpdate struct { + *Channel + // TODO: BeforeUpdate +} + +// 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 diff --git a/examples/threads/main.go b/examples/threads/main.go new file mode 100644 index 000000000..fcbf5a465 --- /dev/null +++ b/examples/threads/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "time" + + "github.com/bwmarrin/discordgo" +) + +var ( + BotToken = flag.String("token", "", "Bot token") +) + +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("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.StartMessageThreadComplex(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(time.Second * 10) + if time.Since(games[m.ChannelID]) >= time.Second*10 { + s.ChannelEditComplex(m.ChannelID, &discordgo.ChannelEdit{ + Archived: true, + Locked: true, + }) + } + } + }) + 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") + +} diff --git a/message.go b/message.go index 402ab1f45..ab23e0db7 100644 --- a/message.go +++ b/message.go @@ -37,8 +37,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 ) @@ -125,10 +127,20 @@ 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"` } // UnmarshalJSON is a helper function to unmarshal the Message. diff --git a/restapi.go b/restapi.go index e3817d233..42935dc02 100644 --- a/restapi.go +++ b/restapi.go @@ -2414,6 +2414,185 @@ func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) return } +// ------------------------------------------------------------------------------------------------ +// Functions specific to threads +// ------------------------------------------------------------------------------------------------ + +// StartMessageThreadComplex +func (s *Session) StartMessageThreadComplex(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 +} + +func (s *Session) StartMessageThread(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) { + return s.StartMessageThreadComplex(channelID, messageID, &ThreadStart{ + Name: name, + AutoArchiveDuration: archiveDuration, + }) +} + +func (s *Session) StartThreadComplex(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 +} + +func (s *Session) StartThread(channelID, name string, archiveDuration int) (ch *Channel, err error) { + return s.StartThreadComplex(channelID, &ThreadStart{ + Name: name, + AutoArchiveDuration: archiveDuration, + }) +} + +func (s *Session) JoinThread(id string) error { + endpoint := EndpointThreadMember(id, "@me") + _, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint) + return err +} + +func (s *Session) LeaveThread(id string) error { + endpoint := EndpointThreadMember(id, "@me") + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + return err +} + +func (s *Session) RemoveThreadMember(threadID, memberID string) error { + endpoint := EndpointThreadMember(threadID, memberID) + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + return err +} + +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 +} + +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 +} + +func (s *Session) ActiveThreads(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 +} + +func (s *Session) GuildActiveThreads(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 +} + +func (s *Session) ArchivedThreads(channelID string, before Timestamp, limit int) (threads *ThreadsList, err error) { + endpoint := EndpointChannelPublicArchivedThreads(channelID) + v := url.Values{} + if before != "" { + v.Set("before", string(before)) + } + + 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 +} +func (s *Session) ArchivedPrivateThreads(channelID string, before Timestamp, limit int) (threads *ThreadsList, err error) { + endpoint := EndpointChannelPrivateArchivedThreads(channelID) + v := url.Values{} + if before != "" { + v.Set("before", string(before)) + } + + 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 +} +func (s *Session) ArchivedJoinedPrivateThreads(channelID string, before Timestamp, limit int) (threads *ThreadsList, err error) { + endpoint := EndpointChannelJoinedPrivateArchivedThreads(channelID) + v := url.Values{} + if before != "" { + v.Set("before", string(before)) + } + + 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 // ------------------------------------------------------------------------------------------------ diff --git a/state.go b/state.go index 2c21b19cd..110deb95e 100644 --- a/state.go +++ b/state.go @@ -40,6 +40,7 @@ type State struct { // MaxMessageCount represents how many messages per channel the state will store. MaxMessageCount int TrackChannels bool + TrackThreads bool TrackEmojis bool TrackMembers bool TrackRoles bool @@ -59,6 +60,7 @@ func NewState() *State { Guilds: []*Guild{}, }, TrackChannels: true, + TrackThreads: true, TrackEmojis: true, TrackMembers: true, TrackRoles: true, @@ -93,6 +95,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 +129,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 } @@ -465,6 +475,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 +485,18 @@ func (s *State) ChannelAdd(channel *Channel) error { if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { s.PrivateChannels = append(s.PrivateChannels, channel) - } else { - guild, ok := s.guildMap[channel.GuildID] - if !ok { - return ErrStateNotFound - } + 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,15 +526,26 @@ func (s *State) ChannelRemove(channel *Channel) error { break } } - } else { - guild, err := s.Guild(channel.GuildID) - if err != nil { - return err - } + delete(s.channelMap, channel.ID) + return nil + } - s.Lock() - defer s.Unlock() + guild, err := s.Guild(channel.GuildID) + if err != nil { + return err + } + 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 +559,49 @@ 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 + for _, t := range guild.Threads { + if !t.ThreadMetadata.Archived && tls.ChannelIDs != nil { + for _, v := range tls.ChannelIDs { + if t.ParentID == v { + goto remove + } + } + guild.Threads[index] = t + index++ + continue + } + remove: + 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 +} + // 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) { @@ -913,7 +986,26 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { if s.TrackChannels { err = s.ChannelRemove(t.Channel) } - case *MessageCreate: + case *ThreadCreate: + if s.TrackThreads { + msglog(LogDebug, 1, "thread created %s (%q) in %s", t.ID, t.Name, t.ParentID) + err = s.ChannelAdd(t.Channel) + } + case *ThreadUpdate: + if s.TrackThreads { + msglog(LogDebug, 1, "thread updated %s (%q) in %s", t.ID, t.Name, t.ParentID) + err = s.ChannelAdd(t.Channel) + } + case *ThreadDelete: + if s.TrackThreads { + msglog(LogDebug, 1, "thread deleted %s (%q) in %s", t.ID, t.Name, t.ParentID) + err = s.ChannelRemove(t.Channel) + } + case *ThreadListSync: + if s.TrackThreads { + err = s.ThreadListSync(t) + } + case *MessageCreate: // TODO: last message id in thread if s.MaxMessageCount != 0 { err = s.MessageAdd(t.Message) } diff --git a/structs.go b/structs.go index b4baa6b13..d0aa4ec56 100644 --- a/structs.go +++ b/structs.go @@ -226,13 +226,16 @@ type ChannelType int // Block contains known ChannelType values const ( - ChannelTypeGuildText ChannelType = 0 - ChannelTypeDM ChannelType = 1 - ChannelTypeGuildVoice ChannelType = 2 - ChannelTypeGroupDM ChannelType = 3 - ChannelTypeGuildCategory ChannelType = 4 - ChannelTypeGuildNews ChannelType = 5 - ChannelTypeGuildStore ChannelType = 6 + ChannelTypeGuildText ChannelType = 0 + ChannelTypeDM ChannelType = 1 + ChannelTypeGuildVoice ChannelType = 2 + ChannelTypeGroupDM ChannelType = 3 + 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 { // Empty if the channel has no pinned messages. LastPinTimestamp Timestamp `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,23 @@ 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"` } // Mention returns a string which mentions the channel @@ -305,6 +318,11 @@ func (c *Channel) Mention() string { return fmt.Sprintf("<#%s>", c.ID) } +// IsThreads 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 +334,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 +367,55 @@ type PermissionOverwrite struct { Allow int64 `json:"allow,string"` } +// ThreadStart stores all parameters you can use with StartMessageThreadComplex or StartThreadComplex +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 Timestamp `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 Timestamp `json:"join_timestamp"` + // Any user-thread settings, currently only used for notifications + Flags int +} + +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 +581,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. @@ -1250,16 +1329,20 @@ type IdentifyProperties struct { // Constants for the different bit offsets of text channel permissions const ( // Deprecated: PermissionReadMessages has been replaced with PermissionViewChannel for text and voice channels - PermissionReadMessages = 0x0000000000000400 - PermissionSendMessages = 0x0000000000000800 - PermissionSendTTSMessages = 0x0000000000001000 - PermissionManageMessages = 0x0000000000002000 - PermissionEmbedLinks = 0x0000000000004000 - PermissionAttachFiles = 0x0000000000008000 - PermissionReadMessageHistory = 0x0000000000010000 - PermissionMentionEveryone = 0x0000000000020000 - PermissionUseExternalEmojis = 0x0000000000040000 - PermissionUseSlashCommands = 0x0000000080000000 + PermissionReadMessages = 0x0000000000000400 + PermissionSendMessages = 0x0000000000000800 + PermissionSendTTSMessages = 0x0000000000001000 + PermissionManageMessages = 0x0000000000002000 + PermissionEmbedLinks = 0x0000000000004000 + PermissionAttachFiles = 0x0000000000008000 + PermissionReadMessageHistory = 0x0000000000010000 + PermissionMentionEveryone = 0x0000000000020000 + PermissionUseExternalEmojis = 0x0000000000040000 + PermissionUseSlashCommands = 0x0000000080000000 + PermissionManageThreads = 0x0000000400000000 + PermissionCreatePublicThreads = 0x0000000800000000 + PermissionCreatePrivateThreads = 0x0000001000000000 + PermissionSendMessagesInThreads = 0x0000004000000000 ) // Constants for the different bit offsets of voice permissions From 2052f869b4e5bf873487bcc80abce2c53bb00ee9 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sun, 19 Dec 2021 15:24:59 +0300 Subject: [PATCH 03/21] feat(threads): documentation --- examples/threads/main.go | 18 ++++++++----- restapi.go | 57 +++++++++++++++++++++++++++++++++++++--- structs.go | 3 ++- types.go | 5 ++++ 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/examples/threads/main.go b/examples/threads/main.go index fcbf5a465..fc1e2a196 100644 --- a/examples/threads/main.go +++ b/examples/threads/main.go @@ -12,17 +12,20 @@ import ( "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("Ready") + fmt.Println("Bot is ready") }) s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { if strings.Contains(m.Content, "ping") { @@ -36,18 +39,21 @@ func main() { if err != nil { panic(err) } - _, _ = s.ChannelMessageSend(thread.ID, "Pong") + _, _ = s.ChannelMessageSend(thread.ID, "pong") m.ChannelID = thread.ID } else { - _, _ = s.ChannelMessageSendReply(m.ChannelID, "Pong", m.Reference()) + _, _ = s.ChannelMessageSendReply(m.ChannelID, "pong", m.Reference()) } games[m.ChannelID] = time.Now() - <-time.After(time.Second * 10) - if time.Since(games[m.ChannelID]) >= time.Second*10 { - s.ChannelEditComplex(m.ChannelID, &discordgo.ChannelEdit{ + <-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) + } } } }) diff --git a/restapi.go b/restapi.go index 42935dc02..ede829494 100644 --- a/restapi.go +++ b/restapi.go @@ -2162,11 +2162,20 @@ func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *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) { +// threadID : Sends a message to the specified thread within a webhook's channel. The thread will automatically be unarchived. +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 @@ -2418,7 +2427,10 @@ func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) // Functions specific to threads // ------------------------------------------------------------------------------------------------ -// StartMessageThreadComplex +// StartMessageThreadComplex 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) StartMessageThreadComplex(channelID, messageID string, data *ThreadStart) (ch *Channel, err error) { endpoint := EndpointChannelMessageThread(channelID, messageID) var body []byte @@ -2431,6 +2443,11 @@ func (s *Session) StartMessageThreadComplex(channelID, messageID string, data *T return } +// StartMessageThread 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) StartMessageThread(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) { return s.StartMessageThreadComplex(channelID, messageID, &ThreadStart{ Name: name, @@ -2438,6 +2455,9 @@ func (s *Session) StartMessageThread(channelID, messageID string, name string, a }) } +// StartThreadComplex creates a new thread. +// channelID : Channel to create thread in +// data : Parameters of the thread func (s *Session) StartThreadComplex(channelID string, data *ThreadStart) (ch *Channel, err error) { endpoint := EndpointChannelThreads(channelID) var body []byte @@ -2450,6 +2470,10 @@ func (s *Session) StartThreadComplex(channelID string, data *ThreadStart) (ch *C return } +// StartThread creates a new thread. +// channelID : Channel to create thread in +// name : Name of the thread +// archiveDuration : Auto archive duration (in minutes) func (s *Session) StartThread(channelID, name string, archiveDuration int) (ch *Channel, err error) { return s.StartThreadComplex(channelID, &ThreadStart{ Name: name, @@ -2457,24 +2481,35 @@ func (s *Session) StartThread(channelID, name string, archiveDuration int) (ch * }) } +// JoinThread adds current user to a thread func (s *Session) JoinThread(id string) error { endpoint := EndpointThreadMember(id, "@me") _, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint) return err } +// LeaveThread removes current user to a thread func (s *Session) LeaveThread(id string) error { endpoint := EndpointThreadMember(id, "@me") _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) return err } +// AddThreadMember adds another member to a thread +func (s *Session) AddThreadMember(threadID, memberID string) error { + endpoint := EndpointThreadMember(threadID, memberID) + _, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint) + return err +} + +// RemoveThreadMember removes another member from a thread func (s *Session) RemoveThreadMember(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 @@ -2488,6 +2523,7 @@ func (s *Session) ThreadMember(threadID, memberID string) (member *ThreadMember, 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)) @@ -2500,6 +2536,7 @@ func (s *Session) ThreadMembers(threadID string) (members []*ThreadMember, err e return } +// ActiveThreads returns all active threads for specified channel. func (s *Session) ActiveThreads(channelID string) (threads *ThreadsList, err error) { var body []byte body, err = s.RequestWithBucketID("GET", EndpointChannelActiveThreads(channelID), nil, EndpointChannelActiveThreads(channelID)) @@ -2511,6 +2548,7 @@ func (s *Session) ActiveThreads(channelID string) (threads *ThreadsList, err err return } +// GuildActiveThreads returns all active threads for specified guild. func (s *Session) GuildActiveThreads(guildID string) (threads *ThreadsList, err error) { var body []byte body, err = s.RequestWithBucketID("GET", EndpointGuildActiveThreads(guildID), nil, EndpointGuildActiveThreads(guildID)) @@ -2522,6 +2560,9 @@ func (s *Session) GuildActiveThreads(guildID string) (threads *ThreadsList, err return } +// ArchivedThreads 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) ArchivedThreads(channelID string, before Timestamp, limit int) (threads *ThreadsList, err error) { endpoint := EndpointChannelPublicArchivedThreads(channelID) v := url.Values{} @@ -2546,6 +2587,10 @@ func (s *Session) ArchivedThreads(channelID string, before Timestamp, limit int) err = unmarshal(body, &threads) return } + +// ArchivedPrivateThreads 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) ArchivedPrivateThreads(channelID string, before Timestamp, limit int) (threads *ThreadsList, err error) { endpoint := EndpointChannelPrivateArchivedThreads(channelID) v := url.Values{} @@ -2569,6 +2614,10 @@ func (s *Session) ArchivedPrivateThreads(channelID string, before Timestamp, lim err = unmarshal(body, &threads) return } + +// ArchivedJoinedPrivateThreads 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) ArchivedJoinedPrivateThreads(channelID string, before Timestamp, limit int) (threads *ThreadsList, err error) { endpoint := EndpointChannelJoinedPrivateArchivedThreads(channelID) v := url.Values{} @@ -2763,7 +2812,7 @@ func (s *Session) InteractionResponseDelete(appID string, interaction *Interacti // wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise) // data : Data of the message to send. func (s *Session) FollowupMessageCreate(appID string, interaction *Interaction, wait bool, data *WebhookParams) (*Message, error) { - return s.WebhookExecute(appID, interaction.Token, wait, data) + return s.WebhookExecute(appID, interaction.Token, wait, "", data) } // FollowupMessageEdit edits a followup message of an interaction. diff --git a/structs.go b/structs.go index d0aa4ec56..15c91f0fb 100644 --- a/structs.go +++ b/structs.go @@ -318,7 +318,7 @@ func (c *Channel) Mention() string { return fmt.Sprintf("<#%s>", c.ID) } -// IsThreads is a helper function to determine if channel is a thread or not +// 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 } @@ -403,6 +403,7 @@ type ThreadMember struct { 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"` diff --git a/types.go b/types.go index c0ce01315..6cffbd9f4 100644 --- a/types.go +++ b/types.go @@ -24,6 +24,11 @@ func (t Timestamp) Parse() (time.Time, error) { return time.Parse(time.RFC3339, string(t)) } +// NewTimestamp constructs a Timestamp from specified time. +func NewTimestamp(t time.Time) Timestamp { + return Timestamp(t.Format(time.RFC3339)) +} + // RESTError stores error information about a request with a bad response code. // Message is not always present, there are cases where api calls can fail // without returning a json message. From 85a65a5b3f3b4403f3d81370385c940b4f06ea2d Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sun, 19 Dec 2021 15:56:51 +0300 Subject: [PATCH 04/21] feat(threads): membership caching --- state.go | 111 ++++++++++++++++++++++++++++++++++++++++++++--------- structs.go | 8 +++- 2 files changed, 100 insertions(+), 19 deletions(-) diff --git a/state.go b/state.go index 110deb95e..ebf281501 100644 --- a/state.go +++ b/state.go @@ -38,14 +38,15 @@ type State struct { Ready // MaxMessageCount represents how many messages per channel the state will store. - MaxMessageCount int - TrackChannels bool - TrackThreads bool - TrackEmojis bool - TrackMembers bool - TrackRoles bool - TrackVoice bool - TrackPresences bool + MaxMessageCount int + TrackChannels bool + TrackThreads bool + TrackEmojis bool + TrackMembers bool + TrackThreadMembers bool + TrackRoles bool + TrackVoice bool + TrackPresences bool guildMap map[string]*Guild channelMap map[string]*Channel @@ -59,16 +60,17 @@ func NewState() *State { PrivateChannels: []*Channel{}, Guilds: []*Guild{}, }, - TrackChannels: true, - TrackThreads: true, - TrackEmojis: true, - TrackMembers: true, - TrackRoles: true, - TrackVoice: true, - TrackPresences: true, - guildMap: make(map[string]*Guild), - channelMap: make(map[string]*Channel), - memberMap: make(map[string]map[string]*Member), + TrackChannels: true, + TrackThreads: true, + TrackEmojis: true, + TrackMembers: true, + TrackThreadMembers: true, + TrackRoles: true, + TrackVoice: true, + TrackPresences: true, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), + memberMap: make(map[string]map[string]*Member), } } @@ -602,6 +604,59 @@ func (s *State) ThreadListSync(tls *ThreadListSync) error { 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() + 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 + } + } + } + s.Unlock() + + for _, addedMember := range tmu.AddedMembers { + s.Lock() + thread.Members = append(thread.Members, addedMember.ThreadMember) + s.Unlock() + 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 + } + } + } + s.Lock() + thread.MemberCount = tmu.MemberCount + s.Unlock() + + 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) { @@ -741,6 +796,12 @@ func (s *State) MessageAdd(message *Message) error { if len(c.Messages) > s.MaxMessageCount { c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:] } + + if c.IsThread() { + c.MessageCount++ + c.LastMessageID = message.ID + } + return nil } @@ -765,6 +826,12 @@ func (s *State) messageRemoveByID(channelID, messageID string) error { for i, m := range c.Messages { if m.ID == messageID { + if c.IsThread() { + if i == len(c.Messages)-1 && len(c.Messages) > 0 { + c.LastMessageID = c.Messages[i-1].ID + } + c.MessageCount-- + } c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) return nil } @@ -1001,6 +1068,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { msglog(LogDebug, 1, "thread deleted %s (%q) in %s", t.ID, t.Name, t.ParentID) 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) diff --git a/structs.go b/structs.go index 15c91f0fb..9ffdc1fce 100644 --- a/structs.go +++ b/structs.go @@ -311,6 +311,9 @@ type Channel struct { 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 @@ -318,8 +321,11 @@ func (c *Channel) Mention() string { return fmt.Sprintf("<#%s>", c.ID) } +// Thread is a subchannel inside an existing channel +type Thread = Channel + // IsThread is a helper function to determine if channel is a thread or not -func (c *Channel) IsThread() bool { +func (c *Thread) IsThread() bool { return c.Type == ChannelTypeGuildPublicThread || c.Type == ChannelTypeGuildPrivateThread || c.Type == ChannelTypeGuildNewsThread } From 1a55a00a11dd71beb604666483bde474b38d7fdf Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 21 Dec 2021 23:03:39 +0300 Subject: [PATCH 05/21] feat(threads): added type to StartThread method --- restapi.go | 3 ++- state.go | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/restapi.go b/restapi.go index ede829494..e6b6a23c6 100644 --- a/restapi.go +++ b/restapi.go @@ -2474,9 +2474,10 @@ func (s *Session) StartThreadComplex(channelID string, data *ThreadStart) (ch *C // channelID : Channel to create thread in // name : Name of the thread // archiveDuration : Auto archive duration (in minutes) -func (s *Session) StartThread(channelID, name string, archiveDuration int) (ch *Channel, err error) { +func (s *Session) StartThread(channelID, name string, typ ChannelType, archiveDuration int) (ch *Channel, err error) { return s.StartThreadComplex(channelID, &ThreadStart{ Name: name, + Type: typ, AutoArchiveDuration: archiveDuration, }) } diff --git a/state.go b/state.go index ebf281501..096250435 100644 --- a/state.go +++ b/state.go @@ -615,7 +615,6 @@ func (s *State) ThreadMembersUpdate(tmu *ThreadMembersUpdate) error { for _, removedMember := range tmu.RemovedMembers { if member.ID == removedMember { thread.Members = append(thread.Members[:idx], thread.Members[idx+1:]...) - break } } From 40d1c33fcc7ee89763d080f93e7d21b548855b3c Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 24 Dec 2021 21:26:41 +0300 Subject: [PATCH 06/21] fix: replaced missing Timestamp definitions with time.Time --- structs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structs.go b/structs.go index 643333dbd..f45350c63 100644 --- a/structs.go +++ b/structs.go @@ -389,7 +389,7 @@ type ThreadMetadata struct { // 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 Timestamp `json:"archive_timestamp"` + 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 @@ -404,7 +404,7 @@ type ThreadMember struct { // The id of the user UserID string `json:"user_id,omitempty"` // The time the current user last joined the thread - JoinTimestamp Timestamp `json:"join_timestamp"` + JoinTimestamp time.Time `json:"join_timestamp"` // Any user-thread settings, currently only used for notifications Flags int } From 43375698817b5df461ac468478a6e76b7305c767 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 24 Dec 2021 21:29:45 +0300 Subject: [PATCH 07/21] chore: removed debug logs --- state.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/state.go b/state.go index 06286c444..d0c7dacc0 100644 --- a/state.go +++ b/state.go @@ -1054,17 +1054,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { } case *ThreadCreate: if s.TrackThreads { - msglog(LogDebug, 1, "thread created %s (%q) in %s", t.ID, t.Name, t.ParentID) err = s.ChannelAdd(t.Channel) } case *ThreadUpdate: if s.TrackThreads { - msglog(LogDebug, 1, "thread updated %s (%q) in %s", t.ID, t.Name, t.ParentID) err = s.ChannelAdd(t.Channel) } case *ThreadDelete: if s.TrackThreads { - msglog(LogDebug, 1, "thread deleted %s (%q) in %s", t.ID, t.Name, t.ParentID) err = s.ChannelRemove(t.Channel) } case *ThreadMemberUpdate: From dcadeca6558f87ddab1f29d610ec04a1620db009 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 24 Dec 2021 21:30:32 +0300 Subject: [PATCH 08/21] chore: removed thread alias for channel type --- structs.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/structs.go b/structs.go index f45350c63..dec6338a7 100644 --- a/structs.go +++ b/structs.go @@ -321,11 +321,8 @@ func (c *Channel) Mention() string { return fmt.Sprintf("<#%s>", c.ID) } -// Thread is a subchannel inside an existing channel -type Thread = Channel - // IsThread is a helper function to determine if channel is a thread or not -func (c *Thread) IsThread() bool { +func (c *Channel) IsThread() bool { return c.Type == ChannelTypeGuildPublicThread || c.Type == ChannelTypeGuildPrivateThread || c.Type == ChannelTypeGuildNewsThread } From 6b2e5f3cd432c2a33ebc3bb8b5994b5fb9287f22 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 24 Dec 2021 21:34:06 +0300 Subject: [PATCH 09/21] feat(webhooks): separated thread option into method --- restapi.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/restapi.go b/restapi.go index ed27dde0b..9856255e3 100644 --- a/restapi.go +++ b/restapi.go @@ -2158,12 +2158,7 @@ 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) -// threadID : Sends a message to the specified thread within a webhook's channel. The thread will automatically be unarchived. -func (s *Session) WebhookExecute(webhookID, token string, wait bool, threadID string, 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{} @@ -2197,6 +2192,23 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, threadID st 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) +} + +// WebhookExecute 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, "", data) +} + // WebhookMessage gets a webhook message. // webhookID : The ID of a webhook // token : The auth token for the webhook @@ -2813,7 +2825,7 @@ func (s *Session) InteractionResponseDelete(appID string, interaction *Interacti // wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise) // data : Data of the message to send. func (s *Session) FollowupMessageCreate(appID string, interaction *Interaction, wait bool, data *WebhookParams) (*Message, error) { - return s.WebhookExecute(appID, interaction.Token, wait, "", data) + return s.WebhookExecute(appID, interaction.Token, wait, data) } // FollowupMessageEdit edits a followup message of an interaction. From 5955bcf478c55f4adcfc17f8238641031c9bdbc4 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 24 Dec 2021 22:20:36 +0300 Subject: [PATCH 10/21] fix(state): ThreadMembersUpdate member duplication bug --- state.go | 72 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/state.go b/state.go index d0c7dacc0..fe51156b3 100644 --- a/state.go +++ b/state.go @@ -192,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 - } - - guild, err := s.Guild(guildID) - if err != nil { - return err +func (s *State) presenceAdd(guildID string, presence *Presence) error { + guild, ok := s.guildMap[guildID] + if !ok { + return ErrStateNotFound } - s.Lock() - defer s.Unlock() - for i, p := range guild.Presences { if p.User.ID == presence.User.ID { //guild.Presences[i] = presence @@ -245,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 { @@ -291,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 - } - - guild, err := s.Guild(member.GuildID) - if err != nil { - return err +func (s *State) memberAdd(member *Member) error { + guild, ok := s.guildMap[member.GuildID] + if !ok { + return ErrStateNotFound } - s.Lock() - defer s.Unlock() - members, ok := s.memberMap[member.GuildID] if !ok { return ErrStateNotFound @@ -323,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 { @@ -611,6 +618,8 @@ func (s *State) ThreadMembersUpdate(tmu *ThreadMembersUpdate) error { return err } s.Lock() + defer s.Unlock() + for idx, member := range thread.Members { for _, removedMember := range tmu.RemovedMembers { if member.ID == removedMember { @@ -619,28 +628,23 @@ func (s *State) ThreadMembersUpdate(tmu *ThreadMembersUpdate) error { } } } - s.Unlock() for _, addedMember := range tmu.AddedMembers { - s.Lock() thread.Members = append(thread.Members, addedMember.ThreadMember) - s.Unlock() if addedMember.Member != nil { - err = s.MemberAdd(addedMember.Member) + err = s.memberAdd(addedMember.Member) if err != nil { return err } } if addedMember.Presence != nil { - err = s.PresenceAdd(tmu.GuildID, addedMember.Presence) + err = s.presenceAdd(tmu.GuildID, addedMember.Presence) if err != nil { return err } } } - s.Lock() thread.MemberCount = tmu.MemberCount - s.Unlock() return nil } From 1651052fe1acc0d691e7468a020d9293930853d5 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 24 Dec 2021 22:22:43 +0300 Subject: [PATCH 11/21] fix: golint --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 9856255e3..5e1d730ec 100644 --- a/restapi.go +++ b/restapi.go @@ -2200,7 +2200,7 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho return s.webhookExecute(webhookID, token, wait, "", data) } -// WebhookExecute executes a webhook in a thread. +// 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) From 2939156e984c588a4ab28bf7f7a4270a56093106 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sat, 15 Jan 2022 14:20:54 +0300 Subject: [PATCH 12/21] feat(threads): pr fixes and BeforeUpdate in ThreadUpdate --- events.go | 2 +- restapi.go | 50 +++++++++++++++++++++++++------------------------- state.go | 14 ++++++++++---- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/events.go b/events.go index c601674dd..402f2dbb7 100644 --- a/events.go +++ b/events.go @@ -81,7 +81,7 @@ type ThreadCreate struct { // ThreadUpdate is the data for a ThreadUpdate event. type ThreadUpdate struct { *Channel - // TODO: BeforeUpdate + BeforeUpdate *Channel `json:"-"` } // ThreadDelete is the data for a ThreadDelete event. diff --git a/restapi.go b/restapi.go index 5e1d730ec..fdc06f71c 100644 --- a/restapi.go +++ b/restapi.go @@ -2455,22 +2455,22 @@ func (s *Session) StartMessageThreadComplex(channelID, messageID string, data *T return } -// StartMessageThread creates a new thread from an existing message. +// 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) StartMessageThread(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) { +func (s *Session) MessageThreadStart(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) { return s.StartMessageThreadComplex(channelID, messageID, &ThreadStart{ Name: name, AutoArchiveDuration: archiveDuration, }) } -// StartThreadComplex creates a new thread. +// ThreadComplexStart creates a new thread. // channelID : Channel to create thread in // data : Parameters of the thread -func (s *Session) StartThreadComplex(channelID string, data *ThreadStart) (ch *Channel, err error) { +func (s *Session) ThreadComplexStart(channelID string, data *ThreadStart) (ch *Channel, err error) { endpoint := EndpointChannelThreads(channelID) var body []byte body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) @@ -2482,41 +2482,41 @@ func (s *Session) StartThreadComplex(channelID string, data *ThreadStart) (ch *C return } -// StartThread creates a new thread. +// 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) StartThread(channelID, name string, typ ChannelType, archiveDuration int) (ch *Channel, err error) { - return s.StartThreadComplex(channelID, &ThreadStart{ +func (s *Session) ThreadStart(channelID, name string, typ ChannelType, archiveDuration int) (ch *Channel, err error) { + return s.ThreadComplexStart(channelID, &ThreadStart{ Name: name, Type: typ, AutoArchiveDuration: archiveDuration, }) } -// JoinThread adds current user to a thread -func (s *Session) JoinThread(id string) error { +// 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 } -// LeaveThread removes current user to a thread -func (s *Session) LeaveThread(id string) error { +// 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 } -// AddThreadMember adds another member to a thread -func (s *Session) AddThreadMember(threadID, memberID string) error { +// 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 } -// RemoveThreadMember removes another member from a thread -func (s *Session) RemoveThreadMember(threadID, memberID string) error { +// 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 @@ -2549,8 +2549,8 @@ func (s *Session) ThreadMembers(threadID string) (members []*ThreadMember, err e return } -// ActiveThreads returns all active threads for specified channel. -func (s *Session) ActiveThreads(channelID string) (threads *ThreadsList, err error) { +// 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 { @@ -2561,8 +2561,8 @@ func (s *Session) ActiveThreads(channelID string) (threads *ThreadsList, err err return } -// GuildActiveThreads returns all active threads for specified guild. -func (s *Session) GuildActiveThreads(guildID string) (threads *ThreadsList, err error) { +// 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 { @@ -2573,10 +2573,10 @@ func (s *Session) GuildActiveThreads(guildID string) (threads *ThreadsList, err return } -// ArchivedThreads returns archived threads for specified channel. +// 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) ArchivedThreads(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { +func (s *Session) ThreadsArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { endpoint := EndpointChannelPublicArchivedThreads(channelID) v := url.Values{} if before != nil { @@ -2601,10 +2601,10 @@ func (s *Session) ArchivedThreads(channelID string, before *time.Time, limit int return } -// ArchivedPrivateThreads returns archived private threads for specified channel. +// 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) ArchivedPrivateThreads(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { +func (s *Session) ThreadsPrivateArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { endpoint := EndpointChannelPrivateArchivedThreads(channelID) v := url.Values{} if before != nil { @@ -2628,10 +2628,10 @@ func (s *Session) ArchivedPrivateThreads(channelID string, before *time.Time, li return } -// ArchivedJoinedPrivateThreads returns archived joined private threads for specified channel. +// 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) ArchivedJoinedPrivateThreads(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { +func (s *Session) ThreadsPrivateJoinedArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { endpoint := EndpointChannelJoinedPrivateArchivedThreads(channelID) v := url.Values{} if before != nil { diff --git a/state.go b/state.go index fe51156b3..f15f00cea 100644 --- a/state.go +++ b/state.go @@ -582,19 +582,20 @@ func (s *State) ThreadListSync(tls *ThreadListSync) error { // 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 { - goto remove + delete(s.channelMap, t.ID) + continue outer } } guild.Threads[index] = t index++ - continue + } else { + delete(s.channelMap, t.ID) } - remove: - delete(s.channelMap, t.ID) } guild.Threads = guild.Threads[:index] for _, t := range tls.Threads { @@ -1062,6 +1063,11 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { } 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: From f2dc014cf7c1623f4afc3dd575f25cf03bef4520 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sat, 15 Jan 2022 14:30:18 +0300 Subject: [PATCH 13/21] feat: removed unnecessary todo --- state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/state.go b/state.go index f15f00cea..56f185139 100644 --- a/state.go +++ b/state.go @@ -1086,7 +1086,7 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { if s.TrackThreads { err = s.ThreadListSync(t) } - case *MessageCreate: // TODO: last message id in thread + case *MessageCreate: if s.MaxMessageCount != 0 { err = s.MessageAdd(t.Message) } From 4ca359fd2cc304e5d0ec2937e25c0c487a1f2096 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sat, 15 Jan 2022 14:31:51 +0300 Subject: [PATCH 14/21] feat(state): removed thread last message update in MessageAdd --- state.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/state.go b/state.go index 56f185139..56c726ddd 100644 --- a/state.go +++ b/state.go @@ -800,12 +800,6 @@ func (s *State) MessageAdd(message *Message) error { if len(c.Messages) > s.MaxMessageCount { c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:] } - - if c.IsThread() { - c.MessageCount++ - c.LastMessageID = message.ID - } - return nil } From 1d308df1b1b7102094503a257cb18808f177f07b Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 2 Feb 2022 22:59:00 +0300 Subject: [PATCH 15/21] Revert "feat(state): removed thread last message update in MessageAdd" This reverts commit 4ca359fd2cc304e5d0ec2937e25c0c487a1f2096. --- state.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/state.go b/state.go index 56c726ddd..56f185139 100644 --- a/state.go +++ b/state.go @@ -800,6 +800,12 @@ func (s *State) MessageAdd(message *Message) error { if len(c.Messages) > s.MaxMessageCount { c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:] } + + if c.IsThread() { + c.MessageCount++ + c.LastMessageID = message.ID + } + return nil } From efed25327d168014f87a6877c76545f5e4645dfb Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 2 Feb 2022 23:01:09 +0300 Subject: [PATCH 16/21] 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. --- state.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/state.go b/state.go index 56f185139..6e744d885 100644 --- a/state.go +++ b/state.go @@ -802,7 +802,6 @@ func (s *State) MessageAdd(message *Message) error { } if c.IsThread() { - c.MessageCount++ c.LastMessageID = message.ID } @@ -834,7 +833,6 @@ func (s *State) messageRemoveByID(channelID, messageID string) error { if i == len(c.Messages)-1 && len(c.Messages) > 0 { c.LastMessageID = c.Messages[i-1].ID } - c.MessageCount-- } c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) return nil From 9321cd32910001f61f5272342f2ca56eb6874815 Mon Sep 17 00:00:00 2001 From: Fedor Lapshin Date: Sat, 5 Feb 2022 15:47:47 +0300 Subject: [PATCH 17/21] fix(restapi): passing threadID in WebhookThreadExecute --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 7439500f0..0c41989db 100644 --- a/restapi.go +++ b/restapi.go @@ -2220,7 +2220,7 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho // 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, "", data) + return s.webhookExecute(webhookID, token, wait, threadID, data) } // WebhookMessage gets a webhook message. From b1bb26d57404e4bdc32e53d333f49bbec349a1c2 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Feb 2022 16:43:09 +0300 Subject: [PATCH 18/21] feat(state): dropped last_message_id updates for threads --- state.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/state.go b/state.go index 6e744d885..e75be8950 100644 --- a/state.go +++ b/state.go @@ -801,10 +801,6 @@ func (s *State) MessageAdd(message *Message) error { c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:] } - if c.IsThread() { - c.LastMessageID = message.ID - } - return nil } @@ -829,12 +825,8 @@ func (s *State) messageRemoveByID(channelID, messageID string) error { for i, m := range c.Messages { if m.ID == messageID { - if c.IsThread() { - if i == len(c.Messages)-1 && len(c.Messages) > 0 { - c.LastMessageID = c.Messages[i-1].ID - } - } c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) + return nil } } From a4c537519a91142c2bbb9a9403c6b04e4887b683 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Feb 2022 20:46:31 +0300 Subject: [PATCH 19/21] fix: gofmt --- message.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/message.go b/message.go index 00ea50a45..9916dad29 100644 --- a/message.go +++ b/message.go @@ -139,11 +139,11 @@ type Message struct { // 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. + + // An array of Sticker objects, if any were sent. StickerItems []*Sticker `json:"sticker_items"` } From 505ff58f9f0c05f2fe99386b1b210996d2f54846 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Feb 2022 22:33:51 +0300 Subject: [PATCH 20/21] feat(events#ThreadCreate): added newly_created field --- events.go | 1 + 1 file changed, 1 insertion(+) diff --git a/events.go b/events.go index 402f2dbb7..c20e90e1b 100644 --- a/events.go +++ b/events.go @@ -76,6 +76,7 @@ type ChannelPinsUpdate struct { // 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. From 03c194716ebd0972f9da6d0ec901a9192dc80e7b Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Feb 2022 22:44:11 +0300 Subject: [PATCH 21/21] feat(restapi)!: corrected names of thread functions --- examples/threads/main.go | 2 +- restapi.go | 12 ++++++------ structs.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/threads/main.go b/examples/threads/main.go index fc1e2a196..101a257d0 100644 --- a/examples/threads/main.go +++ b/examples/threads/main.go @@ -30,7 +30,7 @@ func main() { 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.StartMessageThreadComplex(m.ChannelID, m.ID, &discordgo.ThreadStart{ + thread, err := s.MessageThreadStartComplex(m.ChannelID, m.ID, &discordgo.ThreadStart{ Name: "Pong game with " + m.Author.Username, AutoArchiveDuration: 60, Invitable: false, diff --git a/restapi.go b/restapi.go index 7b9a049c3..7544e4ea0 100644 --- a/restapi.go +++ b/restapi.go @@ -2189,11 +2189,11 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i // Functions specific to threads // ------------------------------------------------------------------------------------------------ -// StartMessageThreadComplex creates a new thread from an existing message. +// 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) StartMessageThreadComplex(channelID, messageID string, data *ThreadStart) (ch *Channel, err error) { +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) @@ -2211,16 +2211,16 @@ func (s *Session) StartMessageThreadComplex(channelID, messageID string, data *T // 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.StartMessageThreadComplex(channelID, messageID, &ThreadStart{ + return s.MessageThreadStartComplex(channelID, messageID, &ThreadStart{ Name: name, AutoArchiveDuration: archiveDuration, }) } -// ThreadComplexStart creates a new thread. +// ThreadStartComplex creates a new thread. // channelID : Channel to create thread in // data : Parameters of the thread -func (s *Session) ThreadComplexStart(channelID string, data *ThreadStart) (ch *Channel, err error) { +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) @@ -2237,7 +2237,7 @@ func (s *Session) ThreadComplexStart(channelID string, data *ThreadStart) (ch *C // 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.ThreadComplexStart(channelID, &ThreadStart{ + return s.ThreadStartComplex(channelID, &ThreadStart{ Name: name, Type: typ, AutoArchiveDuration: archiveDuration, diff --git a/structs.go b/structs.go index 75390fc8b..ad1360ceb 100644 --- a/structs.go +++ b/structs.go @@ -370,7 +370,7 @@ type PermissionOverwrite struct { Allow int64 `json:"allow,string"` } -// ThreadStart stores all parameters you can use with StartMessageThreadComplex or StartThreadComplex +// 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"`