Added embed support; minor changes to codeblock formatting

This commit is contained in:
diamondburned (Forefront) 2020-07-05 17:18:40 -07:00 committed by diamondburned
parent fbf95b9b6c
commit 983e18a9d5
10 changed files with 423 additions and 64 deletions

View File

@ -428,11 +428,10 @@ func (ch *Channel) TypingTimeout() time.Duration {
func (ch *Channel) TypingSubscribe(ti cchat.TypingIndicator) (func(), error) {
return ch.session.AddHandler(func(t *gateway.TypingStartEvent) {
if t.ChannelID != ch.id {
return
}
if t, err := NewTyper(ch.session.Store, t); err == nil {
ti.AddTyper(t)
if t.ChannelID == ch.id {
if typer, err := NewTyper(ch.session.Store, t); err == nil {
ti.AddTyper(typer)
}
}
}), nil
}

View File

@ -6,6 +6,7 @@ import (
"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"
)
@ -179,7 +180,7 @@ func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry)
entries = append(entries, cchat.CompletionEntry{
Raw: emoji.String(),
Text: text.Rich{Content: ":" + emoji.Name + ":"},
IconURL: URLSized(emoji.EmojiURL(), 32), // small
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
Image: true,
})
if len(entries) >= MaxCompletion {

3
go.mod
View File

@ -4,8 +4,9 @@ go 1.14
require (
github.com/diamondburned/arikawa v0.9.5
github.com/diamondburned/cchat v0.0.40
github.com/diamondburned/cchat v0.0.41
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
github.com/pkg/errors v0.9.1
github.com/yuin/goldmark v1.1.30

4
go.sum
View File

@ -37,8 +37,12 @@ github.com/diamondburned/cchat v0.0.39 h1:Hxd7swmAIECm0MBd5wb1IFvreChwDFwnAshqgA
github.com/diamondburned/cchat v0.0.39/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I=
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/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=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=

View File

@ -1,14 +1,13 @@
package discord
import (
"net/url"
"strconv"
"time"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/segments"
"github.com/diamondburned/cchat-discord/urlutils"
"github.com/diamondburned/cchat/text"
)
@ -55,21 +54,7 @@ func (m messageHeader) Time() time.Time {
// AvatarURL wraps the URL with URL queries for the avatar.
func AvatarURL(URL string) string {
return URLSized(URL, 64)
}
// URLSized wraps the URL with the size query.
func URLSized(URL string, size int) string {
u, err := url.Parse(URL)
if err != nil {
return URL
}
q := u.Query()
q.Set("size", strconv.Itoa(size))
u.RawQuery = q.Encode()
return u.String()
return urlutils.Sized(URL, 64)
}
type Author struct {
@ -202,6 +187,9 @@ func NewMessage(m discord.Message, s *Session, author Author) Message {
}
func (m Message) Author() cchat.MessageAuthor {
if !m.author.id.Valid() {
return nil
}
return m.author
}

View File

@ -14,8 +14,9 @@ var _ text.Codeblocker = (*CodeblockSegment)(nil)
func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkStatus {
if enter {
// Open the block by adding formatting and all.
r.startBlock()
defer r.endBlock()
r.buf.WriteString("---\n")
// Create a segment.
seg := CodeblockSegment{
@ -34,6 +35,10 @@ func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkSta
// Close the segment.
seg.end = r.i()
r.append(seg)
// Close the block.
r.buf.WriteString("\n---")
r.endBlock()
}
return ast.WalkContinue

235
segments/embed.go Normal file
View File

@ -0,0 +1,235 @@
package segments
import (
"fmt"
"time"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/cchat-discord/urlutils"
"github.com/diamondburned/ningen/md"
"github.com/dustin/go-humanize"
)
var imageExts = []string{".jpg", ".jpeg", ".png", ".webp", ".gif"}
func (r *TextRenderer) renderEmbeds(embeds []discord.Embed, m *discord.Message, s state.Store) {
for _, embed := range embeds {
r.startBlock()
r.buf.WriteString("---\n")
r.renderEmbed(embed, m, s)
r.buf.WriteString("---") // render prepends newline already
r.endBlock()
}
}
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.buf.WriteByte(' ')
}
start, end := r.writeString(a.Name)
r.buf.WriteByte('\n')
if a.URL != "" {
r.append(LinkSegment{
start,
end,
a.URL,
})
}
}
if embed.Title != "" {
start, end := r.writeString(embed.Title)
r.buf.WriteByte('\n')
if embed.URL != "" {
r.append(LinkSegment{
start,
end,
embed.URL,
})
}
}
if embed.Description != "" {
// Since Discord embeds' descriptions are technically Markdown, we can
// borrow our Markdown parser for this.
node := md.ParseWithMessage([]byte(embed.Description), s, m, false)
// Create a new renderer with inherited state and buffer but a new byte
// source.
desc := r.clone([]byte(embed.Description))
// Walk using the newly created state.
desc.walk(node)
// Join the created state.
r.join(desc)
// Write a new line.
r.buf.WriteByte('\n')
}
if len(embed.Fields) > 0 {
// Pad another new line.
r.buf.WriteByte('\n')
// Write fields indented once.
for _, field := range embed.Fields {
fmt.Fprintf(r.buf, "\t%s: %s\n", field.Name, field.Value)
}
}
if f := embed.Footer; f != nil && f.Text != "" {
if f.ProxyIcon != "" {
r.append(EmbedFooter(r.i(), *f))
r.buf.WriteByte(' ')
}
r.buf.WriteString(f.Text)
r.buf.WriteByte('\n')
}
if embed.Timestamp.Valid() {
if embed.Footer != nil {
r.buf.WriteString(" - ")
}
r.buf.WriteString(embed.Timestamp.Format(time.RFC1123))
r.buf.WriteByte('\n')
}
}
func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
// Don't do anything if there are no attachments.
if len(attachments) == 0 {
return
}
// Start a new block before rendering attachments.
r.startBlock()
// Render all attachments. Newline delimited.
for i, attachment := range attachments {
r.renderAttachment(attachment)
if i != len(attachments) {
r.buf.WriteByte('\n')
}
}
}
func (r *TextRenderer) renderAttachment(a discord.Attachment) {
if urlutils.ExtIs(a.Proxy, imageExts) {
r.append(EmbedAttachment(r.i(), a))
return
}
start, end := r.writeStringf(
"File: %s (%s)",
a.Filename, humanize.Bytes(a.Size),
)
r.append(LinkSegment{
start,
end,
a.URL,
})
}
type AvatarSegment struct {
start int
url string
text string
}
func EmbedAuthor(start int, a discord.EmbedAuthor) AvatarSegment {
return AvatarSegment{
start: start,
url: a.ProxyIcon,
text: "Avatar",
}
}
// EmbedFooter uses an avatar segment to comply with Discord.
func EmbedFooter(start int, a discord.EmbedFooter) AvatarSegment {
return AvatarSegment{
start: start,
url: a.ProxyIcon,
text: "Icon",
}
}
func (a AvatarSegment) Bounds() (int, int) {
return a.start, a.start
}
// Avatar returns the avatar URL.
func (a AvatarSegment) Avatar() (url string) {
return a.url
}
// AvatarSize returns the size of a small emoji.
func (a AvatarSegment) AvatarSize() int {
return InlineEmojiSize
}
func (a AvatarSegment) AvatarText() string {
return a.text
}
type ImageSegment struct {
start int
url string
w, h int
text string
}
func EmbedImage(start int, i discord.EmbedImage, text string) ImageSegment {
return ImageSegment{
start: start,
url: i.Proxy,
w: int(i.Width),
h: int(i.Height),
text: text,
}
}
func EmbedThumbnail(start int, t discord.EmbedThumbnail, text string) ImageSegment {
return ImageSegment{
start: start,
url: t.Proxy,
w: int(t.Width),
h: int(t.Height),
text: text,
}
}
func EmbedAttachment(start int, a discord.Attachment) ImageSegment {
return ImageSegment{
start: start,
url: a.Proxy,
w: int(a.Width),
h: int(a.Height),
text: fmt.Sprintf("%s (%s)", a.Filename, humanize.Bytes(a.Size)),
}
}
func (i ImageSegment) Bounds() (start, end int) {
return i.start, i.start
}
// Image returns the URL.
func (i ImageSegment) Image() string {
return i.url
}
func (i ImageSegment) ImageSize() (w, h int) {
return i.w, i.h
}
func (i ImageSegment) ImageText() string {
return i.text
}

View File

@ -14,18 +14,15 @@ var _ text.Linker = (*LinkSegment)(nil)
func (r *TextRenderer) link(n *ast.Link, enter bool) ast.WalkStatus {
if enter {
// Make a segment with a start pointing to the end of buffer.
seg := LinkSegment{
start: r.i(),
url: string(n.Destination),
}
// Write the actual title.
r.buf.Write(n.Title)
start, end := r.write(n.Title)
// Close the segment.
seg.end = r.i()
r.append(seg)
r.append(LinkSegment{
start,
end,
string(n.Destination),
})
}
return ast.WalkContinue
@ -33,15 +30,13 @@ func (r *TextRenderer) link(n *ast.Link, enter bool) ast.WalkStatus {
func (r *TextRenderer) autoLink(n *ast.AutoLink, enter bool) ast.WalkStatus {
if enter {
seg := LinkSegment{
start: r.i(),
url: string(n.URL(r.src)),
}
start, end := r.write(n.URL(r.src))
r.buf.Write(n.URL(r.src))
seg.end = r.i()
r.append(seg)
r.append(LinkSegment{
start,
end,
string(n.URL((r.src))),
})
}
return ast.WalkContinue

View File

@ -2,7 +2,7 @@ package segments
import (
"bytes"
"strings"
"fmt"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/state"
@ -11,18 +11,22 @@ import (
"github.com/yuin/goldmark/ast"
)
type TextRenderer struct {
buf *bytes.Buffer
src []byte
segs []text.Segment
inls inlineState
}
func ParseMessage(m *discord.Message, s state.Store) text.Rich {
return ParseWithMessage([]byte(m.Content), s, m, true)
var content = []byte(m.Content)
var node = md.ParseWithMessage(content, s, m, true)
r := NewTextReader(content, node)
r.walk(node)
r.renderEmbeds(m.Embeds, m, s)
r.renderAttachments(m.Attachments)
return text.Rich{
Content: r.String(),
Segments: r.segs,
}
}
func ParseWithMessage(b []byte, s state.Store, m *discord.Message, msg bool) text.Rich {
func ParseWithMessage(b []byte, m *discord.Message, s state.Store, msg bool) text.Rich {
node := md.ParseWithMessage(b, s, m, msg)
return RenderNode(b, node)
}
@ -33,28 +37,89 @@ func Parse(b []byte) text.Rich {
}
func RenderNode(source []byte, n ast.Node) text.Rich {
buf := &bytes.Buffer{}
buf.Grow(len(source))
r := TextRenderer{
src: source,
buf: buf,
segs: make([]text.Segment, 0, n.ChildCount()),
}
ast.Walk(n, r.renderNode)
r := NewTextReader(source, n)
r.walk(n)
return text.Rich{
Content: buf.String(),
Content: r.String(),
Segments: r.segs,
}
}
type TextRenderer struct {
buf *bytes.Buffer
src []byte
segs []text.Segment
inls inlineState
}
func NewTextReader(src []byte, node ast.Node) TextRenderer {
buf := &bytes.Buffer{}
buf.Grow(len(src))
return TextRenderer{
src: src,
buf: buf,
segs: make([]text.Segment, 0, node.ChildCount()),
}
}
// String returns a stringified version of Bytes().
func (r *TextRenderer) String() string {
return string(r.Bytes())
}
// Bytes returns the plain content of the buffer with right spaces trimmed as
// best as it could, that is, the function will not trim right spaces that
// segments use.
func (r *TextRenderer) Bytes() []byte {
// Get the rightmost index out of all the segments.
var rightmost int
for _, seg := range r.segs {
if _, end := seg.Bounds(); end > rightmost {
rightmost = end
}
}
// Get the original byte slice.
org := r.buf.Bytes()
// Trim the right spaces.
trbuf := bytes.TrimRight(org, "\n")
// If we trimmed way too far, then slice so that we get as far as the
// rightmost segment.
if len(trbuf) < rightmost {
return org[:rightmost]
}
// Else, we're safe returning the trimmed slice.
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
}
func (r *TextRenderer) write(b []byte) (start, end int) {
start = r.i()
r.buf.Write(b)
end = r.i()
return
}
// startBlock guarantees enough indentation to start a new block.
func (r *TextRenderer) startBlock() {
var maxNewlines = 0
@ -71,7 +136,10 @@ func (r *TextRenderer) startBlock() {
}
// Write the padding.
r.buf.WriteString(strings.Repeat("\n", maxNewlines))
r.buf.Grow(maxNewlines)
for i := 0; i < maxNewlines; i++ {
r.buf.WriteByte('\n')
}
}
func (r *TextRenderer) endBlock() {
@ -79,6 +147,8 @@ func (r *TextRenderer) endBlock() {
r.startBlock()
}
// peekLast returns the previous byte that matches the offset, or 0 if the
// offset goes past the first byte.
func (r *TextRenderer) peekLast(offset int) byte {
if i := r.buf.Len() - offset - 1; i > 0 {
return r.buf.Bytes()[i]
@ -90,6 +160,23 @@ func (r *TextRenderer) append(segs ...text.Segment) {
r.segs = append(r.segs, segs...)
}
// clone returns a shallow copy of TextRenderer with the new source.
func (r *TextRenderer) clone(src []byte) *TextRenderer {
cpy := *r
cpy.src = src
return &cpy
}
// join combines the states from renderer with r. Use this with clone.
func (r *TextRenderer) join(renderer *TextRenderer) {
r.segs = renderer.segs
r.inls = renderer.inls
}
func (r *TextRenderer) walk(n ast.Node) {
ast.Walk(n, r.renderNode)
}
func (r *TextRenderer) renderNode(n ast.Node, enter bool) (ast.WalkStatus, error) {
switch n := n.(type) {
case *ast.Document:

44
urlutils/urlutils.go Normal file
View File

@ -0,0 +1,44 @@
package urlutils
import (
"net/url"
"path"
"strconv"
"strings"
)
// Sized wraps the URL with the size query.
func Sized(URL string, size int) string {
u, err := url.Parse(URL)
if err != nil {
return URL
}
q := u.Query()
q.Set("size", strconv.Itoa(size))
u.RawQuery = q.Encode()
return u.String()
}
// Ext returns the lowercased file extension of the URL.
func Ext(URL string) string {
u, err := url.Parse(URL)
if err != nil {
return ""
}
return strings.ToLower(path.Ext(u.Path))
}
func ExtIs(URL string, exts []string) bool {
var ext = Ext(URL)
for _, e := range exts {
if e == ext {
return true
}
}
return false
}