added message type support, better mention segs

This commit is contained in:
diamondburned 2021-01-05 18:40:01 -08:00
parent 14970d0e05
commit f5ac9a2422
13 changed files with 489 additions and 226 deletions

2
go.mod
View File

@ -3,7 +3,7 @@ module github.com/diamondburned/cchat-discord
go 1.14 go 1.14
require ( 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/cchat v0.3.17
github.com/diamondburned/ningen/v2 v2.0.0-20210101084041-d9a5058b63b5 github.com/diamondburned/ningen/v2 v2.0.0-20210101084041-d9a5058b63b5
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0

2
go.sum
View File

@ -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-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 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-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 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9IKBYU=
github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk= github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk=

View File

@ -9,83 +9,55 @@ 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/shared" "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/emoji"
"github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
type Member struct { type Member struct {
channel shared.Channel channel shared.Channel
userID discord.UserID mention mention.User
origName string // use if cache is stale presence gateway.Presence
} }
// New creates a new list member. it.Member must not be nil. // New creates a new list member. it.Member must not be nil.
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.WithState(ch.State.State)
user.SetMember(ch.GuildID, &opItem.Member.Member)
user.SetPresence(opItem.Member.Presence)
return &Member{ return &Member{
channel: ch, channel: ch,
userID: opItem.Member.User.ID, presence: opItem.Member.Presence,
origName: opItem.Member.User.Username, mention: *user,
} }
} }
func (l *Member) ID() cchat.ID { func (l *Member) ID() cchat.ID {
return l.userID.String() return l.mention.UserID().String()
} }
func (l *Member) Name() text.Rich { func (l *Member) Name() text.Rich {
g, err := l.channel.State.Cabinet.Guild(l.channel.GuildID) content := l.mention.DisplayName()
if err != nil {
return text.Plain(l.origName) 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) AsIconer() cchat.Iconer { return l }
func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) { func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) {
m, err := l.channel.State.Member(l.channel.GuildID, l.userID) c.SetIcon(l.mention.Avatar())
if err != nil {
return nil, err
}
c.SetIcon(urlutils.AvatarURL(m.User.AvatarURL()))
return func() {}, nil return func() {}, nil
} }
func (l *Member) Status() cchat.Status { func (l *Member) Status() cchat.Status {
p, err := l.channel.State.Cabinet.Presence(l.channel.GuildID, l.userID) switch l.presence.Status {
if err != nil {
return cchat.StatusUnknown
}
switch p.Status {
case gateway.OnlineStatus: case gateway.OnlineStatus:
return cchat.StatusOnline return cchat.StatusOnline
case gateway.DoNotDisturbStatus: case gateway.DoNotDisturbStatus:
@ -100,16 +72,11 @@ func (l *Member) Status() cchat.Status {
} }
func (l *Member) Secondary() text.Rich { func (l *Member) Secondary() text.Rich {
p, err := l.channel.State.Cabinet.Presence(l.channel.GuildID, l.userID) if len(l.presence.Activities) == 0 {
if err != nil { return text.Rich{}
return text.Plain("")
} }
if len(p.Activities) > 0 { return formatSmallActivity(l.presence.Activities[0])
return formatSmallActivity(p.Activities[0])
}
return text.Plain("")
} }
func formatSmallActivity(ac discord.Activity) text.Rich { func formatSmallActivity(ac discord.Activity) text.Rich {

View File

@ -46,8 +46,8 @@ func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text
} }
// richMember appends the member name directly into rich. // richMember appends the member name directly into rich.
func richMember( func richMember(rich *text.Rich,
rich *text.Rich, m discord.Member, g discord.Guild, s *state.Instance) (start, end int) { m discord.Member, g discord.Guild, s *state.Instance) (start, end int) {
var displayName = m.User.Username var displayName = m.User.Username
if m.Nick != "" { if m.Nick != "" {
@ -65,15 +65,17 @@ func richMember(
} }
// Append a clickable user popup. // Append a clickable user popup.
useg := mention.MemberSegment(start, end, g, m) user := mention.NewUser(m.User)
useg.WithState(s.State) user.WithState(s.State)
rich.Segments = append(rich.Segments, useg) user.SetMember(g.ID, &m)
rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user))
return return
} }
func richUser( func richUser(rich *text.Rich,
rich *text.Rich, u discord.User, s *state.Instance) (start, end int) { u discord.User, s *state.Instance) (start, end int) {
start, end = segutil.Write(rich, u.Username) start, end = segutil.Write(rich, u.Username)
@ -86,9 +88,10 @@ func richUser(
} }
// Append a clickable user popup. // Append a clickable user popup.
useg := mention.UserSegment(start, end, u) user := mention.NewUser(u)
useg.WithState(s.State) user.WithState(s.State)
rich.Segments = append(rich.Segments, useg)
rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user))
return return
} }

View File

@ -1,6 +1,7 @@
package message package message
import ( import (
"log"
"strings" "strings"
"time" "time"
@ -9,8 +10,10 @@ import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments" "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/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/text" "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 { func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
var content text.Rich // Ensure the validity of ReferencedMessage.
m.ReferencedMessage = ReferencedMessage(m, s, true)
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)
}
// Render the message content. // Render the message content.
segments.ParseMessageRich(&content, &m, s.Cabinet)
// Request members in mentions if we're in a guild. var content text.Rich
if m.GuildID.IsValid() {
for _, segment := range content.Segments {
mention, ok := segment.(*mention.Segment)
if !ok {
continue
}
// If this is not a user mention, then skip. If we already have a switch m.Type {
// member, then skip. We could check this using the timestamp, as we case discord.ChannelPinnedMessage:
// might have a user set into the member field. writeSegmented(&content, "Pinned ", "a message", " to this channel.",
if mention.User == nil || mention.User.Member.Joined.IsValid() { func(i, j int) text.Segment {
continue if m.ReferencedMessage == nil {
} return nil
}
return reference.NewMessageSegment(i, j, m.ReferencedMessage.ID)
},
)
// Request the member. case discord.GuildMemberJoinMessage:
s.MemberState.RequestMember(m.GuildID, mention.User.Member.User.ID) 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{ return Message{
messageHeader: newHeaderNonce(m, m.Nonce), messageHeader: newHeaderNonce(m, m.Nonce),
author: author, 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 { func (m Message) Author() cchat.Author {
if !m.author.id.IsValid() { if !m.author.id.IsValid() {
return nil return nil

View File

@ -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 { type Segment struct {
empty.TextSegment empty.TextSegment
start, end int start, end int

View File

@ -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) { func ParseMessageRich(rich *text.Rich, m *discord.Message, s store.Cabinet) {
var content = []byte(m.Content) var content = []byte(m.Content)
if len(content) == 0 {
return
}
var node = md.ParseWithMessage(content, s, m, true) 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.Grow(len(rich.Content))
r.Buffer.WriteString(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...) rich.Segments = append(rich.Segments, r.Segments...)
} }
func ParseWithMessage(b []byte, m *discord.Message, s store.Cabinet, msg bool) text.Rich { func ParseWithMessage(b []byte, m *discord.Message, s store.Cabinet) text.Rich {
node := md.ParseWithMessage(b, s, m, msg) var rich text.Rich
return renderer.RenderNode(b, node) ParseWithMessageRich(&rich, b, m, s)
return rich
} }
func ParseWithMessageRich(b []byte, m *discord.Message, s store.Cabinet, msg bool) text.Rich { func ParseWithMessageRich(rich *text.Rich, b []byte, m *discord.Message, s store.Cabinet) {
node := md.ParseWithMessage(b, s, m, msg) if len(b) == 0 {
return renderer.RenderNode(b, node) 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
} }

View File

@ -4,12 +4,27 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat-discord/internal/segments/renderer" "github.com/diamondburned/cchat-discord/internal/segments/renderer"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen/v2"
"github.com/diamondburned/ningen/v2/md"
) )
type Channel struct { type Channel struct {
discord.Channel 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 { func NewChannel(ch discord.Channel) *Channel {
return &Channel{ return &Channel{
Channel: ch, Channel: ch,
@ -26,5 +41,13 @@ func (ch *Channel) MentionInfo() text.Rich {
return 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,
}
} }

View File

@ -23,12 +23,17 @@ func mention(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
case n.Channel != nil: case n.Channel != nil:
seg.Start, seg.End = r.WriteString("#" + n.Channel.Name) seg.Start, seg.End = r.WriteString("#" + n.Channel.Name)
seg.Channel = NewChannel(*n.Channel) seg.Channel = NewChannel(*n.Channel)
case n.GuildUser != nil: case n.GuildUser != nil:
seg.Start, seg.End = r.WriteString("@" + n.GuildUser.Username) 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: case n.GuildRole != nil:
seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name) seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name)
seg.Role = NewRole(*n.GuildRole) seg.Role = NewRole(*n.GuildRole)
default: default:
// Unexpected error; skip. // Unexpected error; skip.
return ast.WalkSkipChildren return ast.WalkSkipChildren

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/arikawa/v2/state/store" "github.com/diamondburned/arikawa/v2/state/store"
"github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/colored"
"github.com/diamondburned/cchat-discord/internal/segments/inline" "github.com/diamondburned/cchat-discord/internal/segments/inline"
@ -15,7 +16,7 @@ import (
"github.com/diamondburned/ningen/v2" "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 { type NameSegment struct {
empty.TextSegment empty.TextSegment
start int start int
@ -25,26 +26,11 @@ type NameSegment struct {
var _ text.Segment = (*NameSegment)(nil) var _ text.Segment = (*NameSegment)(nil)
func UserSegment(start, end int, u discord.User) NameSegment { func NewSegment(start, end int, user *User) NameSegment {
return NameSegment{ return NameSegment{
start: start, start: start,
end: end, end: end,
um: User{ 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,
},
} }
} }
@ -70,11 +56,17 @@ func (m NameSegment) AsColorer() text.Colorer {
} }
type User struct { type User struct {
ningen *ningen.State user discord.User
store store.Cabinet guildID discord.GuildID
Guild discord.Guild store store.Cabinet
Member discord.Member ningen *ningen.State
// optional prefetching
guild *discord.Guild
member *discord.Member
presence *gateway.Presence
color uint32 color uint32
hasColor bool hasColor bool
@ -88,34 +80,59 @@ var (
) )
// NewUser creates a new user mention. // NewUser creates a new user mention.
func NewUser(store store.Cabinet, guild discord.GuildID, guser discord.GuildUser) *User { func NewUser(u discord.User) *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{}
}
return &User{ return &User{
store: store, user: u,
Guild: *g, store: store.NoopCabinet,
Member: *guser.Member,
} }
} }
// 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) { func (um *User) WithState(state *ningen.State) {
um.ningen = state um.ningen = state
um.store = state.Cabinet 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. // 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. // 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 um.fetchedColor = true
return false return false
} }
g, err := um.store.Guild(um.Guild.ID) // We do have a valid GuildID, but the store might be a Noop, so we
if err != nil { // shouldn't mark it as fetched.
um.fetchedColor = true guild := um.getGuild()
member := um.getMember()
if guild == nil || member == nil {
return false return false
} }
um.fetchedColor = true um.fetchedColor = true
um.color, um.hasColor = MemberColor(*g, um.Member) um.color, um.hasColor = MemberColor(*guild, *member)
return um.hasColor return um.hasColor
} }
@ -155,80 +175,79 @@ func (um *User) AvatarSize() int {
} }
func (um *User) AvatarText() string { func (um *User) AvatarText() string {
if um.Member.Nick != "" { return um.DisplayName()
return um.Member.Nick
}
return um.Member.User.Username
} }
func (um *User) Avatar() (url string) { 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 { func (um *User) MentionInfo() text.Rich {
var content bytes.Buffer var content bytes.Buffer
var segment text.Rich var segment text.Rich
// Write the username if the user has a nickname. content.WriteString("Username: ")
if um.Member.Nick != "" { content.WriteString(um.user.Username)
content.WriteString("Username: ") content.WriteByte('#')
content.WriteString(um.Member.User.Username) content.WriteString(um.user.Discriminator)
content.WriteByte('#') content.WriteString("\n\n")
content.WriteString(um.Member.User.Discriminator)
content.WriteString("\n\n")
}
// Write extra information if any, but only if we have the guild state. // Write extra information if any, but only if we have the guild state.
if len(um.Member.RoleIDs) > 0 && um.Guild.ID.IsValid() { if um.guildID.IsValid() {
// Write a prepended new line, as role writes will always prepend a new guild := um.getGuild()
// line. This is to prevent a trailing new line. member := um.getMember()
formatSectionf(&segment, &content, "Roles")
for _, id := range um.Member.RoleIDs { if guild != nil && member != nil {
rl, ok := findRole(um.Guild.Roles, id) // Write a prepended new line, as role writes will always prepend a
if !ok { // new line. This is to prevent a trailing new line.
continue 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. // End section.
content.WriteByte('\n') content.WriteString("\n\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. // Does the user have rich presence? If so, write.
content.WriteString("\n\n") 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 // These information can only be obtained from the state. As such, we check
// if the state is given. // if the state is given.
if um.ningen != nil { 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. // Write the user's note if any.
if note := um.ningen.NoteState.Note(um.Member.User.ID); note != "" { formatSectionf(&segment, &content, "Note")
formatSectionf(&segment, &content, "Note") content.WriteRune('\n')
content.WriteRune('\n')
if note := um.ningen.NoteState.Note(um.user.ID); note != "" {
start, end := segutil.WriteStringBuf(&content, note) start, end := segutil.WriteStringBuf(&content, note)
segutil.Add(&segment, inline.NewSegment(start, end, text.AttributeMonospace)) segutil.Add(&segment, inline.NewSegment(start, end, text.AttributeMonospace))
} else {
content.WriteString("\n\n") 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 // 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") segment.Content = strings.TrimSuffix(content.String(), "\n")
return segment 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
}

View File

@ -3,6 +3,7 @@ package reference
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/segments/segutil"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
) )
@ -23,6 +24,13 @@ type MessageSegment struct {
var _ text.Segment = (*MessageSegment)(nil) 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 { func NewMessageSegment(start, end int, msgID discord.MessageID) MessageSegment {
return MessageSegment{ return MessageSegment{
start: start, start: start,

View File

@ -8,7 +8,6 @@ import (
"github.com/diamondburned/arikawa/v2/state/store" "github.com/diamondburned/arikawa/v2/state/store"
"github.com/diamondburned/cchat-discord/internal/segments/segutil" "github.com/diamondburned/cchat-discord/internal/segments/segutil"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen/v2/md"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
) )
@ -23,24 +22,6 @@ func Register(kind ast.NodeKind, r Renderer) {
var smallRenderers = map[ast.NodeKind]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 { type Text struct {
Buffer *bytes.Buffer Buffer *bytes.Buffer
Source []byte Source []byte
@ -53,14 +34,13 @@ type Text struct {
Store store.Cabinet Store store.Cabinet
} }
func New(src []byte, node ast.Node) *Text { func New(src []byte) *Text {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
buf.Grow(len(src)) buf.Grow(len(src))
return &Text{ return &Text{
Source: src, Source: src,
Buffer: buf, Buffer: buf,
Segments: make([]text.Segment, 0, node.ChildCount()),
} }
} }
@ -178,6 +158,10 @@ func (r *Text) Join(renderer *Text) {
// Walk walks on the given node with the RenderNode as the walker function. // Walk walks on the given node with the RenderNode as the walker function.
func (r *Text) Walk(n ast.Node) { func (r *Text) Walk(n ast.Node) {
if r.Segments == nil {
r.Segments = make([]text.Segment, 0, n.ChildCount())
}
ast.Walk(n, r.RenderNode) ast.Walk(n, r.RenderNode)
} }

View File

@ -2,6 +2,7 @@ package segutil
import ( import (
"bytes" "bytes"
"strings"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
@ -29,6 +30,13 @@ func WriteStringBuf(w *bytes.Buffer, b string) (start, end int) {
return start, end 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) { func Add(r *text.Rich, seg ...text.Segment) {
r.Segments = append(r.Segments, seg...) r.Segments = append(r.Segments, seg...)
} }