diff --git a/go.mod b/go.mod index b9f12e5..f12ac79 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/diamondburned/cchat-discord go 1.14 require ( - github.com/diamondburned/arikawa/v2 v2.0.0-20210101083335-169b36126239 + 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/dustin/go-humanize v1.0.0 diff --git a/go.sum b/go.sum index ea6b3c9..c1865f7 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44/go.mod h1 github.com/diamondburned/arikawa/v2 v2.0.0-20210101074829-c6d8c741e883/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= github.com/diamondburned/arikawa/v2 v2.0.0-20210101083335-169b36126239 h1:ogL6/TJJecNYkvREJa+nHZ326b+QjHN/eLXMUtiyz/A= github.com/diamondburned/arikawa/v2 v2.0.0-20210101083335-169b36126239/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= +github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c h1:6n1EqFEPZbtm0pj8vtS7VzZuWvg7v04UL9hAcpK3lNk= +github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= github.com/diamondburned/cchat v0.0.34 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9IKBYU= github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk= diff --git a/internal/discord/channel/message/memberlist/member.go b/internal/discord/channel/message/memberlist/member.go index 6a8cbae..c258214 100644 --- a/internal/discord/channel/message/memberlist/member.go +++ b/internal/discord/channel/message/memberlist/member.go @@ -9,83 +9,55 @@ import ( "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" - "github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/emoji" "github.com/diamondburned/cchat-discord/internal/segments/mention" - "github.com/diamondburned/cchat-discord/internal/urlutils" "github.com/diamondburned/cchat/text" ) type Member struct { channel shared.Channel - userID discord.UserID - origName string // use if cache is stale + mention mention.User + presence gateway.Presence } // New creates a new list member. it.Member must not be nil. 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) + return &Member{ channel: ch, - userID: opItem.Member.User.ID, - origName: opItem.Member.User.Username, + presence: opItem.Member.Presence, + mention: *user, } } func (l *Member) ID() cchat.ID { - return l.userID.String() + return l.mention.UserID().String() } func (l *Member) Name() text.Rich { - g, err := l.channel.State.Cabinet.Guild(l.channel.GuildID) - if err != nil { - return text.Plain(l.origName) + content := l.mention.DisplayName() + + return text.Rich{ + Content: content, + Segments: []text.Segment{ + mention.NewSegment(0, len(content), &l.mention), + }, } - - m, err := l.channel.State.Cabinet.Member(l.channel.GuildID, l.userID) - if err != nil { - return text.Plain(l.origName) - } - - var name = m.User.Username - if m.Nick != "" { - name = m.Nick - } - - mention := mention.MemberSegment(0, len(name), *g, *m) - mention.WithState(l.channel.State.State) - - var txt = text.Rich{ - Content: name, - Segments: []text.Segment{mention}, - } - - if c := discord.MemberColor(*g, *m); c != discord.DefaultMemberColor { - txt.Segments = append(txt.Segments, colored.New(len(name), uint32(c))) - } - - return txt } func (l *Member) AsIconer() cchat.Iconer { return l } func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) { - m, err := l.channel.State.Member(l.channel.GuildID, l.userID) - if err != nil { - return nil, err - } - - c.SetIcon(urlutils.AvatarURL(m.User.AvatarURL())) - + c.SetIcon(l.mention.Avatar()) return func() {}, nil } func (l *Member) Status() cchat.Status { - p, err := l.channel.State.Cabinet.Presence(l.channel.GuildID, l.userID) - if err != nil { - return cchat.StatusUnknown - } - - switch p.Status { + switch l.presence.Status { case gateway.OnlineStatus: return cchat.StatusOnline case gateway.DoNotDisturbStatus: @@ -100,16 +72,11 @@ func (l *Member) Status() cchat.Status { } func (l *Member) Secondary() text.Rich { - p, err := l.channel.State.Cabinet.Presence(l.channel.GuildID, l.userID) - if err != nil { - return text.Plain("") + if len(l.presence.Activities) == 0 { + return text.Rich{} } - if len(p.Activities) > 0 { - return formatSmallActivity(p.Activities[0]) - } - - return text.Plain("") + return formatSmallActivity(l.presence.Activities[0]) } func formatSmallActivity(ac discord.Activity) text.Rich { diff --git a/internal/discord/message/author.go b/internal/discord/message/author.go index 403eda1..833b4f0 100644 --- a/internal/discord/message/author.go +++ b/internal/discord/message/author.go @@ -46,8 +46,8 @@ func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text } // 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) { +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 != "" { @@ -65,15 +65,17 @@ func richMember( } // Append a clickable user popup. - useg := mention.MemberSegment(start, end, g, m) - useg.WithState(s.State) - rich.Segments = append(rich.Segments, useg) + 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) { +func richUser(rich *text.Rich, + u discord.User, s *state.Instance) (start, end int) { start, end = segutil.Write(rich, u.Username) @@ -86,9 +88,10 @@ func richUser( } // Append a clickable user popup. - useg := mention.UserSegment(start, end, u) - useg.WithState(s.State) - rich.Segments = append(rich.Segments, useg) + user := mention.NewUser(u) + user.WithState(s.State) + + rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user)) return } diff --git a/internal/discord/message/message.go b/internal/discord/message/message.go index 353266a..b68ce9a 100644 --- a/internal/discord/message/message.go +++ b/internal/discord/message/message.go @@ -1,6 +1,7 @@ package message import ( + "log" "strings" "time" @@ -9,8 +10,10 @@ import ( "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" ) @@ -159,46 +162,111 @@ func NewDirectMessage(m discord.Message, s *state.Instance) Message { } func NewMessage(m discord.Message, s *state.Instance, author Author) Message { - var content text.Rich - - if ref := ReferencedMessage(m, s, true); ref != nil { - // TODO: markup support - var refmsg = "> " + ref.Content - if len(refmsg) > 120 { - refmsg = refmsg[:120] + "..." - } - - content.Content = strings.ReplaceAll(refmsg, "\n", " ") + "\n" - content.Segments = []text.Segment{ - reference.NewMessageSegment(0, len(content.Content), ref.ID), - } - - author.AddMessageReference(*ref, s) - } + // Ensure the validity of ReferencedMessage. + m.ReferencedMessage = ReferencedMessage(m, s, true) // Render the message content. - segments.ParseMessageRich(&content, &m, s.Cabinet) - // Request members in mentions if we're in a guild. - if m.GuildID.IsValid() { - for _, segment := range content.Segments { - mention, ok := segment.(*mention.Segment) - if !ok { - continue - } + var content text.Rich - // If this is not a user mention, then skip. If we already have a - // member, then skip. We could check this using the timestamp, as we - // might have a user set into the member field. - if mention.User == nil || mention.User.Member.Joined.IsValid() { - continue - } + 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) + }, + ) - // Request the member. - s.MemberState.RequestMember(m.GuildID, mention.User.Member.User.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, @@ -206,6 +274,44 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message { } } +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 diff --git a/internal/segments/inline/inline.go b/internal/segments/inline/inline.go index c676946..42876fb 100644 --- a/internal/segments/inline/inline.go +++ b/internal/segments/inline/inline.go @@ -62,6 +62,18 @@ func DimSuffix(prefix, suffix string) text.Rich { } } +func Write(rich *text.Rich, content string, attr text.Attribute) { + start := len(rich.Content) + rich.Content += content + end := len(rich.Content) + + rich.Segments = append(rich.Segments, Segment{ + start: start, + end: end, + attributes: Attribute(attr), + }) +} + type Segment struct { empty.TextSegment start, end int diff --git a/internal/segments/md.go b/internal/segments/md.go index 9ca242f..2890d3e 100644 --- a/internal/segments/md.go +++ b/internal/segments/md.go @@ -25,9 +25,13 @@ func ParseMessage(m *discord.Message, s store.Cabinet) text.Rich { func ParseMessageRich(rich *text.Rich, m *discord.Message, s store.Cabinet) { var content = []byte(m.Content) + if len(content) == 0 { + return + } + var node = md.ParseWithMessage(content, s, m, true) - r := renderer.New(content, node) + r := renderer.New(content) r.Buffer.Grow(len(rich.Content)) r.Buffer.WriteString(rich.Content) @@ -43,12 +47,80 @@ func ParseMessageRich(rich *text.Rich, m *discord.Message, s store.Cabinet) { rich.Segments = append(rich.Segments, r.Segments...) } -func ParseWithMessage(b []byte, m *discord.Message, s store.Cabinet, msg bool) text.Rich { - node := md.ParseWithMessage(b, s, m, msg) - return renderer.RenderNode(b, node) +func ParseWithMessage(b []byte, m *discord.Message, s store.Cabinet) text.Rich { + var rich text.Rich + ParseWithMessageRich(&rich, b, m, s) + return rich } -func ParseWithMessageRich(b []byte, m *discord.Message, s store.Cabinet, msg bool) text.Rich { - node := md.ParseWithMessage(b, s, m, msg) - return renderer.RenderNode(b, node) +func ParseWithMessageRich(rich *text.Rich, b []byte, m *discord.Message, s store.Cabinet) { + if len(b) == 0 { + return + } + + node := md.ParseWithMessage(b, s, m, true) + + r := renderer.New(b) + r.Buffer.Grow(len(rich.Content)) + r.Buffer.WriteString(rich.Content) + + r.WithState(m, s) + r.Walk(node) + + rich.Content = r.String() + rich.Segments = append(rich.Segments, r.Segments...) +} + +// Ellipsize caps the length of the rendered text segment to be not longer than +// the given length. The ellipsize will be appended if it is. +func Ellipsize(rich text.Rich, maxLen int) text.Rich { + if maxLen > len(rich.Content) { + maxLen = len(rich.Content) - 1 + if maxLen <= 0 { + return text.Rich{} + } + + rich.Content += "…" + } + + return Substring(rich, 0, maxLen) +} + +// Substring slices the given rich text. +func Substring(rich text.Rich, start, end int) text.Rich { + substring := text.Rich{ + Content: rich.Content[start:end], + Segments: make([]text.Segment, 0, len(rich.Segments)), + } + + for _, seg := range rich.Segments { + i, j := seg.Bounds() + + // Bound-check: check if the starting point is within the range. + if start <= i && i <= end { + // If the current segment is cleanly within the bound, then we can + // directly insert it. + if j <= end { + substring.Segments = append(substring.Segments, seg) + continue + } + + substring.Segments = append(substring.Segments, trimmedSegment{ + Segment: seg, + start: i, // preserve the segment's starting point + end: end, + }) + } + } + + return substring +} + +type trimmedSegment struct { + text.Segment + start, end int +} + +func (seg trimmedSegment) Bounds() (int, int) { + return seg.start, seg.end } diff --git a/internal/segments/mention/channel.go b/internal/segments/mention/channel.go index 9273fae..051c48d 100644 --- a/internal/segments/mention/channel.go +++ b/internal/segments/mention/channel.go @@ -4,12 +4,27 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat-discord/internal/segments/renderer" "github.com/diamondburned/cchat/text" + "github.com/diamondburned/ningen/v2" + "github.com/diamondburned/ningen/v2/md" ) type Channel struct { discord.Channel } +func NewChannelFromID(s *ningen.State, chID discord.ChannelID) *Channel { + ch, err := s.Channel(chID) + if err != nil { + return &Channel{ + Channel: discord.Channel{ID: chID, Name: "unknown channel"}, + } + } + + return &Channel{ + Channel: *ch, + } +} + func NewChannel(ch discord.Channel) *Channel { return &Channel{ Channel: ch, @@ -26,5 +41,13 @@ func (ch *Channel) MentionInfo() text.Rich { return text.Rich{} } - return renderer.Parse([]byte(topic)) + bytes := []byte(topic) + + r := renderer.New(bytes) + r.Walk(md.Parse(bytes)) + + return text.Rich{ + Content: r.String(), + Segments: r.Segments, + } } diff --git a/internal/segments/mention/mention.go b/internal/segments/mention/mention.go index c1ea544..ae01d8e 100644 --- a/internal/segments/mention/mention.go +++ b/internal/segments/mention/mention.go @@ -23,12 +23,17 @@ func mention(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus { case n.Channel != nil: seg.Start, seg.End = r.WriteString("#" + n.Channel.Name) seg.Channel = NewChannel(*n.Channel) + case n.GuildUser != nil: seg.Start, seg.End = r.WriteString("@" + n.GuildUser.Username) - seg.User = NewUser(r.Store, r.Message.GuildID, *n.GuildUser) + seg.User = NewUser(n.GuildUser.User) + seg.User.store = r.Store + seg.User.SetMember(r.Message.GuildID, n.GuildUser.Member) + case n.GuildRole != nil: seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name) seg.Role = NewRole(*n.GuildRole) + default: // Unexpected error; skip. return ast.WalkSkipChildren diff --git a/internal/segments/mention/user.go b/internal/segments/mention/user.go index b1f5b7a..98de133 100644 --- a/internal/segments/mention/user.go +++ b/internal/segments/mention/user.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/diamondburned/arikawa/v2/discord" + "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/state/store" "github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/inline" @@ -15,7 +16,7 @@ import ( "github.com/diamondburned/ningen/v2" ) -// NameSegment represents a clickable member name; it does not implement colors. +// NameSegment represents a clickable member name. type NameSegment struct { empty.TextSegment start int @@ -25,26 +26,11 @@ type NameSegment struct { var _ text.Segment = (*NameSegment)(nil) -func UserSegment(start, end int, u discord.User) NameSegment { +func NewSegment(start, end int, user *User) NameSegment { return NameSegment{ start: start, end: end, - um: User{ - store: store.NoopCabinet, - Member: discord.Member{User: u}, - }, - } -} - -func MemberSegment(start, end int, guild discord.Guild, m discord.Member) NameSegment { - return NameSegment{ - start: start, - end: end, - um: User{ - store: store.NoopCabinet, - Guild: guild, - Member: m, - }, + um: *user, } } @@ -70,11 +56,17 @@ func (m NameSegment) AsColorer() text.Colorer { } type User struct { - ningen *ningen.State - store store.Cabinet + user discord.User + guildID discord.GuildID - Guild discord.Guild - Member discord.Member + store store.Cabinet + ningen *ningen.State + + // optional prefetching + + guild *discord.Guild + member *discord.Member + presence *gateway.Presence color uint32 hasColor bool @@ -88,34 +80,59 @@ var ( ) // NewUser creates a new user mention. -func NewUser(store store.Cabinet, guild discord.GuildID, guser discord.GuildUser) *User { - if guser.Member == nil { - m, err := store.Member(guild, guser.ID) - if err != nil { - guser.Member = &discord.Member{} - } else { - guser.Member = m - } - } - - guser.Member.User = guser.User - - // Get the guild for the role slice. If not, then too bad. - g, err := store.Guild(guild) - if err != nil { - g = &discord.Guild{} - } - +func NewUser(u discord.User) *User { return &User{ - store: store, - Guild: *g, - Member: *guser.Member, + user: u, + store: store.NoopCabinet, } } +// User returns the internal user. +func (um *User) User() discord.User { + return um.user +} + +// UserID returns the user ID. +func (um *User) UserID() discord.UserID { + return um.user.ID +} + +// SetGuildID sets the user's guild ID. +func (um *User) SetGuildID(guildID discord.GuildID) { + um.guildID = guildID + um.HasColor() // prefetch +} + +// SetMember 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() +} + +// SetPresence sets the internal presence to reduce roundtrips or cache hits. +func (um *User) SetPresence(p gateway.Presence) { + um.presence = &p +} + +// WithState sets the internal state for usage. func (um *User) WithState(state *ningen.State) { um.ningen = state um.store = state.Cabinet + um.HasColor() // prefetch +} + +// DisplayName returns either the nickname or the username. +func (um *User) DisplayName() string { + if um.guildID.IsValid() { + m, err := um.store.Member(um.guildID, um.user.ID) + if err == nil && m.Nick != "" { + return m.Nick + } + } + + return um.user.Username } // HasColor returns true if the current user has a color. @@ -125,19 +142,22 @@ func (um *User) HasColor() bool { } // We don't have any member color if we have neither the member nor guild. - if !um.Guild.ID.IsValid() || !um.Member.User.ID.IsValid() { + if !um.guildID.IsValid() || !um.user.ID.IsValid() { um.fetchedColor = true return false } - g, err := um.store.Guild(um.Guild.ID) - if err != nil { - um.fetchedColor = true + // We do have a valid GuildID, but the store might be a Noop, so we + // shouldn't mark it as fetched. + guild := um.getGuild() + member := um.getMember() + + if guild == nil || member == nil { return false } um.fetchedColor = true - um.color, um.hasColor = MemberColor(*g, um.Member) + um.color, um.hasColor = MemberColor(*guild, *member) return um.hasColor } @@ -155,80 +175,79 @@ func (um *User) AvatarSize() int { } func (um *User) AvatarText() string { - if um.Member.Nick != "" { - return um.Member.Nick - } - return um.Member.User.Username + return um.DisplayName() } func (um *User) Avatar() (url string) { - return urlutils.AvatarURL(um.Member.User.AvatarURL()) + return urlutils.AvatarURL(um.user.AvatarURL()) } func (um *User) MentionInfo() text.Rich { var content bytes.Buffer var segment text.Rich - // Write the username if the user has a nickname. - if um.Member.Nick != "" { - content.WriteString("Username: ") - content.WriteString(um.Member.User.Username) - content.WriteByte('#') - content.WriteString(um.Member.User.Discriminator) - content.WriteString("\n\n") - } + content.WriteString("Username: ") + content.WriteString(um.user.Username) + content.WriteByte('#') + content.WriteString(um.user.Discriminator) + content.WriteString("\n\n") // Write extra information if any, but only if we have the guild state. - if len(um.Member.RoleIDs) > 0 && um.Guild.ID.IsValid() { - // Write a prepended new line, as role writes will always prepend a new - // line. This is to prevent a trailing new line. - formatSectionf(&segment, &content, "Roles") + if um.guildID.IsValid() { + guild := um.getGuild() + member := um.getMember() - for _, id := range um.Member.RoleIDs { - rl, ok := findRole(um.Guild.Roles, id) - if !ok { - continue + if guild != nil && member != nil { + // Write a prepended new line, as role writes will always prepend a + // new line. This is to prevent a trailing new line. + formatSectionf(&segment, &content, "Roles") + + for _, id := range member.RoleIDs { + rl, ok := findRole(guild.Roles, id) + if !ok { + continue + } + + // Prepend a new line before each item. + content.WriteByte('\n') + // Write exactly the role name, then grab the segment and color + // it. + start, end := segutil.WriteStringBuf(&content, "@"+rl.Name) + // But we only add the color if the role has one. + if rgb := rl.Color.Uint32(); rgb > 0 { + segutil.Add(&segment, colored.NewSegment(start, end, rgb)) + } } - // Prepend a new line before each item. - content.WriteByte('\n') - // Write exactly the role name, then grab the segment and color it. - start, end := segutil.WriteStringBuf(&content, "@"+rl.Name) - // But we only add the color if the role has one. - if rgb := rl.Color.Uint32(); rgb > 0 { - segutil.Add(&segment, colored.NewSegment(start, end, rgb)) - } + // End section. + content.WriteString("\n\n") } + } - // End section. - content.WriteString("\n\n") + // Does the user have rich presence? If so, write. + if p := um.getPresence(); p != nil { + for _, ac := range p.Activities { + formatActivity(&segment, &content, ac) + content.WriteString("\n\n") + } } // These information can only be obtained from the state. As such, we check // if the state is given. if um.ningen != nil { - // Does the user have rich presence? If so, write. - if p, err := um.store.Presence(um.Guild.ID, um.Member.User.ID); err == nil { - for _, ac := range p.Activities { - formatActivity(&segment, &content, ac) - content.WriteString("\n\n") - } - } else if um.Guild.ID.IsValid() { - // If we're still in a guild, then we can ask Discord for that - // member with their presence attached. - um.ningen.MemberState.RequestMember(um.Guild.ID, um.Member.User.ID) - } - // Write the user's note if any. - if note := um.ningen.NoteState.Note(um.Member.User.ID); note != "" { - formatSectionf(&segment, &content, "Note") - content.WriteRune('\n') + formatSectionf(&segment, &content, "Note") + content.WriteRune('\n') + if note := um.ningen.NoteState.Note(um.user.ID); note != "" { start, end := segutil.WriteStringBuf(&content, note) segutil.Add(&segment, inline.NewSegment(start, end, text.AttributeMonospace)) - - content.WriteString("\n\n") + } else { + start, end := segutil.WriteStringBuf(&content, "empty") + segutil.Add(&segment, inline.NewSegment(start, end, text.AttributeDimmed)) } + + content.WriteString("\n\n") } // Assign the written content into the text segment and return it after @@ -236,3 +255,57 @@ func (um *User) MentionInfo() text.Rich { segment.Content = strings.TrimSuffix(content.String(), "\n") return segment } + +func (um *User) getGuild() *discord.Guild { + if um.guild != nil { + return um.guild + } + + g, err := um.store.Guild(um.guildID) + if err != nil { + return nil + } + + um.guild = g + return g +} + +func (um *User) getMember() *discord.Member { + if !um.guildID.IsValid() { + return nil + } + + if um.member != nil { + return um.member + } + + m, err := um.store.Member(um.guildID, um.user.ID) + if err != nil { + if um.ningen != nil { + um.ningen.MemberState.RequestMember(um.guildID, um.user.ID) + } + + return nil + } + + um.member = m + return m +} + +func (um *User) getPresence() *gateway.Presence { + if um.presence != nil { + return um.presence + } + + p, err := um.store.Presence(um.guildID, um.user.ID) + if err != nil { + if um.guildID.IsValid() && um.ningen != nil { + um.ningen.MemberState.RequestMember(um.guildID, um.user.ID) + } + + return nil + } + + um.presence = p + return p +} diff --git a/internal/segments/reference/reference.go b/internal/segments/reference/reference.go index dd69518..aa180ec 100644 --- a/internal/segments/reference/reference.go +++ b/internal/segments/reference/reference.go @@ -3,6 +3,7 @@ package reference import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/segments/segutil" "github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/utils/empty" ) @@ -23,6 +24,13 @@ type MessageSegment struct { var _ text.Segment = (*MessageSegment)(nil) +// Write appends to the given rich text the reference to the message ID with the +// given text. +func Write(rich *text.Rich, msgID discord.MessageID, text string) { + start, end := segutil.Write(rich, text) + segutil.Add(rich, NewMessageSegment(start, end, msgID)) +} + func NewMessageSegment(start, end int, msgID discord.MessageID) MessageSegment { return MessageSegment{ start: start, diff --git a/internal/segments/renderer/renderer.go b/internal/segments/renderer/renderer.go index 27bd47c..d948b6d 100644 --- a/internal/segments/renderer/renderer.go +++ b/internal/segments/renderer/renderer.go @@ -8,7 +8,6 @@ import ( "github.com/diamondburned/arikawa/v2/state/store" "github.com/diamondburned/cchat-discord/internal/segments/segutil" "github.com/diamondburned/cchat/text" - "github.com/diamondburned/ningen/v2/md" "github.com/yuin/goldmark/ast" ) @@ -23,24 +22,6 @@ func Register(kind ast.NodeKind, r Renderer) { var smallRenderers = map[ast.NodeKind]Renderer{} -// Parse parses the raw Markdown bytes into a rich text. -func Parse(b []byte) text.Rich { - node := md.Parse(b) - return RenderNode(b, node) -} - -// RenderNode renders the given raw Markdown bytes with the parsed AST node into -// a rich text. -func RenderNode(source []byte, n ast.Node) text.Rich { - r := New(source, n) - r.Walk(n) - - return text.Rich{ - Content: r.String(), - Segments: r.Segments, - } -} - type Text struct { Buffer *bytes.Buffer Source []byte @@ -53,14 +34,13 @@ type Text struct { Store store.Cabinet } -func New(src []byte, node ast.Node) *Text { +func New(src []byte) *Text { buf := &bytes.Buffer{} buf.Grow(len(src)) return &Text{ - Source: src, - Buffer: buf, - Segments: make([]text.Segment, 0, node.ChildCount()), + Source: src, + Buffer: buf, } } @@ -178,6 +158,10 @@ func (r *Text) Join(renderer *Text) { // Walk walks on the given node with the RenderNode as the walker function. func (r *Text) Walk(n ast.Node) { + if r.Segments == nil { + r.Segments = make([]text.Segment, 0, n.ChildCount()) + } + ast.Walk(n, r.RenderNode) } diff --git a/internal/segments/segutil/segutil.go b/internal/segments/segutil/segutil.go index 3b359ad..19e1f9c 100644 --- a/internal/segments/segutil/segutil.go +++ b/internal/segments/segutil/segutil.go @@ -2,6 +2,7 @@ package segutil import ( "bytes" + "strings" "github.com/diamondburned/cchat/text" ) @@ -29,6 +30,13 @@ func WriteStringBuf(w *bytes.Buffer, b string) (start, end int) { return start, end } +func WriteStringBuilder(w *strings.Builder, b string) (start, end int) { + start = w.Len() + w.WriteString(b) + end = w.Len() + return start, end +} + func Add(r *text.Rich, seg ...text.Segment) { r.Segments = append(r.Segments, seg...) }