owncast/models/chatMessage.go
gingervitis 8a74af202d
Update chat message visibility for moderation (#524)
* update message viz in db

* create admin endpoint to update message visibility

* convert UpdateMessageVisibility api to take in an array of IDs to change visibility on instead

* Support requesting filtered or unfiltered chat messages

* Handle UPDATE chat events on front and backend for toggling messages

* Return entire message with UPDATE events

* Remove the UPDATE message type

* Revert "Remove the UPDATE message type"

This reverts commit 3a83df3d492f7ecf2bab65e845aa2b0365d3a7f6.

* update -> visibility update

* completely remove messages when they turn hidden on VISIBILITY-UPDATEs, and insert them if they turn visible

* Explicitly set visibility

* Fix multi-id sql updates

* increate scroll buffer a bit so chat scrolls when new large messages come in

* Add automated test around chat moderation

* Add new chat admin APIs to api spec

* Commit updated API documentation

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: Owncast <owncast@owncast.online>
2020-12-29 13:35:33 -08:00

115 lines
2.8 KiB
Go

package models
import (
"bytes"
"strings"
"time"
"github.com/microcosm-cc/bluemonday"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html"
"mvdan.cc/xurls"
)
// ChatEvent represents a single chat message.
type ChatEvent struct {
ClientID string `json:"-"`
Author string `json:"author,omitempty"`
Body string `json:"body,omitempty"`
ID string `json:"id"`
MessageType string `json:"type"`
Visible bool `json:"visible"`
Timestamp time.Time `json:"timestamp,omitempty"`
}
// Valid checks to ensure the message is valid.
func (m ChatEvent) Valid() bool {
return m.Author != "" && m.Body != "" && m.ID != ""
}
// RenderAndSanitizeMessageBody will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
// the message into something safe and renderable for clients.
func (m *ChatEvent) RenderAndSanitizeMessageBody() {
raw := m.Body
// Set the new, sanitized and rendered message body
m.Body = RenderAndSanitize(raw)
}
// RenderAndSanitize will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
// the message into something safe and renderable for clients.
func RenderAndSanitize(raw string) string {
rendered := renderMarkdown(raw)
safe := sanitize(rendered)
// Set the new, sanitized and rendered message body
return strings.TrimSpace(safe)
}
func renderMarkdown(raw string) string {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
extension.NewLinkify(
extension.WithLinkifyAllowedProtocols([][]byte{
[]byte("http:"),
[]byte("https:"),
}),
extension.WithLinkifyURLRegexp(
xurls.Strict,
),
),
),
)
trimmed := strings.TrimSpace(raw)
var buf bytes.Buffer
if err := markdown.Convert([]byte(trimmed), &buf); err != nil {
panic(err)
}
return buf.String()
}
func sanitize(raw string) string {
p := bluemonday.StrictPolicy()
// Require URLs to be parseable by net/url.Parse
p.AllowStandardURLs()
// Allow links
p.AllowAttrs("href").OnElements("a")
// Force all URLs to have "noreferrer" in their rel attribute.
p.RequireNoReferrerOnLinks(true)
// Links will get target="_blank" added to them.
p.AddTargetBlankToFullyQualifiedLinks(true)
// Allow paragraphs
p.AllowElements("br")
p.AllowElements("p")
// Allow img tags
p.AllowElements("img")
p.AllowAttrs("src").OnElements("img")
p.AllowAttrs("alt").OnElements("img")
p.AllowAttrs("title").OnElements("img")
// Custom emoji have a class already specified.
// We should only allow classes on emoji, not *all* imgs.
// But TODO.
p.AllowAttrs("class").OnElements("img")
// Allow bold
p.AllowElements("strong")
// Allow emphasis
p.AllowElements("em")
return p.Sanitize(raw)
}