Added additional bonus stuff and bug fixes

This commit is contained in:
diamondburned 2020-07-29 17:00:03 -07:00
parent af912db554
commit f7c016f5a1
6 changed files with 266 additions and 108 deletions

View File

@ -227,7 +227,7 @@ func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (
continue
}
ct.UpdateMessage(NewMessageUpdateAuthor(msg, member, *g))
ct.UpdateMessage(NewMessageUpdateAuthor(msg, member, *g, ch.session))
}
}
}))
@ -472,7 +472,7 @@ func (ch *Channel) TypingSubscribe(ti cchat.TypingIndicator) (func(), error) {
if t.ChannelID != ch.id || t.UserID == ch.session.userID {
return
}
if typer, err := NewTyper(ch.session.Store, t); err == nil {
if typer, err := NewTyper(ch.session, t); err == nil {
ti.AddTyper(typer)
}
}), nil

View File

@ -4,7 +4,6 @@ import (
"strings"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/urlutils"
"github.com/diamondburned/cchat/text"
@ -12,13 +11,13 @@ import (
const MaxCompletion = 15
func completionUserEntry(s state.Store, u discord.User, g *discord.Guild) cchat.CompletionEntry {
func completionUserEntry(s *Session, u discord.User, g *discord.Guild) cchat.CompletionEntry {
if g != nil {
m, err := s.Member(g.ID, u.ID)
m, err := s.Store.Member(g.ID, u.ID)
if err == nil {
return cchat.CompletionEntry{
Raw: u.Mention(),
Text: RenderMemberName(*m, *g),
Text: RenderMemberName(*m, *g, s),
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
IconURL: u.AvatarURL(),
}
@ -51,7 +50,7 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
// Record the current author and add the entry to the list.
authors[msg.Author.ID] = struct{}{}
entries = append(entries, completionUserEntry(ch.session.Store, msg.Author, g))
entries = append(entries, completionUserEntry(ch.session, msg.Author, g))
if len(entries) >= MaxCompletion {
return
@ -108,7 +107,7 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
if contains(match, mem.User.Username, mem.Nick) {
entries = append(entries, cchat.CompletionEntry{
Raw: mem.User.Mention(),
Text: RenderMemberName(mem, *g),
Text: RenderMemberName(mem, *g, ch.session),
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
IconURL: mem.User.AvatarURL(),
})

View File

@ -63,7 +63,7 @@ type Author struct {
avatar string
}
func NewUser(u discord.User) Author {
func NewUser(u discord.User, s *Session) Author {
var name = text.Rich{Content: u.Username}
if u.Bot {
name.Content += " "
@ -73,9 +73,9 @@ func NewUser(u discord.User) Author {
}
// Append a clickable user popup.
name.Segments = append(name.Segments,
segments.UserSegment(0, len(name.Content), u),
)
useg := segments.UserSegment(0, len(name.Content), u)
useg.WithState(s.State)
name.Segments = append(name.Segments, useg)
return Author{
id: u.ID,
@ -84,15 +84,15 @@ func NewUser(u discord.User) Author {
}
}
func NewGuildMember(m discord.Member, g discord.Guild) Author {
func NewGuildMember(m discord.Member, g discord.Guild, s *Session) Author {
return Author{
id: m.User.ID,
name: RenderMemberName(m, g),
name: RenderMemberName(m, g, s),
avatar: AvatarURL(m.User.AvatarURL()),
}
}
func RenderMemberName(m discord.Member, g discord.Guild) text.Rich {
func RenderMemberName(m discord.Member, g discord.Guild, s *Session) text.Rich {
var name = text.Rich{
Content: m.User.Username,
}
@ -118,9 +118,9 @@ func RenderMemberName(m discord.Member, g discord.Guild) text.Rich {
}
// Append a clickable user popup.
name.Segments = append(name.Segments,
segments.MemberSegment(0, len(name.Content), g, m),
)
useg := segments.MemberSegment(0, len(name.Content), g, m)
useg.WithState(s.State)
name.Segments = append(name.Segments, useg)
return name
}
@ -163,10 +163,12 @@ func NewMessageUpdateContent(msg discord.Message, s *Session) Message {
}
}
func NewMessageUpdateAuthor(msg discord.Message, member discord.Member, g discord.Guild) Message {
func NewMessageUpdateAuthor(
msg discord.Message, member discord.Member, g discord.Guild, s *Session) Message {
return Message{
messageHeader: newHeader(msg),
author: NewGuildMember(member, g),
author: NewGuildMember(member, g, s),
}
}
@ -176,7 +178,7 @@ func NewMessageCreate(c *gateway.MessageCreateEvent, s *Session) Message {
// This should not error.
g, err := s.Store.Guild(c.GuildID)
if err != nil {
return NewMessage(c.Message, s, NewUser(c.Author))
return NewMessage(c.Message, s, NewUser(c.Author, s))
}
if c.Member == nil {
@ -184,10 +186,10 @@ func NewMessageCreate(c *gateway.MessageCreateEvent, s *Session) Message {
}
if c.Member == nil {
s.MemberState.RequestMember(c.GuildID, c.Author.ID)
return NewMessage(c.Message, s, NewUser(c.Author))
return NewMessage(c.Message, s, NewUser(c.Author, s))
}
return NewMessage(c.Message, s, NewGuildMember(*c.Member, *g))
return NewMessage(c.Message, s, NewGuildMember(*c.Member, *g, s))
}
// NewBacklogMessage uses the session to create a message fetched from the
@ -197,27 +199,52 @@ func NewBacklogMessage(m discord.Message, s *Session, g discord.Guild) Message {
// If the message doesn't have a guild, then we don't need all the
// complicated member fetching process.
if !m.GuildID.Valid() {
return NewMessage(m, s, NewUser(m.Author))
return NewMessage(m, s, NewUser(m.Author, s))
}
mem, err := s.Store.Member(m.GuildID, m.Author.ID)
if err != nil {
s.MemberState.RequestMember(m.GuildID, m.Author.ID)
return NewMessage(m, s, NewUser(m.Author))
return NewMessage(m, s, NewUser(m.Author, s))
}
return NewMessage(m, s, NewGuildMember(*mem, g))
return NewMessage(m, s, NewGuildMember(*mem, g, s))
}
func NewDirectMessage(m discord.Message, s *Session) Message {
return NewMessage(m, s, NewUser(m.Author))
return NewMessage(m, s, NewUser(m.Author, s))
}
func NewMessage(m discord.Message, s *Session, author Author) Message {
// Render the message content.
var content = segments.ParseMessage(&m, s.Store)
// Request members in mentions if we're in a guild.
if m.GuildID.Valid() {
for _, segment := range content.Segments {
if mention, ok := segment.(*segments.MentionSegment); ok {
// If this is not a user mention, then skip.
if mention.GuildUser == nil {
continue
}
// 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 m := mention.GuildUser.Member; m != nil && m.Joined.Valid() {
continue
}
// Request the member.
s.MemberState.RequestMember(m.GuildID, mention.GuildUser.ID)
}
}
}
return Message{
messageHeader: newHeader(m),
author: author,
content: segments.ParseMessage(&m, s.Store),
content: content,
}
}

View File

@ -7,7 +7,9 @@ import (
"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"
)
@ -18,11 +20,13 @@ type NameSegment struct {
guild discord.Guild
member discord.Member
state *ningen.State // optional
}
var (
_ text.Segment = (*NameSegment)(nil)
_ text.Mentioner = (*NameSegment)(nil)
_ text.Segment = (*NameSegment)(nil)
_ text.Mentioner = (*NameSegment)(nil)
_ text.MentionerAvatar = (*NameSegment)(nil)
)
func UserSegment(start, end int, u discord.User) NameSegment {
@ -42,12 +46,23 @@ func MemberSegment(start, end int, guild discord.Guild, m discord.Member) NameSe
}
}
// 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)
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 {
@ -59,9 +74,10 @@ type MentionSegment struct {
}
var (
_ text.Segment = (*MentionSegment)(nil)
_ text.Colorer = (*MentionSegment)(nil)
_ text.Mentioner = (*MentionSegment)(nil)
_ 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 {
@ -133,33 +149,29 @@ func (m MentionSegment) MentionInfo() text.Rich {
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 {
content := strings.Builder{}
content.WriteByte('#')
content.WriteString(m.Channel.Name)
var topic = m.Channel.Topic
if m.Channel.NSFW {
content.WriteString(" (NSFW)")
topic = "(NSFW)\n" + topic
}
if m.Channel.Topic != "" {
content.WriteByte('\n')
content.WriteString(m.Channel.Topic)
if topic == "" {
return text.Rich{}
}
return text.Rich{
Content: content.String(),
}
return Parse([]byte(topic))
}
func (m MentionSegment) userInfo() text.Rich {
// // We should have a member if there's nil. Sometimes when the members aren't
// // prefetched, the markdown parser can miss them. We can check this again.
// if m.GuildUser.Member == nil && m.guild.Valid() {
// // Best effort; fine if it's nil.
// m.GuildUser.Member, _ = m.store.Member(m.guild, m.GuildUser.ID)
// }
if m.GuildUser.Member == nil {
m.GuildUser.Member = &discord.Member{
User: m.GuildUser.User,
@ -172,52 +184,67 @@ func (m MentionSegment) userInfo() text.Rich {
g = &discord.Guild{}
}
return userInfo(*g, *m.GuildUser.Member)
return userInfo(*g, *m.GuildUser.Member, nil)
}
func userInfo(guild discord.Guild, member discord.Member) text.Rich {
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
// Make a large avatar if there's one.
if member.User.Avatar != "" {
segmentadd(&segment, AvatarSegment{
start: 0,
url: member.User.AvatarURL(), // full URL
text: "Avatar",
size: 72, // large
})
// Space out.
content.WriteByte(' ')
}
// Write the nickname if there's one; else, write the username only.
// Write the username if the user has a nickname.
if member.Nick != "" {
content.WriteString(member.Nick)
content.WriteByte(' ')
start, end := writestringbuf(&content, fmt.Sprintf(
"(%s#%s)",
member.User.Username,
member.User.Discriminator,
))
segmentadd(&segment, InlineSegment{
start: start,
end: end,
attributes: text.AttrDimmed,
})
} else {
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.Valid() {
// Write a prepended new line, as role writes will always prepend a new
// line. This is to prevent a trailing new line.
content.WriteString("\n\n--- Roles ---")
formatSectionf(&segment, &content, "Roles")
for _, id := range member.RoleIDs {
rl, ok := findRole(guild.Roles, id)
@ -234,13 +261,125 @@ func userInfo(guild discord.Guild, member discord.Member) text.Rich {
segmentadd(&segment, NewColoredSegment(start, end, rl.Color.Uint32()))
}
}
// End section.
content.WriteString("\n\n")
}
// Assign the written content into the text segment and return it.
segment.Content = content.String()
// 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.Valid() {
// 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(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.Valid() {
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, userID discord.Snowflake) *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.Snowflake) (discord.Role, bool) {
for _, role := range roles {
if role.ID == id {
@ -249,19 +388,3 @@ func findRole(roles []discord.Role, id discord.Snowflake) (discord.Role, bool) {
}
return discord.Role{}, false
}
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 segment
}

View File

@ -5,7 +5,6 @@ import (
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/cchat"
"github.com/pkg/errors"
)
@ -24,27 +23,27 @@ func NewTyperAuthor(author Author, ev *gateway.TypingStartEvent) Typer {
}
}
func NewTyper(store state.Store, ev *gateway.TypingStartEvent) (*Typer, error) {
func NewTyper(s *Session, ev *gateway.TypingStartEvent) (*Typer, error) {
if ev.GuildID.Valid() {
g, err := store.Guild(ev.GuildID)
g, err := s.Store.Guild(ev.GuildID)
if err != nil {
return nil, err
}
if ev.Member == nil {
ev.Member, err = store.Member(ev.GuildID, ev.UserID)
ev.Member, err = s.Store.Member(ev.GuildID, ev.UserID)
if err != nil {
return nil, err
}
}
return &Typer{
Author: NewGuildMember(*ev.Member, *g),
Author: NewGuildMember(*ev.Member, *g, s),
time: ev.Timestamp,
}, nil
}
c, err := store.Channel(ev.ChannelID)
c, err := s.Store.Channel(ev.ChannelID)
if err != nil {
return nil, err
}
@ -52,7 +51,7 @@ func NewTyper(store state.Store, ev *gateway.TypingStartEvent) (*Typer, error) {
for _, user := range c.DMRecipients {
if user.ID == ev.UserID {
return &Typer{
Author: NewUser(user),
Author: NewUser(user, s),
time: ev.Timestamp,
}, nil
}

View File

@ -5,6 +5,8 @@ import (
"path"
"strconv"
"strings"
"github.com/diamondburned/arikawa/discord"
)
// AvatarURL wraps the URL with URL queries for the avatar.
@ -55,3 +57,11 @@ func ExtIs(URL string, exts []string) bool {
return false
}
// AssetURL generates the image URL from the given asset image ID.
func AssetURL(appID discord.Snowflake, imageID string) string {
if strings.HasPrefix(imageID, "spotify:") {
return "https://i.scdn.co/image/" + strings.TrimPrefix(imageID, "spotify:")
}
return "https://cdn.discordapp.com/app-assets/" + appID.String() + "/" + imageID + ".png"
}