mirror of
				https://github.com/owncast/owncast.git
				synced 2025-10-31 18:18:06 +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") | 	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") | ||||||
|  | |||||||
| @ -61,6 +61,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { | |||||||
| 		SocketHostOverride:        data.GetWebsocketOverrideHost(), | 		SocketHostOverride:        data.GetWebsocketOverrideHost(), | ||||||
| 		VideoServingEndpoint:      data.GetVideoServingEndpoint(), | 		VideoServingEndpoint:      data.GetVideoServingEndpoint(), | ||||||
| 		ChatEstablishedUserMode:   data.GetChatEstbalishedUsersOnlyMode(), | 		ChatEstablishedUserMode:   data.GetChatEstbalishedUsersOnlyMode(), | ||||||
|  | 		ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(), | ||||||
|  | 		ChatSlurFilterEnabled:     data.GetChatSlurFilterEnabled(), | ||||||
| 		HideViewerCount:           data.GetHideViewerCount(), | 		HideViewerCount:           data.GetHideViewerCount(), | ||||||
| 		DisableSearchIndexing:     data.GetDisableSearchIndexing(), | 		DisableSearchIndexing:     data.GetDisableSearchIndexing(), | ||||||
| 		VideoSettings: videoSettings{ | 		VideoSettings: videoSettings{ | ||||||
| @ -122,6 +124,8 @@ type serverConfigAdminResponse struct { | |||||||
| 	ChatDisabled              bool                        `json:"chatDisabled"` | 	ChatDisabled              bool                        `json:"chatDisabled"` | ||||||
| 	ChatJoinMessagesEnabled   bool                        `json:"chatJoinMessagesEnabled"` | 	ChatJoinMessagesEnabled   bool                        `json:"chatJoinMessagesEnabled"` | ||||||
| 	ChatEstablishedUserMode   bool                        `json:"chatEstablishedUserMode"` | 	ChatEstablishedUserMode   bool                        `json:"chatEstablishedUserMode"` | ||||||
|  | 	ChatSpamProtectionEnabled bool                        `json:"chatSpamProtectionEnabled"` | ||||||
|  | 	ChatSlurFilterEnabled     bool                        `json:"chatSlurFilterEnabled"` | ||||||
| 	DisableSearchIndexing     bool                        `json:"disableSearchIndexing"` | 	DisableSearchIndexing     bool                        `json:"disableSearchIndexing"` | ||||||
| 	StreamKeyOverridden       bool                        `json:"streamKeyOverridden"` | 	StreamKeyOverridden       bool                        `json:"streamKeyOverridden"` | ||||||
| 	HideViewerCount           bool                        `json:"hideViewerCount"` | 	HideViewerCount           bool                        `json:"hideViewerCount"` | ||||||
|  | |||||||
| @ -34,6 +34,7 @@ type webConfigResponse struct { | |||||||
| 	MaxSocketPayloadSize       int                          `json:"maxSocketPayloadSize"` | 	MaxSocketPayloadSize       int                          `json:"maxSocketPayloadSize"` | ||||||
| 	HideViewerCount            bool                         `json:"hideViewerCount"` | 	HideViewerCount            bool                         `json:"hideViewerCount"` | ||||||
| 	ChatDisabled               bool                         `json:"chatDisabled"` | 	ChatDisabled               bool                         `json:"chatDisabled"` | ||||||
|  | 	ChatSpamProtectionDisabled bool                         `json:"chatSpamProtectionDisabled"` | ||||||
| 	NSFW                       bool                         `json:"nsfw"` | 	NSFW                       bool                         `json:"nsfw"` | ||||||
| 	Authentication             authenticationConfigResponse `json:"authentication"` | 	Authentication             authenticationConfigResponse `json:"authentication"` | ||||||
| } | } | ||||||
| @ -130,6 +131,7 @@ func getConfigResponse() webConfigResponse { | |||||||
| 		StreamTitle:                data.GetStreamTitle(), | 		StreamTitle:                data.GetStreamTitle(), | ||||||
| 		SocialHandles:              socialHandles, | 		SocialHandles:              socialHandles, | ||||||
| 		ChatDisabled:               data.GetChatDisabled(), | 		ChatDisabled:               data.GetChatDisabled(), | ||||||
|  | 		ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(), | ||||||
| 		ExternalActions:            data.GetExternalActions(), | 		ExternalActions:            data.GetExternalActions(), | ||||||
| 		CustomStyles:               data.GetCustomStyles(), | 		CustomStyles:               data.GetCustomStyles(), | ||||||
| 		MaxSocketPayloadSize:       config.MaxSocketPayloadSize, | 		MaxSocketPayloadSize:       config.MaxSocketPayloadSize, | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ 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" | ||||||
| ) | ) | ||||||
| @ -22,6 +23,7 @@ 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 | ||||||
|  | 	messageFilter *ChatMessageFilter | ||||||
| 	conn          *websocket.Conn | 	conn          *websocket.Conn | ||||||
| 	User          *user.User `json:"user"` | 	User          *user.User `json:"user"` | ||||||
| 	server        *Server | 	server        *Server | ||||||
| @ -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() { | ||||||
|  | |||||||
							
								
								
									
										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" | 	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
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -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
									
									
									
									
									
								
							
							
						
						
									
										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/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= | ||||||
|  | |||||||
| @ -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)) | ||||||
|  |  | ||||||
|  | |||||||
| @ -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,7 +179,9 @@ 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]}> | ||||||
|  |         <Col md={24} lg={12}> | ||||||
|  |           <div className="form-module"> | ||||||
|             <ToggleSwitch |             <ToggleSwitch | ||||||
|               fieldName="chatDisabled" |               fieldName="chatDisabled" | ||||||
|               {...FIELD_PROPS_DISABLE_CHAT} |               {...FIELD_PROPS_DISABLE_CHAT} | ||||||
| @ -179,12 +195,6 @@ export default function ConfigChat() { | |||||||
|               checked={formDataValues.chatJoinMessagesEnabled} |               checked={formDataValues.chatJoinMessagesEnabled} | ||||||
|               onChange={handleChatJoinMessagesEnabledChange} |               onChange={handleChatJoinMessagesEnabledChange} | ||||||
|             /> |             /> | ||||||
|         <ToggleSwitch |  | ||||||
|           fieldName="chatEstablishedUserMode" |  | ||||||
|           {...CHAT_ESTABLISHED_USER_MODE} |  | ||||||
|           checked={formDataValues.chatEstablishedUserMode} |  | ||||||
|           onChange={handleEstablishedUserModeChange} |  | ||||||
|         /> |  | ||||||
|             <TextFieldWithSubmit |             <TextFieldWithSubmit | ||||||
|               fieldName="welcomeMessage" |               fieldName="welcomeMessage" | ||||||
|               {...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE} |               {...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE} | ||||||
| @ -219,6 +229,30 @@ export default function ConfigChat() { | |||||||
|               )} |               )} | ||||||
|             /> |             /> | ||||||
|           </div> |           </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> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -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; | ||||||
|  | |||||||
| @ -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: '', | ||||||
|  | |||||||
| @ -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, | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Gabe Kangas
					Gabe Kangas