diff --git a/controllers/admin/config.go b/controllers/admin/config.go index 5fd78e7df..0648803ed 100644 --- a/controllers/admin/config.go +++ b/controllers/admin/config.go @@ -802,6 +802,42 @@ func SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) { 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 { if r.Method != controllers.POST { controllers.WriteSimpleResponse(w, false, r.Method+" not supported") diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index 2429fdfa1..43e2720df 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -49,20 +49,22 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { CustomJavascript: data.GetCustomJavascript(), AppearanceVariables: data.GetCustomColorVariableValues(), }, - FFmpegPath: ffmpeg, - AdminPassword: data.GetAdminPassword(), - StreamKeys: data.GetStreamKeys(), - StreamKeyOverridden: config.TemporaryStreamKey != "", - WebServerPort: config.WebServerPort, - WebServerIP: config.WebServerIP, - RTMPServerPort: data.GetRTMPPortNumber(), - ChatDisabled: data.GetChatDisabled(), - ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(), - SocketHostOverride: data.GetWebsocketOverrideHost(), - VideoServingEndpoint: data.GetVideoServingEndpoint(), - ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), - HideViewerCount: data.GetHideViewerCount(), - DisableSearchIndexing: data.GetDisableSearchIndexing(), + FFmpegPath: ffmpeg, + AdminPassword: data.GetAdminPassword(), + StreamKeys: data.GetStreamKeys(), + StreamKeyOverridden: config.TemporaryStreamKey != "", + WebServerPort: config.WebServerPort, + WebServerIP: config.WebServerIP, + RTMPServerPort: data.GetRTMPPortNumber(), + ChatDisabled: data.GetChatDisabled(), + ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(), + SocketHostOverride: data.GetWebsocketOverrideHost(), + VideoServingEndpoint: data.GetVideoServingEndpoint(), + ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), + ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(), + ChatSlurFilterEnabled: data.GetChatSlurFilterEnabled(), + HideViewerCount: data.GetHideViewerCount(), + DisableSearchIndexing: data.GetDisableSearchIndexing(), VideoSettings: videoSettings{ VideoQualityVariants: videoQualityVariants, LatencyLevel: data.GetStreamLatencyLevel().Level, @@ -100,31 +102,33 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { } type serverConfigAdminResponse struct { - InstanceDetails webConfigResponse `json:"instanceDetails"` - Notifications notificationsConfigResponse `json:"notifications"` - YP yp `json:"yp"` - FFmpegPath string `json:"ffmpegPath"` - AdminPassword string `json:"adminPassword"` - SocketHostOverride string `json:"socketHostOverride,omitempty"` - WebServerIP string `json:"webServerIP"` - VideoCodec string `json:"videoCodec"` - VideoServingEndpoint string `json:"videoServingEndpoint"` - S3 models.S3 `json:"s3"` - Federation federationConfigResponse `json:"federation"` - SupportedCodecs []string `json:"supportedCodecs"` - ExternalActions []models.ExternalAction `json:"externalActions"` - ForbiddenUsernames []string `json:"forbiddenUsernames"` - SuggestedUsernames []string `json:"suggestedUsernames"` - StreamKeys []models.StreamKey `json:"streamKeys"` - VideoSettings videoSettings `json:"videoSettings"` - RTMPServerPort int `json:"rtmpServerPort"` - WebServerPort int `json:"webServerPort"` - ChatDisabled bool `json:"chatDisabled"` - ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` - ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` - DisableSearchIndexing bool `json:"disableSearchIndexing"` - StreamKeyOverridden bool `json:"streamKeyOverridden"` - HideViewerCount bool `json:"hideViewerCount"` + InstanceDetails webConfigResponse `json:"instanceDetails"` + Notifications notificationsConfigResponse `json:"notifications"` + YP yp `json:"yp"` + FFmpegPath string `json:"ffmpegPath"` + AdminPassword string `json:"adminPassword"` + SocketHostOverride string `json:"socketHostOverride,omitempty"` + WebServerIP string `json:"webServerIP"` + VideoCodec string `json:"videoCodec"` + VideoServingEndpoint string `json:"videoServingEndpoint"` + S3 models.S3 `json:"s3"` + Federation federationConfigResponse `json:"federation"` + SupportedCodecs []string `json:"supportedCodecs"` + ExternalActions []models.ExternalAction `json:"externalActions"` + ForbiddenUsernames []string `json:"forbiddenUsernames"` + SuggestedUsernames []string `json:"suggestedUsernames"` + StreamKeys []models.StreamKey `json:"streamKeys"` + VideoSettings videoSettings `json:"videoSettings"` + RTMPServerPort int `json:"rtmpServerPort"` + WebServerPort int `json:"webServerPort"` + ChatDisabled bool `json:"chatDisabled"` + ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` + ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` + ChatSpamProtectionEnabled bool `json:"chatSpamProtectionEnabled"` + ChatSlurFilterEnabled bool `json:"chatSlurFilterEnabled"` + DisableSearchIndexing bool `json:"disableSearchIndexing"` + StreamKeyOverridden bool `json:"streamKeyOverridden"` + HideViewerCount bool `json:"hideViewerCount"` } type videoSettings struct { diff --git a/controllers/config.go b/controllers/config.go index ceec87692..a8ea9f599 100644 --- a/controllers/config.go +++ b/controllers/config.go @@ -16,26 +16,27 @@ import ( ) type webConfigResponse struct { - AppearanceVariables map[string]string `json:"appearanceVariables"` - Name string `json:"name"` - CustomStyles string `json:"customStyles"` - StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream - OfflineMessage string `json:"offlineMessage"` - Logo string `json:"logo"` - Version string `json:"version"` - SocketHostOverride string `json:"socketHostOverride,omitempty"` - ExtraPageContent string `json:"extraPageContent"` - Summary string `json:"summary"` - Tags []string `json:"tags"` - SocialHandles []models.SocialHandle `json:"socialHandles"` - ExternalActions []models.ExternalAction `json:"externalActions"` - Notifications notificationsConfigResponse `json:"notifications"` - Federation federationConfigResponse `json:"federation"` - MaxSocketPayloadSize int `json:"maxSocketPayloadSize"` - HideViewerCount bool `json:"hideViewerCount"` - ChatDisabled bool `json:"chatDisabled"` - NSFW bool `json:"nsfw"` - Authentication authenticationConfigResponse `json:"authentication"` + AppearanceVariables map[string]string `json:"appearanceVariables"` + Name string `json:"name"` + CustomStyles string `json:"customStyles"` + StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream + OfflineMessage string `json:"offlineMessage"` + Logo string `json:"logo"` + Version string `json:"version"` + SocketHostOverride string `json:"socketHostOverride,omitempty"` + ExtraPageContent string `json:"extraPageContent"` + Summary string `json:"summary"` + Tags []string `json:"tags"` + SocialHandles []models.SocialHandle `json:"socialHandles"` + ExternalActions []models.ExternalAction `json:"externalActions"` + Notifications notificationsConfigResponse `json:"notifications"` + Federation federationConfigResponse `json:"federation"` + MaxSocketPayloadSize int `json:"maxSocketPayloadSize"` + HideViewerCount bool `json:"hideViewerCount"` + ChatDisabled bool `json:"chatDisabled"` + ChatSpamProtectionDisabled bool `json:"chatSpamProtectionDisabled"` + NSFW bool `json:"nsfw"` + Authentication authenticationConfigResponse `json:"authentication"` } type federationConfigResponse struct { @@ -118,26 +119,27 @@ func getConfigResponse() webConfigResponse { } return webConfigResponse{ - Name: data.GetServerName(), - Summary: serverSummary, - OfflineMessage: offlineMessage, - Logo: "/logo", - Tags: data.GetServerMetadataTags(), - Version: config.GetReleaseString(), - NSFW: data.GetNSFW(), - SocketHostOverride: data.GetWebsocketOverrideHost(), - ExtraPageContent: pageContent, - StreamTitle: data.GetStreamTitle(), - SocialHandles: socialHandles, - ChatDisabled: data.GetChatDisabled(), - ExternalActions: data.GetExternalActions(), - CustomStyles: data.GetCustomStyles(), - MaxSocketPayloadSize: config.MaxSocketPayloadSize, - Federation: federationResponse, - Notifications: notificationsResponse, - Authentication: authenticationResponse, - AppearanceVariables: data.GetCustomColorVariableValues(), - HideViewerCount: data.GetHideViewerCount(), + Name: data.GetServerName(), + Summary: serverSummary, + OfflineMessage: offlineMessage, + Logo: "/logo", + Tags: data.GetServerMetadataTags(), + Version: config.GetReleaseString(), + NSFW: data.GetNSFW(), + SocketHostOverride: data.GetWebsocketOverrideHost(), + ExtraPageContent: pageContent, + StreamTitle: data.GetStreamTitle(), + SocialHandles: socialHandles, + ChatDisabled: data.GetChatDisabled(), + ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(), + ExternalActions: data.GetExternalActions(), + CustomStyles: data.GetCustomStyles(), + MaxSocketPayloadSize: config.MaxSocketPayloadSize, + Federation: federationResponse, + Notifications: notificationsResponse, + Authentication: authenticationResponse, + AppearanceVariables: data.GetCustomColorVariableValues(), + HideViewerCount: data.GetHideViewerCount(), } } diff --git a/core/chat/chatclient.go b/core/chat/chatclient.go index fac4ed491..df344f17b 100644 --- a/core/chat/chatclient.go +++ b/core/chat/chatclient.go @@ -13,19 +13,21 @@ import ( "github.com/gorilla/websocket" "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/geoip" ) // Client represents a single chat client. type Client struct { - ConnectedAt time.Time `json:"connectedAt"` - timeoutTimer *time.Timer - rateLimiter *rate.Limiter - conn *websocket.Conn - User *user.User `json:"user"` - server *Server - Geo *geoip.GeoDetails `json:"geo"` + ConnectedAt time.Time `json:"connectedAt"` + timeoutTimer *time.Timer + rateLimiter *rate.Limiter + messageFilter *ChatMessageFilter + conn *websocket.Conn + User *user.User `json:"user"` + server *Server + Geo *geoip.GeoDetails `json:"geo"` // Buffered channel of outbound messages. send chan []byte accessToken string @@ -90,6 +92,7 @@ func (c *Client) readPump() { // Allow 3 messages every two seconds. limit := rate.Every(2 * time.Second / 3) c.rateLimiter = rate.NewLimiter(limit, 1) + c.messageFilter = NewMessageFilter() defer func() { c.close() @@ -129,6 +132,12 @@ func (c *Client) readPump() { 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)) c.handleEvent(message) } @@ -200,7 +209,13 @@ func (c *Client) close() { } 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() { diff --git a/core/chat/messageFilter.go b/core/chat/messageFilter.go new file mode 100644 index 000000000..86d11ec02 --- /dev/null +++ b/core/chat/messageFilter.go @@ -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) +} diff --git a/core/chat/messageFilter_test.go b/core/chat/messageFilter_test.go new file mode 100644 index 000000000..37ca31823 --- /dev/null +++ b/core/chat/messageFilter_test.go @@ -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) + } + } +} diff --git a/core/data/config.go b/core/data/config.go index 47cc860af..ca440dc56 100644 --- a/core/data/config.go +++ b/core/data/config.go @@ -59,6 +59,8 @@ const ( suggestedUsernamesKey = "suggested_usernames" chatJoinMessagesEnabledKey = "chat_join_messages_enabled" chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode" + chatSpamProtectionEnabledKey = "chat_spam_protection_enabled" + chatSlurFilterEnabledKey = "chat_slur_filter_enabled" notificationsEnabledKey = "notifications_enabled" discordConfigurationKey = "discord_configuration" browserPushConfigurationKey = "browser_push_configuration" @@ -528,6 +530,36 @@ func GetChatEstbalishedUsersOnlyMode() bool { 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. func GetExternalActions() []models.ExternalAction { configEntry, err := _datastore.Get(externalActionsKey) diff --git a/go.mod b/go.mod index cce223231..354bd2c08 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( require github.com/SherClockHolmes/webpush-go v1.3.0 require ( + github.com/TwiN/go-away v1.6.13 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 72b79ab71..f858ad57e 100644 --- a/go.sum +++ b/go.sum @@ -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/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= 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/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= diff --git a/router/router.go b/router/router.go index c1866eb5f..0c6dfc53b 100644 --- a/router/router.go +++ b/router/router.go @@ -210,6 +210,11 @@ func Start() error { // Set the suggested chat usernames that will be assigned automatically 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 http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec)) diff --git a/web/pages/admin/config-chat.tsx b/web/pages/admin/config-chat.tsx index 3593b85ea..c3fb6a2d5 100644 --- a/web/pages/admin/config-chat.tsx +++ b/web/pages/admin/config-chat.tsx @@ -1,4 +1,4 @@ -import { Typography } from 'antd'; +import { Col, Row, Typography } from 'antd'; import React, { ReactElement, useContext, useEffect, useState } from 'react'; import { TEXTFIELD_TYPE_TEXTAREA } from '../../components/admin/TextField'; import { TextFieldWithSubmit } from '../../components/admin/TextFieldWithSubmit'; @@ -16,6 +16,7 @@ import { API_CHAT_FORBIDDEN_USERNAMES, API_CHAT_SUGGESTED_USERNAMES, FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED, + FIELD_PROPS_ENABLE_CHAT_SLUR_FILTER, CHAT_ESTABLISHED_USER_MODE, FIELD_PROPS_DISABLE_CHAT, postConfigUpdateToAPI, @@ -23,6 +24,7 @@ import { TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES, TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES, TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE, + FIELD_PROPS_ENABLE_SPAM_PROTECTION, } from '../../utils/config-constants'; import { ServerStatusContext } from '../../utils/server-status-context'; @@ -43,6 +45,8 @@ export default function ConfigChat() { instanceDetails, suggestedUsernames, chatEstablishedUserMode, + chatSpamProtectionEnabled, + chatSlurFilterEnabled, } = serverConfig; const { welcomeMessage } = instanceDetails; @@ -65,6 +69,14 @@ export default function ConfigChat() { 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() { setForbiddenUsernameSaveState(null); } @@ -155,6 +167,8 @@ export default function ConfigChat() { suggestedUsernames, welcomeMessage, chatEstablishedUserMode, + chatSpamProtectionEnabled, + chatSlurFilterEnabled, }); }, [serverConfig]); @@ -165,60 +179,80 @@ export default function ConfigChat() { return (
Chat Settings -
- - - - -
-
- -
-
- -
+ + +
+ + + +
+
+ +
+
+ +
+ + +
+ + + +
+ +
); } diff --git a/web/types/config-section.ts b/web/types/config-section.ts index 948a60375..1fec049dd 100644 --- a/web/types/config-section.ts +++ b/web/types/config-section.ts @@ -152,6 +152,8 @@ export interface ConfigDetails { forbiddenUsernames: string[]; suggestedUsernames: string[]; chatDisabled: boolean; + chatSpamProtectionEnabled: boolean; + chatSlurFilterEnabled: boolean; federation: Federation; notifications: NotificationsConfig; chatJoinMessagesEnabled: boolean; diff --git a/web/utils/config-constants.tsx b/web/utils/config-constants.tsx index 4d5f2e22f..d20e6bb19 100644 --- a/web/utils/config-constants.tsx +++ b/web/utils/config-constants.tsx @@ -38,6 +38,8 @@ const API_HIDE_VIEWER_COUNT = '/hideviewercount'; const API_CHAT_DISABLE = '/chat/disable'; const API_CHAT_JOIN_MESSAGES_ENABLED = '/chat/joinmessagesenabled'; 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_SOCKET_HOST_OVERRIDE = '/sockethostoverride'; const API_VIDEO_SERVING_ENDPOINT = '/videoservingendpoint'; @@ -258,6 +260,14 @@ export const FIELD_PROPS_DISABLE_CHAT = { 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 = { apiPath: API_CHAT_JOIN_MESSAGES_ENABLED, configPath: '', @@ -266,6 +276,14 @@ export const FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED = { 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 = { apiPath: API_CHAT_ESTABLISHED_MODE, configPath: '', diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index 2db00eeeb..991cdc625 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -69,6 +69,8 @@ const initialServerConfigState: ConfigDetails = { forbiddenUsernames: [], suggestedUsernames: [], chatDisabled: false, + chatSpamProtectionEnabled: true, + chatSlurFilterEnabled: false, chatJoinMessagesEnabled: true, chatEstablishedUserMode: false, hideViewerCount: false,