1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-10-03 08:08:47 +00:00
arikawa/state/store/defaultstore/message.go
diamondburned efde3f4ea6
state, handler: Refactor state storage and sync handlers
This commit refactors a lot of packages.

It refactors the handler package, removing the Synchronous field and
replacing it the AddSyncHandler API, which allows each handler to
control whether or not it should be ran synchronously independent of
other handlers. This is useful for libraries that need to guarantee the
incoming order of events.

It also refactors the store interfaces to accept more interfaces. This
is to make the API more consistent as well as reducing potential useless
copies. The public-facing state API should still be the same, so this
change will mostly concern users with their own store implementations.

Several miscellaneous functions (such as a few in package gateway) were
modified to be more suitable to other packages, but those functions
should rarely ever be used, anyway.

Several tests are also fixed within this commit, namely fixing state's
intents bug.
2021-11-03 15:16:02 -07:00

237 lines
5.9 KiB
Go

package defaultstore
import (
"sync"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/internal/moreatomic"
"github.com/diamondburned/arikawa/v3/state/store"
)
type Message struct {
channels moreatomic.Map
maxMsgs int
}
var _ store.MessageStore = (*Message)(nil)
type messages struct {
mut sync.RWMutex
messages []discord.Message
}
func NewMessage(maxMsgs int) *Message {
return &Message{
channels: *moreatomic.NewMap(func() interface{} {
return &messages{
messages: []discord.Message{}, // never use a nil slice
}
}),
maxMsgs: maxMsgs,
}
}
func (s *Message) Reset() error {
return s.channels.Reset()
}
func (s *Message) Message(chID discord.ChannelID, mID discord.MessageID) (*discord.Message, error) {
iv, ok := s.channels.Load(chID)
if !ok {
return nil, store.ErrNotFound
}
msgs := iv.(*messages)
msgs.mut.RLock()
defer msgs.mut.RUnlock()
for _, m := range msgs.messages {
if m.ID == mID {
return &m, nil
}
}
return nil, store.ErrNotFound
}
func (s *Message) Messages(channelID discord.ChannelID) ([]discord.Message, error) {
iv, ok := s.channels.Load(channelID)
if !ok {
return nil, store.ErrNotFound
}
msgs := iv.(*messages)
msgs.mut.RLock()
defer msgs.mut.RUnlock()
return append([]discord.Message(nil), msgs.messages...), nil
}
func (s *Message) MaxMessages() int {
return s.maxMsgs
}
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)
msgs.mut.Lock()
defer msgs.mut.Unlock()
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
}
}
return nil
}
if len(msgs.messages) == 0 {
msgs.messages = []discord.Message{*message}
}
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...)
}
} else if pos > 0 && len(msgs.messages) < s.maxMsgs {
msgs.messages = append(msgs.messages, *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, dst *discord.Message) {
// Thanks, Discord.
if src.Content != "" {
dst.Content = src.Content
}
if src.EditedTimestamp.IsValid() {
dst.EditedTimestamp = src.EditedTimestamp
}
if src.Mentions != nil {
dst.Mentions = src.Mentions
}
if src.Embeds != nil {
dst.Embeds = src.Embeds
}
if src.Attachments != nil {
dst.Attachments = src.Attachments
}
if src.Timestamp.IsValid() {
dst.Timestamp = src.Timestamp
}
if src.Author.ID.IsValid() {
dst.Author = src.Author
}
if src.Reactions != nil {
dst.Reactions = src.Reactions
}
if src.Components != nil {
dst.Components = src.Components
}
}
func (s *Message) MessageRemove(channelID discord.ChannelID, messageID discord.MessageID) error {
iv, ok := s.channels.Load(channelID)
if !ok {
return nil
}
msgs := iv.(*messages)
msgs.mut.Lock()
defer msgs.mut.Unlock()
for i, m := range msgs.messages {
if m.ID == messageID {
msgs.messages = append(msgs.messages[:i], msgs.messages[i+1:]...)
return nil
}
}
return nil
}