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>
This commit is contained in:
gingervitis
2020-12-29 13:35:33 -08:00
committed by GitHub
parent 0452c4c5fc
commit 8a74af202d
18 changed files with 375 additions and 64 deletions

View File

@@ -14,7 +14,7 @@ func Setup(listener models.ChatListener) {
clients := make(map[string]*Client)
addCh := make(chan *Client)
delCh := make(chan *Client)
sendAllCh := make(chan models.ChatMessage)
sendAllCh := make(chan models.ChatEvent)
pingCh := make(chan models.PingMessage)
doneCh := make(chan bool)
errCh := make(chan error)
@@ -51,7 +51,7 @@ func Start() error {
}
// SendMessage sends a message to all.
func SendMessage(message models.ChatMessage) {
func SendMessage(message models.ChatEvent) {
if _server == nil {
return
}
@@ -60,12 +60,12 @@ func SendMessage(message models.ChatMessage) {
}
// GetMessages gets all of the messages.
func GetMessages() []models.ChatMessage {
func GetMessages(filtered bool) []models.ChatEvent {
if _server == nil {
return []models.ChatMessage{}
return []models.ChatEvent{}
}
return getChatHistory()
return getChatHistory(filtered)
}
func GetClient(clientID string) *Client {

View File

@@ -30,7 +30,7 @@ type Client struct {
socketID string // How we identify a single websocket client.
ws *websocket.Conn
ch chan models.ChatMessage
ch chan models.ChatEvent
pingch chan models.PingMessage
usernameChangeChannel chan models.NameChangeEvent
@@ -38,10 +38,11 @@ type Client struct {
}
const (
CHAT = "CHAT"
NAMECHANGE = "NAME_CHANGE"
PING = "PING"
PONG = "PONG"
CHAT = "CHAT"
NAMECHANGE = "NAME_CHANGE"
PING = "PING"
PONG = "PONG"
VISIBILITYUPDATE = "VISIBILITY-UPDATE"
)
// NewClient creates a new chat client.
@@ -50,7 +51,7 @@ func NewClient(ws *websocket.Conn) *Client {
log.Panicln("ws cannot be nil")
}
ch := make(chan models.ChatMessage, channelBufSize)
ch := make(chan models.ChatEvent, channelBufSize)
doneCh := make(chan bool)
pingch := make(chan models.PingMessage)
usernameChangeChannel := make(chan models.NameChangeEvent)
@@ -68,7 +69,7 @@ func (c *Client) GetConnection() *websocket.Conn {
return c.ws
}
func (c *Client) Write(msg models.ChatMessage) {
func (c *Client) Write(msg models.ChatEvent) {
select {
case c.ch <- msg:
default:
@@ -176,7 +177,7 @@ func (c *Client) userChangedName(data []byte) {
}
func (c *Client) chatMessageReceived(data []byte) {
var msg models.ChatMessage
var msg models.ChatEvent
err := json.Unmarshal(data, &msg)
if err != nil {
log.Errorln(err)

28
core/chat/messages.go Normal file
View File

@@ -0,0 +1,28 @@
package chat
import (
log "github.com/sirupsen/logrus"
)
func SetMessagesVisibility(messageIDs []string, visibility bool) error {
// Save new message visibility
if err := saveMessageVisibility(messageIDs, visibility); err != nil {
log.Errorln(err)
return err
}
// Send an update event to all clients for each message.
// Note: Our client expects a single message at a time, so we can't just
// send an array of messages in a single update.
for _, id := range messageIDs {
message, err := getMessageById(id)
if err != nil {
log.Errorln(err)
continue
}
message.MessageType = VISIBILITYUPDATE
_server.sendAll(message)
}
return nil
}

View File

@@ -2,6 +2,7 @@ package chat
import (
"database/sql"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
@@ -38,7 +39,7 @@ func createTable() {
}
}
func addMessage(message models.ChatMessage) {
func addMessage(message models.ChatEvent) {
tx, err := _db.Begin()
if err != nil {
log.Fatal(err)
@@ -60,11 +61,16 @@ func addMessage(message models.ChatMessage) {
}
}
func getChatHistory() []models.ChatMessage {
history := make([]models.ChatMessage, 0)
func getChatHistory(filtered bool) []models.ChatEvent {
history := make([]models.ChatEvent, 0)
// Get all messages sent within the past day
rows, err := _db.Query("SELECT * FROM messages WHERE visible = 1 AND messageType != 'SYSTEM' AND datetime(timestamp) >=datetime('now', '-1 Day')")
var query = "SELECT * FROM messages WHERE messageType != 'SYSTEM' AND datetime(timestamp) >=datetime('now', '-1 Day')"
if filtered {
query = query + " AND visible = 1"
}
rows, err := _db.Query(query)
if err != nil {
log.Fatal(err)
}
@@ -85,7 +91,7 @@ func getChatHistory() []models.ChatMessage {
break
}
message := models.ChatMessage{}
message := models.ChatEvent{}
message.ID = id
message.Author = author
message.Body = body
@@ -102,3 +108,64 @@ func getChatHistory() []models.ChatMessage {
return history
}
func saveMessageVisibility(messageIDs []string, visible bool) error {
tx, err := _db.Begin()
if err != nil {
log.Fatal(err)
}
stmt, err := tx.Prepare("UPDATE messages SET visible=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
if err != nil {
log.Fatal(err)
return err
}
defer stmt.Close()
args := make([]interface{}, len(messageIDs)+1)
args[0] = visible
for i, id := range messageIDs {
args[i+1] = id
}
_, err = stmt.Exec(args...)
if err != nil {
log.Fatal(err)
return err
}
if err = tx.Commit(); err != nil {
log.Fatal(err)
return err
}
return nil
}
func getMessageById(messageID string) (models.ChatEvent, error) {
var query = "SELECT * FROM messages WHERE id = ?"
row := _db.QueryRow(query, messageID)
var id string
var author string
var body string
var messageType string
var visible int
var timestamp time.Time
err := row.Scan(&id, &author, &body, &messageType, &visible, &timestamp)
if err != nil {
log.Errorln(err)
return models.ChatEvent{}, err
}
return models.ChatEvent{
ID: id,
Author: author,
Body: body,
MessageType: messageType,
Visible: visible == 1,
Timestamp: timestamp,
}, nil
}

View File

@@ -28,7 +28,7 @@ type server struct {
addCh chan *Client
delCh chan *Client
sendAllCh chan models.ChatMessage
sendAllCh chan models.ChatEvent
pingCh chan models.PingMessage
doneCh chan bool
errCh chan error
@@ -45,7 +45,7 @@ func (s *server) remove(c *Client) {
}
// SendToAll sends a message to all of the connected clients.
func (s *server) SendToAll(msg models.ChatMessage) {
func (s *server) SendToAll(msg models.ChatEvent) {
s.sendAllCh <- msg
}
@@ -54,7 +54,7 @@ func (s *server) err(err error) {
s.errCh <- err
}
func (s *server) sendAll(msg models.ChatMessage) {
func (s *server) sendAll(msg models.ChatEvent) {
for _, c := range s.Clients {
c.Write(msg)
}
@@ -153,7 +153,7 @@ func (s *server) sendWelcomeMessageToClient(c *Client) {
time.Sleep(7 * time.Second)
initialChatMessageText := fmt.Sprintf("Welcome to %s! %s", config.Config.InstanceDetails.Title, config.Config.InstanceDetails.Summary)
initialMessage := models.ChatMessage{ClientID: "owncast-server", Author: config.Config.InstanceDetails.Name, Body: initialChatMessageText, ID: "initial-message-1", MessageType: "SYSTEM", Visible: true, Timestamp: time.Now()}
initialMessage := models.ChatEvent{ClientID: "owncast-server", Author: config.Config.InstanceDetails.Name, Body: initialChatMessageText, ID: "initial-message-1", MessageType: "SYSTEM", Visible: true, Timestamp: time.Now()}
c.Write(initialMessage)
}()
}

View File

@@ -21,11 +21,11 @@ func (cl ChatListenerImpl) ClientRemoved(clientID string) {
}
// MessageSent is for when a message is sent.
func (cl ChatListenerImpl) MessageSent(message models.ChatMessage) {
func (cl ChatListenerImpl) MessageSent(message models.ChatEvent) {
}
// SendMessageToChat sends a message to the chat server.
func SendMessageToChat(message models.ChatMessage) error {
func SendMessageToChat(message models.ChatEvent) error {
if !message.Valid() {
return errors.New("invalid chat message; id, author, and body are required")
}
@@ -36,6 +36,6 @@ func SendMessageToChat(message models.ChatMessage) error {
}
// GetAllChatMessages gets all of the chat messages.
func GetAllChatMessages() []models.ChatMessage {
return chat.GetMessages()
func GetAllChatMessages(filtered bool) []models.ChatEvent {
return chat.GetMessages(filtered)
}