2020-09-08 04:44:09 +00:00
|
|
|
package message
|
2020-06-16 03:57:33 +00:00
|
|
|
|
|
|
|
import (
|
2021-01-06 02:40:01 +00:00
|
|
|
"log"
|
2021-01-01 22:35:35 +00:00
|
|
|
"strings"
|
2020-06-16 03:57:33 +00:00
|
|
|
"time"
|
|
|
|
|
2020-12-20 05:44:26 +00:00
|
|
|
"github.com/diamondburned/arikawa/v2/discord"
|
|
|
|
"github.com/diamondburned/arikawa/v2/gateway"
|
2020-06-16 03:57:33 +00:00
|
|
|
"github.com/diamondburned/cchat"
|
2020-09-08 04:44:09 +00:00
|
|
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
|
|
|
"github.com/diamondburned/cchat-discord/internal/segments"
|
2021-01-06 02:40:01 +00:00
|
|
|
"github.com/diamondburned/cchat-discord/internal/segments/inline"
|
2020-10-07 01:53:15 +00:00
|
|
|
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
2020-12-31 02:58:36 +00:00
|
|
|
"github.com/diamondburned/cchat-discord/internal/segments/reference"
|
2021-01-06 02:40:01 +00:00
|
|
|
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
|
2020-06-16 03:57:33 +00:00
|
|
|
"github.com/diamondburned/cchat/text"
|
|
|
|
)
|
|
|
|
|
|
|
|
type messageHeader struct {
|
2020-08-15 22:37:44 +00:00
|
|
|
id discord.MessageID
|
2020-06-16 03:57:33 +00:00
|
|
|
time discord.Timestamp
|
2020-12-19 05:46:12 +00:00
|
|
|
nonce string
|
2020-08-15 22:37:44 +00:00
|
|
|
channelID discord.ChannelID
|
|
|
|
guildID discord.GuildID
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var _ cchat.MessageHeader = (*messageHeader)(nil)
|
|
|
|
|
|
|
|
func newHeader(msg discord.Message) messageHeader {
|
2021-01-03 04:44:30 +00:00
|
|
|
return messageHeader{
|
2020-06-16 03:57:33 +00:00
|
|
|
id: msg.ID,
|
|
|
|
time: msg.Timestamp,
|
|
|
|
channelID: msg.ChannelID,
|
|
|
|
guildID: msg.GuildID,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-19 05:46:12 +00:00
|
|
|
func newHeaderNonce(msg discord.Message, nonce string) messageHeader {
|
|
|
|
h := newHeader(msg)
|
|
|
|
h.nonce = nonce
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
2020-06-16 03:57:33 +00:00
|
|
|
func NewHeaderDelete(d *gateway.MessageDeleteEvent) messageHeader {
|
|
|
|
return messageHeader{
|
|
|
|
id: d.ID,
|
|
|
|
time: discord.Timestamp(time.Now()),
|
|
|
|
channelID: d.ChannelID,
|
|
|
|
guildID: d.GuildID,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 23:20:51 +00:00
|
|
|
func (m messageHeader) ID() cchat.ID {
|
2020-06-16 03:57:33 +00:00
|
|
|
return m.id.String()
|
|
|
|
}
|
|
|
|
|
2020-12-19 05:46:12 +00:00
|
|
|
func (m messageHeader) Nonce() string { return m.nonce }
|
|
|
|
|
2020-12-17 08:01:58 +00:00
|
|
|
func (m messageHeader) MessageID() discord.MessageID { return m.id }
|
|
|
|
func (m messageHeader) ChannelID() discord.ChannelID { return m.channelID }
|
|
|
|
func (m messageHeader) GuildID() discord.GuildID { return m.guildID }
|
|
|
|
|
2020-06-16 03:57:33 +00:00
|
|
|
func (m messageHeader) Time() time.Time {
|
|
|
|
return m.time.Time()
|
|
|
|
}
|
|
|
|
|
|
|
|
type Message struct {
|
|
|
|
messageHeader
|
|
|
|
|
|
|
|
author Author
|
|
|
|
content text.Rich
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
mentioned bool
|
|
|
|
}
|
|
|
|
|
2020-10-05 03:45:34 +00:00
|
|
|
var (
|
|
|
|
_ cchat.MessageCreate = (*Message)(nil)
|
|
|
|
_ cchat.MessageUpdate = (*Message)(nil)
|
|
|
|
_ cchat.MessageDelete = (*Message)(nil)
|
2020-12-19 05:46:12 +00:00
|
|
|
_ cchat.Noncer = (*Message)(nil)
|
2020-10-05 03:45:34 +00:00
|
|
|
)
|
|
|
|
|
2020-12-19 05:46:12 +00:00
|
|
|
// NewGuildMessageCreate uses the session to create a message. It does not do
|
|
|
|
// API calls. Member is optional. This is the only call that populates the Nonce
|
|
|
|
// in the header.
|
|
|
|
func NewGuildMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message {
|
|
|
|
// Copy and change the nonce.
|
|
|
|
message := c.Message
|
|
|
|
message.Nonce = s.Nonces.Load(c.Nonce)
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
user := mention.NewUser(c.Author)
|
|
|
|
user.WithState(s.State)
|
|
|
|
user.WithGuildID(c.GuildID)
|
2020-06-16 03:57:33 +00:00
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
if c.Member != nil {
|
|
|
|
user.WithMember(*c.Member)
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
user.Prefetch()
|
|
|
|
|
|
|
|
return NewMessage(message, s, NewAuthor(user))
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewBacklogMessage uses the session to create a message fetched from the
|
|
|
|
// backlog. It takes in an existing guild and tries to fetch a new member, if
|
|
|
|
// it's nil.
|
2021-01-06 04:53:49 +00:00
|
|
|
func NewBacklogMessage(m discord.Message, s *state.Instance) Message {
|
2020-06-16 03:57:33 +00:00
|
|
|
// If the message doesn't have a guild, then we don't need all the
|
|
|
|
// complicated member fetching process.
|
2020-08-15 22:37:44 +00:00
|
|
|
if !m.GuildID.IsValid() {
|
2021-01-06 04:53:49 +00:00
|
|
|
return NewDirectMessage(m, s)
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
user := mention.NewUser(m.Author)
|
|
|
|
user.WithGuildID(m.GuildID)
|
|
|
|
user.WithState(s.State)
|
|
|
|
user.Prefetch()
|
2020-06-16 03:57:33 +00:00
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
return NewMessage(m, s, NewAuthor(user))
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
// NewDirectMessage creates a new direct message.
|
2020-09-08 04:44:09 +00:00
|
|
|
func NewDirectMessage(m discord.Message, s *state.Instance) Message {
|
2021-01-06 04:53:49 +00:00
|
|
|
user := mention.NewUser(m.Author)
|
|
|
|
user.WithState(s.State)
|
|
|
|
user.Prefetch()
|
|
|
|
|
|
|
|
return NewMessage(m, s, NewAuthor(user))
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewAuthorUpdate creates a new message that contains a new author.
|
|
|
|
func NewAuthorUpdate(msg discord.Message, m discord.Member, s *state.Instance) Message {
|
|
|
|
user := mention.NewUser(msg.Author)
|
|
|
|
user.WithState(s.State)
|
|
|
|
user.WithGuildID(msg.GuildID)
|
|
|
|
user.WithMember(m)
|
|
|
|
|
|
|
|
author := NewAuthor(user)
|
|
|
|
if ref := ReferencedMessage(msg, s, true); ref != nil {
|
|
|
|
author.AddMessageReference(*ref, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
return Message{
|
|
|
|
messageHeader: newHeader(msg),
|
|
|
|
author: author,
|
|
|
|
}
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
// NewContentUpdate creates a new message that does not have an author. It
|
|
|
|
// should be used for UpdateMessage only.
|
|
|
|
func NewContentUpdate(msg discord.Message, s *state.Instance) Message {
|
|
|
|
// Check if content is empty.
|
|
|
|
if msg.Content == "" {
|
|
|
|
// Then grab the content from the state.
|
|
|
|
m, err := s.Cabinet.Message(msg.ChannelID, msg.ID)
|
|
|
|
if err == nil {
|
|
|
|
msg = *m
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return newMessageContent(&msg, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewMessage creates a new message from the given author. It may modify author
|
|
|
|
// to add a message reference.
|
2020-09-08 04:44:09 +00:00
|
|
|
func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
|
2021-01-06 04:53:49 +00:00
|
|
|
message := newMessageContent(&m, s)
|
|
|
|
message.author = author
|
|
|
|
|
|
|
|
if m.ReferencedMessage != nil {
|
|
|
|
message.author.AddMessageReference(*m.ReferencedMessage, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
return message
|
|
|
|
}
|
2021-01-06 02:40:01 +00:00
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
// newMessageContent creates a new message with a content only. The given
|
|
|
|
// message will have its ReferencedMessage field validated and filled if
|
|
|
|
// available.
|
|
|
|
func newMessageContent(m *discord.Message, s *state.Instance) Message {
|
|
|
|
// Ensure the validity of ReferencedMessage.
|
|
|
|
m.ReferencedMessage = ReferencedMessage(*m, s, true)
|
2021-01-06 02:40:01 +00:00
|
|
|
|
2020-12-31 02:58:36 +00:00
|
|
|
var content text.Rich
|
|
|
|
|
2021-01-06 02:40:01 +00:00
|
|
|
switch m.Type {
|
|
|
|
case discord.ChannelPinnedMessage:
|
|
|
|
writeSegmented(&content, "Pinned ", "a message", " to this channel.",
|
|
|
|
func(i, j int) text.Segment {
|
|
|
|
if m.ReferencedMessage == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return reference.NewMessageSegment(i, j, m.ReferencedMessage.ID)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
case discord.GuildMemberJoinMessage:
|
|
|
|
content.Content = "Joined the server."
|
|
|
|
|
|
|
|
case discord.CallMessage:
|
|
|
|
content.Content = "Calling you."
|
|
|
|
|
|
|
|
case discord.ChannelIconChangeMessage:
|
|
|
|
content.Content = "Changed the channel icon."
|
2021-01-06 04:53:49 +00:00
|
|
|
|
2021-01-06 02:40:01 +00:00
|
|
|
case discord.ChannelNameChangeMessage:
|
|
|
|
writeSegmented(&content, "Changed the channel name to ", m.Content, ".",
|
|
|
|
func(i, j int) text.Segment {
|
|
|
|
return mention.Segment{
|
|
|
|
Start: i,
|
|
|
|
End: j,
|
|
|
|
Channel: mention.NewChannelFromID(s.State, m.ChannelID),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
case discord.RecipientAddMessage:
|
|
|
|
if len(m.Mentions) == 0 {
|
|
|
|
content.Content = "Added recipient to the group."
|
|
|
|
break
|
2020-12-31 02:58:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
writeSegmented(&content,
|
|
|
|
"Added ", m.Mentions[0].Username, " to the group.",
|
|
|
|
segmentFuncFromMention(*m, s),
|
2021-01-06 02:40:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
case discord.RecipientRemoveMessage:
|
|
|
|
if len(m.Mentions) == 0 {
|
|
|
|
content.Content = "Removed recipient from the group."
|
|
|
|
break
|
2020-12-31 02:58:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
writeSegmented(&content,
|
|
|
|
"Removed ", m.Mentions[0].Username, " from the group.",
|
|
|
|
segmentFuncFromMention(*m, s),
|
2021-01-06 02:40:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
case discord.NitroBoostMessage:
|
|
|
|
content.Content = "Boosted the server."
|
|
|
|
case discord.NitroTier1Message:
|
|
|
|
content.Content = "The server is now Nitro Boosted to Tier 1."
|
|
|
|
case discord.NitroTier2Message:
|
|
|
|
content.Content = "The server is now Nitro Boosted to Tier 2."
|
|
|
|
case discord.NitroTier3Message:
|
|
|
|
content.Content = "The server is now Nitro Boosted to Tier 3."
|
|
|
|
|
|
|
|
case discord.ChannelFollowAddMessage:
|
2021-01-06 04:53:49 +00:00
|
|
|
log.Printf("[Discord] Unknown message type: %#v\n", m)
|
2021-01-06 02:40:01 +00:00
|
|
|
content.Content = "Type = discord.ChannelFollowAddMessage"
|
|
|
|
|
|
|
|
case discord.GuildDiscoveryDisqualifiedMessage:
|
2021-01-06 04:53:49 +00:00
|
|
|
log.Printf("[Discord] Unknown message type: %#v\n", m)
|
2021-01-06 02:40:01 +00:00
|
|
|
content.Content = "Type = discord.GuildDiscoveryDisqualifiedMessage"
|
|
|
|
|
|
|
|
case discord.GuildDiscoveryRequalifiedMessage:
|
2021-01-06 04:53:49 +00:00
|
|
|
log.Printf("[Discord] Unknown message type: %#v\n", m)
|
2021-01-06 02:40:01 +00:00
|
|
|
content.Content = "Type = discord.GuildDiscoveryRequalifiedMessage"
|
|
|
|
|
|
|
|
case discord.ApplicationCommandMessage:
|
|
|
|
fallthrough
|
|
|
|
case discord.InlinedReplyMessage:
|
|
|
|
fallthrough
|
|
|
|
case discord.DefaultMessage:
|
|
|
|
fallthrough
|
|
|
|
default:
|
2021-01-06 04:53:49 +00:00
|
|
|
return newRegularContent(*m, s)
|
2020-12-31 02:58:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 02:40:01 +00:00
|
|
|
segutil.Add(&content, inline.NewSegment(
|
|
|
|
0, len(content.Content),
|
|
|
|
text.AttributeDimmed|text.AttributeItalics,
|
|
|
|
))
|
2020-07-30 00:00:03 +00:00
|
|
|
|
2021-01-06 02:40:01 +00:00
|
|
|
return Message{
|
2021-01-06 04:53:49 +00:00
|
|
|
messageHeader: newHeaderNonce(*m, m.Nonce),
|
2021-01-06 02:40:01 +00:00
|
|
|
content: content,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-06 04:53:49 +00:00
|
|
|
func newRegularContent(m discord.Message, s *state.Instance) Message {
|
2021-01-06 02:40:01 +00:00
|
|
|
var content text.Rich
|
|
|
|
|
|
|
|
if m.ReferencedMessage != nil {
|
2021-01-06 04:53:49 +00:00
|
|
|
refContent := []byte(m.ReferencedMessage.Content)
|
|
|
|
segments.ParseWithMessageRich(&content, refContent, &m, s.Cabinet)
|
|
|
|
|
2021-01-06 02:40:01 +00:00
|
|
|
content = segments.Ellipsize(content, 100)
|
|
|
|
content.Content += "\n"
|
|
|
|
|
|
|
|
segutil.Add(&content,
|
|
|
|
reference.NewMessageSegment(0, len(content.Content)-1, m.ReferencedMessage.ID),
|
|
|
|
)
|
2020-07-30 00:00:03 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 02:40:01 +00:00
|
|
|
segments.ParseMessageRich(&content, &m, s.Cabinet)
|
|
|
|
|
2020-06-16 03:57:33 +00:00
|
|
|
return Message{
|
2020-12-19 05:46:12 +00:00
|
|
|
messageHeader: newHeaderNonce(m, m.Nonce),
|
2020-07-30 00:00:03 +00:00
|
|
|
content: content,
|
2020-06-16 03:57:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-08 04:44:09 +00:00
|
|
|
func (m Message) Author() cchat.Author {
|
2021-01-06 04:53:49 +00:00
|
|
|
if m.author.user == nil {
|
2020-07-06 00:18:40 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-06-16 03:57:33 +00:00
|
|
|
return m.author
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m Message) Content() text.Rich {
|
|
|
|
return m.content
|
|
|
|
}
|
|
|
|
|
2020-12-19 05:46:12 +00:00
|
|
|
func (m Message) Nonce() string {
|
|
|
|
return m.nonce
|
|
|
|
}
|
|
|
|
|
2020-06-16 03:57:33 +00:00
|
|
|
func (m Message) Mentioned() bool {
|
|
|
|
return m.mentioned
|
|
|
|
}
|
2020-12-31 02:58:36 +00:00
|
|
|
|
|
|
|
// ReferencedMessage searches for the referenced message if needed.
|
|
|
|
func ReferencedMessage(m discord.Message, s *state.Instance, wait bool) (reply *discord.Message) {
|
|
|
|
// Deleted or does not exist.
|
|
|
|
if m.Reference == nil || !m.Reference.MessageID.IsValid() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-01-02 08:52:53 +00:00
|
|
|
// Check these in case.
|
|
|
|
if !m.Reference.ChannelID.IsValid() {
|
|
|
|
m.Reference.ChannelID = m.ChannelID
|
|
|
|
}
|
|
|
|
if !m.Reference.GuildID.IsValid() {
|
|
|
|
m.Reference.GuildID = m.GuildID
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.ReferencedMessage != nil {
|
|
|
|
// Set these in case Discord acts dumb.
|
|
|
|
m.ReferencedMessage.GuildID = m.Reference.GuildID
|
|
|
|
m.ReferencedMessage.ChannelID = m.Reference.ChannelID
|
|
|
|
return m.ReferencedMessage
|
|
|
|
}
|
|
|
|
|
2020-12-31 02:58:36 +00:00
|
|
|
if !wait {
|
|
|
|
reply, _ = s.Cabinet.Message(m.Reference.ChannelID, m.Reference.MessageID)
|
|
|
|
} else {
|
|
|
|
reply, _ = s.Message(m.Reference.ChannelID, m.Reference.MessageID)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2021-01-06 04:53:49 +00:00
|
|
|
|
|
|
|
// segmentFuncFromMention returns a function that gets the message's first
|
|
|
|
// mention and returns a segment created from it. It returns nil if the message
|
|
|
|
// does not have any mentions.
|
|
|
|
func segmentFuncFromMention(m discord.Message, s *state.Instance) func(i, j int) text.Segment {
|
|
|
|
return func(i, j int) text.Segment {
|
|
|
|
if len(m.Mentions) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
firstMention := m.Mentions[0]
|
|
|
|
|
|
|
|
user := mention.NewUser(firstMention.User)
|
|
|
|
user.WithGuildID(m.GuildID)
|
|
|
|
user.WithState(s.State)
|
|
|
|
|
|
|
|
if firstMention.Member != nil {
|
|
|
|
user.WithMember(*firstMention.Member)
|
|
|
|
}
|
|
|
|
|
|
|
|
user.Prefetch()
|
|
|
|
|
|
|
|
return mention.NewSegment(i, j, user)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeSegmented(rich *text.Rich, start, mid, end string, f func(i, j int) text.Segment) {
|
|
|
|
var builder strings.Builder
|
|
|
|
|
|
|
|
builder.WriteString(start)
|
|
|
|
i, j := segutil.WriteStringBuilder(&builder, start)
|
|
|
|
builder.WriteString(end)
|
|
|
|
|
|
|
|
rich.Content = builder.String()
|
|
|
|
|
|
|
|
if seg := f(i, j); seg != nil {
|
|
|
|
rich.Segments = append(rich.Segments, f(i, j))
|
|
|
|
}
|
|
|
|
}
|