diff --git a/.gitignore b/.gitignore index 03ccb9f0d..99a23c32d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ webroot/static/content.md hls/ dist/ data/ -admin/ transcoder.log chat.db .yp.key diff --git a/controllers/admin/chat.go b/controllers/admin/chat.go new file mode 100644 index 000000000..3c2ba2265 --- /dev/null +++ b/controllers/admin/chat.go @@ -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) + } +} diff --git a/controllers/chat.go b/controllers/chat.go index 6290cd938..5ecff7902 100644 --- a/controllers/chat.go +++ b/controllers/chat.go @@ -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 diff --git a/core/chat/chat.go b/core/chat/chat.go index fc10dbc60..8b829d2d8 100644 --- a/core/chat/chat.go +++ b/core/chat/chat.go @@ -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 { diff --git a/core/chat/client.go b/core/chat/client.go index 654ffd762..d868c3a76 100644 --- a/core/chat/client.go +++ b/core/chat/client.go @@ -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) diff --git a/core/chat/messages.go b/core/chat/messages.go new file mode 100644 index 000000000..5cf5dcd43 --- /dev/null +++ b/core/chat/messages.go @@ -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 +} diff --git a/core/chat/persistence.go b/core/chat/persistence.go index 13b60f16e..04c17b7d4 100644 --- a/core/chat/persistence.go +++ b/core/chat/persistence.go @@ -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 +} diff --git a/core/chat/server.go b/core/chat/server.go index eec9b0000..9ed01c32a 100644 --- a/core/chat/server.go +++ b/core/chat/server.go @@ -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) }() } diff --git a/core/chatListener.go b/core/chatListener.go index 684658283..feec2f10f 100644 --- a/core/chatListener.go +++ b/core/chatListener.go @@ -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) } diff --git a/doc/api/index.html b/doc/api/index.html index 0110c0d58..220de0bdf 100644 --- a/doc/api/index.html +++ b/doc/api/index.html @@ -561,7 +561,7 @@ data-styled.g143[id="sc-global-kJtbWf1"]{content:"sc-global-kJtbWf1,"}/*!sc*/ -

Server status and broadcaster

Authorizations:

Responses

Response samples

Content type
application/json
{
  • "broadcaster": {
    },
  • "online": true,
  • "viewerCount": 3,
  • "overallPeakViewerCount": 4,
  • "sessionPeakViewerCount": 4,
  • "versionNumber": "0.0.3",
  • "disableUpgradeChecks": false
}

Disconnect Broadcaster

Disconnect the active inbound stream, if one exists, and terminate the broadcast.

Authorizations:

Responses

Response samples

Content type
application/json
Example
{
  • "success": true,
  • "message": "inbound stream disconnected"
}

Return a list of currently connected clients

Return a list of currently connected clients with optional geo details.

+

Response samples

Content type
application/json
Example
{
  • "success": true,
  • "message": "context specific success message"
}

Return a list of currently connected clients

Return a list of currently connected clients with optional geo details.

Authorizations:

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Return recent log entries

Returns server logs.

Authorizations:

Responses

Request samples

Content type
application/json
{
  • "key": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "changed"
}

Change the extra page content. Pre-release, do not use.

Change the extra page content in memory, but not on disk.

Authorizations:
Request Body schema: application/json
content
string

Responses

Request samples

Content type
application/json
{
  • "content": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "changed"
}

Server Configuration

Get the current configuration of the Owncast server.

-
Authorizations:

Responses

Response samples

Content type
application/json
{
  • "instanceDetails": {
    },
  • "ffmpegPath": "string",
  • "webServerPort": 0,
  • "s3": {
    },
  • "videoSettings": {
    },
  • "yp": {
    }
}

Viewers Over Time

Get the tracked viewer count over the collected period.

-
Authorizations:

Responses

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Hardware Stats

Get the CPU, Memory and Disk utilization levels over the collected period.

-
Authorizations:

Responses

Response samples

Content type
application/json
{
  • "cpu": [
    ],
  • "memory": [
    ],
  • "disk": [
    ]
}

Chat

Endpoints related to the chat interface.

+
Authorizations:

Responses

Response samples

Content type
application/json
{
  • "instanceDetails": {
    },
  • "ffmpegPath": "string",
  • "webServerPort": 0,
  • "s3": {
    },
  • "videoSettings": {
    },
  • "yp": {
    }
}

Chat messages, unfiltered.

Get a list of all chat messages with no filters applied.

+
Authorizations:

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Update the visibility of chat messages.

Pass an array of IDs you want to change the chat visibility of.

+
Authorizations:
Request Body schema: application/json
visible
boolean

Are these messages visible in "Get the CPU, Memory and Disk utilization levels over the collected period."

+
idArray
Array of strings

Responses

Request samples

Content type
application/json
{
  • "visible": true,
  • "idArray": [
    ]
}

Response samples

Content type
application/json
Example
{
  • "success": true,
  • "message": "context specific success message"
}

Viewers Over Time

Get the tracked viewer count over the collected period.

+
Authorizations:

Responses

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Hardware Stats

Get the CPU, Memory and Disk utilization levels over the collected period.

+
Authorizations:

Responses

Response samples

Content type
application/json
{
  • "cpu": [
    ],
  • "memory": [
    ],
  • "disk": [
    ]
}

Chat

Endpoints related to the chat interface.

Historical Chat Messages

Used to get all chat messages prior to connecting to the websocket.

-

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Get Custom Emoji

Get a list of custom emoji that are supported in chat.

-

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Server

Information

The client configuration. Information useful for the user interface.

-

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "title": "string",
  • "summary": "string",
  • "logo": "string",
  • "tags": [
    ],
  • "socialHandles": [],
  • "extraPageContent": "<p>This page is <strong>super</strong> cool!",
  • "version": "Owncast v0.0.3-macOS (ef3796a033b32a312ebf5b334851cbf9959e7ecb)"
}

Current Status

This endpoint is used to discover when a server is broadcasting, the number of active viewers as well as other useful information for updating the user interface.

-

Responses

Response samples

Content type
application/json
{
  • "lastConnectTime": "2020-10-03T21:36:22-05:00",
  • "lastDisconnectTime": null,
  • "online": true,
  • "overallMaxViewerCount": 420,
  • "sessionMaxViewerCount": 12,
  • "viewerCount": 7
}

Yellow Pages Information

Information to be used in the Yellow Pages service, a global directory of Owncast servers.

-

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "description": "string",
  • "logo": "string",
  • "nsfw": true,
  • "tags": [
    ],
  • "online": true,
  • "viewerCount": 0,
  • "overallMaxViewerCount": 0,
  • "sessionMaxViewerCount": 0,
  • "lastConnectTime": "2019-08-24T14:15:22Z"
}

Pre-release

Update Stream Key. Pre-release, do not use.

Change the stream key in memory, but not in the config file. This will require all broadcasters to be reconfigured to connect again.

+

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Get Custom Emoji

Get a list of custom emoji that are supported in chat.

+

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Server

Information

The client configuration. Information useful for the user interface.

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "title": "string",
  • "summary": "string",
  • "logo": "string",
  • "tags": [
    ],
  • "socialHandles": [],
  • "extraPageContent": "<p>This page is <strong>super</strong> cool!",
  • "version": "Owncast v0.0.3-macOS (ef3796a033b32a312ebf5b334851cbf9959e7ecb)"
}

Current Status

This endpoint is used to discover when a server is broadcasting, the number of active viewers as well as other useful information for updating the user interface.

+

Responses

Response samples

Content type
application/json
{
  • "lastConnectTime": "2020-10-03T21:36:22-05:00",
  • "lastDisconnectTime": null,
  • "online": true,
  • "overallMaxViewerCount": 420,
  • "sessionMaxViewerCount": 12,
  • "viewerCount": 7
}

Yellow Pages Information

Information to be used in the Yellow Pages service, a global directory of Owncast servers.

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "description": "string",
  • "logo": "string",
  • "nsfw": true,
  • "tags": [
    ],
  • "online": true,
  • "viewerCount": 0,
  • "overallMaxViewerCount": 0,
  • "sessionMaxViewerCount": 0,
  • "lastConnectTime": "2019-08-24T14:15:22Z"
}

Pre-release

Update Stream Key. Pre-release, do not use.

Change the stream key in memory, but not in the config file. This will require all broadcasters to be reconfigured to connect again.

Authorizations:
Request Body schema: application/json
key
string

Responses

Request samples

Content type
application/json
{
  • "key": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "changed"
}

Change the extra page content. Pre-release, do not use.

Change the extra page content in memory, but not on disk.

+

Request samples

Content type
application/json
{
  • "key": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "changed"
}

Change the extra page content. Pre-release, do not use.

Change the extra page content in memory, but not on disk.

Authorizations:
Request Body schema: application/json
content
string

Responses

Request samples

Content type
application/json
{
  • "content": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "changed"
}
+

Request samples

Content type
application/json
{
  • "content": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "changed"
}