package mention import ( "bytes" "strings" "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/state/store" "github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/inline" "github.com/diamondburned/cchat-discord/internal/segments/segutil" "github.com/diamondburned/cchat-discord/internal/urlutils" "github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/ningen/v2" ) // NameSegment represents a clickable member name; it does not implement colors. type NameSegment struct { empty.TextSegment start int end int um User } var _ text.Segment = (*NameSegment)(nil) func UserSegment(start, end int, u discord.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, }, } } // WithState assigns a ningen state into the given name segment. This allows the // popovers to have additional information such as user notes. func (m *NameSegment) WithState(state *ningen.State) { m.um.WithState(state) } func (m NameSegment) Bounds() (start, end int) { return m.start, m.end } func (m NameSegment) AsMentioner() text.Mentioner { return &m.um } func (m NameSegment) AsAvatarer() text.Avatarer { return &m.um } // AsColorer only returns User if the user actually has a colored role. func (m NameSegment) AsColorer() text.Colorer { if m.um.HasColor() { return &m.um } return nil } type User struct { ningen *ningen.State store store.Cabinet Guild discord.Guild Member discord.Member color uint32 hasColor bool fetchedColor bool } var ( _ text.Colorer = (*User)(nil) _ text.Avatarer = (*User)(nil) _ text.Mentioner = (*User)(nil) ) // 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{} } return &User{ store: store, Guild: *g, Member: *guser.Member, } } func (um *User) WithState(state *ningen.State) { um.ningen = state um.store = state.Cabinet } // HasColor returns true if the current user has a color. func (um *User) HasColor() bool { if um.fetchedColor { return um.hasColor } // 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() { um.fetchedColor = true return false } g, err := um.store.Guild(um.Guild.ID) if err != nil { um.fetchedColor = true return false } um.fetchedColor = true um.color, um.hasColor = MemberColor(*g, um.Member) return um.hasColor } func (um *User) Color() uint32 { if um.HasColor() { return text.SolidColor(um.color) } return colored.Blurple } func (um *User) AvatarSize() int { return 96 } func (um *User) AvatarText() string { if um.Member.Nick != "" { return um.Member.Nick } return um.Member.User.Username } func (um *User) Avatar() (url string) { return urlutils.AvatarURL(um.Member.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") } // 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") for _, id := range um.Member.RoleIDs { rl, ok := findRole(um.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)) } } // 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 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') start, end := segutil.WriteStringBuf(&content, note) segutil.Add(&segment, inline.NewSegment(start, end, text.AttributeMonospace)) 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 }