mirror of
https://github.com/owncast/owncast.git
synced 2024-11-21 12:18:02 +03: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:
parent
04eaf8c20e
commit
a450e62397
14 changed files with 352 additions and 142 deletions
|
@ -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")
|
||||||
|
|
|
@ -49,20 +49,22 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
CustomJavascript: data.GetCustomJavascript(),
|
CustomJavascript: data.GetCustomJavascript(),
|
||||||
AppearanceVariables: data.GetCustomColorVariableValues(),
|
AppearanceVariables: data.GetCustomColorVariableValues(),
|
||||||
},
|
},
|
||||||
FFmpegPath: ffmpeg,
|
FFmpegPath: ffmpeg,
|
||||||
AdminPassword: data.GetAdminPassword(),
|
AdminPassword: data.GetAdminPassword(),
|
||||||
StreamKeys: data.GetStreamKeys(),
|
StreamKeys: data.GetStreamKeys(),
|
||||||
StreamKeyOverridden: config.TemporaryStreamKey != "",
|
StreamKeyOverridden: config.TemporaryStreamKey != "",
|
||||||
WebServerPort: config.WebServerPort,
|
WebServerPort: config.WebServerPort,
|
||||||
WebServerIP: config.WebServerIP,
|
WebServerIP: config.WebServerIP,
|
||||||
RTMPServerPort: data.GetRTMPPortNumber(),
|
RTMPServerPort: data.GetRTMPPortNumber(),
|
||||||
ChatDisabled: data.GetChatDisabled(),
|
ChatDisabled: data.GetChatDisabled(),
|
||||||
ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(),
|
ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(),
|
||||||
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
||||||
VideoServingEndpoint: data.GetVideoServingEndpoint(),
|
VideoServingEndpoint: data.GetVideoServingEndpoint(),
|
||||||
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
|
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
|
||||||
HideViewerCount: data.GetHideViewerCount(),
|
ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(),
|
||||||
DisableSearchIndexing: data.GetDisableSearchIndexing(),
|
ChatSlurFilterEnabled: data.GetChatSlurFilterEnabled(),
|
||||||
|
HideViewerCount: data.GetHideViewerCount(),
|
||||||
|
DisableSearchIndexing: data.GetDisableSearchIndexing(),
|
||||||
VideoSettings: videoSettings{
|
VideoSettings: videoSettings{
|
||||||
VideoQualityVariants: videoQualityVariants,
|
VideoQualityVariants: videoQualityVariants,
|
||||||
LatencyLevel: data.GetStreamLatencyLevel().Level,
|
LatencyLevel: data.GetStreamLatencyLevel().Level,
|
||||||
|
@ -100,31 +102,33 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverConfigAdminResponse struct {
|
type serverConfigAdminResponse struct {
|
||||||
InstanceDetails webConfigResponse `json:"instanceDetails"`
|
InstanceDetails webConfigResponse `json:"instanceDetails"`
|
||||||
Notifications notificationsConfigResponse `json:"notifications"`
|
Notifications notificationsConfigResponse `json:"notifications"`
|
||||||
YP yp `json:"yp"`
|
YP yp `json:"yp"`
|
||||||
FFmpegPath string `json:"ffmpegPath"`
|
FFmpegPath string `json:"ffmpegPath"`
|
||||||
AdminPassword string `json:"adminPassword"`
|
AdminPassword string `json:"adminPassword"`
|
||||||
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
||||||
WebServerIP string `json:"webServerIP"`
|
WebServerIP string `json:"webServerIP"`
|
||||||
VideoCodec string `json:"videoCodec"`
|
VideoCodec string `json:"videoCodec"`
|
||||||
VideoServingEndpoint string `json:"videoServingEndpoint"`
|
VideoServingEndpoint string `json:"videoServingEndpoint"`
|
||||||
S3 models.S3 `json:"s3"`
|
S3 models.S3 `json:"s3"`
|
||||||
Federation federationConfigResponse `json:"federation"`
|
Federation federationConfigResponse `json:"federation"`
|
||||||
SupportedCodecs []string `json:"supportedCodecs"`
|
SupportedCodecs []string `json:"supportedCodecs"`
|
||||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||||
ForbiddenUsernames []string `json:"forbiddenUsernames"`
|
ForbiddenUsernames []string `json:"forbiddenUsernames"`
|
||||||
SuggestedUsernames []string `json:"suggestedUsernames"`
|
SuggestedUsernames []string `json:"suggestedUsernames"`
|
||||||
StreamKeys []models.StreamKey `json:"streamKeys"`
|
StreamKeys []models.StreamKey `json:"streamKeys"`
|
||||||
VideoSettings videoSettings `json:"videoSettings"`
|
VideoSettings videoSettings `json:"videoSettings"`
|
||||||
RTMPServerPort int `json:"rtmpServerPort"`
|
RTMPServerPort int `json:"rtmpServerPort"`
|
||||||
WebServerPort int `json:"webServerPort"`
|
WebServerPort int `json:"webServerPort"`
|
||||||
ChatDisabled bool `json:"chatDisabled"`
|
ChatDisabled bool `json:"chatDisabled"`
|
||||||
ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"`
|
ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"`
|
||||||
ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"`
|
ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"`
|
||||||
DisableSearchIndexing bool `json:"disableSearchIndexing"`
|
ChatSpamProtectionEnabled bool `json:"chatSpamProtectionEnabled"`
|
||||||
StreamKeyOverridden bool `json:"streamKeyOverridden"`
|
ChatSlurFilterEnabled bool `json:"chatSlurFilterEnabled"`
|
||||||
HideViewerCount bool `json:"hideViewerCount"`
|
DisableSearchIndexing bool `json:"disableSearchIndexing"`
|
||||||
|
StreamKeyOverridden bool `json:"streamKeyOverridden"`
|
||||||
|
HideViewerCount bool `json:"hideViewerCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type videoSettings struct {
|
type videoSettings struct {
|
||||||
|
|
|
@ -16,26 +16,27 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type webConfigResponse struct {
|
type webConfigResponse struct {
|
||||||
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CustomStyles string `json:"customStyles"`
|
CustomStyles string `json:"customStyles"`
|
||||||
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
||||||
OfflineMessage string `json:"offlineMessage"`
|
OfflineMessage string `json:"offlineMessage"`
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
||||||
ExtraPageContent string `json:"extraPageContent"`
|
ExtraPageContent string `json:"extraPageContent"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
||||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||||
Notifications notificationsConfigResponse `json:"notifications"`
|
Notifications notificationsConfigResponse `json:"notifications"`
|
||||||
Federation federationConfigResponse `json:"federation"`
|
Federation federationConfigResponse `json:"federation"`
|
||||||
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
|
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
|
||||||
HideViewerCount bool `json:"hideViewerCount"`
|
HideViewerCount bool `json:"hideViewerCount"`
|
||||||
ChatDisabled bool `json:"chatDisabled"`
|
ChatDisabled bool `json:"chatDisabled"`
|
||||||
NSFW bool `json:"nsfw"`
|
ChatSpamProtectionDisabled bool `json:"chatSpamProtectionDisabled"`
|
||||||
Authentication authenticationConfigResponse `json:"authentication"`
|
NSFW bool `json:"nsfw"`
|
||||||
|
Authentication authenticationConfigResponse `json:"authentication"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type federationConfigResponse struct {
|
type federationConfigResponse struct {
|
||||||
|
@ -118,26 +119,27 @@ func getConfigResponse() webConfigResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
return webConfigResponse{
|
return webConfigResponse{
|
||||||
Name: data.GetServerName(),
|
Name: data.GetServerName(),
|
||||||
Summary: serverSummary,
|
Summary: serverSummary,
|
||||||
OfflineMessage: offlineMessage,
|
OfflineMessage: offlineMessage,
|
||||||
Logo: "/logo",
|
Logo: "/logo",
|
||||||
Tags: data.GetServerMetadataTags(),
|
Tags: data.GetServerMetadataTags(),
|
||||||
Version: config.GetReleaseString(),
|
Version: config.GetReleaseString(),
|
||||||
NSFW: data.GetNSFW(),
|
NSFW: data.GetNSFW(),
|
||||||
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
||||||
ExtraPageContent: pageContent,
|
ExtraPageContent: pageContent,
|
||||||
StreamTitle: data.GetStreamTitle(),
|
StreamTitle: data.GetStreamTitle(),
|
||||||
SocialHandles: socialHandles,
|
SocialHandles: socialHandles,
|
||||||
ChatDisabled: data.GetChatDisabled(),
|
ChatDisabled: data.GetChatDisabled(),
|
||||||
ExternalActions: data.GetExternalActions(),
|
ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(),
|
||||||
CustomStyles: data.GetCustomStyles(),
|
ExternalActions: data.GetExternalActions(),
|
||||||
MaxSocketPayloadSize: config.MaxSocketPayloadSize,
|
CustomStyles: data.GetCustomStyles(),
|
||||||
Federation: federationResponse,
|
MaxSocketPayloadSize: config.MaxSocketPayloadSize,
|
||||||
Notifications: notificationsResponse,
|
Federation: federationResponse,
|
||||||
Authentication: authenticationResponse,
|
Notifications: notificationsResponse,
|
||||||
AppearanceVariables: data.GetCustomColorVariableValues(),
|
Authentication: authenticationResponse,
|
||||||
HideViewerCount: data.GetHideViewerCount(),
|
AppearanceVariables: data.GetCustomColorVariableValues(),
|
||||||
|
HideViewerCount: data.GetHideViewerCount(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,19 +13,21 @@ import (
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
"github.com/owncast/owncast/core/user"
|
||||||
"github.com/owncast/owncast/geoip"
|
"github.com/owncast/owncast/geoip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents a single chat client.
|
// Client represents a single chat client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ConnectedAt time.Time `json:"connectedAt"`
|
ConnectedAt time.Time `json:"connectedAt"`
|
||||||
timeoutTimer *time.Timer
|
timeoutTimer *time.Timer
|
||||||
rateLimiter *rate.Limiter
|
rateLimiter *rate.Limiter
|
||||||
conn *websocket.Conn
|
messageFilter *ChatMessageFilter
|
||||||
User *user.User `json:"user"`
|
conn *websocket.Conn
|
||||||
server *Server
|
User *user.User `json:"user"`
|
||||||
Geo *geoip.GeoDetails `json:"geo"`
|
server *Server
|
||||||
|
Geo *geoip.GeoDetails `json:"geo"`
|
||||||
// Buffered channel of outbound messages.
|
// Buffered channel of outbound messages.
|
||||||
send chan []byte
|
send chan []byte
|
||||||
accessToken string
|
accessToken string
|
||||||
|
@ -90,6 +92,7 @@ func (c *Client) readPump() {
|
||||||
// Allow 3 messages every two seconds.
|
// Allow 3 messages every two seconds.
|
||||||
limit := rate.Every(2 * time.Second / 3)
|
limit := rate.Every(2 * time.Second / 3)
|
||||||
c.rateLimiter = rate.NewLimiter(limit, 1)
|
c.rateLimiter = rate.NewLimiter(limit, 1)
|
||||||
|
c.messageFilter = NewMessageFilter()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
c.close()
|
c.close()
|
||||||
|
@ -129,6 +132,12 @@ func (c *Client) readPump() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this message passes the optional language filter
|
||||||
|
if data.GetChatSlurFilterEnabled() && !c.messageFilter.Allow(string(message)) {
|
||||||
|
c.sendAction("Sorry, that message contained language that is not allowed in this chat.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space))
|
message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space))
|
||||||
c.handleEvent(message)
|
c.handleEvent(message)
|
||||||
}
|
}
|
||||||
|
@ -200,7 +209,13 @@ func (c *Client) close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) passesRateLimit() bool {
|
func (c *Client) passesRateLimit() bool {
|
||||||
return c.User.IsModerator() || (c.rateLimiter.Allow() && !c.inTimeout)
|
// If spam rate limiting is disabled, or the user is a moderator, always
|
||||||
|
// allow the message.
|
||||||
|
if !data.GetChatSpamProtectionEnabled() || c.User.IsModerator() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (c.rateLimiter.Allow() && !c.inTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) startChatRejectionTimeout() {
|
func (c *Client) startChatRejectionTimeout() {
|
||||||
|
|
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,60 +179,80 @@ export default function ConfigChat() {
|
||||||
return (
|
return (
|
||||||
<div className="config-server-details-form">
|
<div className="config-server-details-form">
|
||||||
<Title>Chat Settings</Title>
|
<Title>Chat Settings</Title>
|
||||||
<div className="form-module config-server-details-container">
|
<Row gutter={[45, 16]}>
|
||||||
<ToggleSwitch
|
<Col md={24} lg={12}>
|
||||||
fieldName="chatDisabled"
|
<div className="form-module">
|
||||||
{...FIELD_PROPS_DISABLE_CHAT}
|
<ToggleSwitch
|
||||||
checked={!formDataValues.chatDisabled}
|
fieldName="chatDisabled"
|
||||||
reversed
|
{...FIELD_PROPS_DISABLE_CHAT}
|
||||||
onChange={handleChatDisableChange}
|
checked={!formDataValues.chatDisabled}
|
||||||
/>
|
reversed
|
||||||
<ToggleSwitch
|
onChange={handleChatDisableChange}
|
||||||
fieldName="chatJoinMessagesEnabled"
|
/>
|
||||||
{...FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED}
|
<ToggleSwitch
|
||||||
checked={formDataValues.chatJoinMessagesEnabled}
|
fieldName="chatJoinMessagesEnabled"
|
||||||
onChange={handleChatJoinMessagesEnabledChange}
|
{...FIELD_PROPS_CHAT_JOIN_MESSAGES_ENABLED}
|
||||||
/>
|
checked={formDataValues.chatJoinMessagesEnabled}
|
||||||
<ToggleSwitch
|
onChange={handleChatJoinMessagesEnabledChange}
|
||||||
fieldName="chatEstablishedUserMode"
|
/>
|
||||||
{...CHAT_ESTABLISHED_USER_MODE}
|
<TextFieldWithSubmit
|
||||||
checked={formDataValues.chatEstablishedUserMode}
|
fieldName="welcomeMessage"
|
||||||
onChange={handleEstablishedUserModeChange}
|
{...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE}
|
||||||
/>
|
type={TEXTFIELD_TYPE_TEXTAREA}
|
||||||
<TextFieldWithSubmit
|
value={formDataValues.welcomeMessage}
|
||||||
fieldName="welcomeMessage"
|
initialValue={welcomeMessage}
|
||||||
{...TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE}
|
onChange={handleFieldChange}
|
||||||
type={TEXTFIELD_TYPE_TEXTAREA}
|
/>
|
||||||
value={formDataValues.welcomeMessage}
|
<br />
|
||||||
initialValue={welcomeMessage}
|
<br />
|
||||||
onChange={handleFieldChange}
|
<EditValueArray
|
||||||
/>
|
title={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.label}
|
||||||
<br />
|
placeholder={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.placeholder}
|
||||||
<br />
|
description={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.tip}
|
||||||
<EditValueArray
|
values={formDataValues.forbiddenUsernames}
|
||||||
title={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.label}
|
handleDeleteIndex={handleDeleteForbiddenUsernameIndex}
|
||||||
placeholder={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.placeholder}
|
handleCreateString={handleCreateForbiddenUsername}
|
||||||
description={TEXTFIELD_PROPS_CHAT_FORBIDDEN_USERNAMES.tip}
|
submitStatus={forbiddenUsernameSaveState}
|
||||||
values={formDataValues.forbiddenUsernames}
|
/>
|
||||||
handleDeleteIndex={handleDeleteForbiddenUsernameIndex}
|
<br />
|
||||||
handleCreateString={handleCreateForbiddenUsername}
|
<br />
|
||||||
submitStatus={forbiddenUsernameSaveState}
|
<EditValueArray
|
||||||
/>
|
title={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.label}
|
||||||
<br />
|
placeholder={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.placeholder}
|
||||||
<br />
|
description={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.tip}
|
||||||
<EditValueArray
|
values={formDataValues.suggestedUsernames}
|
||||||
title={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.label}
|
handleDeleteIndex={handleDeleteSuggestedUsernameIndex}
|
||||||
placeholder={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.placeholder}
|
handleCreateString={handleCreateSuggestedUsername}
|
||||||
description={TEXTFIELD_PROPS_CHAT_SUGGESTED_USERNAMES.tip}
|
submitStatus={suggestedUsernameSaveState}
|
||||||
values={formDataValues.suggestedUsernames}
|
continuousStatusMessage={getSuggestedUsernamesLimitWarning(
|
||||||
handleDeleteIndex={handleDeleteSuggestedUsernameIndex}
|
formDataValues.suggestedUsernames.length,
|
||||||
handleCreateString={handleCreateSuggestedUsername}
|
)}
|
||||||
submitStatus={suggestedUsernameSaveState}
|
/>
|
||||||
continuousStatusMessage={getSuggestedUsernamesLimitWarning(
|
</div>
|
||||||
formDataValues.suggestedUsernames.length,
|
</Col>
|
||||||
)}
|
<Col md={24} lg={12}>
|
||||||
/>
|
<div className="form-module">
|
||||||
</div>
|
<ToggleSwitch
|
||||||
|
fieldName="chatSpamProtectionEnabled"
|
||||||
|
{...FIELD_PROPS_ENABLE_SPAM_PROTECTION}
|
||||||
|
checked={formDataValues.chatSpamProtectionEnabled}
|
||||||
|
onChange={handleChatSpamProtectionChange}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
fieldName="chatEstablishedUserMode"
|
||||||
|
{...CHAT_ESTABLISHED_USER_MODE}
|
||||||
|
checked={formDataValues.chatEstablishedUserMode}
|
||||||
|
onChange={handleEstablishedUserModeChange}
|
||||||
|
/>
|
||||||
|
<ToggleSwitch
|
||||||
|
fieldName="chatSlurFilterEnabled"
|
||||||
|
{...FIELD_PROPS_ENABLE_CHAT_SLUR_FILTER}
|
||||||
|
checked={formDataValues.chatSlurFilterEnabled}
|
||||||
|
onChange={handleChatSlurFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue