mirror of
https://github.com/diamondburned/arikawa.git
synced 2025-08-30 18:34:51 +00:00
We now use the .Member in each incoming message and store that into the gateway cache (state). Theoretically, this member can get stale, but in reality bots scarcely ever use messages, so they won't be affected by this, and clients should still rely on the RequestGuildMembers command to get the latest member information.
510 lines
14 KiB
Go
510 lines
14 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/diamondburned/arikawa/v3/discord"
|
|
"github.com/diamondburned/arikawa/v3/gateway"
|
|
"github.com/diamondburned/arikawa/v3/state/store"
|
|
)
|
|
|
|
func (s *State) hookSession() {
|
|
s.Session.AddSyncHandler(func(event interface{}) {
|
|
// Call the pre-handler before the state handler.
|
|
if s.PreHandler != nil {
|
|
s.PreHandler.Call(event)
|
|
}
|
|
|
|
// Run the state handler.
|
|
s.onEvent(event)
|
|
|
|
switch event := event.(type) {
|
|
case *gateway.ReadyEvent:
|
|
s.Handler.Call(event)
|
|
s.handleReady(event)
|
|
case *gateway.GuildCreateEvent:
|
|
s.Handler.Call(event)
|
|
s.handleGuildCreate(event)
|
|
case *gateway.GuildDeleteEvent:
|
|
s.Handler.Call(event)
|
|
s.handleGuildDelete(event)
|
|
|
|
// https://github.com/discord/discord-api-docs/commit/01665c4
|
|
case *gateway.MessageCreateEvent:
|
|
if event.Member != nil {
|
|
event.Member.User = event.Author
|
|
}
|
|
s.Handler.Call(event)
|
|
|
|
case *gateway.MessageUpdateEvent:
|
|
if event.Member != nil {
|
|
event.Member.User = event.Author
|
|
}
|
|
s.Handler.Call(event)
|
|
|
|
default:
|
|
s.Handler.Call(event)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *State) onEvent(iface interface{}) {
|
|
switch ev := iface.(type) {
|
|
case *gateway.ReadyEvent:
|
|
// Acquire the ready mutex, but since we're only writing the value and
|
|
// not anything in it, we should be fine.
|
|
s.readyMu.Lock()
|
|
s.ready = *ev
|
|
s.readyMu.Unlock()
|
|
|
|
// Reset the store before proceeding.
|
|
if err := s.Cabinet.Reset(); err != nil {
|
|
s.stateErr(err, "failed to reset state in Ready")
|
|
}
|
|
|
|
// Handle guilds
|
|
for i := range ev.Guilds {
|
|
s.batchLog(storeGuildCreate(s.Cabinet, &ev.Guilds[i]))
|
|
}
|
|
|
|
// Handle guild presences
|
|
for i, presence := range ev.Presences {
|
|
if err := s.Cabinet.PresenceSet(presence.GuildID, &ev.Presences[i], false); err != nil {
|
|
s.stateErr(err, "failed to set presence in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle private channels
|
|
for i := range ev.PrivateChannels {
|
|
if err := s.Cabinet.ChannelSet(&ev.PrivateChannels[i], false); err != nil {
|
|
s.stateErr(err, "failed to set channel in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle user
|
|
if err := s.Cabinet.MyselfSet(ev.User, false); err != nil {
|
|
s.stateErr(err, "failed to set self in Ready")
|
|
}
|
|
|
|
case *gateway.ReadySupplementalEvent:
|
|
// Handle guilds
|
|
for _, guild := range ev.Guilds {
|
|
// Handle guild voice states
|
|
for i := range guild.VoiceStates {
|
|
v := &guild.VoiceStates[i]
|
|
if err := s.Cabinet.VoiceStateSet(guild.ID, v, false); err != nil {
|
|
s.stateErr(err, "failed to set guild voice state in Ready Supplemental")
|
|
}
|
|
}
|
|
}
|
|
|
|
friendPresences := gateway.ConvertSupplementalPresences(ev.MergedPresences.Friends)
|
|
for i := range friendPresences {
|
|
if err := s.Cabinet.PresenceSet(0, &friendPresences[i], false); err != nil {
|
|
s.stateErr(err, "failed to set friend presence in Ready Supplemental")
|
|
}
|
|
}
|
|
|
|
// Discord uses weird indexing, so we'll need the Guilds slice.
|
|
ready := s.Ready()
|
|
|
|
for i := 0; i < len(ready.Guilds) && i < len(ev.MergedMembers); i++ {
|
|
guild := ready.Guilds[i]
|
|
|
|
members := gateway.ConvertSupplementalMembers(ev.MergedMembers[i])
|
|
for i := range members {
|
|
if err := s.Cabinet.MemberSet(guild.ID, &members[i], false); err != nil {
|
|
s.stateErr(err, "failed to set friend presence in Ready Supplemental")
|
|
}
|
|
}
|
|
|
|
presences := gateway.ConvertSupplementalPresences(ev.MergedPresences.Guilds[i])
|
|
for i := range presences {
|
|
if err := s.Cabinet.PresenceSet(guild.ID, &presences[i], false); err != nil {
|
|
s.stateErr(err, "failed to set member presence in Ready Supplemental")
|
|
}
|
|
}
|
|
}
|
|
|
|
case *gateway.GuildCreateEvent:
|
|
s.batchLog(storeGuildCreate(s.Cabinet, ev))
|
|
|
|
case *gateway.GuildUpdateEvent:
|
|
if err := s.Cabinet.GuildSet(&ev.Guild, true); err != nil {
|
|
s.stateErr(err, "failed to update guild in state")
|
|
}
|
|
|
|
case *gateway.GuildDeleteEvent:
|
|
if err := s.Cabinet.GuildRemove(ev.ID); err != nil && !ev.Unavailable {
|
|
s.stateErr(err, "failed to delete guild in state")
|
|
}
|
|
|
|
case *gateway.GuildMemberAddEvent:
|
|
if err := s.Cabinet.MemberSet(ev.GuildID, &ev.Member, false); err != nil {
|
|
s.stateErr(err, "failed to add a member in state")
|
|
}
|
|
|
|
case *gateway.GuildMemberUpdateEvent:
|
|
m, err := s.Cabinet.Member(ev.GuildID, ev.User.ID)
|
|
if err != nil {
|
|
// We can't do much here.
|
|
m = &discord.Member{}
|
|
}
|
|
|
|
// Update available fields from ev into m
|
|
ev.UpdateMember(m)
|
|
|
|
if err := s.Cabinet.MemberSet(ev.GuildID, m, true); err != nil {
|
|
s.stateErr(err, "failed to update a member in state")
|
|
}
|
|
|
|
case *gateway.GuildMemberRemoveEvent:
|
|
if err := s.Cabinet.MemberRemove(ev.GuildID, ev.User.ID); err != nil {
|
|
s.stateErr(err, "failed to remove a member in state")
|
|
}
|
|
|
|
case *gateway.GuildMembersChunkEvent:
|
|
for i := range ev.Members {
|
|
if err := s.Cabinet.MemberSet(ev.GuildID, &ev.Members[i], false); err != nil {
|
|
s.stateErr(err, "failed to add a member from chunk in state")
|
|
}
|
|
}
|
|
|
|
for i := range ev.Presences {
|
|
if err := s.Cabinet.PresenceSet(ev.GuildID, &ev.Presences[i], false); err != nil {
|
|
s.stateErr(err, "failed to add a presence from chunk in state")
|
|
}
|
|
}
|
|
|
|
case *gateway.GuildRoleCreateEvent:
|
|
if err := s.Cabinet.RoleSet(ev.GuildID, &ev.Role, false); err != nil {
|
|
s.stateErr(err, "failed to add a role in state")
|
|
}
|
|
|
|
case *gateway.GuildRoleUpdateEvent:
|
|
if err := s.Cabinet.RoleSet(ev.GuildID, &ev.Role, true); err != nil {
|
|
s.stateErr(err, "failed to update a role in state")
|
|
}
|
|
|
|
case *gateway.GuildRoleDeleteEvent:
|
|
if err := s.Cabinet.RoleRemove(ev.GuildID, ev.RoleID); err != nil {
|
|
s.stateErr(err, "failed to remove a role in state")
|
|
}
|
|
|
|
case *gateway.GuildEmojisUpdateEvent:
|
|
if err := s.Cabinet.EmojiSet(ev.GuildID, ev.Emojis, true); err != nil {
|
|
s.stateErr(err, "failed to update emojis in state")
|
|
}
|
|
|
|
case *gateway.ChannelCreateEvent:
|
|
if err := s.Cabinet.ChannelSet(&ev.Channel, false); err != nil {
|
|
s.stateErr(err, "failed to create a channel in state")
|
|
}
|
|
|
|
case *gateway.ChannelUpdateEvent:
|
|
if err := s.Cabinet.ChannelSet(&ev.Channel, true); err != nil {
|
|
s.stateErr(err, "failed to update a channel in state")
|
|
}
|
|
|
|
case *gateway.ChannelDeleteEvent:
|
|
if err := s.Cabinet.ChannelRemove(&ev.Channel); err != nil {
|
|
s.stateErr(err, "failed to remove a channel in state")
|
|
}
|
|
|
|
case *gateway.ChannelPinsUpdateEvent:
|
|
// not tracked.
|
|
|
|
case *gateway.ThreadListSyncEvent:
|
|
for i := range ev.Threads {
|
|
if err := s.Cabinet.ChannelSet(&ev.Threads[i], true); err != nil {
|
|
s.stateErr(err, "failed to set a thread in state sync")
|
|
}
|
|
}
|
|
|
|
case *gateway.ThreadCreateEvent:
|
|
if err := s.Cabinet.ChannelSet(&ev.Channel, false); err != nil {
|
|
s.stateErr(err, "failed to create a thread in state")
|
|
}
|
|
|
|
case *gateway.ThreadUpdateEvent:
|
|
if err := s.Cabinet.ChannelSet(&ev.Channel, true); err != nil {
|
|
s.stateErr(err, "failed to update a thread in state")
|
|
}
|
|
|
|
case *gateway.ThreadDeleteEvent:
|
|
if ch, err := s.Cabinet.Channel(ev.ID); err == nil {
|
|
if err := s.Cabinet.ChannelRemove(ch); err != nil {
|
|
s.stateErr(err, "failed to delete a thread in state")
|
|
}
|
|
}
|
|
|
|
case *gateway.MessageCreateEvent:
|
|
if ev.GuildID.IsValid() && ev.Member != nil {
|
|
if err := s.Cabinet.MemberStore.MemberSet(ev.GuildID, ev.Member, false); err != nil {
|
|
s.stateErr(err, "failed to add a member in state from message create")
|
|
}
|
|
}
|
|
if err := s.Cabinet.MessageSet(&ev.Message, false); err != nil {
|
|
s.stateErr(err, "failed to add a message in state")
|
|
}
|
|
|
|
case *gateway.MessageUpdateEvent:
|
|
if ev.GuildID.IsValid() && ev.Member != nil {
|
|
if err := s.Cabinet.MemberStore.MemberSet(ev.GuildID, ev.Member, false); err != nil {
|
|
s.stateErr(err, "failed to add a member in state from message create")
|
|
}
|
|
}
|
|
if err := s.Cabinet.MessageSet(&ev.Message, true); err != nil {
|
|
s.stateErr(err, "failed to update a message in state")
|
|
}
|
|
|
|
case *gateway.MessageDeleteEvent:
|
|
if err := s.Cabinet.MessageRemove(ev.ChannelID, ev.ID); err != nil {
|
|
s.stateErr(err, "failed to delete a message in state")
|
|
}
|
|
|
|
case *gateway.MessageDeleteBulkEvent:
|
|
for _, id := range ev.IDs {
|
|
if err := s.Cabinet.MessageRemove(ev.ChannelID, id); err != nil {
|
|
s.stateErr(err, "failed to delete bulk messages in state")
|
|
}
|
|
}
|
|
|
|
case *gateway.MessageReactionAddEvent:
|
|
var me bool
|
|
if u, _ := s.Cabinet.Me(); u != nil {
|
|
me = ev.UserID == u.ID
|
|
}
|
|
|
|
s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
|
|
if i := findReaction(m.Reactions, ev.Emoji); i > -1 {
|
|
// Copy the reactions slice so it's not racy.
|
|
m.Reactions = append([]discord.Reaction(nil), m.Reactions...)
|
|
m.Reactions[i].Count++
|
|
m.Reactions[i].Me = m.Reactions[i].Me || me
|
|
} else {
|
|
old := m.Reactions
|
|
m.Reactions = make([]discord.Reaction, 0, len(old)+1)
|
|
m.Reactions = append(m.Reactions, old...)
|
|
m.Reactions = append(m.Reactions, discord.Reaction{
|
|
Count: 1,
|
|
Me: me,
|
|
Emoji: ev.Emoji,
|
|
})
|
|
}
|
|
return true
|
|
})
|
|
|
|
case *gateway.MessageReactionRemoveEvent:
|
|
s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
|
|
var i = findReaction(m.Reactions, ev.Emoji)
|
|
if i < 0 {
|
|
return false
|
|
}
|
|
|
|
r := &m.Reactions[i]
|
|
// If the count is 0:
|
|
if (r.Count - 1) < 1 {
|
|
old := m.Reactions
|
|
m.Reactions = make([]discord.Reaction, len(m.Reactions)-1)
|
|
copy(m.Reactions[0:], old[:i])
|
|
copy(m.Reactions[i:], old[i+1:])
|
|
} else {
|
|
r.Count--
|
|
if r.Me { // If reaction removal is the user's
|
|
u, err := s.Cabinet.Me()
|
|
if err == nil && ev.UserID == u.ID {
|
|
r.Me = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
case *gateway.MessageReactionRemoveAllEvent:
|
|
s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
|
|
m.Reactions = nil
|
|
return true
|
|
})
|
|
|
|
case *gateway.MessageReactionRemoveEmojiEvent:
|
|
s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
|
|
var i = findReaction(m.Reactions, ev.Emoji)
|
|
if i < 0 {
|
|
return false
|
|
}
|
|
m.Reactions = append(m.Reactions[:i], m.Reactions[i+1:]...)
|
|
return true
|
|
})
|
|
|
|
case *gateway.PresenceUpdateEvent:
|
|
if err := s.Cabinet.PresenceSet(ev.GuildID, &ev.Presence, true); err != nil {
|
|
s.stateErr(err, "failed to update presence in state")
|
|
}
|
|
|
|
case *gateway.PresencesReplaceEvent:
|
|
for _, p := range *ev {
|
|
if err := s.Cabinet.PresenceSet(p.GuildID, &p.Presence, true); err != nil {
|
|
s.stateErr(err, "failed to update presence in state")
|
|
}
|
|
}
|
|
|
|
case *gateway.SessionsReplaceEvent:
|
|
// TODO
|
|
|
|
case *gateway.UserGuildSettingsUpdateEvent:
|
|
// TODO
|
|
|
|
case *gateway.UserSettingsUpdateEvent:
|
|
s.readyMu.Lock()
|
|
s.ready.UserSettings = &ev.UserSettings
|
|
s.readyMu.Unlock()
|
|
|
|
case *gateway.UserNoteUpdateEvent:
|
|
// TODO
|
|
|
|
case *gateway.UserUpdateEvent:
|
|
if err := s.Cabinet.MyselfSet(ev.User, true); err != nil {
|
|
s.stateErr(err, "failed to update myself from USER_UPDATE")
|
|
}
|
|
|
|
case *gateway.VoiceStateUpdateEvent:
|
|
vs := &ev.VoiceState
|
|
if !vs.ChannelID.IsValid() {
|
|
if err := s.Cabinet.VoiceStateRemove(vs.GuildID, vs.UserID); err != nil {
|
|
s.stateErr(err, "failed to remove voice state from state")
|
|
}
|
|
} else {
|
|
if err := s.Cabinet.VoiceStateSet(vs.GuildID, vs, true); err != nil {
|
|
s.stateErr(err, "failed to update voice state in state")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *State) stateErr(err error, wrap string) {
|
|
s.StateLog(fmt.Errorf("%s: %w", wrap, err))
|
|
}
|
|
|
|
func (s *State) batchLog(errors []error) {
|
|
for _, err := range errors {
|
|
s.StateLog(err)
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func (s *State) editMessage(ch discord.ChannelID, msg discord.MessageID, fn func(m *discord.Message) bool) {
|
|
m, err := s.Cabinet.Message(ch, msg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Copy the messages.
|
|
cpy := *m
|
|
m = &cpy
|
|
|
|
if !fn(m) {
|
|
return
|
|
}
|
|
|
|
if err := s.Cabinet.MessageSet(m, true); err != nil {
|
|
s.stateErr(err, "failed to save message in reaction add")
|
|
}
|
|
}
|
|
|
|
func findReaction(rs []discord.Reaction, emoji discord.Emoji) int {
|
|
for i := range rs {
|
|
if rs[i].Emoji.ID == emoji.ID && rs[i].Emoji.Name == emoji.Name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func storeGuildCreate(cab *store.Cabinet, guild *gateway.GuildCreateEvent) []error {
|
|
if guild.Unavailable {
|
|
return nil
|
|
}
|
|
|
|
stack, errs := newErrorStack()
|
|
|
|
if err := cab.GuildSet(&guild.Guild, false); err != nil {
|
|
errs(err, "failed to set guild in Ready")
|
|
}
|
|
|
|
// Handle guild emojis
|
|
if len(guild.Emojis) > 0 {
|
|
if err := cab.EmojiSet(guild.ID, guild.Emojis, false); err != nil {
|
|
errs(err, "failed to set guild emojis")
|
|
}
|
|
}
|
|
|
|
// Handle guild member
|
|
for i := range guild.Members {
|
|
if err := cab.MemberSet(guild.ID, &guild.Members[i], false); err != nil {
|
|
errs(err, "failed to set guild member in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle guild channels
|
|
for _, ch := range guild.Channels {
|
|
// I HATE Discord.
|
|
ch := ch
|
|
ch.GuildID = guild.ID
|
|
|
|
if err := cab.ChannelSet(&ch, false); err != nil {
|
|
errs(err, "failed to set guild channel in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle threads.
|
|
for _, ch := range guild.Threads {
|
|
ch := ch
|
|
ch.GuildID = guild.ID
|
|
|
|
if err := cab.ChannelSet(&ch, false); err != nil {
|
|
errs(err, "failed to set guild thread in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle guild presences
|
|
for _, p := range guild.Presences {
|
|
p := p
|
|
p.GuildID = guild.ID
|
|
|
|
if err := cab.PresenceSet(guild.ID, &p, false); err != nil {
|
|
errs(err, "failed to set guild presence in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle guild voice states
|
|
for _, v := range guild.VoiceStates {
|
|
v := v
|
|
v.GuildID = guild.ID
|
|
|
|
if err := cab.VoiceStateSet(guild.ID, &v, false); err != nil {
|
|
errs(err, "failed to set guild voice state in Ready")
|
|
}
|
|
}
|
|
|
|
// Handle guild roles
|
|
for _, r := range guild.Roles {
|
|
r := r
|
|
|
|
if err := cab.RoleSet(guild.ID, &r, false); err != nil {
|
|
errs(err, "failed to set role in Ready")
|
|
}
|
|
}
|
|
|
|
return *stack
|
|
}
|
|
|
|
func newErrorStack() (*[]error, func(error, string)) {
|
|
var errs = new([]error)
|
|
return errs, func(err error, wrap string) {
|
|
*errs = append(*errs, fmt.Errorf("%s: %w", wrap, err))
|
|
}
|
|
}
|