Skip to content

Commit

Permalink
policyeval: add initial code for evaluating users in room against ban…
Browse files Browse the repository at this point in the history
… lists
  • Loading branch information
tulir committed Sep 1, 2024
1 parent 08bb3d6 commit c1a6c9a
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 35 deletions.
53 changes: 32 additions & 21 deletions cmd/meowlnir/eventhandling.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package main
import (
"context"
"fmt"
"runtime"
"strings"
"time"

"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"go.mau.fi/meowlnir/policylist"
)

func (m *Meowlnir) AddEventHandlers() {
Expand All @@ -23,12 +22,13 @@ func (m *Meowlnir) AddEventHandlers() {
m.EventProcessor.On(event.StateUnstablePolicyRoom, m.UpdatePolicyList)
m.EventProcessor.On(event.StateUnstablePolicyServer, m.UpdatePolicyList)
m.EventProcessor.On(event.StateMember, m.HandleMember)
m.EventProcessor.On(event.StateMember, m.PolicyEvaluator.HandleMember)
m.EventProcessor.On(event.EventMessage, m.HandleCommand)
}

func (m *Meowlnir) UpdatePolicyList(ctx context.Context, evt *event.Event) {
added, removed := m.PolicyStore.Update(evt)
fmt.Println(added, removed)
m.PolicyEvaluator.HandlePolicyListChange(ctx, added, removed)
}

const tempAdmin = "@tulir:maunium.net"
Expand All @@ -44,39 +44,50 @@ func (m *Meowlnir) HandleMember(ctx context.Context, evt *event.Event) {
}
}

func (m *Meowlnir) LoadBanList(ctx context.Context, roomID id.RoomID) (*policylist.Room, error) {
state, err := m.Client.State(ctx, roomID)
if err != nil {
return nil, fmt.Errorf("failed to get room state: %w", err)
}
m.PolicyStore.Add(roomID, state)
return nil, nil
}

func (m *Meowlnir) HandleCommand(ctx context.Context, evt *event.Event) {
if evt.Sender != tempAdmin {
return
}
m.Client.State(ctx, evt.RoomID)
fields := strings.Fields(evt.Content.AsMessage().Body)
cmd := fields[0]
args := fields[1:]
m.Log.Info().Str("command", cmd).Msg("Handling command")
switch strings.ToLower(cmd) {
case "!join":
m.Client.JoinRoomByID(ctx, id.RoomID(args[0]))
for _, arg := range args {
m.Client.JoinRoomByID(ctx, id.RoomID(arg))
}
case "!load":
_, err := m.LoadBanList(ctx, id.RoomID(args[0]))
if err != nil {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to load ban list: %v", err))
} else {
m.Client.SendNotice(ctx, evt.RoomID, "Ban list loaded")
for _, arg := range args {
start := time.Now()
err := m.PolicyEvaluator.Subscribe(ctx, id.RoomID(arg))
dur := time.Since(start)
if err != nil {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to load ban list: %v", err))
} else {
m.Client.SendNotice(ctx, evt.RoomID, "Ban list loaded in "+dur.String())
}
}
runtime.GC()
case "!protect":
for _, arg := range args {
start := time.Now()
err := m.PolicyEvaluator.Protect(ctx, id.RoomID(arg))
dur := time.Since(start)
if err != nil {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to protect %s: %v", arg, err))
} else {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Protected %s in %s", arg, dur))
}
}
case "!match":
start := time.Now()
match := m.PolicyStore.MatchUser(nil, id.UserID(args[0]))
dur := time.Since(start)
if match != nil {
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Matched: %s set recommendation %s for %s at %s: %s", match.RawEvent.Sender, match.Recommendation, match.Entity, time.UnixMilli(match.RawEvent.Timestamp), match.Reason))
m.Client.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Matched in %s: %s set recommendation %s for %s at %s: %s", dur, match.Sender, match.Recommendation, match.Entity, time.UnixMilli(match.Timestamp), match.Reason))
} else {
m.Client.SendNotice(ctx, evt.RoomID, "No match")
m.Client.SendNotice(ctx, evt.RoomID, "No match in "+dur.String())
}
}
}
21 changes: 14 additions & 7 deletions cmd/meowlnir/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"maunium.net/go/mautrix/crypto/cryptohelper"

"go.mau.fi/meowlnir/config"
"go.mau.fi/meowlnir/database"
"go.mau.fi/meowlnir/policyeval"
"go.mau.fi/meowlnir/policylist"
"go.mau.fi/meowlnir/synapsedb"
)
Expand All @@ -37,14 +39,15 @@ var wantHelp, _ = flag.MakeHelpFlag()
type Meowlnir struct {
Config *config.Config
Log *zerolog.Logger
DB *dbutil.Database
DB *database.Database
SynapseDB *synapsedb.SynapseDB
Client *mautrix.Client
Crypto *cryptohelper.CryptoHelper
AS *appservice.AppService
EventProcessor *appservice.EventProcessor

PolicyStore *policylist.Store
PolicyStore *policylist.Store
PolicyEvaluator *policyeval.PolicyEvaluator
}

var MinSpecVersion = mautrix.SpecV111
Expand All @@ -65,11 +68,13 @@ func (m *Meowlnir) Init(ctx context.Context, configPath string, noSaveConfig boo
Time("built_at", ParsedBuildTime).
Str("go_version", runtime.Version()).
Msg("Initializing Meowlnir")
m.DB, err = dbutil.NewFromConfig("meowlnir", m.Config.Database, dbutil.ZeroLogger(m.Log.With().Str("db_section", "main").Logger()))
var mainDB *dbutil.Database
mainDB, err = dbutil.NewFromConfig("meowlnir", m.Config.Database, dbutil.ZeroLogger(m.Log.With().Str("db_section", "main").Logger()))
if err != nil {
m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to connect to Meowlnir database")
os.Exit(12)
}
m.DB = database.New(mainDB)
var synapseDB *dbutil.Database
synapseDB, err = dbutil.NewFromConfig("", m.Config.SynapseDB, dbutil.ZeroLogger(m.Log.With().Str("db_section", "synapse").Logger()))
if err != nil {
Expand All @@ -83,8 +88,6 @@ func (m *Meowlnir) Init(ctx context.Context, configPath string, noSaveConfig boo
os.Exit(12)
}

m.PolicyStore = policylist.NewStore()

m.Log.Debug().Msg("Preparing Matrix client")
m.AS, err = appservice.CreateFull(appservice.CreateOpts{
Registration: &appservice.Registration{
Expand Down Expand Up @@ -112,10 +115,14 @@ func (m *Meowlnir) Init(ctx context.Context, configPath string, noSaveConfig boo
m.AS.Log = m.Log.With().Str("component", "matrix").Logger()
m.EventProcessor = appservice.NewEventProcessor(m.AS)
m.EventProcessor.Start(ctx)
m.AddEventHandlers()
m.Client = m.AS.BotClient()
m.Client.SetAppServiceDeviceID = true

m.PolicyStore = policylist.NewStore()
m.PolicyEvaluator = policyeval.NewPolicyEvaluator(m.Client, m.PolicyStore)

m.AddEventHandlers()

for {
resp, err := m.Client.Versions(ctx)
if err != nil {
Expand All @@ -138,7 +145,7 @@ func (m *Meowlnir) Init(ctx context.Context, configPath string, noSaveConfig boo
m.ensureBotRegistered(ctx)

m.Log.Debug().Msg("Preparing crypto helper")
m.Crypto, err = cryptohelper.NewCryptoHelper(m.Client, []byte(m.Config.Appservice.PickleKey), m.DB)
m.Crypto, err = cryptohelper.NewCryptoHelper(m.Client, []byte(m.Config.Appservice.PickleKey), mainDB)
if err != nil {
m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to create crypto helper")
os.Exit(16)
Expand Down
13 changes: 13 additions & 0 deletions database/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package database

import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)

type TakenAction struct {
PolicyList id.RoomID
RuleEntity string
TargetUser id.UserID
Action event.PolicyRecommendation
}
15 changes: 15 additions & 0 deletions database/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package database

import (
"go.mau.fi/util/dbutil"
)

type Database struct {
*dbutil.Database
}

func New(db *dbutil.Database) *Database {
return &Database{
Database: db,
}
}
7 changes: 7 additions & 0 deletions database/upgrades/00-latest.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- v0 -> v1 (compatible with v1+): Latest schema
CREATE TABLE taken_action (
policy_list TEXT NOT NULL,
rule_entity TEXT NOT NULL,
target_user TEXT NOT NULL,
action TEXT NOT NULL
);
16 changes: 16 additions & 0 deletions database/upgrades/upgrades.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package upgrades

import (
"embed"

"go.mau.fi/util/dbutil"
)

var Table dbutil.UpgradeTable

//go:embed *.sql
var rawUpgrades embed.FS

func init() {
Table.RegisterFS(rawUpgrades)
}
106 changes: 106 additions & 0 deletions policyeval/evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package policyeval

import (
"context"
"fmt"
"slices"
"sync"
"time"

"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"go.mau.fi/meowlnir/policylist"
)

type PolicyEvaluator struct {
Client *mautrix.Client
Store *policylist.Store

Subscriptions []id.RoomID
ProtectedRooms []id.RoomID
users map[id.UserID][]id.RoomID
usersLock sync.RWMutex
}

func NewPolicyEvaluator(client *mautrix.Client, store *policylist.Store) *PolicyEvaluator {
return &PolicyEvaluator{
Client: client,
Store: store,
users: make(map[id.UserID][]id.RoomID),
}
}

func (pe *PolicyEvaluator) Subscribe(ctx context.Context, roomID id.RoomID) error {
if slices.Contains(pe.Subscriptions, roomID) {
return nil
}
if !pe.Store.Contains(roomID) {
state, err := pe.Client.State(ctx, roomID)
if err != nil {
return fmt.Errorf("failed to get room state: %w", err)
}
pe.Store.Add(roomID, state)
}
pe.Subscriptions = append(pe.Subscriptions, roomID)
return nil
}

func (pe *PolicyEvaluator) Protect(ctx context.Context, roomID id.RoomID) error {
members, err := pe.Client.Members(ctx, roomID)
if err != nil {
return fmt.Errorf("failed to get room members: %w", err)
}
pe.ProtectedRooms = append(pe.ProtectedRooms, roomID)
start := time.Now()
for _, evt := range members.Chunk {
pe.HandleMember(ctx, evt)
}
zerolog.Ctx(ctx).Debug().Stringer("duration", time.Since(start)).Msg("Processed room members for protection")
return nil
}

func (pe *PolicyEvaluator) updateUser(userID id.UserID, roomID id.RoomID, add bool) {
pe.usersLock.Lock()
defer pe.usersLock.Unlock()
if add {
if !slices.Contains(pe.users[userID], roomID) {
pe.users[userID] = append(pe.users[userID], roomID)
}
} else if idx := slices.Index(pe.users[userID], roomID); idx >= 0 {
deleted := slices.Delete(pe.users[userID], idx, idx+1)
if len(deleted) == 0 {
delete(pe.users, userID)
} else {
pe.users[userID] = deleted
}
}
}

func (pe *PolicyEvaluator) HandlePolicyListChange(ctx context.Context, added, removed *policylist.Policy) {
zerolog.Ctx(ctx).Info().
Any("added", added).
Any("removed", removed).
Msg("Policy list change")
}

func (pe *PolicyEvaluator) HandleMember(ctx context.Context, evt *event.Event) {
if !slices.Contains(pe.ProtectedRooms, evt.RoomID) {
return
}
switch evt.Content.AsMember().Membership {
case event.MembershipJoin, event.MembershipInvite, event.MembershipKnock:
pe.updateUser(id.UserID(evt.GetStateKey()), evt.RoomID, true)
policy := pe.Store.MatchUser(pe.Subscriptions, id.UserID(evt.GetStateKey()))
if policy != nil {
zerolog.Ctx(ctx).Info().
Str("user_id", evt.GetStateKey()).
Any("policy", policy).
Msg("Matched user in membership event")
}
case event.MembershipLeave, event.MembershipBan:
pe.updateUser(id.UserID(evt.GetStateKey()), evt.RoomID, false)
}
}
18 changes: 12 additions & 6 deletions policylist/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import (

"go.mau.fi/util/glob"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)

// Policy represents a single moderation policy event with the relevant data parsed out.
type Policy struct {
*event.ModPolicyContent
Pattern glob.Glob
RawEvent *event.Event
Pattern glob.Glob

StateKey string
Sender id.UserID
Type event.Type
Timestamp int64
ID id.EventID
}

type dplNode struct {
Expand Down Expand Up @@ -66,9 +72,9 @@ func (l *List) removeLL(node *dplNode) {
func (l *List) Add(value *Policy) (*Policy, bool) {
l.lock.Lock()
defer l.lock.Unlock()
existing, ok := l.keys[*value.RawEvent.StateKey]
existing, ok := l.keys[value.StateKey]
if ok {
if typeQuality(existing.RawEvent.Type) > typeQuality(value.RawEvent.Type) {
if typeQuality(existing.Type) > typeQuality(value.Type) {
// There's an existing policy with the same state key, but a newer event type, ignore this one.
return nil, false
} else if existing.Entity == value.Entity {
Expand All @@ -81,7 +87,7 @@ func (l *List) Add(value *Policy) (*Policy, bool) {
delete(l.static, existing.Entity)
}
node := &dplNode{Policy: value}
l.keys[*value.RawEvent.StateKey] = node
l.keys[value.StateKey] = node
if _, isStatic := value.Pattern.(glob.ExactGlob); isStatic {
l.static[value.Entity] = value
} else {
Expand All @@ -100,7 +106,7 @@ func (l *List) Add(value *Policy) (*Policy, bool) {
func (l *List) Remove(eventType event.Type, stateKey string) *Policy {
l.lock.Lock()
defer l.lock.Unlock()
if value, ok := l.keys[stateKey]; ok && eventType == value.RawEvent.Type {
if value, ok := l.keys[stateKey]; ok && eventType == value.Type {
l.removeLL(value)
delete(l.static, value.Entity)
delete(l.keys, stateKey)
Expand Down
Loading

0 comments on commit c1a6c9a

Please sign in to comment.