arikawa/state/store/defaultstore/message.go

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
}