1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2025-11-09 03:55:10 +00:00

state: Add paginating State.Messages (#213)

* Store,State: Add update param to all store.XXXStore.XXXSet methods

* State: add paginating Messages

* Store: Fix test error

* store: merge shouldPrependMessage and shouldAppendMessage into single messageInsertPosition
This commit is contained in:
Maximilian von Lindern 2021-06-03 21:39:49 +02:00 committed by GitHub
parent 40e1a3757d
commit a808b52f00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 298 additions and 205 deletions

View file

@ -335,7 +335,7 @@ func (s *State) Me() (*discord.User, error) {
return nil, err
}
return u, s.Cabinet.MyselfSet(*u)
return u, s.Cabinet.MyselfSet(*u, false)
}
////
@ -352,7 +352,7 @@ func (s *State) Channel(id discord.ChannelID) (c *discord.Channel, err error) {
}
if s.tracksChannel(c) {
err = s.Cabinet.ChannelSet(*c)
err = s.Cabinet.ChannelSet(*c, false)
}
return
@ -373,7 +373,7 @@ func (s *State) Channels(guildID discord.GuildID) (cs []discord.Channel, err err
if s.Gateway.HasIntents(gateway.IntentGuilds) {
for _, c := range cs {
if err = s.Cabinet.ChannelSet(c); err != nil {
if err = s.Cabinet.ChannelSet(c, false); err != nil {
return
}
}
@ -393,7 +393,7 @@ func (s *State) CreatePrivateChannel(recipient discord.UserID) (*discord.Channel
return nil, err
}
return c, s.Cabinet.ChannelSet(*c)
return c, s.Cabinet.ChannelSet(*c, false)
}
// PrivateChannels gets the direct messages of the user.
@ -410,7 +410,7 @@ func (s *State) PrivateChannels() ([]discord.Channel, error) {
}
for _, c := range cs {
if err := s.Cabinet.ChannelSet(c); err != nil {
if err := s.Cabinet.ChannelSet(c, false); err != nil {
return nil, err
}
}
@ -437,7 +437,7 @@ func (s *State) Emoji(
return nil, err
}
if err = s.Cabinet.EmojiSet(guildID, es); err != nil {
if err = s.Cabinet.EmojiSet(guildID, es, false); err != nil {
return
}
@ -464,7 +464,7 @@ func (s *State) Emojis(guildID discord.GuildID) (es []discord.Emoji, err error)
}
if s.Gateway.HasIntents(gateway.IntentGuildEmojis) {
err = s.Cabinet.EmojiSet(guildID, es)
err = s.Cabinet.EmojiSet(guildID, es, false)
}
return
@ -499,7 +499,7 @@ func (s *State) Guilds() (gs []discord.Guild, err error) {
if s.Gateway.HasIntents(gateway.IntentGuilds) {
for _, g := range gs {
if err = s.Cabinet.GuildSet(g); err != nil {
if err = s.Cabinet.GuildSet(g, false); err != nil {
return
}
}
@ -536,7 +536,7 @@ func (s *State) Members(guildID discord.GuildID) (ms []discord.Member, err error
if s.Gateway.HasIntents(gateway.IntentGuildMembers) {
for _, m := range ms {
if err = s.Cabinet.MemberSet(guildID, m); err != nil {
if err = s.Cabinet.MemberSet(guildID, m, false); err != nil {
return
}
}
@ -568,7 +568,7 @@ func (s *State) Message(
go func() {
c, cerr = s.Session.Channel(channelID)
if cerr == nil && s.Gateway.HasIntents(gateway.IntentGuilds) {
cerr = s.Cabinet.ChannelSet(*c)
cerr = s.Cabinet.ChannelSet(*c, false)
}
wg.Done()
@ -589,78 +589,97 @@ func (s *State) Message(
m.ChannelID = c.ID
m.GuildID = c.GuildID
if s.tracksMessage(m) {
err = s.Cabinet.MessageSet(*m)
}
return m, err
}
// Messages fetches maximum 100 messages from the API, if it has to. There is
// no limit if it's from the State storage.
func (s *State) Messages(channelID discord.ChannelID) ([]discord.Message, error) {
// TODO: Think of a design that doesn't rely on MaxMessages().
var maxMsgs = s.MaxMessages()
ms, err := s.Cabinet.Messages(channelID)
if err == nil && (len(ms) == 0 || s.tracksMessage(&ms[0])) {
// If the state already has as many messages as it can, skip the API.
if maxMsgs <= len(ms) {
return ms, nil
}
// Messages returns a slice filled with the most recent messages sent in the
// channel with the passed ID. The method automatically paginates until it
// reaches the passed limit, or, if the limit is set to 0, has fetched all
// messages in the channel.
//
// As the underlying endpoint is capped at a maximum of 100 messages per
// request, at maximum a total of limit/100 rounded up requests will be made,
// although they may be less, if no more messages are available or there are
// cached messages.
// When fetching the messages, those with the highest ID, will be fetched
// first. The returned slice will be sorted from latest to oldest.
func (s *State) Messages(channelID discord.ChannelID, limit uint) ([]discord.Message, error) {
storeMessages, err := s.Cabinet.Messages(channelID)
if err == nil && s.tracksMessage(&storeMessages[0]) {
// Is the channel tiny?
s.fewMutex.Lock()
if _, ok := s.fewMessages[channelID]; ok {
s.fewMutex.Unlock()
return ms, nil
return storeMessages, nil
}
// No, fetch from the state.
// No, fetch from the API.
s.fewMutex.Unlock()
} else {
// Something wrong with the cached messages, make sure they aren't
// returned.
storeMessages = nil
}
ms, err = s.Session.Messages(channelID, uint(maxMsgs))
// Store already has enough messages.
if len(storeMessages) >= int(limit) && limit > 0 {
return storeMessages[:limit], nil
}
// Decrease the limit, if we aren't fetching all messages.
if limit > 0 {
limit -= uint(len(storeMessages))
}
var before discord.MessageID = 0
if len(storeMessages) > 0 {
before = storeMessages[len(storeMessages)-1].ID
}
apiMessages, err := s.Session.MessagesBefore(channelID, before, limit)
if err != nil {
return nil, err
}
// New messages fetched weirdly does not have GuildID filled. We'll try and
// get it for consistency with incoming message creates.
var guildID discord.GuildID
// A bit too convoluted, but whatever.
c, err := s.Channel(channelID)
if err == nil {
// If it's 0, it's 0 anyway. We don't need a check here.
guildID = c.GuildID
if len(storeMessages)+len(apiMessages) < s.MaxMessages() {
// Tiny channel, store this.
s.fewMutex.Lock()
s.fewMessages[channelID] = struct{}{}
s.fewMutex.Unlock()
}
if len(ms) > 0 && s.tracksMessage(&ms[0]) {
// Iterate in reverse, since the store is expected to prepend the latest
// messages.
for i := len(ms) - 1; i >= 0; i-- {
// Set the guild ID, fine if it's 0 (it's already 0 anyway).
ms[i].GuildID = guildID
if len(apiMessages) == 0 {
return storeMessages, nil
}
if err := s.Cabinet.MessageSet(ms[i]); err != nil {
// New messages fetched weirdly does not have GuildID filled. If we have
// cached messages, we can use their GuildID. Otherwise, we need to fetch
// it from the api.
var guildID discord.GuildID
if len(storeMessages) > 0 {
guildID = storeMessages[0].GuildID
} else {
c, err := s.Channel(channelID)
if err == nil {
// If it's 0, it's 0 anyway. We don't need a check here.
guildID = c.GuildID
}
}
for _, m := range apiMessages {
m.GuildID = guildID
}
if s.tracksMessage(&apiMessages[0]) && len(storeMessages) < s.MaxMessages() {
// Only add as many messages as the store can hold.
for _, m := range apiMessages[:s.MaxMessages()-len(storeMessages)] {
if err := s.Cabinet.MessageSet(m, false); err != nil {
return nil, err
}
}
}
if len(ms) < maxMsgs {
// Tiny channel, store this.
s.fewMutex.Lock()
s.fewMessages[channelID] = struct{}{}
s.fewMutex.Unlock()
return ms, nil
}
// Since the latest messages are at the end and we already know the maxMsgs,
// we could slice this right away.
return ms[:maxMsgs], nil
return append(storeMessages, apiMessages...), nil
}
////
@ -717,7 +736,7 @@ func (s *State) Role(guildID discord.GuildID, roleID discord.RoleID) (target *di
}
if s.Gateway.HasIntents(gateway.IntentGuilds) {
if err = s.RoleSet(guildID, r); err != nil {
if err = s.RoleSet(guildID, r, false); err != nil {
return
}
}
@ -743,7 +762,7 @@ func (s *State) Roles(guildID discord.GuildID) ([]discord.Role, error) {
if s.Gateway.HasIntents(gateway.IntentGuilds) {
for _, r := range rs {
if err := s.RoleSet(guildID, r); err != nil {
if err := s.RoleSet(guildID, r, false); err != nil {
return rs, err
}
}
@ -755,7 +774,7 @@ func (s *State) Roles(guildID discord.GuildID) ([]discord.Role, error) {
func (s *State) fetchGuild(id discord.GuildID) (g *discord.Guild, err error) {
g, err = s.Session.Guild(id)
if err == nil && s.Gateway.HasIntents(gateway.IntentGuilds) {
err = s.Cabinet.GuildSet(*g)
err = s.Cabinet.GuildSet(*g, false)
}
return
@ -764,7 +783,7 @@ func (s *State) fetchGuild(id discord.GuildID) (g *discord.Guild, err error) {
func (s *State) fetchMember(gID discord.GuildID, uID discord.UserID) (m *discord.Member, err error) {
m, err = s.Session.Member(gID, uID)
if err == nil && s.Gateway.HasIntents(gateway.IntentGuildMembers) {
err = s.Cabinet.MemberSet(gID, *m)
err = s.Cabinet.MemberSet(gID, *m, false)
}
return

View file

@ -69,20 +69,20 @@ func (s *State) onEvent(iface interface{}) {
// Handle guild presences
for _, p := range ev.Presences {
if err := s.Cabinet.PresenceSet(p.GuildID, p); err != nil {
if err := s.Cabinet.PresenceSet(p.GuildID, p, false); err != nil {
s.stateErr(err, "failed to set presence in Ready")
}
}
// Handle private channels
for _, ch := range ev.PrivateChannels {
if err := s.Cabinet.ChannelSet(ch); err != nil {
if err := s.Cabinet.ChannelSet(ch, false); err != nil {
s.stateErr(err, "failed to set channel in Ready")
}
}
// Handle user
if err := s.Cabinet.MyselfSet(ev.User); err != nil {
if err := s.Cabinet.MyselfSet(ev.User, false); err != nil {
s.stateErr(err, "failed to set self in Ready")
}
@ -91,7 +91,7 @@ func (s *State) onEvent(iface interface{}) {
for _, guild := range ev.Guilds {
// Handle guild voice states
for _, v := range guild.VoiceStates {
if err := s.Cabinet.VoiceStateSet(guild.ID, v); err != nil {
if err := s.Cabinet.VoiceStateSet(guild.ID, v, false); err != nil {
s.stateErr(err, "failed to set guild voice state in Ready Supplemental")
}
}
@ -99,7 +99,7 @@ func (s *State) onEvent(iface interface{}) {
for _, friend := range ev.MergedPresences.Friends {
sPresence := gateway.ConvertSupplementalPresence(friend)
if err := s.Cabinet.PresenceSet(0, sPresence); err != nil {
if err := s.Cabinet.PresenceSet(0, sPresence, false); err != nil {
s.stateErr(err, "failed to set friend presence in Ready Supplemental")
}
}
@ -112,14 +112,14 @@ func (s *State) onEvent(iface interface{}) {
for _, member := range ev.MergedMembers[i] {
sMember := gateway.ConvertSupplementalMember(member)
if err := s.Cabinet.MemberSet(guild.ID, sMember); err != nil {
if err := s.Cabinet.MemberSet(guild.ID, sMember, false); err != nil {
s.stateErr(err, "failed to set friend presence in Ready Supplemental")
}
}
for _, member := range ev.MergedPresences.Guilds[i] {
sPresence := gateway.ConvertSupplementalPresence(member)
if err := s.Cabinet.PresenceSet(guild.ID, sPresence); err != nil {
if err := s.Cabinet.PresenceSet(guild.ID, sPresence, false); err != nil {
s.stateErr(err, "failed to set member presence in Ready Supplemental")
}
}
@ -129,7 +129,7 @@ func (s *State) onEvent(iface interface{}) {
s.batchLog(storeGuildCreate(s.Cabinet, ev))
case *gateway.GuildUpdateEvent:
if err := s.Cabinet.GuildSet(ev.Guild); err != nil {
if err := s.Cabinet.GuildSet(ev.Guild, true); err != nil {
s.stateErr(err, "failed to update guild in state")
}
@ -139,7 +139,7 @@ func (s *State) onEvent(iface interface{}) {
}
case *gateway.GuildMemberAddEvent:
if err := s.Cabinet.MemberSet(ev.GuildID, ev.Member); err != nil {
if err := s.Cabinet.MemberSet(ev.GuildID, ev.Member, false); err != nil {
s.stateErr(err, "failed to add a member in state")
}
@ -153,7 +153,7 @@ func (s *State) onEvent(iface interface{}) {
// Update available fields from ev into m
ev.Update(m)
if err := s.Cabinet.MemberSet(ev.GuildID, *m); err != nil {
if err := s.Cabinet.MemberSet(ev.GuildID, *m, true); err != nil {
s.stateErr(err, "failed to update a member in state")
}
@ -164,24 +164,24 @@ func (s *State) onEvent(iface interface{}) {
case *gateway.GuildMembersChunkEvent:
for _, m := range ev.Members {
if err := s.Cabinet.MemberSet(ev.GuildID, m); err != nil {
if err := s.Cabinet.MemberSet(ev.GuildID, m, false); err != nil {
s.stateErr(err, "failed to add a member from chunk in state")
}
}
for _, p := range ev.Presences {
if err := s.Cabinet.PresenceSet(ev.GuildID, p); err != nil {
if err := s.Cabinet.PresenceSet(ev.GuildID, p, 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); err != nil {
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); err != nil {
if err := s.Cabinet.RoleSet(ev.GuildID, ev.Role, true); err != nil {
s.stateErr(err, "failed to update a role in state")
}
@ -191,17 +191,17 @@ func (s *State) onEvent(iface interface{}) {
}
case *gateway.GuildEmojisUpdateEvent:
if err := s.Cabinet.EmojiSet(ev.GuildID, ev.Emojis); err != nil {
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); err != nil {
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); err != nil {
if err := s.Cabinet.ChannelSet(ev.Channel, true); err != nil {
s.stateErr(err, "failed to update a channel in state")
}
@ -214,12 +214,12 @@ func (s *State) onEvent(iface interface{}) {
// not tracked.
case *gateway.MessageCreateEvent:
if err := s.Cabinet.MessageSet(ev.Message); err != nil {
if err := s.Cabinet.MessageSet(ev.Message, false); err != nil {
s.stateErr(err, "failed to add a message in state")
}
case *gateway.MessageUpdateEvent:
if err := s.Cabinet.MessageSet(ev.Message); err != nil {
if err := s.Cabinet.MessageSet(ev.Message, true); err != nil {
s.stateErr(err, "failed to update a message in state")
}
@ -295,13 +295,13 @@ func (s *State) onEvent(iface interface{}) {
})
case *gateway.PresenceUpdateEvent:
if err := s.Cabinet.PresenceSet(ev.GuildID, ev.Presence); err != nil {
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); err != nil {
if err := s.Cabinet.PresenceSet(p.GuildID, p.Presence, true); err != nil {
s.stateErr(err, "failed to update presence in state")
}
}
@ -321,7 +321,7 @@ func (s *State) onEvent(iface interface{}) {
// TODO
case *gateway.UserUpdateEvent:
if err := s.Cabinet.MyselfSet(ev.User); err != nil {
if err := s.Cabinet.MyselfSet(ev.User, true); err != nil {
s.stateErr(err, "failed to update myself from USER_UPDATE")
}
@ -332,7 +332,7 @@ func (s *State) onEvent(iface interface{}) {
s.stateErr(err, "failed to remove voice state from state")
}
} else {
if err := s.Cabinet.VoiceStateSet(vs.GuildID, *vs); err != nil {
if err := s.Cabinet.VoiceStateSet(vs.GuildID, *vs, true); err != nil {
s.stateErr(err, "failed to update voice state in state")
}
}
@ -358,7 +358,7 @@ func (s *State) editMessage(ch discord.ChannelID, msg discord.MessageID, fn func
if !fn(m) {
return
}
if err := s.Cabinet.MessageSet(*m); err != nil {
if err := s.Cabinet.MessageSet(*m, true); err != nil {
s.stateErr(err, "failed to save message in reaction add")
}
}
@ -379,20 +379,20 @@ func storeGuildCreate(cab store.Cabinet, guild *gateway.GuildCreateEvent) []erro
stack, errs := newErrorStack()
if err := cab.GuildSet(guild.Guild); err != nil {
if err := cab.GuildSet(guild.Guild, false); err != nil {
errs(err, "failed to set guild in Ready")
}
// Handle guild emojis
if guild.Emojis != nil {
if err := cab.EmojiSet(guild.ID, guild.Emojis); err != nil {
if err := cab.EmojiSet(guild.ID, guild.Emojis, false); err != nil {
errs(err, "failed to set guild emojis")
}
}
// Handle guild member
for _, m := range guild.Members {
if err := cab.MemberSet(guild.ID, m); err != nil {
if err := cab.MemberSet(guild.ID, m, false); err != nil {
errs(err, "failed to set guild member in Ready")
}
}
@ -402,21 +402,21 @@ func storeGuildCreate(cab store.Cabinet, guild *gateway.GuildCreateEvent) []erro
// I HATE Discord.
ch.GuildID = guild.ID
if err := cab.ChannelSet(ch); err != nil {
if err := cab.ChannelSet(ch, false); err != nil {
errs(err, "failed to set guild channel in Ready")
}
}
// Handle guild presences
for _, p := range guild.Presences {
if err := cab.PresenceSet(guild.ID, p); err != nil {
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 {
if err := cab.VoiceStateSet(guild.ID, v); err != nil {
if err := cab.VoiceStateSet(guild.ID, v, false); err != nil {
errs(err, "failed to set guild voice state in Ready")
}
}

View file

@ -105,13 +105,15 @@ func (s *Channel) PrivateChannels() ([]discord.Channel, error) {
}
// ChannelSet sets the Direct Message or Guild channel into the state.
func (s *Channel) ChannelSet(channel discord.Channel) error {
func (s *Channel) ChannelSet(channel discord.Channel, update bool) error {
s.mut.Lock()
defer s.mut.Unlock()
// Update the reference if we can.
if ch, ok := s.channels[channel.ID]; ok {
*ch = channel
if update {
*ch = channel
}
return nil
}

View file

@ -71,8 +71,11 @@ func (s *Emoji) Emojis(guildID discord.GuildID) ([]discord.Emoji, error) {
return es.emojis, nil
}
func (s *Emoji) EmojiSet(guildID discord.GuildID, allEmojis []discord.Emoji) error {
iv, _ := s.guilds.LoadOrStore(guildID)
func (s *Emoji) EmojiSet(guildID discord.GuildID, allEmojis []discord.Emoji, update bool) error {
iv, loaded := s.guilds.LoadOrStore(guildID)
if loaded && !update {
return nil
}
es := iv.(*emojis)

View file

@ -58,10 +58,13 @@ func (s *Guild) Guilds() ([]discord.Guild, error) {
return gs, nil
}
func (s *Guild) GuildSet(guild discord.Guild) error {
func (s *Guild) GuildSet(guild discord.Guild, update bool) error {
s.mut.Lock()
s.guilds[guild.ID] = guild
if _, ok := s.guilds[guild.ID]; !ok || update {
s.guilds[guild.ID] = guild
}
s.mut.Unlock()
return nil
}

View file

@ -38,9 +38,11 @@ func (m *Me) Me() (*discord.User, error) {
return &self, nil
}
func (m *Me) MyselfSet(me discord.User) error {
func (m *Me) MyselfSet(me discord.User, update bool) error {
m.mut.Lock()
m.self = me
if !m.self.ID.IsValid() || update {
m.self = me
}
m.mut.Unlock()
return nil

View file

@ -71,12 +71,14 @@ func (s *Member) Members(guildID discord.GuildID) ([]discord.Member, error) {
return members, nil
}
func (s *Member) MemberSet(guildID discord.GuildID, member discord.Member) error {
func (s *Member) MemberSet(guildID discord.GuildID, m discord.Member, update bool) error {
iv, _ := s.guilds.LoadOrStore(guildID)
gm := iv.(*guildMembers)
gm.mut.Lock()
gm.members[member.User.ID] = member
if _, ok := gm.members[m.User.ID]; !ok || update {
gm.members[m.User.ID] = m
}
gm.mut.Unlock()
return nil

View file

@ -73,7 +73,11 @@ func (s *Message) MaxMessages() int {
return s.maxMsgs
}
func (s *Message) MessageSet(message discord.Message) error {
func (s *Message) MessageSet(message discord.Message, update bool) error {
if s.maxMsgs <= 0 {
return nil
}
iv, _ := s.channels.LoadOrStore(message.ChannelID)
msgs := iv.(*messages)
@ -81,62 +85,103 @@ func (s *Message) MessageSet(message discord.Message) error {
msgs.mut.Lock()
defer msgs.mut.Unlock()
for i, m := range msgs.messages {
if m.ID == message.ID {
DiffMessage(message, &m)
msgs.messages[i] = m
return nil
}
}
// Order: latest to earliest, similar to the API.
// Check if we already have the message. Try to derive the order otherwise.
var insertAt int
// Since we make the order guarantee ourselves, we can trust that we're
// iterating from latest to earliest.
for insertAt < len(msgs.messages) {
// Check if the new message is older. If it is, then we should insert it
// right after this message (or before this message in the list; i-1).
if message.ID > msgs.messages[insertAt].ID {
break
if update {
// Opt for a linear latest-to-oldest search in favor of something like
// sort.Search, since more recent messages are more likely to be edited
// than older ones.
for i, oldMessage := range msgs.messages {
// We found a match, update it.
if oldMessage.ID == message.ID {
DiffMessage(message, &oldMessage)
msgs.messages[i] = oldMessage // Now updated.
return nil
}
}
insertAt++
return nil
}
end := len(msgs.messages)
max := s.MaxMessages()
if len(msgs.messages) == 0 {
msgs.messages = []discord.Message{message}
}
if end == max {
// If insertAt is larger than the length, then the message is older than
// every other messages we have. We have to discard this message here,
// since the store is already full.
if insertAt == end {
return nil
if pos := messageInsertPosition(message, msgs.messages); pos < 0 {
// Messages are full, drop the oldest messages to make room.
if len(msgs.messages) == s.maxMsgs {
copy(msgs.messages[1:], msgs.messages)
msgs.messages[0] = message
} else {
msgs.messages = append([]discord.Message{message}, msgs.messages...)
}
// If the end (length) is approaching the maximum amount, then cap it.
end = max
} else {
// Else, append an empty message to the end.
msgs.messages = append(msgs.messages, discord.Message{})
// Increment to update the length.
end++
} else if pos > 0 && len(msgs.messages) < s.maxMsgs {
msgs.messages = append(msgs.messages, message)
}
// Shift the slice right-wards if the current item is not the last.
if start := insertAt + 1; start < end {
copy(msgs.messages[insertAt+1:], msgs.messages[insertAt:end-1])
}
// Then, set the nth entry.
msgs.messages[insertAt] = message
// We already have this message or we can't append any more messages.
return nil
}
// messageInsertPosition checks if the message should be appended or prepended
// into the passed messages, ordered by time of creation from latest to oldest.
// If the message should be prepended, messageInsertPosition returns -1, and if
// the message should be appended it returns 1. As a third option it returns 0,
// if the message should not be added to the slice, because it would disrupt
// the order.
//
// messageInsertPosition is biased as it will recommend adding the message even
// if timestamps just match, even though the true order cannot be determined in
// that case.
func messageInsertPosition(target discord.Message, messages []discord.Message) int8 {
var (
targetTime = target.ID.Time()
firstTime = messages[0].ID.Time()
lastTime = messages[len(messages)-1].ID.Time()
)
if targetTime.After(firstTime) {
return -1
} else if targetTime.Before(lastTime) {
return 1
}
// Two cases remain, the timestamp is equal to either the latest or oldest
// message, or the message is already contained in message.
// So we compare timestamps. If they are equal, make sure messages doesn't
// contain a message with the same id, in order to prevent insertion of a
// duplicate. If they are not equal, we return 0 as the message would
// violate the order of messages.
// ID timestamps are used, as they provide millisecond accuracy in contrast
// to the second accuracy of discord.Message.Timestamp.
if targetTime.Equal(firstTime) {
// Only iterate as long as timestamps are equal, or there are no more
// messages.
for i := 0; i < len(messages) && targetTime.Equal(messages[i].ID.Time()); i++ {
// Duplicate, don't insert.
if messages[i].ID == target.ID {
return 0
}
}
// No duplicate of message found, so safe to prepend.
return -1
} else if targetTime.Equal(lastTime) {
// Only iterate as long as timestamps are equal, or there are no more
// messages.
for i := len(messages) - 1; i >= 0 && targetTime.Equal(messages[i].ID.Time()); i-- {
// Duplicate, don't insert.
if messages[i].ID == target.ID {
return 0
}
}
// No duplicate of message found, so safe to append.
return 1
}
// Message would violate the order of messages, don't add it.
return 0
}
// DiffMessage fills non-empty fields from src to dst.
func DiffMessage(src discord.Message, dst *discord.Message) {
// Thanks, Discord.

View file

@ -10,23 +10,27 @@ func populate12Store() *Message {
store := NewMessage(10)
// Insert a regular list of messages.
store.MessageSet(discord.Message{ID: 11, ChannelID: 1})
store.MessageSet(discord.Message{ID: 9, ChannelID: 1})
store.MessageSet(discord.Message{ID: 7, ChannelID: 1})
store.MessageSet(discord.Message{ID: 5, ChannelID: 1})
store.MessageSet(discord.Message{ID: 3, ChannelID: 1})
store.MessageSet(discord.Message{ID: 1, ChannelID: 1})
store.MessageSet(discord.Message{ID: 1 << 29, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 28, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 27, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 26, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 25, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 24, ChannelID: 1}, false)
// Try to insert newer messages after inserting new messages.
store.MessageSet(discord.Message{ID: 12, ChannelID: 1})
store.MessageSet(discord.Message{ID: 10, ChannelID: 1})
store.MessageSet(discord.Message{ID: 8, ChannelID: 1})
store.MessageSet(discord.Message{ID: 6, ChannelID: 1})
store.MessageSet(discord.Message{ID: 4, ChannelID: 1})
store.MessageSet(discord.Message{ID: 1 << 30, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 31, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 32, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 33, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 34, ChannelID: 1}, false)
// These messages should be discarded.
store.MessageSet(discord.Message{ID: 2, ChannelID: 1})
store.MessageSet(discord.Message{ID: 0, ChannelID: 1})
// TThese messages should be discarded, due to age.
store.MessageSet(discord.Message{ID: 1 << 23, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 22, ChannelID: 1}, false)
// These should be prepended.
store.MessageSet(discord.Message{ID: 1 << 35, ChannelID: 1}, false)
store.MessageSet(discord.Message{ID: 1 << 36, ChannelID: 1}, false)
return store
}
@ -35,18 +39,17 @@ func TestMessageSet(t *testing.T) {
store := populate12Store()
messages, _ := store.Messages(1)
if len(messages) < store.MaxMessages() {
t.Errorf("store can store %d messages, but only returned %d", store.MaxMessages(),
len(messages))
}
const (
start discord.MessageID = 2
end discord.MessageID = 12
)
maxShift := 36
for i := start; i < end; i++ {
index := i - start
expect := end - i + start
if msgID := messages[index].ID; msgID != expect {
t.Errorf("message at %d has mismatch ID %d, expecting %d", i, msgID, expect)
for i, actual := range messages {
expectID := discord.MessageID(1) << (maxShift - i)
if actual.ID != expectID {
t.Errorf("message at %d has mismatch ID %d, expecting %d", i, actual.ID, expectID)
}
}
}
@ -54,9 +57,9 @@ func TestMessageSet(t *testing.T) {
func TestMessagesUpdate(t *testing.T) {
store := populate12Store()
store.MessageSet(discord.Message{ID: 5, ChannelID: 1, Content: "edited 1"})
store.MessageSet(discord.Message{ID: 6, ChannelID: 1, Content: "edited 2"})
store.MessageSet(discord.Message{ID: 5, ChannelID: 1, Content: "edited 3"})
store.MessageSet(discord.Message{ID: 5, ChannelID: 1, Content: "edited 1"}, true)
store.MessageSet(discord.Message{ID: 6, ChannelID: 1, Content: "edited 2"}, true)
store.MessageSet(discord.Message{ID: 5, ChannelID: 1, Content: "edited 3"}, true)
expect := map[discord.MessageID]string{
5: "edited 3",

View file

@ -72,7 +72,7 @@ func (s *Presence) Presences(guildID discord.GuildID) ([]gateway.Presence, error
return presences, nil
}
func (s *Presence) PresenceSet(guildID discord.GuildID, presence gateway.Presence) error {
func (s *Presence) PresenceSet(guildID discord.GuildID, p gateway.Presence, update bool) error {
iv, _ := s.guilds.LoadOrStore(guildID)
ps := iv.(*presences)
@ -85,7 +85,9 @@ func (s *Presence) PresenceSet(guildID discord.GuildID, presence gateway.Presenc
ps.presences = make(map[discord.UserID]gateway.Presence, 1)
}
ps.presences[presence.User.ID] = presence
if _, ok := ps.presences[p.User.ID]; !ok || update {
ps.presences[p.User.ID] = p
}
return nil
}

View file

@ -71,13 +71,15 @@ func (s *Role) Roles(guildID discord.GuildID) ([]discord.Role, error) {
return roles, nil
}
func (s *Role) RoleSet(guildID discord.GuildID, role discord.Role) error {
func (s *Role) RoleSet(guildID discord.GuildID, role discord.Role, update bool) error {
iv, _ := s.guilds.LoadOrStore(guildID)
rs := iv.(*roles)
rs.mut.Lock()
rs.roles[role.ID] = role
if _, ok := rs.roles[role.ID]; !ok || update {
rs.roles[role.ID] = role
}
rs.mut.Unlock()
return nil

View file

@ -73,13 +73,17 @@ func (s *VoiceState) VoiceStates(guildID discord.GuildID) ([]discord.VoiceState,
return states, nil
}
func (s *VoiceState) VoiceStateSet(guildID discord.GuildID, voiceState discord.VoiceState) error {
func (s *VoiceState) VoiceStateSet(
guildID discord.GuildID, voiceState discord.VoiceState, update bool) error {
iv, _ := s.guilds.LoadOrStore(guildID)
vs := iv.(*voiceStates)
vs.mut.Lock()
vs.voiceStates[voiceState.UserID] = voiceState
if _, ok := vs.voiceStates[voiceState.UserID]; !ok || update {
vs.voiceStates[voiceState.UserID] = voiceState
}
vs.mut.Unlock()
return nil

View file

@ -117,7 +117,7 @@ func (errs *ResetErrors) append(err error) {
// Noop is the value for a NoopStore.
var Noop = NoopStore{}
// Noop is a no-op implementation of all store interfaces. Its getters will
// NoopStore is a no-op implementation of all store interfaces. Its getters will
// always return ErrNotFound, and its setters will never return an error.
type NoopStore = noop
@ -153,19 +153,19 @@ type MeStore interface {
Resetter
Me() (*discord.User, error)
MyselfSet(me discord.User) error
MyselfSet(me discord.User, update bool) error
}
func (noop) Me() (*discord.User, error) { return nil, ErrNotFound }
func (noop) MyselfSet(discord.User) error { return nil }
func (noop) Me() (*discord.User, error) { return nil, ErrNotFound }
func (noop) MyselfSet(discord.User, bool) error { return nil }
// ChannelStore is the store interface for all channels.
type ChannelStore interface {
Resetter
// ChannelStore searches for both DM and guild channels.
// Channel searches for both DM and guild channels.
Channel(discord.ChannelID) (*discord.Channel, error)
// CreatePrivateChannelStore searches for private channels by the recipient ID.
// CreatePrivateChannel searches for private channels by the recipient ID.
// It has the same API as *api.Client does.
CreatePrivateChannel(recipient discord.UserID) (*discord.Channel, error)
@ -177,7 +177,7 @@ type ChannelStore interface {
// Both ChannelSet and ChannelRemove should switch on Type to know if it's a
// private channel or not.
ChannelSet(discord.Channel) error
ChannelSet(c discord.Channel, update bool) error
ChannelRemove(discord.Channel) error
}
@ -195,7 +195,7 @@ func (noop) Channels(discord.GuildID) ([]discord.Channel, error) {
func (noop) PrivateChannels() ([]discord.Channel, error) {
return nil, ErrNotFound
}
func (noop) ChannelSet(discord.Channel) error {
func (noop) ChannelSet(discord.Channel, bool) error {
return nil
}
func (noop) ChannelRemove(discord.Channel) error {
@ -211,7 +211,7 @@ type EmojiStore interface {
// EmojiSet should delete all old emojis before setting new ones. The given
// emojis slice will be a complete list of all emojis.
EmojiSet(discord.GuildID, []discord.Emoji) error
EmojiSet(guildID discord.GuildID, emojis []discord.Emoji, update bool) error
}
var _ EmojiStore = (*noop)(nil)
@ -222,7 +222,7 @@ func (noop) Emoji(discord.GuildID, discord.EmojiID) (*discord.Emoji, error) {
func (noop) Emojis(discord.GuildID) ([]discord.Emoji, error) {
return nil, ErrNotFound
}
func (noop) EmojiSet(discord.GuildID, []discord.Emoji) error {
func (noop) EmojiSet(discord.GuildID, []discord.Emoji, bool) error {
return nil
}
@ -233,7 +233,7 @@ type GuildStore interface {
Guild(discord.GuildID) (*discord.Guild, error)
Guilds() ([]discord.Guild, error)
GuildSet(discord.Guild) error
GuildSet(g discord.Guild, update bool) error
GuildRemove(id discord.GuildID) error
}
@ -241,7 +241,7 @@ var _ GuildStore = (*noop)(nil)
func (noop) Guild(discord.GuildID) (*discord.Guild, error) { return nil, ErrNotFound }
func (noop) Guilds() ([]discord.Guild, error) { return nil, ErrNotFound }
func (noop) GuildSet(discord.Guild) error { return nil }
func (noop) GuildSet(discord.Guild, bool) error { return nil }
func (noop) GuildRemove(discord.GuildID) error { return nil }
// MemberStore is the store interface for all members.
@ -251,7 +251,7 @@ type MemberStore interface {
Member(discord.GuildID, discord.UserID) (*discord.Member, error)
Members(discord.GuildID) ([]discord.Member, error)
MemberSet(discord.GuildID, discord.Member) error
MemberSet(guildID discord.GuildID, m discord.Member, update bool) error
MemberRemove(discord.GuildID, discord.UserID) error
}
@ -263,7 +263,7 @@ func (noop) Member(discord.GuildID, discord.UserID) (*discord.Member, error) {
func (noop) Members(discord.GuildID) ([]discord.Member, error) {
return nil, ErrNotFound
}
func (noop) MemberSet(discord.GuildID, discord.Member) error {
func (noop) MemberSet(discord.GuildID, discord.Member, bool) error {
return nil
}
func (noop) MemberRemove(discord.GuildID, discord.UserID) error {
@ -282,9 +282,15 @@ type MessageStore interface {
// Messages should return messages ordered from latest to earliest.
Messages(discord.ChannelID) ([]discord.Message, error)
// MessageSet should prepend messages into the slice, the latest being in
// front.
MessageSet(discord.Message) error
// MessageSet either updates or adds a new message.
//
// A new message can be added, by setting update to false. Depending on
// timestamp of the message, it will either be prepended or appended.
//
// If update is set to true, MessageSet will check if a message with the
// id of the passed message is stored, and update it if so. Otherwise, if
// there is no such message, it will be discarded.
MessageSet(m discord.Message, update bool) error
MessageRemove(discord.ChannelID, discord.MessageID) error
}
@ -299,7 +305,7 @@ func (noop) Message(discord.ChannelID, discord.MessageID) (*discord.Message, err
func (noop) Messages(discord.ChannelID) ([]discord.Message, error) {
return nil, ErrNotFound
}
func (noop) MessageSet(discord.Message) error {
func (noop) MessageSet(discord.Message, bool) error {
return nil
}
func (noop) MessageRemove(discord.ChannelID, discord.MessageID) error {
@ -314,7 +320,7 @@ type PresenceStore interface {
Presence(discord.GuildID, discord.UserID) (*gateway.Presence, error)
Presences(discord.GuildID) ([]gateway.Presence, error)
PresenceSet(discord.GuildID, gateway.Presence) error
PresenceSet(guildID discord.GuildID, p gateway.Presence, update bool) error
PresenceRemove(discord.GuildID, discord.UserID) error
}
@ -326,7 +332,7 @@ func (noop) Presence(discord.GuildID, discord.UserID) (*gateway.Presence, error)
func (noop) Presences(discord.GuildID) ([]gateway.Presence, error) {
return nil, ErrNotFound
}
func (noop) PresenceSet(discord.GuildID, gateway.Presence) error {
func (noop) PresenceSet(discord.GuildID, gateway.Presence, bool) error {
return nil
}
func (noop) PresenceRemove(discord.GuildID, discord.UserID) error {
@ -340,7 +346,7 @@ type RoleStore interface {
Role(discord.GuildID, discord.RoleID) (*discord.Role, error)
Roles(discord.GuildID) ([]discord.Role, error)
RoleSet(discord.GuildID, discord.Role) error
RoleSet(guildID discord.GuildID, r discord.Role, update bool) error
RoleRemove(discord.GuildID, discord.RoleID) error
}
@ -348,7 +354,7 @@ var _ RoleStore = (*noop)(nil)
func (noop) Role(discord.GuildID, discord.RoleID) (*discord.Role, error) { return nil, ErrNotFound }
func (noop) Roles(discord.GuildID) ([]discord.Role, error) { return nil, ErrNotFound }
func (noop) RoleSet(discord.GuildID, discord.Role) error { return nil }
func (noop) RoleSet(discord.GuildID, discord.Role, bool) error { return nil }
func (noop) RoleRemove(discord.GuildID, discord.RoleID) error { return nil }
// VoiceStateStore is the store interface for all voice states.
@ -358,7 +364,7 @@ type VoiceStateStore interface {
VoiceState(discord.GuildID, discord.UserID) (*discord.VoiceState, error)
VoiceStates(discord.GuildID) ([]discord.VoiceState, error)
VoiceStateSet(discord.GuildID, discord.VoiceState) error
VoiceStateSet(guildID discord.GuildID, s discord.VoiceState, update bool) error
VoiceStateRemove(discord.GuildID, discord.UserID) error
}
@ -370,7 +376,7 @@ func (noop) VoiceState(discord.GuildID, discord.UserID) (*discord.VoiceState, er
func (noop) VoiceStates(discord.GuildID) ([]discord.VoiceState, error) {
return nil, ErrNotFound
}
func (noop) VoiceStateSet(discord.GuildID, discord.VoiceState) error {
func (noop) VoiceStateSet(discord.GuildID, discord.VoiceState, bool) error {
return nil
}
func (noop) VoiceStateRemove(discord.GuildID, discord.UserID) error {