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") | ||||
| } | ||||
|  | ||||
| // 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") | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -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() { | ||||
|  | ||||
							
								
								
									
										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,60 +179,80 @@ export default function ConfigChat() { | ||||
|   return ( | ||||
|     <div className="config-server-details-form"> | ||||
|       <Title>Chat Settings</Title> | ||||
|       <div className="form-module config-server-details-container"> | ||||
|         <ToggleSwitch | ||||
|           fieldName="chatDisabled" | ||||
|           {...FIELD_PROPS_DISABLE_CHAT} | ||||
|           checked={!formDataValues.chatDisabled} | ||||
|           reversed | ||||
|           onChange={handleChatDisableChange} | ||||
|         /> | ||||
|         <ToggleSwitch | ||||
|           fieldName="chatJoinMessagesEnabled" | ||||
|           {...FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED} | ||||
|           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} | ||||
|           type={TEXTFIELD_TYPE_TEXTAREA} | ||||
|           value={formDataValues.welcomeMessage} | ||||
|           initialValue={welcomeMessage} | ||||
|           onChange={handleFieldChange} | ||||
|         /> | ||||
|         <br /> | ||||
|         <br /> | ||||
|         <EditValueArray | ||||
|           title={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.label} | ||||
|           placeholder={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.placeholder} | ||||
|           description={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.tip} | ||||
|           values={formDataValues.forbiddenUsernames} | ||||
|           handleDeleteIndex={handleDeleteForbiddenUsernameIndex} | ||||
|           handleCreateString={handleCreateForbiddenUsername} | ||||
|           submitStatus={forbiddenUsernameSaveState} | ||||
|         /> | ||||
|         <br /> | ||||
|         <br /> | ||||
|         <EditValueArray | ||||
|           title={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.label} | ||||
|           placeholder={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.placeholder} | ||||
|           description={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.tip} | ||||
|           values={formDataValues.suggestedUsernames} | ||||
|           handleDeleteIndex={handleDeleteSuggestedUsernameIndex} | ||||
|           handleCreateString={handleCreateSuggestedUsername} | ||||
|           submitStatus={suggestedUsernameSaveState} | ||||
|           continuousStatusMessage={getSuggestedUsernamesLimitWarning( | ||||
|             formDataValues.suggestedUsernames.length, | ||||
|           )} | ||||
|         /> | ||||
|       </div> | ||||
|       <Row gutter={[45, 16]}> | ||||
|         <Col md={24} lg={12}> | ||||
|           <div className="form-module"> | ||||
|             <ToggleSwitch | ||||
|               fieldName="chatDisabled" | ||||
|               {...FIELD_PROPS_DISABLE_CHAT} | ||||
|               checked={!formDataValues.chatDisabled} | ||||
|               reversed | ||||
|               onChange={handleChatDisableChange} | ||||
|             /> | ||||
|             <ToggleSwitch | ||||
|               fieldName="chatJoinMessagesEnabled" | ||||
|               {...FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED} | ||||
|               checked={formDataValues.chatJoinMessagesEnabled} | ||||
|               onChange={handleChatJoinMessagesEnabledChange} | ||||
|             /> | ||||
|             <TextFieldWithSubmit | ||||
|               fieldName="welcomeMessage" | ||||
|               {...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE} | ||||
|               type={TEXTFIELD_TYPE_TEXTAREA} | ||||
|               value={formDataValues.welcomeMessage} | ||||
|               initialValue={welcomeMessage} | ||||
|               onChange={handleFieldChange} | ||||
|             /> | ||||
|             <br /> | ||||
|             <br /> | ||||
|             <EditValueArray | ||||
|               title={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.label} | ||||
|               placeholder={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.placeholder} | ||||
|               description={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.tip} | ||||
|               values={formDataValues.forbiddenUsernames} | ||||
|               handleDeleteIndex={handleDeleteForbiddenUsernameIndex} | ||||
|               handleCreateString={handleCreateForbiddenUsername} | ||||
|               submitStatus={forbiddenUsernameSaveState} | ||||
|             /> | ||||
|             <br /> | ||||
|             <br /> | ||||
|             <EditValueArray | ||||
|               title={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.label} | ||||
|               placeholder={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.placeholder} | ||||
|               description={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.tip} | ||||
|               values={formDataValues.suggestedUsernames} | ||||
|               handleDeleteIndex={handleDeleteSuggestedUsernameIndex} | ||||
|               handleCreateString={handleCreateSuggestedUsername} | ||||
|               submitStatus={suggestedUsernameSaveState} | ||||
|               continuousStatusMessage={getSuggestedUsernamesLimitWarning( | ||||
|                 formDataValues.suggestedUsernames.length, | ||||
|               )} | ||||
|             /> | ||||
|           </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