diff --git a/policyeval/commands.go b/policyeval/commands.go index fe98a00..d8fe5ba 100644 --- a/policyeval/commands.go +++ b/policyeval/commands.go @@ -30,6 +30,10 @@ func (pe *PolicyEvaluator) HandleCommand(ctx context.Context, evt *event.Event) zerolog.Ctx(ctx).Info().Str("command", cmd).Msg("Handling command") switch cmd { case "!join": + if len(args) == 0 { + pe.sendNotice(ctx, "Usage: `!join ...`") + return + } for _, arg := range args { _, err := pe.Bot.JoinRoom(ctx, arg, nil) if err != nil { @@ -39,6 +43,30 @@ func (pe *PolicyEvaluator) HandleCommand(ctx context.Context, evt *event.Event) } } pe.sendSuccessReaction(ctx, evt.ID) + case "!leave": + if len(args) == 0 { + pe.sendNotice(ctx, "Usage: `!leave ...`") + return + } + var target id.RoomID + if strings.HasPrefix(args[0], "#") { + rawTarget, err := pe.Bot.ResolveAlias(ctx, id.RoomAlias(args[0])) + if err != nil { + pe.sendNotice(ctx, "Failed to resolve alias %q: %v", args[0], err) + return + } + target = rawTarget.RoomID + } else { + target = id.RoomID(args[0]) + } + for _, arg := range args { + _, err := pe.Bot.LeaveRoom(ctx, target, nil) + if err != nil { + pe.sendNotice(ctx, "Failed to leave room %q: %v", arg, err) + } else { + pe.sendNotice(ctx, "Left room %q", arg) + } + } case "!redact": if len(args) < 1 { pe.sendNotice(ctx, "Usage: `!redact [reason]`") @@ -120,6 +148,89 @@ func (pe *PolicyEvaluator) HandleCommand(ctx context.Context, evt *event.Event) Stringer("policy_event_id", resp.EventID). Msg("Sent ban policy from command") pe.sendSuccessReaction(ctx, evt.ID) + case "!remove-ban", "!remove-unban", "!remove-policy": + if len(args) < 2 { + pe.sendNotice(ctx, "Usage: `!remove-policy `") + return + } + list := pe.FindListByShortcode(args[0]) + if list == nil { + pe.sendNotice(ctx, `List %q not found`, args[0]) + return + } + target := args[1] + var match policylist.Match + var entityType policylist.EntityType + if !strings.HasPrefix(target, "@") { + entityType = policylist.EntityTypeServer + match = pe.Store.MatchServer(pe.GetWatchedLists(), target) + } else { + entityType = policylist.EntityTypeUser + match = pe.Store.MatchUser(pe.GetWatchedLists(), id.UserID(target)) + } + var existingStateKey string + if rec := match.Recommendations().BanOrUnban; rec != nil { + if rec.RoomID == list.RoomID { + existingStateKey = rec.StateKey + } + } + policy := &event.ModPolicyContent{} + resp, err := pe.SendPolicy(ctx, list.RoomID, entityType, existingStateKey, policy) + if err != nil { + pe.sendNotice(ctx, `Failed to remove policy: %v`, err) + return + } + zerolog.Ctx(ctx).Info(). + Stringer("policy_list", list.RoomID). + Any("policy", policy). + Stringer("policy_event_id", resp.EventID). + Msg("Removed policy from command") + pe.sendSuccessReaction(ctx, evt.ID) + case "!add-unban": + if len(args) < 2 { + pe.sendNotice(ctx, "Usage: `!add-unban `") + return + } + list := pe.FindListByShortcode(args[0]) + if list == nil { + pe.sendNotice(ctx, `List %q not found`, args[0]) + return + } + target := args[1] + var match policylist.Match + var entityType policylist.EntityType + if !strings.HasPrefix(target, "@") { + entityType = policylist.EntityTypeServer + match = pe.Store.MatchServer(pe.GetWatchedLists(), target) + } else { + entityType = policylist.EntityTypeUser + match = pe.Store.MatchUser(pe.GetWatchedLists(), id.UserID(target)) + } + var existingStateKey string + if rec := match.Recommendations().BanOrUnban; rec != nil { + if rec.Recommendation == event.PolicyRecommendationUnban { + pe.sendNotice(ctx, "`%s` already has an unban recommendation: %s", target, rec.Reason) + return + } else if rec.RoomID == list.RoomID { + existingStateKey = rec.StateKey + } + } + policy := &event.ModPolicyContent{ + Entity: target, + Reason: strings.Join(args[2:], " "), + Recommendation: event.PolicyRecommendationUnban, + } + resp, err := pe.SendPolicy(ctx, list.RoomID, entityType, existingStateKey, policy) + if err != nil { + pe.sendNotice(ctx, `Failed to send unban policy: %v`, err) + return + } + zerolog.Ctx(ctx).Info(). + Stringer("policy_list", list.RoomID). + Any("policy", policy). + Stringer("policy_event_id", resp.EventID). + Msg("Sent unban policy from command") + pe.sendSuccessReaction(ctx, evt.ID) case "!match": start := time.Now() match := pe.Store.MatchUser(nil, id.UserID(args[0])) diff --git a/policyeval/evaluate.go b/policyeval/evaluate.go index 0ef6cbf..9e0b79b 100644 --- a/policyeval/evaluate.go +++ b/policyeval/evaluate.go @@ -91,5 +91,24 @@ func (pe *PolicyEvaluator) ReevaluateAffectedByLists(ctx context.Context, policy } func (pe *PolicyEvaluator) ReevaluateActions(ctx context.Context, actions []*database.TakenAction) { - + for _, action := range actions { + if action.TargetUser == "" { + zerolog.Ctx(ctx).Warn().Any("action", action).Msg("Action has no target user") + continue + } + // unban users that were previously banned by this rule + if action.ActionType == database.TakenActionTypeBanOrUnban && action.Action == event.PolicyRecommendationBan { + // ensure that the user is actually banned in the room + if pe.Bot.StateStore.IsMembership(ctx, action.InRoomID, action.TargetUser, event.MembershipBan) { + // This is hacky + policy := &policylist.Policy{ + RoomID: action.InRoomID, + ModPolicyContent: &event.ModPolicyContent{ + Entity: action.RuleEntity, + }, + } + pe.ApplyUnban(ctx, action.TargetUser, action.InRoomID, policy) + } + } + } } diff --git a/policyeval/execute.go b/policyeval/execute.go index 09689ca..9769afc 100644 --- a/policyeval/execute.go +++ b/policyeval/execute.go @@ -61,6 +61,9 @@ func (pe *PolicyEvaluator) ApplyPolicy(ctx context.Context, userID id.UserID, po // pe.sendNotice(ctx, "Database error in ApplyPolicy (GetAllByTargetUser): %v", err) // return //} + for _, room := range rooms { + pe.ApplyUnban(ctx, userID, room, recs.BanOrUnban) + } } } } @@ -108,6 +111,42 @@ func (pe *PolicyEvaluator) ApplyBan(ctx context.Context, userID id.UserID, roomI } } +func (pe *PolicyEvaluator) ApplyUnban(ctx context.Context, userID id.UserID, roomID id.RoomID, policy *policylist.Policy) { + ta := &database.TakenAction{ + TargetUser: userID, + InRoomID: roomID, + ActionType: database.TakenActionTypeBanOrUnban, + PolicyList: policy.RoomID, + RuleEntity: policy.Entity, + Action: policy.Recommendation, + TakenAt: time.Now(), + } + var err error + if !pe.DryRun { + _, err = pe.Bot.UnbanUser(ctx, roomID, &mautrix.ReqUnbanUser{ + Reason: filterReason(policy.Reason), + UserID: userID, + }) + } + if err != nil { + var respErr mautrix.HTTPError + if errors.As(err, &respErr) { + err = respErr + } + zerolog.Ctx(ctx).Err(err).Any("attempted_action", ta).Msg("Failed to unban user") + pe.sendNotice(ctx, "Failed to unban [%s](%s) in [%s](%s) for %s: %v", userID, userID.URI().MatrixToURL(), roomID, roomID.URI().MatrixToURL(), policy.Reason, err) + return + } + err = pe.DB.TakenAction.Put(ctx, ta) + if err != nil { + zerolog.Ctx(ctx).Err(err).Any("taken_action", ta).Msg("Failed to save taken action") + pe.sendNotice(ctx, "Unbanned [%s](%s) in [%s](%s) for %s, but failed to save to database: %v", userID, userID.URI().MatrixToURL(), roomID, roomID.URI().MatrixToURL(), policy.Reason, err) + } else { + zerolog.Ctx(ctx).Info().Any("taken_action", ta).Msg("Took action") + pe.sendNotice(ctx, "Unbanned [%s](%s) in [%s](%s) for %s", userID, userID.URI().MatrixToURL(), roomID, roomID.URI().MatrixToURL(), policy.Reason) + } +} + func pluralize(value int, unit string) string { if value == 1 { return "1 " + unit