Optionally disable chat rate limiter and add optional chat slur/language filter (#3681)

* feat(chat): basic profanity filter. For #3139

* feat(chat): add setting for disabling chat spam protection. Closes #3523

* feat(chat): wire up the new chat slur filter to admin and chat. Closes #3139
This commit is contained in:
Gabe Kangas 2024-04-09 22:25:41 -07:00 committed by GitHub
parent 04eaf8c20e
commit a450e62397
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 352 additions and 142 deletions

View file

@ -802,6 +802,42 @@ func SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) {
controllers.WriteSimpleResponse(w, true, "custom video serving endpoint updated") controllers.WriteSimpleResponse(w, true, "custom video serving endpoint updated")
} }
// SetChatSpamProtectionEnabled will enable or disable the chat spam protection.
func SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetChatSpamProtectionEnabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat spam protection changed")
}
// SetChatSlurFilterEnabled will enable or disable the chat slur filter.
func SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetChatSlurFilterEnabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat message slur filter changed")
}
func requirePOST(w http.ResponseWriter, r *http.Request) bool { func requirePOST(w http.ResponseWriter, r *http.Request) bool {
if r.Method != controllers.POST { if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported") controllers.WriteSimpleResponse(w, false, r.Method+" not supported")

View file

@ -49,20 +49,22 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
CustomJavascript: data.GetCustomJavascript(), CustomJavascript: data.GetCustomJavascript(),
AppearanceVariables: data.GetCustomColorVariableValues(), AppearanceVariables: data.GetCustomColorVariableValues(),
}, },
FFmpegPath: ffmpeg, FFmpegPath: ffmpeg,
AdminPassword: data.GetAdminPassword(), AdminPassword: data.GetAdminPassword(),
StreamKeys: data.GetStreamKeys(), StreamKeys: data.GetStreamKeys(),
StreamKeyOverridden: config.TemporaryStreamKey != "", StreamKeyOverridden: config.TemporaryStreamKey != "",
WebServerPort: config.WebServerPort, WebServerPort: config.WebServerPort,
WebServerIP: config.WebServerIP, WebServerIP: config.WebServerIP,
RTMPServerPort: data.GetRTMPPortNumber(), RTMPServerPort: data.GetRTMPPortNumber(),
ChatDisabled: data.GetChatDisabled(), ChatDisabled: data.GetChatDisabled(),
ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(), ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(),
SocketHostOverride: data.GetWebsocketOverrideHost(), SocketHostOverride: data.GetWebsocketOverrideHost(),
VideoServingEndpoint: data.GetVideoServingEndpoint(), VideoServingEndpoint: data.GetVideoServingEndpoint(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
HideViewerCount: data.GetHideViewerCount(), ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(),
DisableSearchIndexing: data.GetDisableSearchIndexing(), ChatSlurFilterEnabled: data.GetChatSlurFilterEnabled(),
HideViewerCount: data.GetHideViewerCount(),
DisableSearchIndexing: data.GetDisableSearchIndexing(),
VideoSettings: videoSettings{ VideoSettings: videoSettings{
VideoQualityVariants: videoQualityVariants, VideoQualityVariants: videoQualityVariants,
LatencyLevel: data.GetStreamLatencyLevel().Level, LatencyLevel: data.GetStreamLatencyLevel().Level,
@ -100,31 +102,33 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
} }
type serverConfigAdminResponse struct { type serverConfigAdminResponse struct {
InstanceDetails webConfigResponse `json:"instanceDetails"` InstanceDetails webConfigResponse `json:"instanceDetails"`
Notifications notificationsConfigResponse `json:"notifications"` Notifications notificationsConfigResponse `json:"notifications"`
YP yp `json:"yp"` YP yp `json:"yp"`
FFmpegPath string `json:"ffmpegPath"` FFmpegPath string `json:"ffmpegPath"`
AdminPassword string `json:"adminPassword"` AdminPassword string `json:"adminPassword"`
SocketHostOverride string `json:"socketHostOverride,omitempty"` SocketHostOverride string `json:"socketHostOverride,omitempty"`
WebServerIP string `json:"webServerIP"` WebServerIP string `json:"webServerIP"`
VideoCodec string `json:"videoCodec"` VideoCodec string `json:"videoCodec"`
VideoServingEndpoint string `json:"videoServingEndpoint"` VideoServingEndpoint string `json:"videoServingEndpoint"`
S3 models.S3 `json:"s3"` S3 models.S3 `json:"s3"`
Federation federationConfigResponse `json:"federation"` Federation federationConfigResponse `json:"federation"`
SupportedCodecs []string `json:"supportedCodecs"` SupportedCodecs []string `json:"supportedCodecs"`
ExternalActions []models.ExternalAction `json:"externalActions"` ExternalActions []models.ExternalAction `json:"externalActions"`
ForbiddenUsernames []string `json:"forbiddenUsernames"` ForbiddenUsernames []string `json:"forbiddenUsernames"`
SuggestedUsernames []string `json:"suggestedUsernames"` SuggestedUsernames []string `json:"suggestedUsernames"`
StreamKeys []models.StreamKey `json:"streamKeys"` StreamKeys []models.StreamKey `json:"streamKeys"`
VideoSettings videoSettings `json:"videoSettings"` VideoSettings videoSettings `json:"videoSettings"`
RTMPServerPort int `json:"rtmpServerPort"` RTMPServerPort int `json:"rtmpServerPort"`
WebServerPort int `json:"webServerPort"` WebServerPort int `json:"webServerPort"`
ChatDisabled bool `json:"chatDisabled"` ChatDisabled bool `json:"chatDisabled"`
ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"`
ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"`
DisableSearchIndexing bool `json:"disableSearchIndexing"` ChatSpamProtectionEnabled bool `json:"chatSpamProtectionEnabled"`
StreamKeyOverridden bool `json:"streamKeyOverridden"` ChatSlurFilterEnabled bool `json:"chatSlurFilterEnabled"`
HideViewerCount bool `json:"hideViewerCount"` DisableSearchIndexing bool `json:"disableSearchIndexing"`
StreamKeyOverridden bool `json:"streamKeyOverridden"`
HideViewerCount bool `json:"hideViewerCount"`
} }
type videoSettings struct { type videoSettings struct {

View file

@ -16,26 +16,27 @@ import (
) )
type webConfigResponse struct { type webConfigResponse struct {
AppearanceVariables map[string]string `json:"appearanceVariables"` AppearanceVariables map[string]string `json:"appearanceVariables"`
Name string `json:"name"` Name string `json:"name"`
CustomStyles string `json:"customStyles"` CustomStyles string `json:"customStyles"`
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
OfflineMessage string `json:"offlineMessage"` OfflineMessage string `json:"offlineMessage"`
Logo string `json:"logo"` Logo string `json:"logo"`
Version string `json:"version"` Version string `json:"version"`
SocketHostOverride string `json:"socketHostOverride,omitempty"` SocketHostOverride string `json:"socketHostOverride,omitempty"`
ExtraPageContent string `json:"extraPageContent"` ExtraPageContent string `json:"extraPageContent"`
Summary string `json:"summary"` Summary string `json:"summary"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
SocialHandles []models.SocialHandle `json:"socialHandles"` SocialHandles []models.SocialHandle `json:"socialHandles"`
ExternalActions []models.ExternalAction `json:"externalActions"` ExternalActions []models.ExternalAction `json:"externalActions"`
Notifications notificationsConfigResponse `json:"notifications"` Notifications notificationsConfigResponse `json:"notifications"`
Federation federationConfigResponse `json:"federation"` Federation federationConfigResponse `json:"federation"`
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"` MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
HideViewerCount bool `json:"hideViewerCount"` HideViewerCount bool `json:"hideViewerCount"`
ChatDisabled bool `json:"chatDisabled"` ChatDisabled bool `json:"chatDisabled"`
NSFW bool `json:"nsfw"` ChatSpamProtectionDisabled bool `json:"chatSpamProtectionDisabled"`
Authentication authenticationConfigResponse `json:"authentication"` NSFW bool `json:"nsfw"`
Authentication authenticationConfigResponse `json:"authentication"`
} }
type federationConfigResponse struct { type federationConfigResponse struct {
@ -118,26 +119,27 @@ func getConfigResponse() webConfigResponse {
} }
return webConfigResponse{ return webConfigResponse{
Name: data.GetServerName(), Name: data.GetServerName(),
Summary: serverSummary, Summary: serverSummary,
OfflineMessage: offlineMessage, OfflineMessage: offlineMessage,
Logo: "/logo", Logo: "/logo",
Tags: data.GetServerMetadataTags(), Tags: data.GetServerMetadataTags(),
Version: config.GetReleaseString(), Version: config.GetReleaseString(),
NSFW: data.GetNSFW(), NSFW: data.GetNSFW(),
SocketHostOverride: data.GetWebsocketOverrideHost(), SocketHostOverride: data.GetWebsocketOverrideHost(),
ExtraPageContent: pageContent, ExtraPageContent: pageContent,
StreamTitle: data.GetStreamTitle(), StreamTitle: data.GetStreamTitle(),
SocialHandles: socialHandles, SocialHandles: socialHandles,
ChatDisabled: data.GetChatDisabled(), ChatDisabled: data.GetChatDisabled(),
ExternalActions: data.GetExternalActions(), ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(),
CustomStyles: data.GetCustomStyles(), ExternalActions: data.GetExternalActions(),
MaxSocketPayloadSize: config.MaxSocketPayloadSize, CustomStyles: data.GetCustomStyles(),
Federation: federationResponse, MaxSocketPayloadSize: config.MaxSocketPayloadSize,
Notifications: notificationsResponse, Federation: federationResponse,
Authentication: authenticationResponse, Notifications: notificationsResponse,
AppearanceVariables: data.GetCustomColorVariableValues(), Authentication: authenticationResponse,
HideViewerCount: data.GetHideViewerCount(), AppearanceVariables: data.GetCustomColorVariableValues(),
HideViewerCount: data.GetHideViewerCount(),
} }
} }

View file

@ -13,19 +13,21 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"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/data"
"github.com/owncast/owncast/core/user" "github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/geoip" "github.com/owncast/owncast/geoip"
) )
// Client represents a single chat client. // Client represents a single chat client.
type Client struct { type Client struct {
ConnectedAt time.Time `json:"connectedAt"` ConnectedAt time.Time `json:"connectedAt"`
timeoutTimer *time.Timer timeoutTimer *time.Timer
rateLimiter *rate.Limiter rateLimiter *rate.Limiter
conn *websocket.Conn messageFilter *ChatMessageFilter
User *user.User `json:"user"` conn *websocket.Conn
server *Server User *user.User `json:"user"`
Geo *geoip.GeoDetails `json:"geo"` server *Server
Geo *geoip.GeoDetails `json:"geo"`
// Buffered channel of outbound messages. // Buffered channel of outbound messages.
send chan []byte send chan []byte
accessToken string accessToken string
@ -90,6 +92,7 @@ func (c *Client) readPump() {
// Allow 3 messages every two seconds. // Allow 3 messages every two seconds.
limit := rate.Every(2 * time.Second / 3) limit := rate.Every(2 * time.Second / 3)
c.rateLimiter = rate.NewLimiter(limit, 1) c.rateLimiter = rate.NewLimiter(limit, 1)
c.messageFilter = NewMessageFilter()
defer func() { defer func() {
c.close() c.close()
@ -129,6 +132,12 @@ func (c *Client) readPump() {
continue continue
} }
// Check if this message passes the optional language filter
if data.GetChatSlurFilterEnabled() && !c.messageFilter.Allow(string(message)) {
c.sendAction("Sorry, that message contained language that is not allowed in this chat.")
continue
}
message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space)) message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space))
c.handleEvent(message) c.handleEvent(message)
} }
@ -200,7 +209,13 @@ func (c *Client) close() {
} }
func (c *Client) passesRateLimit() bool { func (c *Client) passesRateLimit() bool {
return c.User.IsModerator() || (c.rateLimiter.Allow() && !c.inTimeout) // If spam rate limiting is disabled, or the user is a moderator, always
// allow the message.
if !data.GetChatSpamProtectionEnabled() || c.User.IsModerator() {
return true
}
return (c.rateLimiter.Allow() && !c.inTimeout)
} }
func (c *Client) startChatRejectionTimeout() { func (c *Client) startChatRejectionTimeout() {

View file

@ -0,0 +1,18 @@
package chat
import (
goaway "github.com/TwiN/go-away"
)
// ChatMessageFilter is a allow/deny chat message filter.
type ChatMessageFilter struct{}
// NewMessageFilter will return an instance of the chat message filter.
func NewMessageFilter() *ChatMessageFilter {
return &ChatMessageFilter{}
}
// Allow will test if this message should be allowed to be sent.
func (*ChatMessageFilter) Allow(message string) bool {
return !goaway.IsProfane(message)
}

View file

@ -0,0 +1,39 @@
package chat
import (
"testing"
)
func TestFiltering(t *testing.T) {
filter := NewMessageFilter()
filteredTestMessages := []string{
"Hello, fucking world!",
"Suck my dick",
"Eat my ass",
"fuck this shit",
"@$$h073",
"F u C k th1$ $h!t",
"u r fag",
"fucking sucks",
}
unfilteredTestMessages := []string{
"bass fish",
"assumptions",
}
for _, m := range filteredTestMessages {
result := filter.Allow(m)
if result {
t.Errorf("%s should be seen as a filtered profane message", m)
}
}
for _, m := range unfilteredTestMessages {
result := filter.Allow(m)
if !result {
t.Errorf("%s should not be filtered", m)
}
}
}

View file

@ -59,6 +59,8 @@ const (
suggestedUsernamesKey = "suggested_usernames" suggestedUsernamesKey = "suggested_usernames"
chatJoinMessagesEnabledKey = "chat_join_messages_enabled" chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode" chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
chatSpamProtectionEnabledKey = "chat_spam_protection_enabled"
chatSlurFilterEnabledKey = "chat_slur_filter_enabled"
notificationsEnabledKey = "notifications_enabled" notificationsEnabledKey = "notifications_enabled"
discordConfigurationKey = "discord_configuration" discordConfigurationKey = "discord_configuration"
browserPushConfigurationKey = "browser_push_configuration" browserPushConfigurationKey = "browser_push_configuration"
@ -528,6 +530,36 @@ func GetChatEstbalishedUsersOnlyMode() bool {
return false return false
} }
// SetChatSpamProtectionEnabled will enable chat spam protection if set to true.
func SetChatSpamProtectionEnabled(enabled bool) error {
return _datastore.SetBool(chatSpamProtectionEnabledKey, enabled)
}
// GetChatSpamProtectionEnabled will return if chat spam protection is enabled.
func GetChatSpamProtectionEnabled() bool {
enabled, err := _datastore.GetBool(chatSpamProtectionEnabledKey)
if err == nil {
return enabled
}
return true
}
// SetChatSlurFilterEnabled will enable the chat slur filter.
func SetChatSlurFilterEnabled(enabled bool) error {
return _datastore.SetBool(chatSlurFilterEnabledKey, enabled)
}
// GetChatSlurFilterEnabled will return if the chat slur filter is enabled.
func GetChatSlurFilterEnabled() bool {
enabled, err := _datastore.GetBool(chatSlurFilterEnabledKey)
if err == nil {
return enabled
}
return false
}
// GetExternalActions will return the registered external actions. // GetExternalActions will return the registered external actions.
func GetExternalActions() []models.ExternalAction { func GetExternalActions() []models.ExternalAction {
configEntry, err := _datastore.Get(externalActionsKey) configEntry, err := _datastore.Get(externalActionsKey)

1
go.mod
View file

@ -59,6 +59,7 @@ require (
require github.com/SherClockHolmes/webpush-go v1.3.0 require github.com/SherClockHolmes/webpush-go v1.3.0
require ( require (
github.com/TwiN/go-away v1.6.13 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect

2
go.sum
View file

@ -2,6 +2,8 @@ github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWG
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
github.com/TwiN/go-away v1.6.13 h1:aB6l/FPXmA5ds+V7I9zdhxzpsLLUvVtEuS++iU/ZmgE=
github.com/TwiN/go-away v1.6.13/go.mod h1:MpvIC9Li3minq+CGgbgUDvQ9tDaeW35k5IXZrF9MVas=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=

View file

@ -210,6 +210,11 @@ func Start() error {
// Set the suggested chat usernames that will be assigned automatically // Set the suggested chat usernames that will be assigned automatically
http.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)) http.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList))
// Enable or disable chat spam protection
http.HandleFunc("/api/admin/config/chat/spamprotectionenabled", middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled))
http.HandleFunc("/api/admin/config/chat/slurfilterenabled", middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled))
// Set video codec // Set video codec
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec)) http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))

View file

@ -1,4 +1,4 @@
import { Typography } from 'antd'; import { Col, Row, Typography } from 'antd';
import React, { ReactElement, useContext, useEffect, useState } from 'react'; import React, { ReactElement, useContext, useEffect, useState } from 'react';
import { TEXTFIELD_TYPE_TEXTAREA } from '../../components/admin/TextField'; import { TEXTFIELD_TYPE_TEXTAREA } from '../../components/admin/TextField';
import { TextFieldWithSubmit } from '../../components/admin/TextFieldWithSubmit'; import { TextFieldWithSubmit } from '../../components/admin/TextFieldWithSubmit';
@ -16,6 +16,7 @@ import {
API_CHAT_FORBIDDEN_USERNAMES, API_CHAT_FORBIDDEN_USERNAMES,
API_CHAT_SUGGESTED_USERNAMES, API_CHAT_SUGGESTED_USERNAMES,
FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED, FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED,
FIELD_PROPS_ENABLE_CHAT_SLUR_FILTER,
CHAT_ESTABLISHED_USER_MODE, CHAT_ESTABLISHED_USER_MODE,
FIELD_PROPS_DISABLE_CHAT, FIELD_PROPS_DISABLE_CHAT,
postConfigUpdateToAPI, postConfigUpdateToAPI,
@ -23,6 +24,7 @@ import {
TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES, TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES,
TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES, TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES,
TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE, TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE,
FIELD_PROPS_ENABLE_SPAM_PROTECTION,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
@ -43,6 +45,8 @@ export default function ConfigChat() {
instanceDetails, instanceDetails,
suggestedUsernames, suggestedUsernames,
chatEstablishedUserMode, chatEstablishedUserMode,
chatSpamProtectionEnabled,
chatSlurFilterEnabled,
} = serverConfig; } = serverConfig;
const { welcomeMessage } = instanceDetails; const { welcomeMessage } = instanceDetails;
@ -65,6 +69,14 @@ export default function ConfigChat() {
handleFieldChange({ fieldName: 'chatEstablishedUserMode', value: enabled }); handleFieldChange({ fieldName: 'chatEstablishedUserMode', value: enabled });
} }
function handleChatSpamProtectionChange(enabled: boolean) {
handleFieldChange({ fieldName: 'chatSpamProtectionEnabled', value: enabled });
}
function handleChatSlurFilterChange(enabled: boolean) {
handleFieldChange({ fieldName: 'chatSlurFilterEnabled', value: enabled });
}
function resetForbiddenUsernameState() { function resetForbiddenUsernameState() {
setForbiddenUsernameSaveState(null); setForbiddenUsernameSaveState(null);
} }
@ -155,6 +167,8 @@ export default function ConfigChat() {
suggestedUsernames, suggestedUsernames,
welcomeMessage, welcomeMessage,
chatEstablishedUserMode, chatEstablishedUserMode,
chatSpamProtectionEnabled,
chatSlurFilterEnabled,
}); });
}, [serverConfig]); }, [serverConfig]);
@ -165,60 +179,80 @@ export default function ConfigChat() {
return ( return (
<div className="config-server-details-form"> <div className="config-server-details-form">
<Title>Chat Settings</Title> <Title>Chat Settings</Title>
<div className="form-module config-server-details-container"> <Row gutter={[45, 16]}>
<ToggleSwitch <Col md={24} lg={12}>
fieldName="chatDisabled" <div className="form-module">
{...FIELD_PROPS_DISABLE_CHAT} <ToggleSwitch
checked={!formDataValues.chatDisabled} fieldName="chatDisabled"
reversed {...FIELD_PROPS_DISABLE_CHAT}
onChange={handleChatDisableChange} checked={!formDataValues.chatDisabled}
/> reversed
<ToggleSwitch onChange={handleChatDisableChange}
fieldName="chatJoinMessagesEnabled" />
{...FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED} <ToggleSwitch
checked={formDataValues.chatJoinMessagesEnabled} fieldName="chatJoinMessagesEnabled"
onChange={handleChatJoinMessagesEnabledChange} {...FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED}
/> checked={formDataValues.chatJoinMessagesEnabled}
<ToggleSwitch onChange={handleChatJoinMessagesEnabledChange}
fieldName="chatEstablishedUserMode" />
{...CHAT_ESTABLISHED_USER_MODE} <TextFieldWithSubmit
checked={formDataValues.chatEstablishedUserMode} fieldName="welcomeMessage"
onChange={handleEstablishedUserModeChange} {...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE}
/> type={TEXTFIELD_TYPE_TEXTAREA}
<TextFieldWithSubmit value={formDataValues.welcomeMessage}
fieldName="welcomeMessage" initialValue={welcomeMessage}
{...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE} onChange={handleFieldChange}
type={TEXTFIELD_TYPE_TEXTAREA} />
value={formDataValues.welcomeMessage} <br />
initialValue={welcomeMessage} <br />
onChange={handleFieldChange} <EditValueArray
/> title={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.label}
<br /> placeholder={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.placeholder}
<br /> description={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.tip}
<EditValueArray values={formDataValues.forbiddenUsernames}
title={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.label} handleDeleteIndex={handleDeleteForbiddenUsernameIndex}
placeholder={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.placeholder} handleCreateString={handleCreateForbiddenUsername}
description={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.tip} submitStatus={forbiddenUsernameSaveState}
values={formDataValues.forbiddenUsernames} />
handleDeleteIndex={handleDeleteForbiddenUsernameIndex} <br />
handleCreateString={handleCreateForbiddenUsername} <br />
submitStatus={forbiddenUsernameSaveState} <EditValueArray
/> title={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.label}
<br /> placeholder={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.placeholder}
<br /> description={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.tip}
<EditValueArray values={formDataValues.suggestedUsernames}
title={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.label} handleDeleteIndex={handleDeleteSuggestedUsernameIndex}
placeholder={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.placeholder} handleCreateString={handleCreateSuggestedUsername}
description={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.tip} submitStatus={suggestedUsernameSaveState}
values={formDataValues.suggestedUsernames} continuousStatusMessage={getSuggestedUsernamesLimitWarning(
handleDeleteIndex={handleDeleteSuggestedUsernameIndex} formDataValues.suggestedUsernames.length,
handleCreateString={handleCreateSuggestedUsername} )}
submitStatus={suggestedUsernameSaveState} />
continuousStatusMessage={getSuggestedUsernamesLimitWarning( </div>
formDataValues.suggestedUsernames.length, </Col>
)} <Col md={24} lg={12}>
/> <div className="form-module">
</div> <ToggleSwitch
fieldName="chatSpamProtectionEnabled"
{...FIELD_PROPS_ENABLE_SPAM_PROTECTION}
checked={formDataValues.chatSpamProtectionEnabled}
onChange={handleChatSpamProtectionChange}
/>
<ToggleSwitch
fieldName="chatEstablishedUserMode"
{...CHAT_ESTABLISHED_USER_MODE}
checked={formDataValues.chatEstablishedUserMode}
onChange={handleEstablishedUserModeChange}
/>
<ToggleSwitch
fieldName="chatSlurFilterEnabled"
{...FIELD_PROPS_ENABLE_CHAT_SLUR_FILTER}
checked={formDataValues.chatSlurFilterEnabled}
onChange={handleChatSlurFilterChange}
/>
</div>
</Col>
</Row>
</div> </div>
); );
} }

View file

@ -152,6 +152,8 @@ export interface ConfigDetails {
forbiddenUsernames: string[]; forbiddenUsernames: string[];
suggestedUsernames: string[]; suggestedUsernames: string[];
chatDisabled: boolean; chatDisabled: boolean;
chatSpamProtectionEnabled: boolean;
chatSlurFilterEnabled: boolean;
federation: Federation; federation: Federation;
notifications: NotificationsConfig; notifications: NotificationsConfig;
chatJoinMessagesEnabled: boolean; chatJoinMessagesEnabled: boolean;

View file

@ -38,6 +38,8 @@ const API_HIDE_VIEWER_COUNT = '/hideviewercount';
const API_CHAT_DISABLE = '/chat/disable'; const API_CHAT_DISABLE = '/chat/disable';
const API_CHAT_JOIN_MESSAGES_ENABLED = '/chat/joinmessagesenabled'; const API_CHAT_JOIN_MESSAGES_ENABLED = '/chat/joinmessagesenabled';
const API_CHAT_ESTABLISHED_MODE = '/chat/establishedusermode'; const API_CHAT_ESTABLISHED_MODE = '/chat/establishedusermode';
const API_CHAT_SPAM_PROTECTION_ENABLED = '/chat/spamprotectionenabled';
const API_CHAT_SLUR_FILTER_ENABLED = '/chat/slurfilterenabled';
const API_DISABLE_SEARCH_INDEXING = '/disablesearchindexing'; const API_DISABLE_SEARCH_INDEXING = '/disablesearchindexing';
const API_SOCKET_HOST_OVERRIDE = '/sockethostoverride'; const API_SOCKET_HOST_OVERRIDE = '/sockethostoverride';
const API_VIDEO_SERVING_ENDPOINT = '/videoservingendpoint'; const API_VIDEO_SERVING_ENDPOINT = '/videoservingendpoint';
@ -258,6 +260,14 @@ export const FIELD_PROPS_DISABLE_CHAT = {
useSubmit: true, useSubmit: true,
}; };
export const FIELD_PROPS_ENABLE_SPAM_PROTECTION = {
apiPath: API_CHAT_SPAM_PROTECTION_ENABLED,
configPath: '',
label: 'Spam Protection',
tip: 'Limits how quickly messages can be sent to prevent spamming.',
useSubmit: true,
};
export const FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED = { export const FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED = {
apiPath: API_CHAT_JOIN_MESSAGES_ENABLED, apiPath: API_CHAT_JOIN_MESSAGES_ENABLED,
configPath: '', configPath: '',
@ -266,6 +276,14 @@ export const FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED = {
useSubmit: true, useSubmit: true,
}; };
export const FIELD_PROPS_ENABLE_CHAT_SLUR_FILTER = {
apiPath: API_CHAT_SLUR_FILTER_ENABLED,
configPath: '',
label: 'Chat language filter',
tip: 'Filters out messages that contain offensive language.',
useSubmit: true,
};
export const CHAT_ESTABLISHED_USER_MODE = { export const CHAT_ESTABLISHED_USER_MODE = {
apiPath: API_CHAT_ESTABLISHED_MODE, apiPath: API_CHAT_ESTABLISHED_MODE,
configPath: '', configPath: '',

View file

@ -69,6 +69,8 @@ const initialServerConfigState: ConfigDetails = {
forbiddenUsernames: [], forbiddenUsernames: [],
suggestedUsernames: [], suggestedUsernames: [],
chatDisabled: false, chatDisabled: false,
chatSpamProtectionEnabled: true,
chatSlurFilterEnabled: false,
chatJoinMessagesEnabled: true, chatJoinMessagesEnabled: true,
chatEstablishedUserMode: false, chatEstablishedUserMode: false,
hideViewerCount: false, hideViewerCount: false,