cchat-discord/segments/mention.go

404 lines
9.7 KiB
Go

package segments
import (
"bytes"
"fmt"
"strings"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/cchat-discord/urlutils"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen"
"github.com/diamondburned/ningen/md"
"github.com/yuin/goldmark/ast"
)
// NameSegment represents a clickable member name; it does not implement colors.
type NameSegment struct {
start, end int
guild discord.Guild
member discord.Member
state *ningen.State // optional
}
var (
_ text.Segment = (*NameSegment)(nil)
_ text.Mentioner = (*NameSegment)(nil)
_ text.MentionerAvatar = (*NameSegment)(nil)
)
func UserSegment(start, end int, u discord.User) NameSegment {
return NameSegment{
start: start,
end: end,
member: discord.Member{User: u},
}
}
func MemberSegment(start, end int, guild discord.Guild, m discord.Member) NameSegment {
return NameSegment{
start: start,
end: end,
guild: guild,
member: m,
}
}
// 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.state = state
}
func (m NameSegment) Bounds() (start, end int) {
return m.start, m.end
}
func (m NameSegment) MentionInfo() text.Rich {
return userInfo(m.guild, m.member, m.state)
}
// Avatar returns the large avatar URL.
func (m NameSegment) Avatar() string {
return m.member.User.AvatarURL()
}
type MentionSegment struct {
start, end int
*md.Mention
store state.Store
guild discord.GuildID
}
var (
_ text.Segment = (*MentionSegment)(nil)
_ text.Colorer = (*MentionSegment)(nil)
_ text.Mentioner = (*MentionSegment)(nil)
_ text.MentionerAvatar = (*MentionSegment)(nil)
)
func (r *TextRenderer) mention(n *md.Mention, enter bool) ast.WalkStatus {
if enter {
var seg = MentionSegment{
Mention: n,
store: r.store,
guild: r.msg.GuildID,
}
switch {
case n.Channel != nil:
seg.start, seg.end = r.writeString("#" + n.Channel.Name)
case n.GuildUser != nil:
seg.start, seg.end = r.writeString("@" + n.GuildUser.Username)
case n.GuildRole != nil:
seg.start, seg.end = r.writeString("@" + n.GuildRole.Name)
default:
// Unexpected error; skip.
return ast.WalkSkipChildren
}
r.append(seg)
}
return ast.WalkContinue
}
func (m MentionSegment) Bounds() (start, end int) {
return m.start, m.end
}
// Color tries to return the color of the mention segment, or it returns the
// usual blurple if none.
func (m MentionSegment) Color() (color uint32) {
// Try digging through what we have for a color.
switch {
case m.GuildUser != nil && m.GuildUser.Member != nil:
g, err := m.store.Guild(m.guild)
if err != nil {
return blurple
}
color = discord.MemberColor(*g, *m.GuildUser.Member).Uint32()
case m.GuildRole != nil && m.GuildRole.Color > 0:
color = m.GuildRole.Color.Uint32()
}
if color > 0 {
return
}
return blurple
}
// TODO
func (m MentionSegment) MentionInfo() text.Rich {
switch {
case m.Channel != nil:
return m.channelInfo()
case m.GuildUser != nil:
return m.userInfo()
case m.GuildRole != nil:
return m.roleInfo()
}
// Unknown; return an empty text.
return text.Rich{}
}
// Avatar returns the user avatar if any, else it returns an empty URL.
func (m MentionSegment) Avatar() string {
if m.GuildUser != nil {
return m.GuildUser.AvatarURL()
}
return ""
}
func (m MentionSegment) channelInfo() text.Rich {
var topic = m.Channel.Topic
if m.Channel.NSFW {
topic = "(NSFW)\n" + topic
}
if topic == "" {
return text.Rich{}
}
return Parse([]byte(topic))
}
func (m MentionSegment) userInfo() text.Rich {
if m.GuildUser.Member == nil {
m.GuildUser.Member = &discord.Member{
User: m.GuildUser.User,
}
}
// Get the guild for the role slice. If not, then too bad.
g, err := m.store.Guild(m.guild)
if err != nil {
g = &discord.Guild{}
}
return userInfo(*g, *m.GuildUser.Member, nil)
}
func (m MentionSegment) roleInfo() text.Rich {
// // We don't have much to write here.
// var segment = text.Rich{
// Content: m.GuildRole.Name,
// }
// // Maybe add a color if we have any.
// if c := m.GuildRole.Color.Uint32(); c > 0 {
// segment.Segments = []text.Segment{
// NewColored(len(m.GuildRole.Name), m.GuildRole.Color.Uint32()),
// }
// }
return text.Rich{}
}
type LargeActivityImage struct {
start int
url string
text string
}
func NewLargeActivityImage(start int, ac discord.Activity) LargeActivityImage {
var text = ac.Assets.LargeText
if text == "" {
text = "Activity Image"
}
return LargeActivityImage{
start: start,
url: urlutils.AssetURL(ac.ApplicationID, ac.Assets.LargeImage),
text: ac.Assets.LargeText,
}
}
func (i LargeActivityImage) Bounds() (start, end int) { return i.start, i.start }
func (i LargeActivityImage) Image() string { return i.url }
func (i LargeActivityImage) ImageSize() (w, h int) { return 60, 60 }
func (i LargeActivityImage) ImageText() string { return i.text }
func userInfo(guild discord.Guild, member discord.Member, state *ningen.State) text.Rich {
var content bytes.Buffer
var segment text.Rich
// Write the username if the user has a nickname.
if member.Nick != "" {
content.WriteString("Username: ")
content.WriteString(member.User.Username)
content.WriteByte('#')
content.WriteString(member.User.Discriminator)
content.WriteString("\n\n")
}
// Write extra information if any, but only if we have the guild state.
if len(member.RoleIDs) > 0 && 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")
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 := writestringbuf(&content, "@"+rl.Name)
// But we only add the color if the role has one.
if color := rl.Color.Uint32(); color > 0 {
segmentadd(&segment, NewColoredSegment(start, end, rl.Color.Uint32()))
}
}
// End section.
content.WriteString("\n\n")
}
// These information can only be obtained from the state. As such, we check
// if the state is given.
if state != nil {
// Does the user have rich presence? If so, write.
if p, err := state.Presence(guild.ID, member.User.ID); err == nil {
for _, ac := range p.Activities {
formatActivity(&segment, &content, ac)
content.WriteString("\n\n")
}
} else if guild.ID.IsValid() {
// If we're still in a guild, then we can ask Discord for that
// member with their presence attached.
state.MemberState.RequestMember(guild.ID, member.User.ID)
}
// Write the user's note if any.
if note := state.NoteState.Note(member.User.ID); note != "" {
formatSectionf(&segment, &content, "Note")
content.WriteRune('\n')
start, end := writestringbuf(&content, note)
segmentadd(&segment, InlineSegment{start, end, text.AttrMonospace})
content.WriteString("\n\n")
}
}
// Assign the written content into the text segment and return it after
// trimming the trailing new line.
segment.Content = strings.TrimSuffix(content.String(), "\n")
return segment
}
func formatSectionf(segment *text.Rich, content *bytes.Buffer, f string, argv ...interface{}) {
// Treat f as a regular string at first.
var str = fmt.Sprintf("%s", f)
// If there are argvs, then treat f as a format string.
if len(argv) > 0 {
str = fmt.Sprintf(str, argv...)
}
start, end := writestringbuf(content, str)
segmentadd(segment, InlineSegment{start, end, text.AttrBold | text.AttrUnderline})
}
func FormatActivity(ac discord.Activity) text.Rich {
var rich text.Rich
var cbuf bytes.Buffer
formatActivity(&rich, &cbuf, ac)
rich.Content = cbuf.String()
return rich
}
func formatActivity(segment *text.Rich, content *bytes.Buffer, ac discord.Activity) {
switch ac.Type {
case discord.GameActivity:
formatSectionf(segment, content, "Playing %s", ac.Name)
content.WriteByte('\n')
case discord.ListeningActivity:
formatSectionf(segment, content, "Listening to %s", ac.Name)
content.WriteByte('\n')
case discord.StreamingActivity:
formatSectionf(segment, content, "Streaming on %s", ac.Name)
content.WriteByte('\n')
case discord.CustomActivity:
formatSectionf(segment, content, "Status")
content.WriteByte('\n')
if ac.Emoji != nil {
if !ac.Emoji.ID.IsValid() {
content.WriteString(ac.Emoji.Name)
} else {
segmentadd(segment, EmojiSegment{
start: content.Len(),
name: ac.Emoji.Name,
emojiURL: ac.Emoji.EmojiURL() + "&size=64",
large: ac.State == "",
})
}
content.WriteByte(' ')
}
default:
formatSectionf(segment, content, "Status")
content.WriteByte('\n')
}
// Insert an image if there's any.
if ac.Assets != nil && ac.Assets.LargeImage != "" {
segmentadd(segment, NewLargeActivityImage(content.Len(), ac))
content.WriteString(" ")
}
if ac.Details != "" {
start, end := writestringbuf(content, ac.Details)
segmentadd(segment, InlineSegment{start, end, text.AttrBold})
content.WriteByte('\n')
}
if ac.State != "" {
content.WriteString(ac.State)
}
}
func getPresence(
state *ningen.State,
guildID discord.GuildID, userID discord.UserID) *discord.Activity {
p, err := state.Presence(guildID, userID)
if err != nil {
return nil
}
if len(p.Activities) > 0 {
return &p.Activities[0]
}
return p.Game
}
func findRole(roles []discord.Role, id discord.RoleID) (discord.Role, bool) {
for _, role := range roles {
if role.ID == id {
return role, true
}
}
return discord.Role{}, false
}