* Initial plan * Initial analysis of inline chat moderation 500 error issue Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix inline chat moderation 500 errors by supporting Authorization header tokens - Modified RequireUserModerationScopeAccesstoken middleware to check both Authorization header (Bearer tokens) and URL query parameters - This fixes the issue where inline chat moderation from the web UI was failing with 500 errors - Maintains backward compatibility with existing code that uses query parameter tokens - The fix aligns the chat endpoint behavior with the integrations endpoint which already supported Bearer tokens Addresses the wiring issue between OpenAPI spec and API implementation identified in issue comments. Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix inline chat moderation 500 errors by adding nil pointer checks Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix nil pointer vulnerabilities across all OpenAPI endpoints Co-authored-by: gabek <414923+gabek@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gabek <414923+gabek@users.noreply.github.com>
408 lines
12 KiB
Go
408 lines
12 KiB
Go
package admin
|
|
|
|
// this is endpoint logic
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/owncast/owncast/core/chat"
|
|
"github.com/owncast/owncast/core/chat/events"
|
|
"github.com/owncast/owncast/models"
|
|
"github.com/owncast/owncast/persistence/authrepository"
|
|
"github.com/owncast/owncast/persistence/chatmessagerepository"
|
|
"github.com/owncast/owncast/persistence/configrepository"
|
|
"github.com/owncast/owncast/persistence/userrepository"
|
|
"github.com/owncast/owncast/utils"
|
|
"github.com/owncast/owncast/webserver/handlers/generated"
|
|
webutils "github.com/owncast/owncast/webserver/utils"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ExternalUpdateMessageVisibility updates an array of message IDs to have the same visiblity.
|
|
func ExternalUpdateMessageVisibility(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
UpdateMessageVisibility(w, r)
|
|
}
|
|
|
|
// UpdateMessageVisibility updates an array of message IDs to have the same visiblity.
|
|
func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
// nolint:goconst
|
|
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
|
return
|
|
}
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
var request generated.MessageVisibilityUpdate
|
|
|
|
if err := decoder.Decode(&request); err != nil {
|
|
log.Errorln(err)
|
|
webutils.WriteSimpleResponse(w, false, "")
|
|
return
|
|
}
|
|
|
|
if request.IdArray == nil || request.Visible == nil {
|
|
webutils.WriteSimpleResponse(w, false, "missing required fields: idArray and visible are required")
|
|
return
|
|
}
|
|
|
|
if err := chat.SetMessagesVisibility(*request.IdArray, *request.Visible); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, "changed")
|
|
}
|
|
|
|
// BanIPAddress will manually ban an IP address.
|
|
func BanIPAddress(w http.ResponseWriter, r *http.Request) {
|
|
if !requirePOST(w, r) {
|
|
return
|
|
}
|
|
|
|
configValue, success := getValueFromRequest(w, r)
|
|
if !success {
|
|
webutils.WriteSimpleResponse(w, false, "unable to ban IP address")
|
|
return
|
|
}
|
|
|
|
authRepository := authrepository.Get()
|
|
|
|
if err := authRepository.BanIPAddress(configValue.Value.(string), "manually added"); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, "error saving IP address ban")
|
|
return
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, "IP address banned")
|
|
}
|
|
|
|
// UnBanIPAddress will remove an IP address ban.
|
|
func UnBanIPAddress(w http.ResponseWriter, r *http.Request) {
|
|
if !requirePOST(w, r) {
|
|
return
|
|
}
|
|
|
|
configValue, success := getValueFromRequest(w, r)
|
|
if !success {
|
|
webutils.WriteSimpleResponse(w, false, "unable to unban IP address")
|
|
return
|
|
}
|
|
|
|
authRepository := authrepository.Get()
|
|
|
|
if err := authRepository.RemoveIPAddressBan(configValue.Value.(string)); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, "error removing IP address ban")
|
|
return
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, "IP address unbanned")
|
|
}
|
|
|
|
// GetIPAddressBans will return all the banned IP addresses.
|
|
func GetIPAddressBans(w http.ResponseWriter, r *http.Request) {
|
|
authRepository := authrepository.Get()
|
|
|
|
bans, err := authRepository.GetIPAddressBans()
|
|
if err != nil {
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
|
|
webutils.WriteResponse(w, bans)
|
|
}
|
|
|
|
// UpdateUserEnabled enable or disable a single user by ID.
|
|
func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
|
return
|
|
}
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
var request generated.UpdateUserEnabledJSONBody
|
|
|
|
if err := decoder.Decode(&request); err != nil {
|
|
log.Errorln(err)
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
|
|
if request.UserId == nil || *request.UserId == "" || request.Enabled == nil {
|
|
webutils.WriteSimpleResponse(w, false, "must provide userId and enabled state")
|
|
return
|
|
}
|
|
|
|
if err := updateUserStatus(request); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
|
|
if !*request.Enabled {
|
|
if err := handleUserDisabling(*request.UserId); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", *request.UserId, *request.Enabled))
|
|
}
|
|
|
|
func updateUserStatus(request generated.UpdateUserEnabledJSONBody) error {
|
|
userRepository := userrepository.Get()
|
|
chatMessageRepository := chatmessagerepository.Get()
|
|
|
|
if err := userRepository.SetEnabled(*request.UserId, *request.Enabled); err != nil {
|
|
log.Errorln("error changing user enabled status", err)
|
|
return err
|
|
}
|
|
|
|
messageIDs, err := chatMessageRepository.GetMessageIdsForUserID(*request.UserId)
|
|
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)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleUserDisabling(userID string) error {
|
|
clients, err := chat.GetClientsForUser(userID)
|
|
if len(clients) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
log.Errorln("error fetching clients for user: ", err)
|
|
return err
|
|
}
|
|
|
|
chat.DisconnectClients(clients)
|
|
userRepository := userrepository.Get()
|
|
disconnectedUser := userRepository.GetUserByID(userID)
|
|
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
|
|
|
|
localIP4Address := "127.0.0.1"
|
|
localIP6Address := "::1"
|
|
|
|
authRepository := authrepository.Get()
|
|
for _, client := range clients {
|
|
ipAddress := client.IPAddress
|
|
if ipAddress != localIP4Address && ipAddress != localIP6Address {
|
|
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
|
|
if err := authRepository.BanIPAddress(ipAddress, reason); err != nil {
|
|
log.Errorln("error banning IP address: ", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDisabledUsers will return all the disabled users.
|
|
func GetDisabledUsers(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
userRepository := userrepository.Get()
|
|
|
|
users := userRepository.GetDisabledUsers()
|
|
webutils.WriteResponse(w, users)
|
|
}
|
|
|
|
// UpdateUserModerator will set the moderator status for a user ID.
|
|
func UpdateUserModerator(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
|
return
|
|
}
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
var req generated.UpdateUserModeratorJSONBody
|
|
|
|
if err := decoder.Decode(&req); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, "")
|
|
return
|
|
}
|
|
|
|
userRepository := userrepository.Get()
|
|
|
|
// Update the user object with new moderation access.
|
|
if err := userRepository.SetModerator(*req.UserId, *req.IsModerator); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
|
|
// Update the clients for this user to know about the moderator access change.
|
|
if err := chat.SendConnectedClientInfoToUser(*req.UserId); err != nil {
|
|
log.Debugln(err)
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, fmt.Sprintf("%s is moderator: %t", *req.UserId, *req.IsModerator))
|
|
}
|
|
|
|
// GetModerators will return a list of moderator users.
|
|
func GetModerators(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
userRepository := userrepository.Get()
|
|
|
|
users := userRepository.GetModeratorUsers()
|
|
webutils.WriteResponse(w, users)
|
|
}
|
|
|
|
// GetChatMessages returns all of the chat messages, unfiltered.
|
|
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
chatMessageRepository := chatmessagerepository.Get()
|
|
messages := chatMessageRepository.GetChatModerationHistory()
|
|
webutils.WriteResponse(w, messages)
|
|
}
|
|
|
|
// SendSystemMessage will send an official "SYSTEM" message to chat on behalf of your server.
|
|
func SendSystemMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
var message events.SystemMessageEvent
|
|
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
|
webutils.InternalErrorHandler(w, err)
|
|
return
|
|
}
|
|
|
|
if err := chat.SendSystemMessage(message.Body, false); err != nil {
|
|
webutils.BadRequestHandler(w, err)
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, "sent")
|
|
}
|
|
|
|
// SendSystemMessageToConnectedClient will handle incoming requests to send a single message to a single connected client by ID.
|
|
func SendSystemMessageToConnectedClient(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
clientIDText, err := utils.GetURLParam(r, "clientId")
|
|
if err != nil {
|
|
webutils.BadRequestHandler(w, err)
|
|
return
|
|
}
|
|
|
|
clientIDNumeric, err := strconv.ParseUint(clientIDText, 10, 32)
|
|
if err != nil {
|
|
webutils.BadRequestHandler(w, err)
|
|
return
|
|
}
|
|
|
|
// var message events.SystemMessageEvent
|
|
var message generated.SendSystemMessageJSONRequestBody
|
|
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
|
webutils.InternalErrorHandler(w, err)
|
|
return
|
|
}
|
|
|
|
if message.Body == nil {
|
|
webutils.WriteSimpleResponse(w, false, "no message body provided")
|
|
return
|
|
}
|
|
|
|
chat.SendSystemMessageToClient(uint(clientIDNumeric), *message.Body)
|
|
webutils.WriteSimpleResponse(w, true, "sent")
|
|
}
|
|
|
|
// SendUserMessage will send a message to chat on behalf of a user. *Depreciated*.
|
|
func SendUserMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
webutils.BadRequestHandler(w, errors.New("no longer supported. see /api/integrations/chat/send"))
|
|
}
|
|
|
|
// SendIntegrationChatMessage will send a chat message on behalf of an external chat integration.
|
|
func SendIntegrationChatMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
name := integration.DisplayName
|
|
|
|
if name == "" {
|
|
webutils.BadRequestHandler(w, errors.New("unknown integration for provided access token"))
|
|
return
|
|
}
|
|
|
|
var event events.UserMessageEvent
|
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
|
webutils.InternalErrorHandler(w, err)
|
|
return
|
|
}
|
|
event.SetDefaults()
|
|
event.RenderBody()
|
|
event.Type = "CHAT"
|
|
|
|
if event.Empty() {
|
|
webutils.BadRequestHandler(w, errors.New("invalid message"))
|
|
return
|
|
}
|
|
|
|
event.User = &models.User{
|
|
ID: integration.ID,
|
|
DisplayName: name,
|
|
DisplayColor: integration.DisplayColor,
|
|
CreatedAt: integration.CreatedAt,
|
|
IsBot: true,
|
|
}
|
|
|
|
if err := chat.Broadcast(&event); err != nil {
|
|
webutils.BadRequestHandler(w, err)
|
|
return
|
|
}
|
|
|
|
chatMessageRepository := chatmessagerepository.Get()
|
|
chatMessageRepository.SaveUserMessage(event)
|
|
|
|
webutils.WriteSimpleResponse(w, true, "sent")
|
|
}
|
|
|
|
// SendChatAction will send a generic chat action.
|
|
func SendChatAction(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
var message events.SystemActionEvent
|
|
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
|
webutils.InternalErrorHandler(w, err)
|
|
return
|
|
}
|
|
|
|
message.SetDefaults()
|
|
message.RenderBody()
|
|
|
|
if err := chat.SendSystemAction(message.Body, false); err != nil {
|
|
webutils.BadRequestHandler(w, err)
|
|
return
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, "sent")
|
|
}
|
|
|
|
// SetEnableEstablishedChatUserMode sets the requirement for a chat user
|
|
// to be "established" for some time before taking part in chat.
|
|
func SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
|
|
if !requirePOST(w, r) {
|
|
return
|
|
}
|
|
|
|
configValue, success := getValueFromRequest(w, r)
|
|
if !success {
|
|
webutils.WriteSimpleResponse(w, false, "unable to update chat established user only mode")
|
|
return
|
|
}
|
|
|
|
configRepository := configrepository.Get()
|
|
if err := configRepository.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil {
|
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
|
return
|
|
}
|
|
|
|
webutils.WriteSimpleResponse(w, true, "chat established users only mode updated")
|
|
}
|