cchat-discord/internal/discord/message/message.go

364 lines
9.6 KiB
Go

package message
import (
"log"
"strings"
"time"
"github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments"
"github.com/diamondburned/cchat-discord/internal/segments/inline"
"github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat-discord/internal/segments/reference"
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
"github.com/diamondburned/cchat/text"
)
type messageHeader struct {
id discord.MessageID
time discord.Timestamp
nonce string
channelID discord.ChannelID
guildID discord.GuildID
}
var _ cchat.MessageHeader = (*messageHeader)(nil)
func newHeader(msg discord.Message) messageHeader {
return messageHeader{
id: msg.ID,
time: msg.Timestamp,
channelID: msg.ChannelID,
guildID: msg.GuildID,
}
}
func newHeaderNonce(msg discord.Message, nonce string) messageHeader {
h := newHeader(msg)
h.nonce = nonce
return h
}
func NewHeaderDelete(d *gateway.MessageDeleteEvent) messageHeader {
return messageHeader{
id: d.ID,
time: discord.Timestamp(time.Now()),
channelID: d.ChannelID,
guildID: d.GuildID,
}
}
func (m messageHeader) ID() cchat.ID {
return m.id.String()
}
func (m messageHeader) Nonce() string { return m.nonce }
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 }
func (m messageHeader) Time() time.Time {
return m.time.Time()
}
type Message struct {
messageHeader
author Author
content text.Rich
// TODO
mentioned bool
}
var (
_ cchat.MessageCreate = (*Message)(nil)
_ cchat.MessageUpdate = (*Message)(nil)
_ cchat.MessageDelete = (*Message)(nil)
_ cchat.Noncer = (*Message)(nil)
)
func NewMessageUpdateContent(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.Content = m.Content
}
}
var content = segments.ParseMessage(&msg, s.Cabinet)
return Message{
messageHeader: newHeader(msg),
content: content,
}
}
func NewMessageUpdateAuthor(
msg discord.Message, member discord.Member, g discord.Guild, s *state.Instance) Message {
author := NewGuildMember(member, g, s)
if ref := ReferencedMessage(msg, s, true); ref != nil {
author.AddMessageReference(*ref, s)
}
return Message{
messageHeader: newHeader(msg),
author: NewGuildMember(member, g, s),
}
}
// 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)
// This should not error.
g, err := s.Cabinet.Guild(c.GuildID)
if err != nil {
return NewMessage(message, s, NewUser(c.Author, s))
}
if c.Member == nil {
c.Member, _ = s.Cabinet.Member(c.GuildID, c.Author.ID)
}
if c.Member == nil {
s.MemberState.RequestMember(c.GuildID, c.Author.ID)
return NewMessage(message, s, NewUser(c.Author, s))
}
return NewMessage(message, s, NewGuildMember(*c.Member, *g, s))
}
// 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.
func NewBacklogMessage(m discord.Message, s *state.Instance, g discord.Guild) Message {
// If the message doesn't have a guild, then we don't need all the
// complicated member fetching process.
if !m.GuildID.IsValid() {
return NewMessage(m, s, NewUser(m.Author, s))
}
mem, err := s.Cabinet.Member(m.GuildID, m.Author.ID)
if err != nil {
s.MemberState.RequestMember(m.GuildID, m.Author.ID)
return NewMessage(m, s, NewUser(m.Author, s))
}
return NewMessage(m, s, NewGuildMember(*mem, g, s))
}
func NewDirectMessage(m discord.Message, s *state.Instance) Message {
return NewMessage(m, s, NewUser(m.Author, s))
}
func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
// Ensure the validity of ReferencedMessage.
m.ReferencedMessage = ReferencedMessage(m, s, true)
// Render the message content.
var content text.Rich
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."
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
}
writeSegmented(&content, "Added ", m.Mentions[0].Username, " to the group.",
func(i, j int) text.Segment {
user := mention.NewUser(m.Mentions[0].User)
user.SetMember(m.GuildID, m.Mentions[0].Member)
segment := mention.NewSegment(i, j, user)
segment.WithState(s.State)
return segment
},
)
case discord.RecipientRemoveMessage:
if len(m.Mentions) == 0 {
content.Content = "Removed recipient from the group."
break
}
writeSegmented(&content, "Removed ", m.Mentions[0].Username, " from the group.",
func(i, j int) text.Segment {
user := mention.NewUser(m.Mentions[0].User)
user.SetMember(m.GuildID, m.Mentions[0].Member)
segment := mention.NewSegment(i, j, user)
segment.WithState(s.State)
return segment
},
)
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:
log.Printf("[Discord] Unknown message type: %#v\n")
content.Content = "Type = discord.ChannelFollowAddMessage"
case discord.GuildDiscoveryDisqualifiedMessage:
log.Printf("[Discord] Unknown message type: %#v\n")
content.Content = "Type = discord.GuildDiscoveryDisqualifiedMessage"
case discord.GuildDiscoveryRequalifiedMessage:
log.Printf("[Discord] Unknown message type: %#v\n")
content.Content = "Type = discord.GuildDiscoveryRequalifiedMessage"
case discord.ApplicationCommandMessage:
fallthrough
case discord.InlinedReplyMessage:
fallthrough
case discord.DefaultMessage:
fallthrough
default:
return newMessage(m, s, author)
}
segutil.Add(&content, inline.NewSegment(
0, len(content.Content),
text.AttributeDimmed|text.AttributeItalics,
))
return Message{
messageHeader: newHeaderNonce(m, m.Nonce),
author: author,
content: content,
}
}
func newMessage(m discord.Message, s *state.Instance, author Author) Message {
var content text.Rich
if m.ReferencedMessage != nil {
segments.ParseWithMessageRich(&content, []byte(m.ReferencedMessage.Content), &m, s.Cabinet)
content = segments.Ellipsize(content, 100)
content.Content += "\n"
segutil.Add(&content,
reference.NewMessageSegment(0, len(content.Content)-1, m.ReferencedMessage.ID),
)
author.AddMessageReference(*m.ReferencedMessage, s)
}
segments.ParseMessageRich(&content, &m, s.Cabinet)
return Message{
messageHeader: newHeaderNonce(m, m.Nonce),
author: author,
content: content,
}
}
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))
}
}
func (m Message) Author() cchat.Author {
if !m.author.id.IsValid() {
return nil
}
return m.author
}
func (m Message) Content() text.Rich {
return m.content
}
func (m Message) Nonce() string {
return m.nonce
}
func (m Message) Mentioned() bool {
return m.mentioned
}
// 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
}
// 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
}
if !wait {
reply, _ = s.Cabinet.Message(m.Reference.ChannelID, m.Reference.MessageID)
} else {
reply, _ = s.Message(m.Reference.ChannelID, m.Reference.MessageID)
}
return
}