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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user