further use the new user mention API

This commit is contained in:
diamondburned 2021-01-05 20:53:49 -08:00
parent f5ac9a2422
commit 46db98bfe9
12 changed files with 271 additions and 335 deletions

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.14
require ( require (
github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c
github.com/diamondburned/cchat v0.3.17 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/dustin/go-humanize v1.0.0
github.com/go-test/deep v1.0.7 github.com/go-test/deep v1.0.7
github.com/lithammer/fuzzysearch v1.1.1 github.com/lithammer/fuzzysearch v1.1.1

2
go.sum
View File

@ -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-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 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-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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

View File

@ -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") 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 { for _, m := range m {
// Discord sucks. // Discord sucks.
m.GuildID = bl.GuildID m.GuildID = bl.GuildID
c.CreateMessage(message.NewBacklogMessage(m, bl.State, *g)) c.CreateMessage(message.NewBacklogMessage(m, bl.State))
} }
return nil return nil

View File

@ -24,8 +24,10 @@ type Member struct {
func NewMember(ch shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember { func NewMember(ch shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
user := mention.NewUser(opItem.Member.User) user := mention.NewUser(opItem.Member.User)
user.WithState(ch.State.State) user.WithState(ch.State.State)
user.SetMember(ch.GuildID, &opItem.Member.Member) user.WithMember(opItem.Member.Member)
user.SetPresence(opItem.Member.Presence) user.WithGuildID(ch.GuildID)
user.WithPresence(opItem.Member.Presence)
user.Prefetch()
return &Member{ return &Member{
channel: ch, channel: ch,

View File

@ -18,7 +18,6 @@ import (
"github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/discord/message"
"github.com/diamondburned/cchat-discord/internal/funcutil" "github.com/diamondburned/cchat-discord/internal/funcutil"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
"github.com/pkg/errors"
) )
type Messenger struct { type Messenger struct {
@ -42,19 +41,7 @@ func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContaine
var addcancel = funcutil.NewCancels() var addcancel = funcutil.NewCancels()
var constructor func(discord.Message) cchat.MessageCreate
if msgr.GuildID.IsValid() { 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. // Subscribe to typing events.
msgr.State.MemberState.Subscribe(msgr.GuildID) msgr.State.MemberState.Subscribe(msgr.GuildID)
@ -64,33 +51,16 @@ func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContaine
return return
} }
messages, err := msgr.Messages() messages, _ := msgr.Messages()
if err != nil {
// TODO: log
return
}
guild, err := msgr.Guild() for _, m := range c.Members {
if err != nil { for _, msg := range messages {
return if msg.Author.ID == m.User.ID {
} ct.UpdateMessage(message.NewAuthorUpdate(msg, m, msgr.State))
// 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
} }
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. // 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. // Iterate from the earliest messages to the latest messages.
for _, m := range m { for _, m := range m {
ct.CreateMessage(constructor(m)) ct.CreateMessage(message.NewBacklogMessage(m, msgr.State))
} }
// Mark this channel as read. // 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) { msgr.State.AddHandler(func(m *gateway.MessageUpdateEvent) {
// If the updated content is empty. TODO: add embed support. // If the updated content is empty. TODO: add embed support.
if m.ChannelID == msgr.ID { 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) { msgr.State.AddHandler(func(m *gateway.MessageDeleteEvent) {

View File

@ -6,6 +6,7 @@ import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/discord/message"
"github.com/diamondburned/cchat-discord/internal/discord/state" "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-discord/internal/urlutils"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
@ -40,23 +41,17 @@ func GuildMessageMentions(
authors[msg.Author.ID] = struct{}{} authors[msg.Author.ID] = struct{}{}
var rich text.Rich user := mention.NewUser(msg.Author)
user.WithGuildID(msg.GuildID)
if guild != nil && state != nil { if guild != nil && state != nil {
m, err := state.Cabinet.Member(guild.ID, msg.Author.ID) user.WithState(state.State)
if err == nil { user.WithGuild(*guild)
rich = message.RenderMemberName(*m, *guild, state)
}
}
// Fallback to searching the author if member fails.
if rich.IsEmpty() {
rich = text.Plain(msg.Author.Username)
} }
entries = append(entries, cchat.CompletionEntry{ entries = append(entries, cchat.CompletionEntry{
Raw: msg.Author.Mention(), Raw: msg.Author.Mention(),
Text: rich, Text: message.RenderAuthorName(user),
Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator), Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator),
IconURL: msg.Author.AvatarURL(), IconURL: msg.Author.AvatarURL(),
}) })
@ -101,7 +96,7 @@ func AllUsers(s *state.Instance, word string) []cchat.CompletionEntry {
raw := r.User.Mention() raw := r.User.Mention()
var status = gateway.UnknownStatus 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 status = p.Status
} }
@ -123,7 +118,7 @@ func AllUsers(s *state.Instance, word string) []cchat.CompletionEntry {
} }
// Search for presences. // Search for presences.
s.PresenceState.Each(0, func(p *gateway.Presence) bool { s.PresenceStore.Each(0, func(p *gateway.Presence) bool {
// Avoid duplicates. // Avoid duplicates.
if _, ok := friends[p.User.ID]; ok { if _, ok := friends[p.User.ID]; ok {
return false return false
@ -237,45 +232,35 @@ func (ch ChannelCompleter) CompleteMentions(word string) []cchat.CompletionEntry
return entries return entries
} }
// If we're in a guild, then we should search for (all) members. // Prioritize searching the guild's presences because we don't need to copy
m, merr := ch.State.Cabinet.Members(ch.GuildID) // slices.
g, gerr := ch.State.Cabinet.Guild(ch.GuildID) ch.State.MemberStore.Each(ch.GuildID, func(m *discord.Member) (stop bool) {
rank := memberMatchString(word, m)
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)
if rank == -1 { if rank == -1 {
continue return false
} }
ensureEntriesMade(&entries) ensureEntriesMade(&entries)
ensureDistancesMade(&distances) 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{ entries = append(entries, cchat.CompletionEntry{
Raw: raw, Raw: raw,
Text: message.RenderMemberName(mem, *g, ch.State), Text: message.RenderAuthorName(user),
Secondary: text.Plain(mem.User.Username + "#" + mem.User.Discriminator), Secondary: text.Plain(m.User.Username + "#" + m.User.Discriminator),
IconURL: urlutils.AvatarURL(mem.User.AvatarURL()), IconURL: user.Avatar(),
}) })
distances[raw] = rank distances[raw] = rank
if len(entries) >= MaxCompletion { return len(entries) >= MaxCompletion
break })
}
}
sortDistances(entries, distances) sortDistances(entries, distances)
return entries return entries

View File

@ -1,7 +1,6 @@
package typer package typer
import ( import (
"errors"
"time" "time"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
@ -9,6 +8,8 @@ import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/message" "github.com/diamondburned/cchat-discord/internal/discord/message"
"github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/pkg/errors"
) )
type Typer struct { type Typer struct {
@ -18,48 +19,47 @@ type Typer struct {
var _ cchat.Typer = (*Typer)(nil) var _ cchat.Typer = (*Typer)(nil)
func NewFromAuthor(author message.Author, ev *gateway.TypingStartEvent) Typer { // New creates a new Typer that satisfies cchat.Typer.
return Typer{
Author: author,
time: ev.Timestamp,
}
}
func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) { func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) {
var user *mention.User
if ev.GuildID.IsValid() { if ev.GuildID.IsValid() {
g, err := s.Cabinet.Guild(ev.GuildID)
if err != nil {
return nil, err
}
if ev.Member == nil { 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 { 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{ user = mention.NewUser(ev.Member.User)
Author: message.NewGuildMember(*ev.Member, *g, s), user.WithMember(*ev.Member)
time: ev.Timestamp, } else {
}, nil 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) for _, recipient := range c.DMRecipients {
if err != nil { if recipient.ID != ev.UserID {
return nil, err continue
} }
for _, user := range c.DMRecipients { user = mention.NewUser(recipient)
if user.ID == ev.UserID { break
return &Typer{
Author: message.NewUser(user, s),
time: ev.Timestamp,
}, nil
} }
} }
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 { func (t Typer) Time() time.Time {

View File

@ -3,101 +3,56 @@ package message
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/colored"
"github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat-discord/internal/segments/reference" "github.com/diamondburned/cchat-discord/internal/segments/reference"
"github.com/diamondburned/cchat-discord/internal/segments/segutil" "github.com/diamondburned/cchat-discord/internal/segments/segutil"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
type Author struct { type Author struct {
id discord.UserID name text.Rich
name text.Rich user *mention.User // same pointer as in name
avatar string
} }
var _ cchat.Author = (*Author)(nil) var _ cchat.Author = (*Author)(nil)
func NewUser(u discord.User, s *state.Instance) Author { // NewAuthor creates a new message author.
var rich text.Rich func NewAuthor(user *mention.User) Author {
richUser(&rich, u, s)
return Author{ return Author{
id: u.ID, name: RenderAuthorName(user),
name: rich, user: user,
avatar: urlutils.AvatarURL(u.AvatarURL()),
} }
} }
func NewGuildMember(m discord.Member, g discord.Guild, s *state.Instance) Author { // RenderAuthorName renders the given user mention into a text segment.
return Author{ func RenderAuthorName(user *mention.User) text.Rich {
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 {
var rich text.Rich var rich text.Rich
richMember(&rich, m, g, s) richUser(&rich, user)
return rich return rich
} }
// richMember appends the member name directly into rich. // richMember appends the member name directly into rich.
func richMember(rich *text.Rich, func richUser(rich *text.Rich, user *mention.User) (start, end int) {
m discord.Member, g discord.Guild, s *state.Instance) (start, end int) { start, end = segutil.Write(rich, user.DisplayName())
var displayName = m.User.Username
if m.Nick != "" {
displayName = m.Nick
}
start, end = segutil.Write(rich, displayName)
// Append the bot prefix if the user is a bot. // Append the bot prefix if the user is a bot.
if m.User.Bot { if user.User().Bot {
rich.Content += " " rich.Content += " "
rich.Segments = append(rich.Segments, rich.Segments = append(rich.Segments,
colored.NewBlurple(segutil.Write(rich, "[BOT]")), 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)) rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user))
return return
} }
func (a Author) ID() cchat.ID { func (a Author) ID() cchat.ID {
return a.id.String() return a.user.UserID().String()
} }
func (a Author) Name() text.Rich { func (a Author) Name() text.Rich {
@ -105,60 +60,54 @@ func (a Author) Name() text.Rich {
} }
func (a Author) Avatar() string { func (a Author) Avatar() string {
return a.avatar return a.user.Avatar()
} }
const authorReplyingTo = " replying to " const authorReplyingTo = " replying to "
// AddUserReply modifies Author to make it appear like it's a message reply. // 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) { func (a *Author) AddUserReply(user discord.User, s *state.Instance) {
a.name.Content += authorReplyingTo 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) { // AddChannelReply adds a reply pointing to a channel. If the given channel is a
a.name.Content += authorReplyingTo + name // 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 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, 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. // AddMessageReference adds a message reference to the author.
func (a *Author) AddMessageReference(ref discord.Message, s *state.Instance) { 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 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, a.name.Segments = append(a.name.Segments,
reference.NewMessageSegment(start, end, ref.ID), reference.NewMessageSegment(start, end, ref.ID),

View File

@ -82,37 +82,6 @@ var (
_ cchat.Noncer = (*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 // 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 // API calls. Member is optional. This is the only call that populates the Nonce
// in the header. // in the header.
@ -121,51 +90,98 @@ func NewGuildMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Mes
message := c.Message message := c.Message
message.Nonce = s.Nonces.Load(c.Nonce) message.Nonce = s.Nonces.Load(c.Nonce)
// This should not error. user := mention.NewUser(c.Author)
g, err := s.Cabinet.Guild(c.GuildID) user.WithState(s.State)
if err != nil { user.WithGuildID(c.GuildID)
return NewMessage(message, s, NewUser(c.Author, s))
if c.Member != nil {
user.WithMember(*c.Member)
} }
if c.Member == nil { user.Prefetch()
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)) return NewMessage(message, s, NewAuthor(user))
} }
// NewBacklogMessage uses the session to create a message fetched from the // 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 // backlog. It takes in an existing guild and tries to fetch a new member, if
// it's nil. // 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 // If the message doesn't have a guild, then we don't need all the
// complicated member fetching process. // complicated member fetching process.
if !m.GuildID.IsValid() { 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) user := mention.NewUser(m.Author)
if err != nil { user.WithGuildID(m.GuildID)
s.MemberState.RequestMember(m.GuildID, m.Author.ID) user.WithState(s.State)
return NewMessage(m, s, NewUser(m.Author, s)) 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 { 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 { // NewAuthorUpdate creates a new message that contains a new author.
// Ensure the validity of ReferencedMessage. func NewAuthorUpdate(msg discord.Message, m discord.Member, s *state.Instance) Message {
m.ReferencedMessage = ReferencedMessage(m, s, true) 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 var content text.Rich
@ -188,6 +204,7 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
case discord.ChannelIconChangeMessage: case discord.ChannelIconChangeMessage:
content.Content = "Changed the channel icon." content.Content = "Changed the channel icon."
case discord.ChannelNameChangeMessage: case discord.ChannelNameChangeMessage:
writeSegmented(&content, "Changed the channel name to ", m.Content, ".", writeSegmented(&content, "Changed the channel name to ", m.Content, ".",
func(i, j int) text.Segment { func(i, j int) text.Segment {
@ -205,14 +222,9 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
break break
} }
writeSegmented(&content, "Added ", m.Mentions[0].Username, " to the group.", writeSegmented(&content,
func(i, j int) text.Segment { "Added ", m.Mentions[0].Username, " to the group.",
user := mention.NewUser(m.Mentions[0].User) segmentFuncFromMention(*m, s),
user.SetMember(m.GuildID, m.Mentions[0].Member)
segment := mention.NewSegment(i, j, user)
segment.WithState(s.State)
return segment
},
) )
case discord.RecipientRemoveMessage: case discord.RecipientRemoveMessage:
@ -221,14 +233,9 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
break break
} }
writeSegmented(&content, "Removed ", m.Mentions[0].Username, " from the group.", writeSegmented(&content,
func(i, j int) text.Segment { "Removed ", m.Mentions[0].Username, " from the group.",
user := mention.NewUser(m.Mentions[0].User) segmentFuncFromMention(*m, s),
user.SetMember(m.GuildID, m.Mentions[0].Member)
segment := mention.NewSegment(i, j, user)
segment.WithState(s.State)
return segment
},
) )
case discord.NitroBoostMessage: 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." content.Content = "The server is now Nitro Boosted to Tier 3."
case discord.ChannelFollowAddMessage: 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" content.Content = "Type = discord.ChannelFollowAddMessage"
case discord.GuildDiscoveryDisqualifiedMessage: 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" content.Content = "Type = discord.GuildDiscoveryDisqualifiedMessage"
case discord.GuildDiscoveryRequalifiedMessage: 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" content.Content = "Type = discord.GuildDiscoveryRequalifiedMessage"
case discord.ApplicationCommandMessage: case discord.ApplicationCommandMessage:
@ -259,7 +266,7 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
case discord.DefaultMessage: case discord.DefaultMessage:
fallthrough fallthrough
default: default:
return newMessage(m, s, author) return newRegularContent(*m, s)
} }
segutil.Add(&content, inline.NewSegment( segutil.Add(&content, inline.NewSegment(
@ -268,52 +275,36 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
)) ))
return Message{ return Message{
messageHeader: newHeaderNonce(m, m.Nonce), messageHeader: newHeaderNonce(*m, m.Nonce),
author: author,
content: content, 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 var content text.Rich
if m.ReferencedMessage != nil { 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 = segments.Ellipsize(content, 100)
content.Content += "\n" content.Content += "\n"
segutil.Add(&content, segutil.Add(&content,
reference.NewMessageSegment(0, len(content.Content)-1, m.ReferencedMessage.ID), reference.NewMessageSegment(0, len(content.Content)-1, m.ReferencedMessage.ID),
) )
author.AddMessageReference(*m.ReferencedMessage, s)
} }
segments.ParseMessageRich(&content, &m, s.Cabinet) segments.ParseMessageRich(&content, &m, s.Cabinet)
return Message{ return Message{
messageHeader: newHeaderNonce(m, m.Nonce), messageHeader: newHeaderNonce(m, m.Nonce),
author: author,
content: content, 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 { func (m Message) Author() cchat.Author {
if !m.author.id.IsValid() { if m.author.user == nil {
return nil return nil
} }
return m.author return m.author
@ -361,3 +352,42 @@ func ReferencedMessage(m discord.Message, s *state.Instance, wait bool) (reply *
return 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))
}
}

View File

@ -8,11 +8,11 @@ import (
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" "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/message"
"github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
"github.com/diamondburned/cchat-discord/internal/funcutil" "github.com/diamondburned/cchat-discord/internal/funcutil"
"github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
) )
@ -167,16 +167,14 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer
isReply = true 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 { if isReply {
c, err := msgs.state.Channel(msg.ChannelID) c, err := msgs.state.Channel(msg.ChannelID)
if err == nil { if err == nil {
switch c.Type { author.AddChannelReply(*c, msgs.state)
case discord.DirectMessage:
author.AddUserReply(c.DMRecipients[0], msgs.state)
case discord.GroupDM:
author.AddReply(shared.ChannelName(*c))
}
} }
} }
@ -188,7 +186,7 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer
return return
} }
ct.UpdateMessage(message.NewMessageUpdateContent(update.Message, msgs.state)) ct.UpdateMessage(message.NewContentUpdate(update.Message, msgs.state))
}), }),
msgs.state.AddHandler(func(del *gateway.MessageDeleteEvent) { msgs.state.AddHandler(func(del *gateway.MessageDeleteEvent) {
if del.GuildID.IsValid() || msgs.acList.isActive(del.ChannelID) { if del.GuildID.IsValid() || msgs.acList.isActive(del.ChannelID) {

View File

@ -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.Start, seg.End = r.WriteString("@" + n.GuildUser.Username)
seg.User = NewUser(n.GuildUser.User) seg.User = NewUser(n.GuildUser.User)
seg.User.store = r.Store 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: case n.GuildRole != nil:
seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name) seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name)

View File

@ -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) { func (m NameSegment) Bounds() (start, end int) {
return m.start, m.end return m.start, m.end
} }
@ -98,21 +92,24 @@ func (um *User) UserID() discord.UserID {
} }
// SetGuildID sets the user's guild ID. // 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.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. // be nil.
func (um *User) SetMember(gID discord.GuildID, m *discord.Member) { func (um *User) WithMember(m discord.Member) {
um.guildID = gID um.member = &m
um.member = m
um.HasColor()
} }
// SetPresence sets the internal presence to reduce roundtrips or cache hits. // WithPresence sets the internal presence to reduce roundtrips or cache hits.
func (um *User) SetPresence(p gateway.Presence) { func (um *User) WithPresence(p gateway.Presence) {
um.presence = &p um.presence = &p
} }
@ -120,7 +117,12 @@ func (um *User) SetPresence(p gateway.Presence) {
func (um *User) WithState(state *ningen.State) { func (um *User) WithState(state *ningen.State) {
um.ningen = state um.ningen = state
um.store = state.Cabinet 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. // DisplayName returns either the nickname or the username.