mirror of
				https://github.com/owncast/owncast.git
				synced 2025-10-31 10:08:10 +08:00 
			
		
		
		
	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:
		| @ -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") | ||||
|  | ||||
| @ -61,6 +61,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { | ||||
| 		SocketHostOverride:        data.GetWebsocketOverrideHost(), | ||||
| 		VideoServingEndpoint:      data.GetVideoServingEndpoint(), | ||||
| 		ChatEstablishedUserMode:   data.GetChatEstbalishedUsersOnlyMode(), | ||||
| 		ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(), | ||||
| 		ChatSlurFilterEnabled:     data.GetChatSlurFilterEnabled(), | ||||
| 		HideViewerCount:           data.GetHideViewerCount(), | ||||
| 		DisableSearchIndexing:     data.GetDisableSearchIndexing(), | ||||
| 		VideoSettings: videoSettings{ | ||||
| @ -122,6 +124,8 @@ type serverConfigAdminResponse struct { | ||||
| 	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"` | ||||
|  | ||||
| @ -34,6 +34,7 @@ type webConfigResponse struct { | ||||
| 	MaxSocketPayloadSize       int                          `json:"maxSocketPayloadSize"` | ||||
| 	HideViewerCount            bool                         `json:"hideViewerCount"` | ||||
| 	ChatDisabled               bool                         `json:"chatDisabled"` | ||||
| 	ChatSpamProtectionDisabled bool                         `json:"chatSpamProtectionDisabled"` | ||||
| 	NSFW                       bool                         `json:"nsfw"` | ||||
| 	Authentication             authenticationConfigResponse `json:"authentication"` | ||||
| } | ||||
| @ -130,6 +131,7 @@ func getConfigResponse() webConfigResponse { | ||||
| 		StreamTitle:                data.GetStreamTitle(), | ||||
| 		SocialHandles:              socialHandles, | ||||
| 		ChatDisabled:               data.GetChatDisabled(), | ||||
| 		ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(), | ||||
| 		ExternalActions:            data.GetExternalActions(), | ||||
| 		CustomStyles:               data.GetCustomStyles(), | ||||
| 		MaxSocketPayloadSize:       config.MaxSocketPayloadSize, | ||||
|  | ||||
| @ -13,6 +13,7 @@ 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" | ||||
| ) | ||||
| @ -22,6 +23,7 @@ type Client struct { | ||||
| 	ConnectedAt   time.Time `json:"connectedAt"` | ||||
| 	timeoutTimer  *time.Timer | ||||
| 	rateLimiter   *rate.Limiter | ||||
| 	messageFilter *ChatMessageFilter | ||||
| 	conn          *websocket.Conn | ||||
| 	User          *user.User `json:"user"` | ||||
| 	server        *Server | ||||
| @ -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() { | ||||
|  | ||||
							
								
								
									
										18
									
								
								core/chat/messageFilter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								core/chat/messageFilter.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										39
									
								
								core/chat/messageFilter_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								core/chat/messageFilter_test.go
									
									
									
									
									
										Normal 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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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) | ||||
|  | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								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 | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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= | ||||
|  | ||||
| @ -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)) | ||||
|  | ||||
|  | ||||
| @ -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,7 +179,9 @@ export default function ConfigChat() { | ||||
|   return ( | ||||
|     <div className="config-server-details-form"> | ||||
|       <Title>Chat Settings</Title> | ||||
|       <div className="form-module config-server-details-container"> | ||||
|       <Row gutter={[45, 16]}> | ||||
|         <Col md={24} lg={12}> | ||||
|           <div className="form-module"> | ||||
|             <ToggleSwitch | ||||
|               fieldName="chatDisabled" | ||||
|               {...FIELD_PROPS_DISABLE_CHAT} | ||||
| @ -179,12 +195,6 @@ export default function ConfigChat() { | ||||
|               checked={formDataValues.chatJoinMessagesEnabled} | ||||
|               onChange={handleChatJoinMessagesEnabledChange} | ||||
|             /> | ||||
|         <ToggleSwitch | ||||
|           fieldName="chatEstablishedUserMode" | ||||
|           {...CHAT_ESTABLISHED_USER_MODE} | ||||
|           checked={formDataValues.chatEstablishedUserMode} | ||||
|           onChange={handleEstablishedUserModeChange} | ||||
|         /> | ||||
|             <TextFieldWithSubmit | ||||
|               fieldName="welcomeMessage" | ||||
|               {...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE} | ||||
| @ -219,6 +229,30 @@ export default function ConfigChat() { | ||||
|               )} | ||||
|             /> | ||||
|           </div> | ||||
|         </Col> | ||||
|         <Col md={24} lg={12}> | ||||
|           <div className="form-module"> | ||||
|             <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> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -152,6 +152,8 @@ export interface ConfigDetails { | ||||
|   forbiddenUsernames: string[]; | ||||
|   suggestedUsernames: string[]; | ||||
|   chatDisabled: boolean; | ||||
|   chatSpamProtectionEnabled: boolean; | ||||
|   chatSlurFilterEnabled: boolean; | ||||
|   federation: Federation; | ||||
|   notifications: NotificationsConfig; | ||||
|   chatJoinMessagesEnabled: boolean; | ||||
|  | ||||
| @ -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: '', | ||||
|  | ||||
| @ -69,6 +69,8 @@ const initialServerConfigState: ConfigDetails = { | ||||
|   forbiddenUsernames: [], | ||||
|   suggestedUsernames: [], | ||||
|   chatDisabled: false, | ||||
|   chatSpamProtectionEnabled: true, | ||||
|   chatSlurFilterEnabled: false, | ||||
|   chatJoinMessagesEnabled: true, | ||||
|   chatEstablishedUserMode: false, | ||||
|   hideViewerCount: false, | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Gabe Kangas
					Gabe Kangas