BareRTC/pkg/message_filters.go

174 lines
4.9 KiB
Go

package barertc
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"git.kirsle.net/apps/barertc/pkg/config"
"git.kirsle.net/apps/barertc/pkg/messages"
)
// Functionality for handling server-side message filtering and reporting.
// filterMessage will check an incoming user message against the configured
// server-side filters and react accordingly. This function also is
// responsible for collecting the recent contexts (10 messages per channel).
//
// Parameters: the rawMsg is their (pre-Markdown-formatted) original message
// (for the message context); the msg pointer is their post-formatted one, which
// may be modified to censor their word before returning.
//
// Returns the matching message filter (or nil) and a boolean (matched).
func (s *Server) filterMessage(sub *Subscriber, rawMsg messages.Message, msg *messages.Message) (*config.MessageFilter, bool) {
// Collect the recent channel context first.
if strings.HasPrefix(msg.Channel, "@") {
// DM
pushDirectMessageContext(sub, sub.Username, msg.Channel[1:], rawMsg)
// If either party is an admin user, waive filtering this DM chat.
if sub.IsAdmin() {
return nil, false
} else if other, err := s.GetSubscriber(msg.Channel[1:]); err == nil && other.IsAdmin() {
return nil, false
}
} else {
// Public channel
pushMessageContext(sub, msg.Channel, rawMsg)
}
// Check it against the configured filters.
var matched bool
for _, filter := range config.Current.MessageFilters {
if !filter.Enabled {
continue
}
for _, phrase := range filter.IterPhrases() {
m := phrase.FindAllStringSubmatch(msg.Message, -1)
for _, match := range m {
// Found a match!
matched = true
// Censor it?
if filter.CensorMessage {
msg.Message = strings.ReplaceAll(msg.Message, match[0], strings.Repeat("*", len(match[0])))
}
}
}
if matched {
return filter, true
}
}
return nil, false
}
// Report the filtered message along with recent context.
func (s *Server) reportFilteredMessage(sub *Subscriber, msg messages.Message) error {
if !WebhookEnabled(WebhookReport) {
return errors.New("report webhook is not enabled on this server")
}
// Prepare the report.
var context string
if strings.HasPrefix(msg.Channel, "@") {
context = getDirectMessageContext(sub.Username, msg.Channel[1:])
} else {
context = getMessageContext(msg.Channel)
}
if _, err := PostWebhook(WebhookReport, WebhookRequest{
Action: WebhookReport,
APIKey: config.Current.AdminAPIKey,
Report: WebhookRequestReport{
FromUsername: sub.Username,
AboutUsername: sub.Username,
Channel: msg.Channel,
Timestamp: time.Now().Format(time.RFC1123),
Reason: "Server Side Message Filter",
Message: msg.Message,
Comment: fmt.Sprintf(
"This is an automated report via server side chat filters.\n\n"+
"The recent context in this channel included the following conversation:\n\n"+
"%s",
context,
),
},
}); err != nil {
return err
}
return nil
}
// Message Context Caching
//
// Hold the recent (10) messages for each channel so in case of automated
// reporting, the context can be delivered in the report.
var (
messageContexts = map[string][]string{}
messageContextMu sync.RWMutex
messageContextSize = 30
)
// Push a message onto the recent messages context.
func pushMessageContext(sub *Subscriber, channel string, msg messages.Message) {
messageContextMu.Lock()
defer messageContextMu.Unlock()
// Initialize the context for new channel the first time.
if _, ok := messageContexts[channel]; !ok {
messageContexts[channel] = []string{}
}
// Append this message to it.
messageContexts[channel] = append(messageContexts[channel], fmt.Sprintf(
"%s [%s] %s",
time.Now().Format("2006-01-02 15:04:05"),
sub.Username,
strings.TrimSpace(msg.Message),
))
// Trim the context to recent messages only.
if len(messageContexts[channel]) > messageContextSize {
messageContexts[channel] = messageContexts[channel][len(messageContexts[channel])-messageContextSize:]
}
}
// Push a message context for DMs. A channel name will be derived consistently
// based on the sorted pair of usernames.
func pushDirectMessageContext(sub *Subscriber, username1, username2 string, msg messages.Message) {
var names = []string{username1, username2}
sort.Strings(names)
pushMessageContext(
sub,
fmt.Sprintf("@%s", strings.Join(names, ":")),
msg,
)
}
// Get the recent message context, pretty printed.
func getMessageContext(channel string) string {
messageContextMu.RLock()
defer messageContextMu.RUnlock()
if _, ok := messageContexts[channel]; !ok {
return "(No recent message history in this channel)"
}
return strings.Join(messageContexts[channel], "\n\n")
}
func getDirectMessageContext(username1, username2 string) string {
var names = []string{username1, username2}
sort.Strings(names)
return getMessageContext(
fmt.Sprintf("@%s", strings.Join(names, ":")),
)
}