1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-discord.git synced 2024-12-22 20:36:45 +00:00

cchat to v0.0.42; added embed and attachment support; colored mentions

This commit is contained in:
diamondburned 2020-07-08 01:35:30 -07:00
parent bea81ed346
commit d0e43cc63b
12 changed files with 279 additions and 52 deletions

2
go.mod
View file

@ -4,7 +4,7 @@ go 1.14
require (
github.com/diamondburned/arikawa v0.9.5
github.com/diamondburned/cchat v0.0.41
github.com/diamondburned/cchat v0.0.42
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249
github.com/dustin/go-humanize v1.0.0
github.com/go-test/deep v1.0.6

2
go.sum
View file

@ -39,6 +39,8 @@ github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0
github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.41 h1:6y32s2wWTiDw4hWN/Gna6ay3uUrRAW5V8Cj0/xLKovw=
github.com/diamondburned/cchat v0.0.41/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.42 h1:FVMLy9hOTxKju8OWDBIStrekbgTHCaH8+GVnV4LOByg=
github.com/diamondburned/cchat v0.0.42/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=

View file

@ -54,7 +54,7 @@ func (m messageHeader) Time() time.Time {
// AvatarURL wraps the URL with URL queries for the avatar.
func AvatarURL(URL string) string {
return urlutils.Sized(URL, 64)
return urlutils.AvatarURL(URL)
}
type Author struct {

View file

@ -17,7 +17,7 @@ func (r *TextRenderer) blockquote(n *ast.Blockquote, enter bool) ast.WalkStatus
defer r.endBlock()
// Create a segment.
var seg = BlockquoteSegment{start: r.i()}
var seg = BlockquoteSegment{start: r.buf.Len()}
// A blockquote contains a paragraph each line. Because Discord.
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
@ -34,7 +34,7 @@ func (r *TextRenderer) blockquote(n *ast.Blockquote, enter bool) ast.WalkStatus
}
// Write the end of the segment.
seg.end = r.i()
seg.end = r.buf.Len()
r.append(seg)
}

View file

@ -20,7 +20,7 @@ func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkSta
// Create a segment.
seg := CodeblockSegment{
start: r.i(),
start: r.buf.Len(),
language: string(n.Language(r.src)),
}
@ -33,7 +33,7 @@ func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkSta
}
// Close the segment.
seg.end = r.i()
seg.end = r.buf.Len()
r.append(seg)
// Close the block.

View file

@ -3,8 +3,9 @@ package segments
import "github.com/diamondburned/cchat/text"
type Colored struct {
strlen int
color uint32
start int
end int
color uint32
}
var (
@ -13,11 +14,15 @@ var (
)
func NewColored(strlen int, color uint32) Colored {
return Colored{strlen, color}
return Colored{0, strlen, color}
}
func NewColoredSegment(start, end int, color uint32) Colored {
return Colored{start, end, color}
}
func (color Colored) Bounds() (start, end int) {
return 0, color.strlen
return color.start, color.end
}
func (color Colored) Color() uint32 {

View file

@ -28,7 +28,7 @@ func (r *TextRenderer) renderEmbeds(embeds []discord.Embed, m *discord.Message,
func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s state.Store) {
if a := embed.Author; a != nil && a.Name != "" {
if a.ProxyIcon != "" {
r.append(EmbedAuthor(r.i(), *a))
r.append(EmbedAuthor(r.buf.Len(), *a))
r.buf.WriteByte(' ')
}
@ -57,6 +57,12 @@ func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s st
}
}
// If we have a thumbnail, then write one.
if embed.Thumbnail != nil {
r.append(EmbedThumbnail(r.buf.Len(), *embed.Thumbnail))
r.buf.WriteByte('\n')
}
if embed.Description != "" {
// Since Discord embeds' descriptions are technically Markdown, we can
// borrow our Markdown parser for this.
@ -84,7 +90,7 @@ func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s st
if f := embed.Footer; f != nil && f.Text != "" {
if f.ProxyIcon != "" {
r.append(EmbedFooter(r.i(), *f))
r.append(EmbedFooter(r.buf.Len(), *f))
r.buf.WriteByte(' ')
}
@ -100,6 +106,12 @@ func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s st
r.buf.WriteString(embed.Timestamp.Format(time.RFC1123))
r.buf.WriteByte('\n')
}
// Write an image if there's one.
if embed.Image != nil {
r.append(EmbedImage(r.buf.Len(), *embed.Image))
r.buf.WriteByte('\n')
}
}
func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
@ -108,8 +120,8 @@ func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
return
}
// Start a new block before rendering attachments.
r.startBlock()
// Start a (small) new block before rendering attachments.
r.startBlockN(1)
// Render all attachments. Newline delimited.
for i, attachment := range attachments {
@ -123,7 +135,7 @@ func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
func (r *TextRenderer) renderAttachment(a discord.Attachment) {
if urlutils.ExtIs(a.Proxy, imageExts) {
r.append(EmbedAttachment(r.i(), a))
r.append(EmbedAttachment(r.buf.Len(), a))
return
}
@ -187,23 +199,23 @@ type ImageSegment struct {
text string
}
func EmbedImage(start int, i discord.EmbedImage, text string) ImageSegment {
func EmbedImage(start int, i discord.EmbedImage) ImageSegment {
return ImageSegment{
start: start,
url: i.Proxy,
w: int(i.Width),
h: int(i.Height),
text: text,
text: fmt.Sprintf("Image (%s)", urlutils.Name(i.URL)),
}
}
func EmbedThumbnail(start int, t discord.EmbedThumbnail, text string) ImageSegment {
func EmbedThumbnail(start int, t discord.EmbedThumbnail) ImageSegment {
return ImageSegment{
start: start,
url: t.Proxy,
w: int(t.Width),
h: int(t.Height),
text: text,
text: fmt.Sprintf("Thumbnail (%s)", urlutils.Name(t.URL)),
}
}

View file

@ -23,7 +23,7 @@ var _ text.Imager = (*EmojiSegment)(nil)
func (r *TextRenderer) emoji(n *md.Emoji, enter bool) ast.WalkStatus {
if enter {
r.append(EmojiSegment{
start: r.i(),
start: r.buf.Len(),
name: n.Name,
large: n.Large,
emojiURL: n.EmojiURL() + "&size=64",

View file

@ -71,7 +71,7 @@ func (r *TextRenderer) inline(n *md.Inline, enter bool) ast.WalkStatus {
// Pop the last segment if it's not empty.
if !r.inls.empty() {
r.inls.end = r.i()
r.inls.end = r.buf.Len()
// Only use this section if the length is not zero.
if r.inls.start != r.inls.end {
@ -86,7 +86,7 @@ func (r *TextRenderer) inline(n *md.Inline, enter bool) ast.WalkStatus {
}
// Update the start pointer of the current segment.
r.inls.start = r.i()
r.inls.start = r.buf.Len()
return ast.WalkContinue
}

View file

@ -16,9 +16,13 @@ func ParseMessage(m *discord.Message, s state.Store) text.Rich {
var node = md.ParseWithMessage(content, s, m, true)
r := NewTextReader(content, node)
// Register the needed states for some renderers.
r.WithState(m, s)
// Render the main message body.
r.walk(node)
r.renderEmbeds(m.Embeds, m, s)
// Render the extra bits.
r.renderAttachments(m.Attachments)
r.renderEmbeds(m.Embeds, m, s)
return text.Rich{
Content: r.String(),
@ -51,6 +55,10 @@ type TextRenderer struct {
src []byte
segs []text.Segment
inls inlineState
// these fields can be nil
msg *discord.Message
store state.Store
}
func NewTextReader(src []byte, node ast.Node) TextRenderer {
@ -64,6 +72,11 @@ func NewTextReader(src []byte, node ast.Node) TextRenderer {
}
}
func (r *TextRenderer) WithState(m *discord.Message, s state.Store) {
r.msg = m
r.store = s
}
// String returns a stringified version of Bytes().
func (r *TextRenderer) String() string {
return string(r.Bytes())
@ -97,41 +110,34 @@ func (r *TextRenderer) Bytes() []byte {
return trbuf
}
// i returns the current cursor position.
func (r *TextRenderer) i() int {
return r.buf.Len()
}
func (r *TextRenderer) writeStringf(f string, v ...interface{}) (start, end int) {
return r.writeString(fmt.Sprintf(f, v...))
}
func (r *TextRenderer) writeString(s string) (start, end int) {
start = r.i()
r.buf.WriteString(s)
end = r.i()
return
return writestringbuf(r.buf, s)
}
func (r *TextRenderer) write(b []byte) (start, end int) {
start = r.i()
r.buf.Write(b)
end = r.i()
return
return writebuf(r.buf, b)
}
// startBlock guarantees enough indentation to start a new block.
func (r *TextRenderer) startBlock() {
r.startBlockN(2)
}
// startBlockN allows a custom block level.
func (r *TextRenderer) startBlockN(n int) {
var maxNewlines = 0
// Peek twice. If the last character is already a new line or we're only at
// the start of line (length 0), then don't pad.
if r.buf.Len() > 0 {
if r.peekLast(0) != '\n' {
maxNewlines++
}
if r.peekLast(1) != '\n' {
maxNewlines++
for i := 0; i < n; i++ {
if r.peekLast(i) != '\n' {
maxNewlines++
}
}
}
@ -218,3 +224,23 @@ func (r *TextRenderer) renderNode(n ast.Node, enter bool) (ast.WalkStatus, error
return ast.WalkContinue, nil
}
// helper global functions
func writebuf(w *bytes.Buffer, b []byte) (start, end int) {
start = w.Len()
w.Write(b)
end = w.Len()
return start, end
}
func writestringbuf(w *bytes.Buffer, b string) (start, end int) {
start = w.Len()
w.WriteString(b)
end = w.Len()
return start, end
}
func segmentadd(r *text.Rich, seg ...text.Segment) {
r.Segments = append(r.Segments, seg...)
}

View file

@ -1,37 +1,86 @@
package segments
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat-discord/urlutils"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen/md"
"github.com/yuin/goldmark/ast"
)
const (
mentionChannel uint8 = iota
mentionUser
mentionRole
)
const blurple = 0x7289DA
type roleInfo struct {
name string
color uint32
position int // used for sorting
}
func (r *TextRenderer) userRoles(user *discord.GuildUser) []roleInfo {
if user.Member == nil || r.msg == nil || !r.msg.GuildID.Valid() {
return nil
}
var roles = make([]roleInfo, 0, len(user.Member.RoleIDs))
for _, roleID := range user.Member.RoleIDs {
r, err := r.store.Role(r.msg.GuildID, roleID)
if err != nil {
continue
}
roles = append(roles, roleInfo{
name: r.Name,
color: r.Color.Uint32(), // default 0
position: r.Position,
})
}
// Sort the roles so the first roles stay in front. We need to do this to
// both render properly and to get the right role color.
sort.Slice(roles, func(i, j int) bool {
return roles[i].position < roles[j].position
})
return roles
}
type MentionSegment struct {
start, end int
*md.Mention
// only non-nil if GuildUser is not nil and is in a guild.
roles []roleInfo
}
var _ text.Segment = (*MentionSegment)(nil)
var (
_ text.Segment = (*MentionSegment)(nil)
_ text.Colorer = (*MentionSegment)(nil)
_ text.Mentioner = (*MentionSegment)(nil)
)
func (r *TextRenderer) mention(n *md.Mention, enter bool) ast.WalkStatus {
if enter {
seg := MentionSegment{start: r.i()}
var seg = MentionSegment{Mention: n}
switch {
case n.Channel != nil:
r.buf.WriteString("#" + n.Channel.Name)
seg.start, seg.end = r.writeString("#" + n.Channel.Name)
case n.GuildUser != nil:
r.buf.WriteString("@" + n.GuildUser.Username)
seg.start, seg.end = r.writeString("@" + n.GuildUser.Username)
seg.roles = r.userRoles(n.GuildUser) // get roles as well
case n.GuildRole != nil:
r.buf.WriteString("@" + n.GuildRole.Name)
seg.start, seg.end = r.writeString("@" + n.GuildRole.Name)
default:
// Unexpected error; skip.
return ast.WalkSkipChildren
}
seg.end = r.i()
r.append(seg)
}
@ -42,7 +91,127 @@ 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() uint32 {
// Try digging through what we have for a color.
switch {
case len(m.roles) > 0:
for _, role := range m.roles {
if role.color > 0 {
return role.color
}
}
case m.GuildRole != nil && m.GuildRole.Color > 0:
return m.GuildRole.Color.Uint32()
}
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{}
}
func (m MentionSegment) channelInfo() text.Rich {
content := strings.Builder{}
content.WriteByte('#')
content.WriteString(m.Channel.Name)
if m.Channel.NSFW {
content.WriteString(" (NSFW)")
}
if m.Channel.Topic != "" {
content.WriteByte('\n')
content.WriteString(m.Channel.Topic)
}
return text.Rich{
Content: content.String(),
}
}
func (m MentionSegment) userInfo() text.Rich {
var content bytes.Buffer
var segment text.Rich
// Make a large avatar if there's one.
if m.GuildUser != nil {
segment.Segments = append(segment.Segments, AvatarSegment{
start: 0,
url: urlutils.AvatarURL(m.GuildUser.AvatarURL()),
text: "Avatar",
})
// Space out.
content.WriteByte(' ')
}
// Write the nickname if there's one; else, write the username only.
if m.GuildUser.Member != nil && m.GuildUser.Member.Nick != "" {
content.WriteString(m.GuildUser.Member.Nick)
content.WriteByte(' ')
start, end := writestringbuf(&content, fmt.Sprintf(
"(%s#%s)",
m.GuildUser.Username,
m.GuildUser.Discriminator,
))
segmentadd(&segment, InlineSegment{
start: start,
end: end,
attributes: text.AttrDimmed,
})
} else {
content.WriteString(m.GuildUser.Username)
content.WriteByte('#')
content.WriteString(m.GuildUser.Discriminator)
}
// Write roles, if any.
if len(m.roles) > 0 {
// 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---\nRoles")
for _, role := range m.roles {
// 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, role.name)
segmentadd(&segment, NewColoredSegment(start, end, role.color))
}
}
// Assign the written content into the text segment and return it.
segment.Content = content.String()
return segment
}
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

@ -7,6 +7,11 @@ import (
"strings"
)
// AvatarURL wraps the URL with URL queries for the avatar.
func AvatarURL(URL string) string {
return Sized(URL, 64)
}
// Sized wraps the URL with the size query.
func Sized(URL string, size int) string {
u, err := url.Parse(URL)
@ -31,6 +36,14 @@ func Ext(URL string) string {
return strings.ToLower(path.Ext(u.Path))
}
func Name(URL string) string {
u, err := url.Parse(URL)
if err != nil {
return URL
}
return path.Base(u.Path)
}
func ExtIs(URL string, exts []string) bool {
var ext = Ext(URL)