Noah Petherbridge
bdb5e6359b
* Re-set user's status if they disconnect and reconnect * Remove "(offline)" text next to ChatServer/ChatClient messages * Make names and pictures in presence messages clickable to open profile cards
174 lines
4.9 KiB
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 = 10
|
|
)
|
|
|
|
// 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, ":")),
|
|
)
|
|
}
|