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:
@@ -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 {
|
||||
|
||||
@@ -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
28
core/chat/messages.go
Normal 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
|
||||
}
|
||||
@@ -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, ×tamp)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user