Skip to content

Commit

Permalink
Update chat message visibility for moderation (owncast#524)
Browse files Browse the repository at this point in the history
* 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 3a83df3.

* 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 <[email protected]>
Co-authored-by: Owncast <[email protected]>
  • Loading branch information
3 people authored Dec 29, 2020
1 parent 0452c4c commit 8a74af2
Show file tree
Hide file tree
Showing 18 changed files with 375 additions and 64 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ webroot/static/content.md
hls/
dist/
data/
admin/
transcoder.log
chat.db
.yp.key
Expand Down
60 changes: 60 additions & 0 deletions controllers/admin/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package admin

// this is endpoint logic

import (
"encoding/json"
"net/http"

"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/chat"
log "github.com/sirupsen/logrus"
)

// UpdateMessageVisibility updates an array of message IDs to have the same visiblity.
func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}

decoder := json.NewDecoder(r.Body)
var request messageVisibilityUpdateRequest // creates an empty struc

err := decoder.Decode(&request) // decode the json into `request`
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}

// // make sql update call here.
// // := means create a new var
// _db := data.GetDatabase()
// updateMessageVisibility(_db, request)

if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}

controllers.WriteSimpleResponse(w, true, "changed")
}

type messageVisibilityUpdateRequest struct {
IDArray []string `json:"idArray"`
Visible bool `json:"visible"`
}

// GetChatMessages returns all of the chat messages, unfiltered.
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
// middleware.EnableCors(&w)
w.Header().Set("Content-Type", "application/json")

messages := core.GetAllChatMessages(false)

if err := json.NewEncoder(w).Encode(messages); err != nil {
log.Errorln(err)
}
}
4 changes: 2 additions & 2 deletions controllers/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ func GetChatMessages(w http.ResponseWriter, r *http.Request) {

switch r.Method {
case http.MethodGet:
messages := core.GetAllChatMessages()
messages := core.GetAllChatMessages(true)

err := json.NewEncoder(w).Encode(messages)
if err != nil {
log.Errorln(err)
}
case http.MethodPost:
var message models.ChatMessage
var message models.ChatEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
internalErrorHandler(w, err)
return
Expand Down
10 changes: 5 additions & 5 deletions core/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
17 changes: 9 additions & 8 deletions core/chat/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ 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

doneCh chan bool
}

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.
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions core/chat/messages.go
Original file line number Diff line number Diff line change
@@ -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
}
77 changes: 72 additions & 5 deletions core/chat/persistence.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chat

import (
"database/sql"
"strings"
"time"

_ "github.com/mattn/go-sqlite3"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -85,7 +91,7 @@ func getChatHistory() []models.ChatMessage {
break
}

message := models.ChatMessage{}
message := models.ChatEvent{}
message.ID = id
message.Author = author
message.Body = body
Expand All @@ -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
}
8 changes: 4 additions & 4 deletions core/chat/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}()
}
8 changes: 4 additions & 4 deletions core/chatListener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
}
30 changes: 17 additions & 13 deletions doc/api/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion models/chatListener.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ package models
type ChatListener interface {
ClientAdded(client Client)
ClientRemoved(clientID string)
MessageSent(message ChatMessage)
MessageSent(message ChatEvent)
}
Loading

0 comments on commit 8a74af2

Please sign in to comment.