@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
@@ -103,7 +104,8 @@ func SendSystemMessage(text string, ephemeral bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ephemeral {
|
if !ephemeral {
|
||||||
saveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
chatMessageRepository.SaveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -131,7 +133,8 @@ func SendFediverseAction(eventType string, userAccountName string, image *string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
saveFederatedAction(message)
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
chatMessageRepository.SaveFederatedAction(message)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -152,7 +155,8 @@ func SendSystemAction(text string, ephemeral bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ephemeral {
|
if !ephemeral {
|
||||||
saveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
chatMessageRepository.SaveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/persistence/userrepository"
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
@@ -172,8 +173,8 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
|
|||||||
// Send chat message sent webhook
|
// Send chat message sent webhook
|
||||||
webhooks.SendChatEvent(&event)
|
webhooks.SendChatEvent(&event)
|
||||||
chatMessagesSentCounter.Inc()
|
chatMessagesSentCounter.Inc()
|
||||||
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
SaveUserMessage(event)
|
chatMessageRepository.SaveUserMessage(event)
|
||||||
eventData.client.MessageCount++
|
eventData.client.MessageCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import (
|
|||||||
|
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetMessagesVisibility will set the visibility of multiple messages by ID.
|
// SetMessagesVisibility will set the visibility of multiple messages by ID.
|
||||||
func SetMessagesVisibility(messageIDs []string, visibility bool) error {
|
func SetMessagesVisibility(messageIDs []string, visibility bool) error {
|
||||||
// Save new message visibility
|
// Save new message visibility
|
||||||
if err := saveMessageVisibility(messageIDs, visibility); err != nil {
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
if err := chatMessageRepository.SetMessageVisibilityForMessageIDs(messageIDs, visibility); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/persistence/authrepository"
|
"github.com/owncast/owncast/persistence/authrepository"
|
||||||
"github.com/owncast/owncast/persistence/tables"
|
"github.com/owncast/owncast/persistence/tables"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _datastore *data.Datastore
|
var _datastore *data.Datastore
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxBacklogHours = 2 // Keep backlog max hours worth of messages
|
maxBacklogHours = 2 // Keep backlog max hours worth of messages
|
||||||
maxBacklogNumber = 50 // Return max number of messages in history request
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupPersistence() {
|
func setupPersistence() {
|
||||||
@@ -37,456 +29,3 @@ func setupPersistence() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveUserMessage will save a single chat event to the messages database.
|
|
||||||
func SaveUserMessage(event events.UserMessageEvent) {
|
|
||||||
saveEvent(event.ID, &event.User.ID, event.Body, event.Type, event.HiddenAt, event.Timestamp, nil, nil, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveFederatedAction(event events.FediverseEngagementEvent) {
|
|
||||||
saveEvent(event.ID, nil, event.Body, event.Type, nil, event.Timestamp, event.Image, &event.Link, &event.UserAccountName, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint: unparam
|
|
||||||
func saveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string) {
|
|
||||||
defer func() {
|
|
||||||
_historyCache = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error saving", eventType, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() // nolint
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare("INSERT INTO messages(id, user_id, body, eventType, hidden_at, timestamp, image, link, title, subtitle) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error saving", eventType, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
if _, err = stmt.Exec(id, userID, body, eventType, hidden, timestamp, image, link, title, subtitle); err != nil {
|
|
||||||
log.Errorln("error saving", eventType, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
log.Errorln("error saving", eventType, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
|
|
||||||
scopes := ""
|
|
||||||
if row.userScopes != nil {
|
|
||||||
scopes = *row.userScopes
|
|
||||||
}
|
|
||||||
|
|
||||||
previousUsernames := []string{}
|
|
||||||
if row.previousUsernames != nil {
|
|
||||||
previousUsernames = strings.Split(*row.previousUsernames, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
displayName := ""
|
|
||||||
if row.userDisplayName != nil {
|
|
||||||
displayName = *row.userDisplayName
|
|
||||||
}
|
|
||||||
|
|
||||||
displayColor := 0
|
|
||||||
if row.userDisplayColor != nil {
|
|
||||||
displayColor = *row.userDisplayColor
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAt := time.Time{}
|
|
||||||
if row.userCreatedAt != nil {
|
|
||||||
createdAt = *row.userCreatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
isBot := (row.userType != nil && *row.userType == "API")
|
|
||||||
scopeSlice := strings.Split(scopes, ",")
|
|
||||||
|
|
||||||
u := models.User{
|
|
||||||
ID: *row.userID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
DisplayColor: displayColor,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
DisabledAt: row.userDisabledAt,
|
|
||||||
NameChangedAt: row.userNameChangedAt,
|
|
||||||
PreviousNames: previousUsernames,
|
|
||||||
AuthenticatedAt: row.userAuthenticatedAt,
|
|
||||||
Authenticated: row.userAuthenticatedAt != nil,
|
|
||||||
Scopes: scopeSlice,
|
|
||||||
IsBot: isBot,
|
|
||||||
}
|
|
||||||
|
|
||||||
message := events.UserMessageEvent{
|
|
||||||
Event: events.Event{
|
|
||||||
Type: row.eventType,
|
|
||||||
ID: row.id,
|
|
||||||
Timestamp: row.timestamp,
|
|
||||||
},
|
|
||||||
UserEvent: events.UserEvent{
|
|
||||||
User: &u,
|
|
||||||
HiddenAt: row.hiddenAt,
|
|
||||||
},
|
|
||||||
MessageEvent: events.MessageEvent{
|
|
||||||
Body: row.body,
|
|
||||||
RawBody: row.body,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeSystemMessageChatEventFromRowData(row rowData) events.SystemMessageEvent {
|
|
||||||
message := events.SystemMessageEvent{
|
|
||||||
Event: events.Event{
|
|
||||||
Type: row.eventType,
|
|
||||||
ID: row.id,
|
|
||||||
Timestamp: row.timestamp,
|
|
||||||
},
|
|
||||||
MessageEvent: events.MessageEvent{
|
|
||||||
Body: row.body,
|
|
||||||
RawBody: row.body,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeActionMessageChatEventFromRowData(row rowData) events.ActionEvent {
|
|
||||||
message := events.ActionEvent{
|
|
||||||
Event: events.Event{
|
|
||||||
Type: row.eventType,
|
|
||||||
ID: row.id,
|
|
||||||
Timestamp: row.timestamp,
|
|
||||||
},
|
|
||||||
MessageEvent: events.MessageEvent{
|
|
||||||
Body: row.body,
|
|
||||||
RawBody: row.body,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeFederatedActionChatEventFromRowData(row rowData) events.FediverseEngagementEvent {
|
|
||||||
message := events.FediverseEngagementEvent{
|
|
||||||
Event: events.Event{
|
|
||||||
Type: row.eventType,
|
|
||||||
ID: row.id,
|
|
||||||
Timestamp: row.timestamp,
|
|
||||||
},
|
|
||||||
MessageEvent: events.MessageEvent{
|
|
||||||
Body: row.body,
|
|
||||||
RawBody: row.body,
|
|
||||||
},
|
|
||||||
Image: row.image,
|
|
||||||
Link: *row.link,
|
|
||||||
UserAccountName: *row.title,
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
type rowData struct {
|
|
||||||
timestamp time.Time
|
|
||||||
image *string
|
|
||||||
previousUsernames *string
|
|
||||||
|
|
||||||
userDisplayName *string
|
|
||||||
userDisplayColor *int
|
|
||||||
userID *string
|
|
||||||
title *string
|
|
||||||
subtitle *string
|
|
||||||
link *string
|
|
||||||
|
|
||||||
userType *string
|
|
||||||
userScopes *string
|
|
||||||
hiddenAt *time.Time
|
|
||||||
userCreatedAt *time.Time
|
|
||||||
userDisabledAt *time.Time
|
|
||||||
userAuthenticatedAt *time.Time
|
|
||||||
userNameChangedAt *time.Time
|
|
||||||
body string
|
|
||||||
eventType models.EventType
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getChat(rows *sql.Rows) ([]interface{}, error) {
|
|
||||||
history := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
row := rowData{}
|
|
||||||
|
|
||||||
// Convert a database row into a chat event
|
|
||||||
if err := rows.Scan(
|
|
||||||
&row.id,
|
|
||||||
&row.userID,
|
|
||||||
&row.body,
|
|
||||||
&row.title,
|
|
||||||
&row.subtitle,
|
|
||||||
&row.image,
|
|
||||||
&row.link,
|
|
||||||
&row.eventType,
|
|
||||||
&row.hiddenAt,
|
|
||||||
&row.timestamp,
|
|
||||||
&row.userDisplayName,
|
|
||||||
&row.userDisplayColor,
|
|
||||||
&row.userCreatedAt,
|
|
||||||
&row.userDisabledAt,
|
|
||||||
&row.previousUsernames,
|
|
||||||
&row.userNameChangedAt,
|
|
||||||
&row.userAuthenticatedAt,
|
|
||||||
&row.userScopes,
|
|
||||||
&row.userType,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var message interface{}
|
|
||||||
|
|
||||||
switch row.eventType {
|
|
||||||
case events.MessageSent:
|
|
||||||
message = makeUserMessageEventFromRowData(row)
|
|
||||||
case events.SystemMessageSent:
|
|
||||||
message = makeSystemMessageChatEventFromRowData(row)
|
|
||||||
case events.ChatActionSent:
|
|
||||||
message = makeActionMessageChatEventFromRowData(row)
|
|
||||||
case events.FediverseEngagementFollow:
|
|
||||||
message = makeFederatedActionChatEventFromRowData(row)
|
|
||||||
case events.FediverseEngagementLike:
|
|
||||||
message = makeFederatedActionChatEventFromRowData(row)
|
|
||||||
case events.FediverseEngagementRepost:
|
|
||||||
message = makeFederatedActionChatEventFromRowData(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
history = append(history, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return history, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _historyCache *[]interface{}
|
|
||||||
|
|
||||||
// GetChatModerationHistory will return all the chat messages suitable for moderation purposes.
|
|
||||||
func GetChatModerationHistory() []interface{} {
|
|
||||||
if _historyCache != nil {
|
|
||||||
return *_historyCache
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error fetching chat moderation history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() // nolint
|
|
||||||
|
|
||||||
// Get all messages regardless of visibility
|
|
||||||
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC"
|
|
||||||
stmt, err := tx.Prepare(query)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error fetching chat moderation history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := stmt.Query()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error fetching chat moderation history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
result, err := getChat(rows)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_historyCache = &result
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
log.Errorln("error fetching chat moderation history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
|
|
||||||
func GetChatHistory() []interface{} {
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error fetching chat history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() // nolint
|
|
||||||
|
|
||||||
// Get all visible messages
|
|
||||||
query := "SELECT messages.id, messages.user_id, messages.body, messages.title, messages.subtitle, messages.image, messages.link, messages.eventType, messages.hidden_at, messages.timestamp, users.display_name, users.display_color, users.created_at, users.disabled_at, users.previous_names, users.namechanged_at, users.authenticated_at, users.scopes, users.type FROM users JOIN messages ON users.id = messages.user_id WHERE hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT ?"
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare(query)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error fetching chat history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := stmt.Query(maxBacklogNumber)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error fetching chat history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
m, err := getChat(rows)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
log.Errorln("error fetching chat history", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert order of messages
|
|
||||||
for i, j := 0, len(m)-1; i < j; i, j = i+1, j-1 {
|
|
||||||
m[i], m[j] = m[j], m[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessagesFromUser returns chat messages that were sent by a specific user.
|
|
||||||
func GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error) {
|
|
||||||
query, err := _datastore.GetQueries().GetMessagesFromUser(context.Background(), sql.NullString{String: userID, Valid: true})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]events.UserMessageEvent, len(query))
|
|
||||||
for i, row := range query {
|
|
||||||
results[i] = events.UserMessageEvent{
|
|
||||||
Event: events.Event{
|
|
||||||
Timestamp: row.Timestamp.Time,
|
|
||||||
ID: row.ID,
|
|
||||||
},
|
|
||||||
MessageEvent: events.MessageEvent{
|
|
||||||
Body: row.Body.String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMessageVisibilityForUserID will bulk change the visibility of messages for a user
|
|
||||||
// and then send out visibility changed events to chat clients.
|
|
||||||
func SetMessageVisibilityForUserID(userID string, visible bool) error {
|
|
||||||
defer func() {
|
|
||||||
_historyCache = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error while setting message visibility", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() // nolint
|
|
||||||
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS ?"
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare(query)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error while setting message visibility", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := stmt.Query(userID)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error while setting message visibility", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
// Get a list of IDs to send to the connected clients to hide
|
|
||||||
ids := make([]string, 0)
|
|
||||||
|
|
||||||
messages, err := getChat(rows)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(messages) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, message := range messages {
|
|
||||||
ids = append(ids, message.(events.UserMessageEvent).ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
log.Errorln("error while setting message visibility ", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the clients to hide/show these messages.
|
|
||||||
return SetMessagesVisibility(ids, visible)
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveMessageVisibility(messageIDs []string, visible bool) error {
|
|
||||||
defer func() {
|
|
||||||
_historyCache = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint:gosec
|
|
||||||
stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
var hiddenAt *time.Time
|
|
||||||
if !visible {
|
|
||||||
now := time.Now()
|
|
||||||
hiddenAt = &now
|
|
||||||
} else {
|
|
||||||
hiddenAt = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
args := make([]interface{}, len(messageIDs)+1)
|
|
||||||
args[0] = hiddenAt
|
|
||||||
for i, id := range messageIDs {
|
|
||||||
args[i+1] = id
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = stmt.Exec(args...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ func SetupPersistence(file string) error {
|
|||||||
_, _ = db.Exec("pragma temp_store = memory")
|
_, _ = db.Exec("pragma temp_store = memory")
|
||||||
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
||||||
|
|
||||||
|
tables.CreateConfigTable(db)
|
||||||
tables.CreateWebhooksTable(db)
|
tables.CreateWebhooksTable(db)
|
||||||
tables.CreateUsersTable(db)
|
tables.CreateUsersTable(db)
|
||||||
tables.CreateAccessTokenTable(db)
|
tables.CreateAccessTokenTable(db)
|
||||||
|
|||||||
@@ -107,19 +107,11 @@ func (ds *Datastore) Save(e models.ConfigEntry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup will create the datastore table and perform initial initialization.
|
// Setup will perform initial initialization.
|
||||||
func (ds *Datastore) Setup() {
|
func (ds *Datastore) Setup() {
|
||||||
ds.cache = make(map[string][]byte)
|
ds.cache = make(map[string][]byte)
|
||||||
ds.DB = GetDatabase()
|
ds.DB = GetDatabase()
|
||||||
ds.DbLock = &sync.Mutex{}
|
ds.DbLock = &sync.Mutex{}
|
||||||
|
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS datastore (
|
|
||||||
"key" string NOT NULL PRIMARY KEY,
|
|
||||||
"value" BLOB,
|
|
||||||
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
||||||
);`
|
|
||||||
|
|
||||||
ds.MustExec(createTableSQL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset will delete all config entries in the datastore and start over.
|
// Reset will delete all config entries in the datastore and start over.
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
// GetMessagesCount will return the number of messages in the database.
|
|
||||||
func GetMessagesCount() int64 {
|
|
||||||
query := `SELECT COUNT(*) FROM messages`
|
|
||||||
rows, err := _db.Query(query)
|
|
||||||
if err != nil || rows.Err() != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var count int64
|
|
||||||
for rows.Next() {
|
|
||||||
if err := rows.Scan(&count); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/nakabonne/tstorage"
|
"github.com/nakabonne/tstorage"
|
||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
"github.com/owncast/owncast/persistence/userrepository"
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -56,7 +56,8 @@ func collectChatClientCount() {
|
|||||||
activeChatClientCount.Set(float64(count))
|
activeChatClientCount.Set(float64(count))
|
||||||
|
|
||||||
// Total message count
|
// Total message count
|
||||||
cmc := data.GetMessagesCount()
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
cmc := chatMessageRepository.GetMessagesCount()
|
||||||
// Insert message count into Prometheus collector.
|
// Insert message count into Prometheus collector.
|
||||||
currentChatMessageCount.Set(float64(cmc))
|
currentChatMessageCount.Set(float64(cmc))
|
||||||
|
|
||||||
|
|||||||
523
persistence/chatmessagerepository/chatmessagerepository.go
Normal file
523
persistence/chatmessagerepository/chatmessagerepository.go
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
package chatmessagerepository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxBacklogNumber = 50 // Return max number of messages in history request
|
||||||
|
|
||||||
|
type ChatMessageRepository interface {
|
||||||
|
SaveUserMessage(event events.UserMessageEvent)
|
||||||
|
SaveFederatedAction(event events.FediverseEngagementEvent)
|
||||||
|
SaveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string)
|
||||||
|
GetChatModerationHistory() []interface{}
|
||||||
|
GetChatHistory() []interface{}
|
||||||
|
GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error)
|
||||||
|
GetMessageIdsForUserID(userID string) ([]string, error)
|
||||||
|
SetMessageVisibilityForMessageIDs(messageIDs []string, visible bool) error
|
||||||
|
GetMessagesCount() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SqlChatMessageRepository struct {
|
||||||
|
datastore *data.Datastore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is temporary during the transition period.
|
||||||
|
var temporaryGlobalInstance ChatMessageRepository
|
||||||
|
|
||||||
|
// Get will return the user repository.
|
||||||
|
func Get() ChatMessageRepository {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
i := New(data.GetDatastore())
|
||||||
|
temporaryGlobalInstance = i
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// New will create a new instance of the UserRepository.
|
||||||
|
func New(datastore *data.Datastore) ChatMessageRepository {
|
||||||
|
r := SqlChatMessageRepository{
|
||||||
|
datastore: datastore,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUserMessage will save a single chat event to the messages database.
|
||||||
|
func (r *SqlChatMessageRepository) SaveUserMessage(event events.UserMessageEvent) {
|
||||||
|
r.SaveEvent(event.ID, &event.User.ID, event.Body, event.Type, event.HiddenAt, event.Timestamp, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlChatMessageRepository) SaveFederatedAction(event events.FediverseEngagementEvent) {
|
||||||
|
r.SaveEvent(event.ID, nil, event.Body, event.Type, nil, event.Timestamp, event.Image, &event.Link, &event.UserAccountName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: unparam
|
||||||
|
func (r *SqlChatMessageRepository) SaveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string) {
|
||||||
|
defer func() {
|
||||||
|
_historyCache = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error saving", eventType, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() // nolint
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare("INSERT INTO messages(id, user_id, body, eventType, hidden_at, timestamp, image, link, title, subtitle) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error saving", eventType, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err = stmt.Exec(id, userID, body, eventType, hidden, timestamp, image, link, title, subtitle); err != nil {
|
||||||
|
log.Errorln("error saving", eventType, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
log.Errorln("error saving", eventType, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
|
||||||
|
scopes := ""
|
||||||
|
if row.userScopes != nil {
|
||||||
|
scopes = *row.userScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
previousUsernames := []string{}
|
||||||
|
if row.previousUsernames != nil {
|
||||||
|
previousUsernames = strings.Split(*row.previousUsernames, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName := ""
|
||||||
|
if row.userDisplayName != nil {
|
||||||
|
displayName = *row.userDisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
displayColor := 0
|
||||||
|
if row.userDisplayColor != nil {
|
||||||
|
displayColor = *row.userDisplayColor
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt := time.Time{}
|
||||||
|
if row.userCreatedAt != nil {
|
||||||
|
createdAt = *row.userCreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
isBot := (row.userType != nil && *row.userType == "API")
|
||||||
|
scopeSlice := strings.Split(scopes, ",")
|
||||||
|
|
||||||
|
u := models.User{
|
||||||
|
ID: *row.userID,
|
||||||
|
DisplayName: displayName,
|
||||||
|
DisplayColor: displayColor,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
DisabledAt: row.userDisabledAt,
|
||||||
|
NameChangedAt: row.userNameChangedAt,
|
||||||
|
PreviousNames: previousUsernames,
|
||||||
|
AuthenticatedAt: row.userAuthenticatedAt,
|
||||||
|
Authenticated: row.userAuthenticatedAt != nil,
|
||||||
|
Scopes: scopeSlice,
|
||||||
|
IsBot: isBot,
|
||||||
|
}
|
||||||
|
|
||||||
|
message := events.UserMessageEvent{
|
||||||
|
Event: events.Event{
|
||||||
|
Type: row.eventType,
|
||||||
|
ID: row.id,
|
||||||
|
Timestamp: row.timestamp,
|
||||||
|
},
|
||||||
|
UserEvent: events.UserEvent{
|
||||||
|
User: &u,
|
||||||
|
HiddenAt: row.hiddenAt,
|
||||||
|
},
|
||||||
|
MessageEvent: events.MessageEvent{
|
||||||
|
Body: row.body,
|
||||||
|
RawBody: row.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSystemMessageChatEventFromRowData(row rowData) events.SystemMessageEvent {
|
||||||
|
message := events.SystemMessageEvent{
|
||||||
|
Event: events.Event{
|
||||||
|
Type: row.eventType,
|
||||||
|
ID: row.id,
|
||||||
|
Timestamp: row.timestamp,
|
||||||
|
},
|
||||||
|
MessageEvent: events.MessageEvent{
|
||||||
|
Body: row.body,
|
||||||
|
RawBody: row.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeActionMessageChatEventFromRowData(row rowData) events.ActionEvent {
|
||||||
|
message := events.ActionEvent{
|
||||||
|
Event: events.Event{
|
||||||
|
Type: row.eventType,
|
||||||
|
ID: row.id,
|
||||||
|
Timestamp: row.timestamp,
|
||||||
|
},
|
||||||
|
MessageEvent: events.MessageEvent{
|
||||||
|
Body: row.body,
|
||||||
|
RawBody: row.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFederatedActionChatEventFromRowData(row rowData) events.FediverseEngagementEvent {
|
||||||
|
message := events.FediverseEngagementEvent{
|
||||||
|
Event: events.Event{
|
||||||
|
Type: row.eventType,
|
||||||
|
ID: row.id,
|
||||||
|
Timestamp: row.timestamp,
|
||||||
|
},
|
||||||
|
MessageEvent: events.MessageEvent{
|
||||||
|
Body: row.body,
|
||||||
|
RawBody: row.body,
|
||||||
|
},
|
||||||
|
Image: row.image,
|
||||||
|
Link: *row.link,
|
||||||
|
UserAccountName: *row.title,
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
type rowData struct {
|
||||||
|
timestamp time.Time
|
||||||
|
image *string
|
||||||
|
previousUsernames *string
|
||||||
|
|
||||||
|
userDisplayName *string
|
||||||
|
userDisplayColor *int
|
||||||
|
userID *string
|
||||||
|
title *string
|
||||||
|
subtitle *string
|
||||||
|
link *string
|
||||||
|
|
||||||
|
userType *string
|
||||||
|
userScopes *string
|
||||||
|
hiddenAt *time.Time
|
||||||
|
userCreatedAt *time.Time
|
||||||
|
userDisabledAt *time.Time
|
||||||
|
userAuthenticatedAt *time.Time
|
||||||
|
userNameChangedAt *time.Time
|
||||||
|
body string
|
||||||
|
eventType models.EventType
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChat(rows *sql.Rows) ([]interface{}, error) {
|
||||||
|
history := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
row := rowData{}
|
||||||
|
|
||||||
|
// Convert a database row into a chat event
|
||||||
|
if err := rows.Scan(
|
||||||
|
&row.id,
|
||||||
|
&row.userID,
|
||||||
|
&row.body,
|
||||||
|
&row.title,
|
||||||
|
&row.subtitle,
|
||||||
|
&row.image,
|
||||||
|
&row.link,
|
||||||
|
&row.eventType,
|
||||||
|
&row.hiddenAt,
|
||||||
|
&row.timestamp,
|
||||||
|
&row.userDisplayName,
|
||||||
|
&row.userDisplayColor,
|
||||||
|
&row.userCreatedAt,
|
||||||
|
&row.userDisabledAt,
|
||||||
|
&row.previousUsernames,
|
||||||
|
&row.userNameChangedAt,
|
||||||
|
&row.userAuthenticatedAt,
|
||||||
|
&row.userScopes,
|
||||||
|
&row.userType,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var message interface{}
|
||||||
|
|
||||||
|
switch row.eventType {
|
||||||
|
case events.MessageSent:
|
||||||
|
message = makeUserMessageEventFromRowData(row)
|
||||||
|
case events.SystemMessageSent:
|
||||||
|
message = makeSystemMessageChatEventFromRowData(row)
|
||||||
|
case events.ChatActionSent:
|
||||||
|
message = makeActionMessageChatEventFromRowData(row)
|
||||||
|
case events.FediverseEngagementFollow:
|
||||||
|
message = makeFederatedActionChatEventFromRowData(row)
|
||||||
|
case events.FediverseEngagementLike:
|
||||||
|
message = makeFederatedActionChatEventFromRowData(row)
|
||||||
|
case events.FediverseEngagementRepost:
|
||||||
|
message = makeFederatedActionChatEventFromRowData(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return history, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _historyCache *[]interface{}
|
||||||
|
|
||||||
|
// GetChatModerationHistory will return all the chat messages suitable for moderation purposes.
|
||||||
|
func (r *SqlChatMessageRepository) GetChatModerationHistory() []interface{} {
|
||||||
|
if _historyCache != nil {
|
||||||
|
return *_historyCache
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error fetching chat moderation history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() // nolint
|
||||||
|
|
||||||
|
// Get all messages regardless of visibility
|
||||||
|
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC"
|
||||||
|
stmt, err := tx.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error fetching chat moderation history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := stmt.Query()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error fetching chat moderation history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stmt.Close()
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result, err := getChat(rows)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_historyCache = &result
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
log.Errorln("error fetching chat moderation history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
|
||||||
|
func (r *SqlChatMessageRepository) GetChatHistory() []interface{} {
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error fetching chat history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() // nolint
|
||||||
|
|
||||||
|
// Get all visible messages
|
||||||
|
query := "SELECT messages.id, messages.user_id, messages.body, messages.title, messages.subtitle, messages.image, messages.link, messages.eventType, messages.hidden_at, messages.timestamp, users.display_name, users.display_color, users.created_at, users.disabled_at, users.previous_names, users.namechanged_at, users.authenticated_at, users.scopes, users.type FROM users JOIN messages ON users.id = messages.user_id WHERE hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT ?"
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error fetching chat history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := stmt.Query(maxBacklogNumber)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error fetching chat history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stmt.Close()
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
m, err := getChat(rows)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
log.Errorln("error fetching chat history", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert order of messages
|
||||||
|
for i, j := 0, len(m)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
m[i], m[j] = m[j], m[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessagesFromUser returns chat messages that were sent by a specific user.
|
||||||
|
func (r *SqlChatMessageRepository) GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error) {
|
||||||
|
query, err := r.datastore.GetQueries().GetMessagesFromUser(context.Background(), sql.NullString{String: userID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]events.UserMessageEvent, len(query))
|
||||||
|
for i, row := range query {
|
||||||
|
results[i] = events.UserMessageEvent{
|
||||||
|
Event: events.Event{
|
||||||
|
Timestamp: row.Timestamp.Time,
|
||||||
|
ID: row.ID,
|
||||||
|
},
|
||||||
|
MessageEvent: events.MessageEvent{
|
||||||
|
Body: row.Body.String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessageIdsForUserID will return the chat message IDs for a specific user.
|
||||||
|
func (r *SqlChatMessageRepository) GetMessageIdsForUserID(userID string) ([]string, error) {
|
||||||
|
defer func() {
|
||||||
|
_historyCache = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("error while setting message visibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() // nolint
|
||||||
|
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS ?"
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("error while setting message visibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := stmt.Query(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error while setting message visibility", err)
|
||||||
|
return nil, errors.New("error while setting message visibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stmt.Close()
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Get a list of IDs to send to the connected clients to hide
|
||||||
|
ids := make([]string, 0)
|
||||||
|
|
||||||
|
messages, err := getChat(rows)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return nil, errors.New("There is a problem enumerating chat message rows. Please report this: " + query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, message := range messages {
|
||||||
|
ids = append(ids, message.(events.UserMessageEvent).ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return nil, errors.New("error while setting message visibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the clients to hide/show these messages.
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlChatMessageRepository) SetMessageVisibilityForMessageIDs(messageIDs []string, visible bool) error {
|
||||||
|
defer func() {
|
||||||
|
_historyCache = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(messageIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gosec
|
||||||
|
stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
var hiddenAt *time.Time
|
||||||
|
if !visible {
|
||||||
|
now := time.Now()
|
||||||
|
hiddenAt = &now
|
||||||
|
} else {
|
||||||
|
hiddenAt = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
args := make([]interface{}, len(messageIDs)+1)
|
||||||
|
args[0] = hiddenAt
|
||||||
|
for i, id := range messageIDs {
|
||||||
|
args[i+1] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = stmt.Exec(args...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessagesCount will return the number of messages in the database.
|
||||||
|
func (r *SqlChatMessageRepository) GetMessagesCount() int64 {
|
||||||
|
query := `SELECT COUNT(*) FROM messages`
|
||||||
|
rows, err := r.datastore.DB.Query(query)
|
||||||
|
if err != nil || rows.Err() != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var count int64
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(&count); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
17
persistence/tables/config.go
Normal file
17
persistence/tables/config.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateConfigTable(db *sql.DB) {
|
||||||
|
createTableSQL := `CREATE TABLE IF NOT EXISTS datastore (
|
||||||
|
"key" string NOT NULL PRIMARY KEY,
|
||||||
|
"value" BLOB,
|
||||||
|
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
utils.MustExec(createTableSQL, db)
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ const establishedUserFailedChatMessage = {
|
|||||||
type: 'CHAT',
|
type: 'CHAT',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const establishedUserSucceedChatMessage = {
|
||||||
|
body: 'this message should send ' + Math.floor(Math.random() * 100),
|
||||||
|
type: 'CHAT',
|
||||||
|
};
|
||||||
|
|
||||||
test('send a chat message', async () => {
|
test('send a chat message', async () => {
|
||||||
const registration = await registerChat();
|
const registration = await registerChat();
|
||||||
const accessToken = registration.accessToken;
|
const accessToken = registration.accessToken;
|
||||||
@@ -36,7 +41,7 @@ test('verify admin can make API call to mark message as hidden', async () => {
|
|||||||
`ws://localhost:8080/ws?accessToken=${accessToken}`,
|
`ws://localhost:8080/ws?accessToken=${accessToken}`,
|
||||||
{
|
{
|
||||||
origin: 'http://localhost:8080',
|
origin: 'http://localhost:8080',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the visibility change comes through the websocket
|
// Verify the visibility change comes through the websocket
|
||||||
@@ -70,7 +75,7 @@ test('verify message has become hidden', async () => {
|
|||||||
return obj.id === messageId;
|
return obj.id === messageId;
|
||||||
});
|
});
|
||||||
expect(message.length).toBe(1);
|
expect(message.length).toBe(1);
|
||||||
// expect(message[0].hiddenAt).toBeTruthy();
|
expect(message[0].hiddenAt).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enable established chat user mode', async () => {
|
test('enable established chat user mode', async () => {
|
||||||
@@ -97,3 +102,20 @@ test('verify rejected message is not in the chat feed', async () => {
|
|||||||
test('disable established chat user mode', async () => {
|
test('disable established chat user mode', async () => {
|
||||||
await sendAdminRequest('config/chat/establishedusermode', false);
|
await sendAdminRequest('config/chat/establishedusermode', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('send a message after established user mode is disabled', async () => {
|
||||||
|
const registration = await registerChat();
|
||||||
|
const accessToken = registration.accessToken;
|
||||||
|
|
||||||
|
await sendChatMessage(establishedUserSucceedChatMessage, accessToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verify message is in the chat feed', async () => {
|
||||||
|
const res = await getAdminResponse('chat/messages');
|
||||||
|
|
||||||
|
const message = res.body.filter((obj) => {
|
||||||
|
return obj.body === establishedUserSucceedChatMessage.body;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.length).toBe(0);
|
||||||
|
});
|
||||||
|
|||||||
0
test/populateContent.sh
Normal file → Executable file
0
test/populateContent.sh
Normal file → Executable file
@@ -4,7 +4,6 @@ package admin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -13,11 +12,13 @@ import (
|
|||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/persistence/authrepository"
|
"github.com/owncast/owncast/persistence/authrepository"
|
||||||
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/persistence/userrepository"
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||||
webutils "github.com/owncast/owncast/webserver/utils"
|
webutils "github.com/owncast/owncast/webserver/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -146,13 +147,20 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func updateUserStatus(request generated.UpdateUserEnabledJSONBody) error {
|
func updateUserStatus(request generated.UpdateUserEnabledJSONBody) error {
|
||||||
userRepository := userrepository.Get()
|
userRepository := userrepository.Get()
|
||||||
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
|
||||||
if err := userRepository.SetEnabled(*request.UserId, *request.Enabled); err != nil {
|
if err := userRepository.SetEnabled(*request.UserId, *request.Enabled); err != nil {
|
||||||
log.Errorln("error changing user enabled status", err)
|
log.Errorln("error changing user enabled status", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*request.Enabled {
|
messageIDs, err := chatMessageRepository.GetMessageIdsForUserID(*request.UserId)
|
||||||
if err := chat.SetMessageVisibilityForUserID(*request.UserId, *request.Enabled); err != nil {
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error fetching user messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*request.Enabled && len(messageIDs) > 0 {
|
||||||
|
if err := chat.SetMessagesVisibility(messageIDs, *request.Enabled); err != nil {
|
||||||
log.Errorln("error changing user messages visibility", err)
|
log.Errorln("error changing user messages visibility", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -247,7 +255,8 @@ func GetModerators(w http.ResponseWriter, r *http.Request) {
|
|||||||
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
|
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
messages := chat.GetChatModerationHistory()
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
messages := chatMessageRepository.GetChatModerationHistory()
|
||||||
webutils.WriteResponse(w, messages)
|
webutils.WriteResponse(w, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +352,8 @@ func SendIntegrationChatMessage(integration models.ExternalAPIUser, w http.Respo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.SaveUserMessage(event)
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
chatMessageRepository.SaveUserMessage(event)
|
||||||
|
|
||||||
webutils.WriteSimpleResponse(w, true, "sent")
|
webutils.WriteSimpleResponse(w, true, "sent")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/persistence/userrepository"
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
@@ -33,7 +33,8 @@ func getChatMessages(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
messages := chat.GetChatHistory()
|
chatMessageRepository := chatmessagerepository.Get()
|
||||||
|
messages := chatMessageRepository.GetChatHistory()
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(messages); err != nil {
|
if err := json.NewEncoder(w).Encode(messages); err != nil {
|
||||||
log.Debugln(err)
|
log.Debugln(err)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
||||||
"github.com/owncast/owncast/persistence/userrepository"
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
"github.com/owncast/owncast/webserver/utils"
|
"github.com/owncast/owncast/webserver/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -58,7 +59,8 @@ func GetUserDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
clients[i] = client
|
clients[i] = client
|
||||||
}
|
}
|
||||||
|
|
||||||
messages, err := chat.GetMessagesFromUser(uid)
|
chatMessagesRepository := chatmessagerepository.Get()
|
||||||
|
messages, err := chatMessagesRepository.GetMessagesFromUser(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user