diff --git a/go.mod b/go.mod index f12ac79..fd2896a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c github.com/diamondburned/cchat v0.3.17 - github.com/diamondburned/ningen/v2 v2.0.0-20210101084041-d9a5058b63b5 + github.com/diamondburned/ningen/v2 v2.0.0-20210106043942-5e3332344ab6 github.com/dustin/go-humanize v1.0.0 github.com/go-test/deep v1.0.7 github.com/lithammer/fuzzysearch v1.1.1 diff --git a/go.sum b/go.sum index c1865f7..c29311f 100644 --- a/go.sum +++ b/go.sum @@ -214,6 +214,8 @@ github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4 h1:ptIpcyB github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo= github.com/diamondburned/ningen/v2 v2.0.0-20210101084041-d9a5058b63b5 h1:GKqBXunV2AC/owpkiaFng1wPxgxE76sQ8HEPAHGj29o= github.com/diamondburned/ningen/v2 v2.0.0-20210101084041-d9a5058b63b5/go.mod h1:WRQCUX/dTH4OPEy3JANLA5D6fbumzp5zk03uSUAZppA= +github.com/diamondburned/ningen/v2 v2.0.0-20210106043942-5e3332344ab6 h1:YTvBovyUXatZbU/+gdLJPmBvisLbJkLQe6pq4BFvcUQ= +github.com/diamondburned/ningen/v2 v2.0.0-20210106043942-5e3332344ab6/go.mod h1:WRQCUX/dTH4OPEy3JANLA5D6fbumzp5zk03uSUAZppA= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/internal/discord/channel/message/backlog/backlogger.go b/internal/discord/channel/message/backlog/backlogger.go index 5517c71..d62235b 100644 --- a/internal/discord/channel/message/backlog/backlogger.go +++ b/internal/discord/channel/message/backlog/backlogger.go @@ -31,17 +31,11 @@ func (bl Backlogger) Backlog(ctx context.Context, b cchat.ID, c cchat.MessagesCo return errors.Wrap(err, "Failed to get messages") } - // Create the backlog without any member information. - g, err := s.Guild(bl.GuildID) - if err != nil { - return errors.Wrap(err, "Failed to get guild") - } - for _, m := range m { // Discord sucks. m.GuildID = bl.GuildID - c.CreateMessage(message.NewBacklogMessage(m, bl.State, *g)) + c.CreateMessage(message.NewBacklogMessage(m, bl.State)) } return nil diff --git a/internal/discord/channel/message/memberlist/member.go b/internal/discord/channel/message/memberlist/member.go index c258214..72756ef 100644 --- a/internal/discord/channel/message/memberlist/member.go +++ b/internal/discord/channel/message/memberlist/member.go @@ -24,8 +24,10 @@ type Member struct { func NewMember(ch shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember { user := mention.NewUser(opItem.Member.User) user.WithState(ch.State.State) - user.SetMember(ch.GuildID, &opItem.Member.Member) - user.SetPresence(opItem.Member.Presence) + user.WithMember(opItem.Member.Member) + user.WithGuildID(ch.GuildID) + user.WithPresence(opItem.Member.Presence) + user.Prefetch() return &Member{ channel: ch, diff --git a/internal/discord/channel/message/message.go b/internal/discord/channel/message/message.go index c57bc63..3e6a69e 100644 --- a/internal/discord/channel/message/message.go +++ b/internal/discord/channel/message/message.go @@ -18,7 +18,6 @@ import ( "github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/funcutil" "github.com/diamondburned/cchat/utils/empty" - "github.com/pkg/errors" ) type Messenger struct { @@ -42,19 +41,7 @@ func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContaine var addcancel = funcutil.NewCancels() - var constructor func(discord.Message) cchat.MessageCreate - if msgr.GuildID.IsValid() { - // Create the backlog without any member information. - g, err := state.Guild(msgr.GuildID) - if err != nil { - return nil, errors.Wrap(err, "Failed to get guild") - } - - constructor = func(m discord.Message) cchat.MessageCreate { - return message.NewBacklogMessage(m, msgr.State, *g) - } - // Subscribe to typing events. msgr.State.MemberState.Subscribe(msgr.GuildID) @@ -64,33 +51,16 @@ func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContaine return } - messages, err := msgr.Messages() - if err != nil { - // TODO: log - return - } + messages, _ := msgr.Messages() - guild, err := msgr.Guild() - if err != nil { - return - } - - // Loop over all messages and replace the author. The latest - // messages are in front. - for _, msg := range messages { - for _, m := range c.Members { - if msg.Author.ID != m.User.ID { - continue + for _, m := range c.Members { + for _, msg := range messages { + if msg.Author.ID == m.User.ID { + ct.UpdateMessage(message.NewAuthorUpdate(msg, m, msgr.State)) } - - ct.UpdateMessage(message.NewMessageUpdateAuthor(msg, m, *guild, msgr.State)) } } })) - } else { - constructor = func(m discord.Message) cchat.MessageCreate { - return message.NewDirectMessage(m, msgr.State) - } } // Only do all this if we even have any messages. @@ -101,7 +71,7 @@ func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContaine // Iterate from the earliest messages to the latest messages. for _, m := range m { - ct.CreateMessage(constructor(m)) + ct.CreateMessage(message.NewBacklogMessage(m, msgr.State)) } // Mark this channel as read. @@ -119,7 +89,7 @@ func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContaine msgr.State.AddHandler(func(m *gateway.MessageUpdateEvent) { // If the updated content is empty. TODO: add embed support. if m.ChannelID == msgr.ID { - ct.UpdateMessage(message.NewMessageUpdateContent(m.Message, msgr.State)) + ct.UpdateMessage(message.NewContentUpdate(m.Message, msgr.State)) } }), msgr.State.AddHandler(func(m *gateway.MessageDeleteEvent) { diff --git a/internal/discord/channel/message/send/complete/mention.go b/internal/discord/channel/message/send/complete/mention.go index 52a41d0..e535a86 100644 --- a/internal/discord/channel/message/send/complete/mention.go +++ b/internal/discord/channel/message/send/complete/mention.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/urlutils" "github.com/diamondburned/cchat/text" ) @@ -40,23 +41,17 @@ func GuildMessageMentions( authors[msg.Author.ID] = struct{}{} - var rich text.Rich + user := mention.NewUser(msg.Author) + user.WithGuildID(msg.GuildID) if guild != nil && state != nil { - m, err := state.Cabinet.Member(guild.ID, msg.Author.ID) - if err == nil { - rich = message.RenderMemberName(*m, *guild, state) - } - } - - // Fallback to searching the author if member fails. - if rich.IsEmpty() { - rich = text.Plain(msg.Author.Username) + user.WithState(state.State) + user.WithGuild(*guild) } entries = append(entries, cchat.CompletionEntry{ Raw: msg.Author.Mention(), - Text: rich, + Text: message.RenderAuthorName(user), Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator), IconURL: msg.Author.AvatarURL(), }) @@ -101,7 +96,7 @@ func AllUsers(s *state.Instance, word string) []cchat.CompletionEntry { raw := r.User.Mention() var status = gateway.UnknownStatus - if p, _ := s.PresenceState.Presence(0, r.UserID); p != nil { + if p, _ := s.PresenceStore.Presence(0, r.UserID); p != nil { status = p.Status } @@ -123,7 +118,7 @@ func AllUsers(s *state.Instance, word string) []cchat.CompletionEntry { } // Search for presences. - s.PresenceState.Each(0, func(p *gateway.Presence) bool { + s.PresenceStore.Each(0, func(p *gateway.Presence) bool { // Avoid duplicates. if _, ok := friends[p.User.ID]; ok { return false @@ -237,45 +232,35 @@ func (ch ChannelCompleter) CompleteMentions(word string) []cchat.CompletionEntry return entries } - // If we're in a guild, then we should search for (all) members. - m, merr := ch.State.Cabinet.Members(ch.GuildID) - g, gerr := ch.State.Cabinet.Guild(ch.GuildID) - - if merr != nil || gerr != nil { - return nil - } - - // If we couldn't find any members, then we can request Discord to - // search for them. - if len(m) == 0 { - ch.State.MemberState.SearchMember(ch.GuildID, word) - return nil - } - - for _, mem := range m { - rank := memberMatchString(word, &mem) + // Prioritize searching the guild's presences because we don't need to copy + // slices. + ch.State.MemberStore.Each(ch.GuildID, func(m *discord.Member) (stop bool) { + rank := memberMatchString(word, m) if rank == -1 { - continue + return false } ensureEntriesMade(&entries) ensureDistancesMade(&distances) - raw := mem.User.Mention() + user := mention.NewUser(m.User) + user.WithGuildID(ch.GuildID) + user.WithMember(*m) + user.WithState(ch.State.State) + + raw := m.User.Mention() entries = append(entries, cchat.CompletionEntry{ Raw: raw, - Text: message.RenderMemberName(mem, *g, ch.State), - Secondary: text.Plain(mem.User.Username + "#" + mem.User.Discriminator), - IconURL: urlutils.AvatarURL(mem.User.AvatarURL()), + Text: message.RenderAuthorName(user), + Secondary: text.Plain(m.User.Username + "#" + m.User.Discriminator), + IconURL: user.Avatar(), }) distances[raw] = rank - if len(entries) >= MaxCompletion { - break - } - } + return len(entries) >= MaxCompletion + }) sortDistances(entries, distances) return entries diff --git a/internal/discord/channel/typer/typer.go b/internal/discord/channel/typer/typer.go index 0d78e68..2551d03 100644 --- a/internal/discord/channel/typer/typer.go +++ b/internal/discord/channel/typer/typer.go @@ -1,7 +1,6 @@ package typer import ( - "errors" "time" "github.com/diamondburned/arikawa/v2/discord" @@ -9,6 +8,8 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat-discord/internal/segments/mention" + "github.com/pkg/errors" ) type Typer struct { @@ -18,48 +19,47 @@ type Typer struct { var _ cchat.Typer = (*Typer)(nil) -func NewFromAuthor(author message.Author, ev *gateway.TypingStartEvent) Typer { - return Typer{ - Author: author, - time: ev.Timestamp, - } -} - +// New creates a new Typer that satisfies cchat.Typer. func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) { + var user *mention.User + if ev.GuildID.IsValid() { - g, err := s.Cabinet.Guild(ev.GuildID) - if err != nil { - return nil, err - } - if ev.Member == nil { - ev.Member, err = s.Cabinet.Member(ev.GuildID, ev.UserID) + m, err := s.Cabinet.Member(ev.GuildID, ev.UserID) if err != nil { - return nil, err + // There's no other way we could get the user (other than to + // check for presences), so we bail. + return nil, errors.Wrap(err, "failed to get member") } + ev.Member = m } - return &Typer{ - Author: message.NewGuildMember(*ev.Member, *g, s), - time: ev.Timestamp, - }, nil - } + user = mention.NewUser(ev.Member.User) + user.WithMember(*ev.Member) + } else { + c, err := s.Cabinet.Channel(ev.ChannelID) + if err != nil { + return nil, errors.Wrap(err, "failed to get channel") + } - c, err := s.Cabinet.Channel(ev.ChannelID) - if err != nil { - return nil, err - } + for _, recipient := range c.DMRecipients { + if recipient.ID != ev.UserID { + continue + } - for _, user := range c.DMRecipients { - if user.ID == ev.UserID { - return &Typer{ - Author: message.NewUser(user, s), - time: ev.Timestamp, - }, nil + user = mention.NewUser(recipient) + break } } - return nil, errors.New("typer not found in state") + user.WithGuildID(ev.GuildID) + user.WithState(s.State) + user.Prefetch() + + return &Typer{ + Author: message.NewAuthor(user), + time: ev.Timestamp, + }, nil } func (t Typer) Time() time.Time { diff --git a/internal/discord/message/author.go b/internal/discord/message/author.go index 833b4f0..5960cd1 100644 --- a/internal/discord/message/author.go +++ b/internal/discord/message/author.go @@ -3,101 +3,56 @@ package message import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/segments/colored" "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-discord/internal/urlutils" "github.com/diamondburned/cchat/text" ) type Author struct { - id discord.UserID - name text.Rich - avatar string + name text.Rich + user *mention.User // same pointer as in name } var _ cchat.Author = (*Author)(nil) -func NewUser(u discord.User, s *state.Instance) Author { - var rich text.Rich - richUser(&rich, u, s) - +// NewAuthor creates a new message author. +func NewAuthor(user *mention.User) Author { return Author{ - id: u.ID, - name: rich, - avatar: urlutils.AvatarURL(u.AvatarURL()), + name: RenderAuthorName(user), + user: user, } } -func NewGuildMember(m discord.Member, g discord.Guild, s *state.Instance) Author { - return Author{ - id: m.User.ID, - name: RenderMemberName(m, g, s), - avatar: urlutils.AvatarURL(m.User.AvatarURL()), - } -} - -func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text.Rich { +// RenderAuthorName renders the given user mention into a text segment. +func RenderAuthorName(user *mention.User) text.Rich { var rich text.Rich - richMember(&rich, m, g, s) + richUser(&rich, user) return rich } // richMember appends the member name directly into rich. -func richMember(rich *text.Rich, - m discord.Member, g discord.Guild, s *state.Instance) (start, end int) { - - var displayName = m.User.Username - if m.Nick != "" { - displayName = m.Nick - } - - start, end = segutil.Write(rich, displayName) +func richUser(rich *text.Rich, user *mention.User) (start, end int) { + start, end = segutil.Write(rich, user.DisplayName()) // Append the bot prefix if the user is a bot. - if m.User.Bot { + if user.User().Bot { rich.Content += " " rich.Segments = append(rich.Segments, colored.NewBlurple(segutil.Write(rich, "[BOT]")), ) } - // Append a clickable user popup. - user := mention.NewUser(m.User) - user.WithState(s.State) - user.SetMember(g.ID, &m) - - rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user)) - - return -} - -func richUser(rich *text.Rich, - u discord.User, s *state.Instance) (start, end int) { - - start, end = segutil.Write(rich, u.Username) - - // Append the bot prefix if the user is a bot. - if u.Bot { - rich.Content += " " - rich.Segments = append(rich.Segments, - colored.NewBlurple(segutil.Write(rich, "[BOT]")), - ) - } - - // Append a clickable user popup. - user := mention.NewUser(u) - user.WithState(s.State) - rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user)) return } func (a Author) ID() cchat.ID { - return a.id.String() + return a.user.UserID().String() } func (a Author) Name() text.Rich { @@ -105,60 +60,54 @@ func (a Author) Name() text.Rich { } func (a Author) Avatar() string { - return a.avatar + return a.user.Avatar() } const authorReplyingTo = " replying to " // AddUserReply modifies Author to make it appear like it's a message reply. -// Specifically, this function is used for direct messages. +// Specifically, this function is used for direct messages in virtual channels. func (a *Author) AddUserReply(user discord.User, s *state.Instance) { a.name.Content += authorReplyingTo - richUser(&a.name, user, s) + + userMention := mention.NewUser(user) + userMention.WithState(s.State) + userMention.Prefetch() + + richUser(&a.name, userMention) } -func (a *Author) AddReply(name string) { - a.name.Content += authorReplyingTo + name -} +// AddChannelReply adds a reply pointing to a channel. If the given channel is a +// direct message channel, then the first recipient will be used instead, and +// the function will operate similar to AddUserReply. +func (a *Author) AddChannelReply(ch discord.Channel, s *state.Instance) { + if ch.Type == discord.DirectMessage && len(ch.DMRecipients) > 0 { + a.AddUserReply(ch.DMRecipients[0], s) + return + } -// // AddMemberReply modifies Author to make it appear like it's a message reply. -// // Specifically, this function is used for guild messages. -// func (a *Author) AddMemberReply(m discord.Member, g discord.Guild, s *state.Instance) { -// a.name.Content += authorReplyingTo -// richMember(&a.name, m, g, s) -// } - -func (a *Author) addAuthorReference(msgref discord.Message, s *state.Instance) { a.name.Content += authorReplyingTo - start, end := richUser(&a.name, msgref.Author, s) + start, end := segutil.Write(&a.name, shared.ChannelName(ch)) a.name.Segments = append(a.name.Segments, - reference.NewMessageSegment(start, end, msgref.ID), + mention.Segment{ + Start: start, + End: end, + Channel: mention.NewChannel(ch), + }, ) } // AddMessageReference adds a message reference to the author. func (a *Author) AddMessageReference(ref discord.Message, s *state.Instance) { - if !ref.GuildID.IsValid() { - a.addAuthorReference(ref, s) - return - } - - g, err := s.Cabinet.Guild(ref.GuildID) - if err != nil { - a.addAuthorReference(ref, s) - return - } - - m, err := s.Cabinet.Member(g.ID, ref.Author.ID) - if err != nil { - a.addAuthorReference(ref, s) - s.MemberState.RequestMember(g.ID, ref.Author.ID) - return - } - a.name.Content += authorReplyingTo - start, end := richMember(&a.name, *m, *g, s) + + userMention := mention.NewUser(ref.Author) + userMention.WithGuildID(ref.GuildID) + userMention.WithState(s.State) + userMention.Prefetch() + + start, end := richUser(&a.name, userMention) a.name.Segments = append(a.name.Segments, reference.NewMessageSegment(start, end, ref.ID), diff --git a/internal/discord/message/message.go b/internal/discord/message/message.go index b68ce9a..617fdbe 100644 --- a/internal/discord/message/message.go +++ b/internal/discord/message/message.go @@ -82,37 +82,6 @@ var ( _ 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. @@ -121,51 +90,98 @@ func NewGuildMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Mes 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)) + user := mention.NewUser(c.Author) + user.WithState(s.State) + user.WithGuildID(c.GuildID) + + if c.Member != nil { + user.WithMember(*c.Member) } - 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)) - } + user.Prefetch() - return NewMessage(message, s, NewGuildMember(*c.Member, *g, s)) + return NewMessage(message, s, NewAuthor(user)) } // 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 { +func NewBacklogMessage(m discord.Message, s *state.Instance) 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)) + return NewDirectMessage(m, 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)) - } + user := mention.NewUser(m.Author) + user.WithGuildID(m.GuildID) + user.WithState(s.State) + user.Prefetch() - return NewMessage(m, s, NewGuildMember(*mem, g, s)) + return NewMessage(m, s, NewAuthor(user)) } +// NewDirectMessage creates a new direct message. func NewDirectMessage(m discord.Message, s *state.Instance) Message { - return NewMessage(m, s, NewUser(m.Author, s)) + user := mention.NewUser(m.Author) + user.WithState(s.State) + user.Prefetch() + + return NewMessage(m, s, NewAuthor(user)) } -func NewMessage(m discord.Message, s *state.Instance, author Author) Message { - // Ensure the validity of ReferencedMessage. - m.ReferencedMessage = ReferencedMessage(m, s, true) +// 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) - // Render the message content. + author := NewAuthor(user) + if ref := ReferencedMessage(msg, s, true); ref != nil { + author.AddMessageReference(*ref, s) + } + + return Message{ + messageHeader: newHeader(msg), + author: author, + } +} + +// 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. +func NewMessage(m discord.Message, s *state.Instance, author Author) Message { + message := newMessageContent(&m, s) + message.author = author + + if m.ReferencedMessage != nil { + message.author.AddMessageReference(*m.ReferencedMessage, s) + } + + return message +} + +// 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) var content text.Rich @@ -188,6 +204,7 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { 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 { @@ -205,14 +222,9 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { 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 - }, + writeSegmented(&content, + "Added ", m.Mentions[0].Username, " to the group.", + segmentFuncFromMention(*m, s), ) case discord.RecipientRemoveMessage: @@ -221,14 +233,9 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { 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 - }, + writeSegmented(&content, + "Removed ", m.Mentions[0].Username, " from the group.", + segmentFuncFromMention(*m, s), ) case discord.NitroBoostMessage: @@ -241,15 +248,15 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { content.Content = "The server is now Nitro Boosted to Tier 3." case discord.ChannelFollowAddMessage: - log.Printf("[Discord] Unknown message type: %#v\n") + log.Printf("[Discord] Unknown message type: %#v\n", m) content.Content = "Type = discord.ChannelFollowAddMessage" case discord.GuildDiscoveryDisqualifiedMessage: - log.Printf("[Discord] Unknown message type: %#v\n") + log.Printf("[Discord] Unknown message type: %#v\n", m) content.Content = "Type = discord.GuildDiscoveryDisqualifiedMessage" case discord.GuildDiscoveryRequalifiedMessage: - log.Printf("[Discord] Unknown message type: %#v\n") + log.Printf("[Discord] Unknown message type: %#v\n", m) content.Content = "Type = discord.GuildDiscoveryRequalifiedMessage" case discord.ApplicationCommandMessage: @@ -259,7 +266,7 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { case discord.DefaultMessage: fallthrough default: - return newMessage(m, s, author) + return newRegularContent(*m, s) } segutil.Add(&content, inline.NewSegment( @@ -268,52 +275,36 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { )) return Message{ - messageHeader: newHeaderNonce(m, m.Nonce), - author: author, + messageHeader: newHeaderNonce(*m, m.Nonce), content: content, } } -func newMessage(m discord.Message, s *state.Instance, author Author) Message { +func newRegularContent(m discord.Message, s *state.Instance) Message { var content text.Rich if m.ReferencedMessage != nil { - segments.ParseWithMessageRich(&content, []byte(m.ReferencedMessage.Content), &m, s.Cabinet) + refContent := []byte(m.ReferencedMessage.Content) + segments.ParseWithMessageRich(&content, refContent, &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() { + if m.author.user == nil { return nil } return m.author @@ -361,3 +352,42 @@ func ReferencedMessage(m discord.Message, s *state.Instance, wait bool) (reply * return } + +// 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)) + } +} diff --git a/internal/discord/private/hub/messages.go b/internal/discord/private/hub/messages.go index e0611a0..1788fe9 100644 --- a/internal/discord/private/hub/messages.go +++ b/internal/discord/private/hub/messages.go @@ -8,11 +8,11 @@ import ( "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat-discord/internal/funcutil" + "github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat/utils/empty" ) @@ -167,16 +167,14 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer isReply = true } - var author = message.NewUser(msg.Author, msgs.state) + user := mention.NewUser(msg.Author) + user.WithState(msgs.state.State) + + var author = message.NewAuthor(user) if isReply { c, err := msgs.state.Channel(msg.ChannelID) if err == nil { - switch c.Type { - case discord.DirectMessage: - author.AddUserReply(c.DMRecipients[0], msgs.state) - case discord.GroupDM: - author.AddReply(shared.ChannelName(*c)) - } + author.AddChannelReply(*c, msgs.state) } } @@ -188,7 +186,7 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer return } - ct.UpdateMessage(message.NewMessageUpdateContent(update.Message, msgs.state)) + ct.UpdateMessage(message.NewContentUpdate(update.Message, msgs.state)) }), msgs.state.AddHandler(func(del *gateway.MessageDeleteEvent) { if del.GuildID.IsValid() || msgs.acList.isActive(del.ChannelID) { diff --git a/internal/segments/mention/mention.go b/internal/segments/mention/mention.go index ae01d8e..71ce31c 100644 --- a/internal/segments/mention/mention.go +++ b/internal/segments/mention/mention.go @@ -28,7 +28,11 @@ func mention(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus { seg.Start, seg.End = r.WriteString("@" + n.GuildUser.Username) seg.User = NewUser(n.GuildUser.User) seg.User.store = r.Store - seg.User.SetMember(r.Message.GuildID, n.GuildUser.Member) + seg.User.WithGuildID(r.Message.GuildID) + if n.GuildUser.Member != nil { + seg.User.WithMember(*n.GuildUser.Member) + } + seg.User.Prefetch() case n.GuildRole != nil: seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name) diff --git a/internal/segments/mention/user.go b/internal/segments/mention/user.go index 98de133..4f88084 100644 --- a/internal/segments/mention/user.go +++ b/internal/segments/mention/user.go @@ -34,12 +34,6 @@ func NewSegment(start, end int, user *User) NameSegment { } } -// WithState assigns a ningen state into the given name segment. This allows the -// popovers to have additional information such as user notes. -func (m *NameSegment) WithState(state *ningen.State) { - m.um.WithState(state) -} - func (m NameSegment) Bounds() (start, end int) { return m.start, m.end } @@ -98,21 +92,24 @@ func (um *User) UserID() discord.UserID { } // SetGuildID sets the user's guild ID. -func (um *User) SetGuildID(guildID discord.GuildID) { +func (um *User) WithGuildID(guildID discord.GuildID) { um.guildID = guildID - um.HasColor() // prefetch } -// SetMember sets the internal member to reduce roundtrips or cache hits. m can +// WithGuild sets the user's guild. +func (um *User) WithGuild(guild discord.Guild) { + um.guildID = guild.ID + um.guild = &guild +} + +// WithMember sets the internal member to reduce roundtrips or cache hits. m can // be nil. -func (um *User) SetMember(gID discord.GuildID, m *discord.Member) { - um.guildID = gID - um.member = m - um.HasColor() +func (um *User) WithMember(m discord.Member) { + um.member = &m } -// SetPresence sets the internal presence to reduce roundtrips or cache hits. -func (um *User) SetPresence(p gateway.Presence) { +// WithPresence sets the internal presence to reduce roundtrips or cache hits. +func (um *User) WithPresence(p gateway.Presence) { um.presence = &p } @@ -120,7 +117,12 @@ func (um *User) SetPresence(p gateway.Presence) { func (um *User) WithState(state *ningen.State) { um.ningen = state um.store = state.Cabinet - um.HasColor() // prefetch +} + +// Prefetch prefetches everything in User. +func (um *User) Prefetch() { + um.HasColor() + um.getPresence() } // DisplayName returns either the nickname or the username.