mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-03-30 19:05:39 +00:00
RedactionSynchronisationProtection invite retraction (#788)
- The _Redaction Synchronisation Protection_ has been improved in a few ways:
- Invitations in protected rooms will be rejected as part of the redaction
process when they are sent from users being redacted (e.g. as a brigading
tactic).
- User redaction will now be triggered on bans and the reason will be scanned
for `automaticallyRedactForReasons` from Draupnir's config.
* Update RedactionSynchronisation for new protection apis.
* Rerwrite redaction synchronisation protection
* Reject invitations on ban.
* Add renderer and simulated redaction synchornisation capability.
* Reduce dependencies of redaction synchronisation protection.
* Allow RedactionSynchronisation to be unit tested.
* Update to MPS 3.1.0.
---------
Signed-off-by: Rory& <root@rory.gay>
Co-authored-by: Rory& <root@rory.gay>
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -12,6 +12,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- The _Redaction Synchronisation Protection_ has been improved in a few ways:
|
||||
- Invitations in protected rooms will be rejected as part of the redaction
|
||||
process when they are sent from users being redacted (e.g. as a brigading
|
||||
tactic).
|
||||
- User redaction will now be triggered on bans and the reason will be scanned
|
||||
for `automaticallyRedactForReasons` from Draupnir's config.
|
||||
|
||||
## [v2.3.0-beta.0]
|
||||
|
||||
In this update we want feedback on new
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"matrix-appservice-bridge": "^10.3.1",
|
||||
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.1-element.6",
|
||||
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.0.0",
|
||||
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.0.0",
|
||||
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.1.0",
|
||||
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.1.0",
|
||||
"pg": "^8.8.0",
|
||||
"yaml": "^2.3.2"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
|
||||
// keep alphabetical please.
|
||||
import "./RoomTakedown/RoomTakedownProtection";
|
||||
import "./BanPropagation";
|
||||
import "./BasicFlooding";
|
||||
import "./FirstMessageIsImage";
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
// SPDX-FileCopyrightText: 2024 - 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
// README: This protection really exists as a stop gap to bring over redaction
|
||||
// functionality over from Draupnir, while we figure out how to add redaction
|
||||
// policies that operate on a timeline cache, which removes the painfull process
|
||||
// that is currently used to repeatedly fetch `/messages`.
|
||||
|
||||
import {
|
||||
AbstractProtection,
|
||||
ActionException,
|
||||
ActionExceptionKind,
|
||||
ActionResult,
|
||||
CapabilitySet,
|
||||
Capability,
|
||||
CapabilityMethodSchema,
|
||||
Membership,
|
||||
MembershipChange,
|
||||
MembershipChangeType,
|
||||
Ok,
|
||||
@@ -26,161 +25,353 @@ import {
|
||||
ProtectionDescription,
|
||||
Recommendation,
|
||||
RoomMembershipRevision,
|
||||
SetMembershipPolicyRevisionIssuer,
|
||||
SetRoomMembership,
|
||||
Task,
|
||||
UnknownConfig,
|
||||
WatchedPolicyRooms,
|
||||
describeCapabilityInterface,
|
||||
describeCapabilityProvider,
|
||||
describeCapabilityRenderer,
|
||||
describeProtection,
|
||||
} from "matrix-protection-suite";
|
||||
import { Draupnir } from "../Draupnir";
|
||||
import { redactUserMessagesIn } from "../utils";
|
||||
import {
|
||||
MatrixGlob,
|
||||
StringRoomID,
|
||||
StringUserID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { Result } from "@gnuxie/typescript-result";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { revisionRulesMatchingUser } from "../commands/unban/UnbanUsers";
|
||||
import { redactUserMessagesIn } from "../utils";
|
||||
|
||||
type RedactionSynchronisationProtectionDescription =
|
||||
ProtectionDescription<Draupnir>;
|
||||
export type RedactionSynchronisationProtectionCapabilitiesSet = {
|
||||
consequences: RedactionSynchronisationConsequences;
|
||||
};
|
||||
|
||||
type RedactionSynchronisationProtectionDescription = ProtectionDescription<
|
||||
Draupnir,
|
||||
UnknownConfig,
|
||||
RedactionSynchronisationProtectionCapabilitiesSet
|
||||
>;
|
||||
|
||||
// FIXME: We really need to design a capability interface for this protection...
|
||||
// TBH doing so, would probably just be the UserConsequences capability...
|
||||
// There shouldn't be much difference between this protection and MemberBanSynchronisation
|
||||
// except this one applies when redaction reasons are given on policies and ban events.
|
||||
|
||||
// FIXME: Need to decide whether to use two consequences...
|
||||
// the reason why we might need two is if calling redaction when there are invited
|
||||
// users will cause issues,, we could just add a second method though.
|
||||
|
||||
// FIXME: Just add the invited users thing, we need that code here or there
|
||||
// so may aswell do it and test it.
|
||||
|
||||
// FIXME: We should consider updating both SetMembership and SetMembershipPolicies
|
||||
// to understand parted members...
|
||||
|
||||
export interface RedactionSynchronisationConsequences extends Capability {
|
||||
redactMessagesIn(
|
||||
userIDOrGlob: StringUserID,
|
||||
reason: string | undefined,
|
||||
roomIDs: StringRoomID[]
|
||||
): Promise<Result<void>>;
|
||||
rejectInvite(
|
||||
roomID: StringRoomID,
|
||||
sender: StringUserID,
|
||||
reciever: StringUserID,
|
||||
reason: string | undefined
|
||||
): Promise<Result<void>>;
|
||||
}
|
||||
|
||||
describeCapabilityInterface({
|
||||
name: "RedactionSynchronisationConsequences",
|
||||
description: "Consequences for the RedactionSynchronisationProtection",
|
||||
schema: Type.Object({
|
||||
redactMessagesIn: CapabilityMethodSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
describeCapabilityProvider({
|
||||
name: "StandardRedactionSynchronisationConsequences",
|
||||
interface: "RedactionSynchronisationConsequences",
|
||||
description: "redacts events and rejects invitations send by the target",
|
||||
factory(description, draupnir: Draupnir) {
|
||||
return Object.freeze({
|
||||
requiredPermissions: [
|
||||
PowerLevelPermission.Redact,
|
||||
PowerLevelPermission.Kick,
|
||||
],
|
||||
requiredStatePermissions: [],
|
||||
requiredEventPermissions: [],
|
||||
async redactMessagesIn(userIDOrGlob, reason, roomIDs) {
|
||||
const redactionResult = await redactUserMessagesIn(
|
||||
draupnir.client,
|
||||
draupnir.managementRoomOutput,
|
||||
userIDOrGlob,
|
||||
roomIDs
|
||||
).then(
|
||||
(_) => Ok(undefined),
|
||||
(error) =>
|
||||
ActionException.Result(
|
||||
`Error redacting messages for ${userIDOrGlob}`,
|
||||
{
|
||||
exception: error,
|
||||
exceptionKind: ActionExceptionKind.Unknown,
|
||||
}
|
||||
)
|
||||
);
|
||||
return redactionResult;
|
||||
},
|
||||
async rejectInvite(roomID, _sender, target, reason) {
|
||||
return await draupnir.clientPlatform
|
||||
.toRoomKicker()
|
||||
.kickUser(roomID, target, reason);
|
||||
},
|
||||
} satisfies RedactionSynchronisationConsequences);
|
||||
},
|
||||
});
|
||||
|
||||
// FIXME: We really need capability rendering to be configurable.
|
||||
describeCapabilityProvider({
|
||||
name: "SimulatedRedactionSynchronisationConsequences",
|
||||
interface: "RedactionSynchronisationConsequences",
|
||||
description: "Simulated redaction consequences",
|
||||
factory(_description, _context) {
|
||||
return Object.freeze({
|
||||
requiredPermissions: [],
|
||||
requiredStatePermissions: [],
|
||||
requiredEventPermissions: [],
|
||||
isSimulated: true,
|
||||
async redactMessagesIn() {
|
||||
return Ok(undefined);
|
||||
},
|
||||
async rejectInvite() {
|
||||
return Ok(undefined);
|
||||
},
|
||||
} satisfies RedactionSynchronisationConsequences);
|
||||
},
|
||||
});
|
||||
|
||||
describeCapabilityRenderer({
|
||||
name: "StandardRedactionSynchronisationConsequencesRenderer",
|
||||
interface: "RedactionSynchronisationConsequences",
|
||||
description: "Doesn't render anything tbh, because it would be too annoying",
|
||||
isDefaultForInterface: true,
|
||||
factory(
|
||||
_protectionDescription,
|
||||
_context,
|
||||
provider: RedactionSynchronisationConsequences
|
||||
) {
|
||||
return Object.freeze({
|
||||
...(provider.isSimulated ? { isSimulated: true } : {}),
|
||||
requiredPermissions: provider.requiredPermissions,
|
||||
requiredStatePermissions: provider.requiredStatePermissions,
|
||||
requiredEventPermissions: provider.requiredEventPermissions,
|
||||
async redactMessagesIn(userIDOrGlob, reason, roomIDs) {
|
||||
return await provider.redactMessagesIn(userIDOrGlob, reason, roomIDs);
|
||||
},
|
||||
async rejectInvite(roomID, sender, reciever, reason) {
|
||||
return await provider.rejectInvite(roomID, sender, reciever, reason);
|
||||
},
|
||||
} satisfies RedactionSynchronisationConsequences);
|
||||
},
|
||||
});
|
||||
|
||||
interface RedactionSynchronisation {
|
||||
// Used to check matching policies at startup.
|
||||
// Not used for applying redactions on match, since the new policy
|
||||
// hook is used for that.
|
||||
handlePermissionRequirementsMet(roomID: StringRoomID): void;
|
||||
// Used to check for when someone is trying to trigger draupnir to cleanup
|
||||
// or new policy was issued
|
||||
handlePolicyChange(policyChange: PolicyRuleChange): void;
|
||||
// Used to handle redactions/reject invitations as a user is banned
|
||||
handleMembershipChange(change: MembershipChange): void;
|
||||
}
|
||||
|
||||
export class StandardRedactionSynchronisation
|
||||
implements RedactionSynchronisation
|
||||
{
|
||||
public constructor(
|
||||
private readonly automaticRedactionReasons: MatrixGlob[],
|
||||
private readonly consequences: RedactionSynchronisationConsequences,
|
||||
private readonly watchedPolicyRooms: WatchedPolicyRooms,
|
||||
private readonly setRoomMembership: SetRoomMembership,
|
||||
private readonly setPoliciesMatchingMembership: SetMembershipPolicyRevisionIssuer
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
handlePermissionRequirementsMet(roomID: StringRoomID): void {
|
||||
const membershipRevision = this.setRoomMembership.getRevision(roomID);
|
||||
if (membershipRevision !== undefined) {
|
||||
this.checkRoomInvitations(membershipRevision);
|
||||
}
|
||||
for (const match of this.setPoliciesMatchingMembership.currentRevision.allMembersWithRules()) {
|
||||
if (membershipRevision?.membershipForUser(match.userID)) {
|
||||
const policyRequiringRedaction = match.policies.find((policy) =>
|
||||
this.isPolicyRequiringRedaction(policy)
|
||||
);
|
||||
if (policyRequiringRedaction !== undefined) {
|
||||
void Task(
|
||||
this.consequences.redactMessagesIn(
|
||||
match.userID,
|
||||
policyRequiringRedaction.reason,
|
||||
[roomID]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handlePolicyChange(change: PolicyRuleChange): void {
|
||||
const policy = change.rule;
|
||||
if (change.changeType === PolicyRuleChangeType.Removed) {
|
||||
return;
|
||||
}
|
||||
if (policy.kind !== PolicyRuleType.User) {
|
||||
return;
|
||||
}
|
||||
if (policy.matchType === PolicyRuleMatchType.HashedLiteral) {
|
||||
return;
|
||||
}
|
||||
if (!this.isPolicyRequiringRedaction(policy)) {
|
||||
return;
|
||||
}
|
||||
const roomsRequiringRedaction =
|
||||
policy.matchType === PolicyRuleMatchType.Literal
|
||||
? this.setRoomMembership.allRooms.filter((revision) =>
|
||||
revision.membershipForUser(StringUserID(policy.entity))
|
||||
)
|
||||
: this.setRoomMembership.allRooms;
|
||||
void Task(
|
||||
this.consequences.redactMessagesIn(
|
||||
StringUserID(policy.entity),
|
||||
policy.reason,
|
||||
roomsRequiringRedaction.map((revision) =>
|
||||
revision.room.toRoomIDOrAlias()
|
||||
)
|
||||
)
|
||||
);
|
||||
for (const revision of roomsRequiringRedaction) {
|
||||
this.checkRoomInvitations(revision);
|
||||
}
|
||||
}
|
||||
handleMembershipChange(change: MembershipChange): void {
|
||||
if (
|
||||
change.membershipChangeType === MembershipChangeType.Banned &&
|
||||
this.automaticRedactionReasons.some((reason) =>
|
||||
reason.test(change.content.reason ?? "<no reason supplied>")
|
||||
)
|
||||
) {
|
||||
void Task(
|
||||
this.consequences.redactMessagesIn(change.userID, undefined, [
|
||||
change.roomID,
|
||||
])
|
||||
);
|
||||
const membershipRevision = this.setRoomMembership.getRevision(
|
||||
change.roomID
|
||||
);
|
||||
if (membershipRevision) {
|
||||
this.checkRoomInvitations(membershipRevision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isPolicyRequiringRedaction(policy: PolicyRule): boolean {
|
||||
return this.automaticRedactionReasons.some((reason) =>
|
||||
reason.test(policy.reason ?? "<no reason supplied>")
|
||||
);
|
||||
}
|
||||
|
||||
private checkRoomInvitations(
|
||||
membershipRevision: RoomMembershipRevision
|
||||
): void {
|
||||
const invites = membershipRevision.membersOfMembership(Membership.Invite);
|
||||
for (const invite of invites) {
|
||||
const relevantRules = revisionRulesMatchingUser(
|
||||
invite.sender,
|
||||
[Recommendation.Takedown, Recommendation.Ban],
|
||||
this.watchedPolicyRooms.currentRevision
|
||||
).filter((policy) => this.isPolicyRequiringRedaction(policy));
|
||||
if (relevantRules.length > 0) {
|
||||
void Task(
|
||||
this.consequences.rejectInvite(
|
||||
invite.roomID,
|
||||
invite.sender,
|
||||
invite.userID,
|
||||
relevantRules.find((policy) => policy.reason !== undefined)?.reason
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RedactionSynchronisationProtection
|
||||
extends AbstractProtection<RedactionSynchronisationProtectionDescription>
|
||||
implements Protection<RedactionSynchronisationProtectionDescription>
|
||||
{
|
||||
private automaticRedactionReasons: MatrixGlob[] = [];
|
||||
private readonly redactionSynchronisation: RedactionSynchronisation;
|
||||
public constructor(
|
||||
description: RedactionSynchronisationProtectionDescription,
|
||||
capabilities: CapabilitySet,
|
||||
capabilities: RedactionSynchronisationProtectionCapabilitiesSet,
|
||||
protectedRoomsSet: ProtectedRoomsSet,
|
||||
private readonly draupnir: Draupnir
|
||||
automaticallyRedactForReasons: string[]
|
||||
) {
|
||||
super(description, capabilities, protectedRoomsSet, {
|
||||
requiredPermissions: [PowerLevelPermission.Redact],
|
||||
});
|
||||
for (const reason of draupnir.config.automaticallyRedactForReasons) {
|
||||
this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
|
||||
}
|
||||
}
|
||||
public redactForNewUserPolicy(policy: PolicyRule): void {
|
||||
const rooms: StringRoomID[] = [];
|
||||
if (policy.matchType === PolicyRuleMatchType.HashedLiteral) {
|
||||
return; // wait for them to be reversed and placed into the model as clear text.
|
||||
}
|
||||
if (policy.matchType === PolicyRuleMatchType.Glob) {
|
||||
this.protectedRoomsSet.allProtectedRooms.forEach((room) =>
|
||||
rooms.push(room.toRoomIDOrAlias())
|
||||
);
|
||||
} else {
|
||||
for (const roomMembership of this.protectedRoomsSet.setRoomMembership
|
||||
.allRooms) {
|
||||
const membership = roomMembership.membershipForUser(
|
||||
policy.entity as StringUserID
|
||||
);
|
||||
if (membership !== undefined) {
|
||||
rooms.push(roomMembership.room.toRoomIDOrAlias());
|
||||
}
|
||||
}
|
||||
}
|
||||
void Task(
|
||||
redactUserMessagesIn(
|
||||
this.draupnir.client,
|
||||
this.draupnir.managementRoomOutput,
|
||||
policy.entity,
|
||||
rooms
|
||||
)
|
||||
super(description, capabilities, protectedRoomsSet, {});
|
||||
this.redactionSynchronisation = new StandardRedactionSynchronisation(
|
||||
automaticallyRedactForReasons.map((reason) => new MatrixGlob(reason)),
|
||||
capabilities.consequences,
|
||||
protectedRoomsSet.watchedPolicyRooms,
|
||||
protectedRoomsSet.setRoomMembership,
|
||||
protectedRoomsSet.setPoliciesMatchingMembership
|
||||
);
|
||||
}
|
||||
|
||||
public async handlePolicyChange(
|
||||
revision: PolicyListRevision,
|
||||
_revision: PolicyListRevision,
|
||||
changes: PolicyRuleChange[]
|
||||
): Promise<ActionResult<void>> {
|
||||
const relevantChanges = changes.filter(
|
||||
(change) =>
|
||||
change.changeType !== PolicyRuleChangeType.Removed &&
|
||||
change.rule.kind === PolicyRuleType.User &&
|
||||
this.automaticRedactionReasons.some((reason) =>
|
||||
reason.test(change.rule.reason ?? "<no reason supplied>")
|
||||
)
|
||||
);
|
||||
// Can't see this fucking up at all when watching a new list :skull:.
|
||||
// So instead, we employ a genius big brain move.
|
||||
// Basically, this stops us from overwhelming draupnir with redaction
|
||||
// requests if the user watches a new list. Very unideal.
|
||||
// however, please see the comment at the top of the file which explains
|
||||
// how this protection **should** work, if it wasn't a stop gap.
|
||||
if (relevantChanges.length > 5) {
|
||||
return Ok(undefined);
|
||||
} else if (relevantChanges.length === 0) {
|
||||
return Ok(undefined);
|
||||
} else {
|
||||
relevantChanges.forEach((change) => {
|
||||
this.redactForNewUserPolicy(change.rule);
|
||||
});
|
||||
return Ok(undefined);
|
||||
}
|
||||
changes.forEach((change) => {
|
||||
this.redactionSynchronisation.handlePolicyChange(change);
|
||||
});
|
||||
return Ok(undefined);
|
||||
}
|
||||
|
||||
// Scan again on ban to make sure we mopped everything up.
|
||||
public async handleMembershipChange(
|
||||
revision: RoomMembershipRevision,
|
||||
_revision: RoomMembershipRevision,
|
||||
changes: MembershipChange[]
|
||||
): Promise<ActionResult<void>> {
|
||||
const isUserJoiningWithPolicyRequiringRedaction = (
|
||||
change: MembershipChange
|
||||
) => {
|
||||
if (
|
||||
change.membershipChangeType === MembershipChangeType.Joined ||
|
||||
change.membershipChangeType === MembershipChangeType.Rejoined
|
||||
) {
|
||||
const policyRevision =
|
||||
this.protectedRoomsSet.watchedPolicyRooms.currentRevision;
|
||||
const matchingPolicy =
|
||||
policyRevision.findRuleMatchingEntity(change.userID, {
|
||||
type: PolicyRuleType.User,
|
||||
recommendation: Recommendation.Ban,
|
||||
searchHashedRules: false,
|
||||
}) ??
|
||||
policyRevision.findRuleMatchingEntity(change.userID, {
|
||||
type: PolicyRuleType.User,
|
||||
recommendation: Recommendation.Takedown,
|
||||
searchHashedRules: false,
|
||||
});
|
||||
return (
|
||||
matchingPolicy !== undefined &&
|
||||
this.automaticRedactionReasons.some((reason) =>
|
||||
reason.test(matchingPolicy.reason ?? "<no reason supplied>")
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const relevantChanges = changes.filter(
|
||||
isUserJoiningWithPolicyRequiringRedaction
|
||||
);
|
||||
for (const change of relevantChanges) {
|
||||
void Task(
|
||||
redactUserMessagesIn(
|
||||
this.draupnir.client,
|
||||
this.draupnir.managementRoomOutput,
|
||||
change.userID,
|
||||
[revision.room.toRoomIDOrAlias()]
|
||||
)
|
||||
);
|
||||
}
|
||||
changes.forEach((change) => {
|
||||
this.redactionSynchronisation.handleMembershipChange(change);
|
||||
});
|
||||
return Ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describeProtection<Record<never, never>, Draupnir>({
|
||||
name: RedactionSynchronisationProtection.name,
|
||||
description:
|
||||
"Redacts messages when a new ban policy has been issued that matches config.automaticallyRedactForReasons. Work in progress.",
|
||||
capabilityInterfaces: {},
|
||||
defaultCapabilities: {},
|
||||
factory(description, protectedRoomsSet, draupnir, capabilities) {
|
||||
return Ok(
|
||||
new RedactionSynchronisationProtection(
|
||||
description,
|
||||
capabilities,
|
||||
protectedRoomsSet,
|
||||
draupnir
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
describeProtection<RedactionSynchronisationProtectionCapabilitiesSet, Draupnir>(
|
||||
{
|
||||
name: RedactionSynchronisationProtection.name,
|
||||
description:
|
||||
"Redacts messages when a new ban policy has been issued that matches config.automaticallyRedactForReasons. Work in progress.",
|
||||
capabilityInterfaces: {
|
||||
consequences: "RedactionSynchronisationConsequences",
|
||||
},
|
||||
defaultCapabilities: {
|
||||
consequences: "StandardRedactionSynchronisationConsequences",
|
||||
},
|
||||
factory(description, protectedRoomsSet, draupnir, capabilities) {
|
||||
return Ok(
|
||||
new RedactionSynchronisationProtection(
|
||||
description,
|
||||
capabilities,
|
||||
protectedRoomsSet,
|
||||
draupnir.config.automaticallyRedactForReasons
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
82
test/unit/protections/RedactionSynchronisationTest.ts
Normal file
82
test/unit/protections/RedactionSynchronisationTest.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import {
|
||||
describeProtectedRoomsSet,
|
||||
Membership,
|
||||
Ok,
|
||||
PolicyRuleType,
|
||||
randomRoomID,
|
||||
randomUserID,
|
||||
Recommendation,
|
||||
} from "matrix-protection-suite";
|
||||
import {
|
||||
RedactionSynchronisationConsequences,
|
||||
StandardRedactionSynchronisation,
|
||||
} from "../../../src/protections/RedactionSynchronisation";
|
||||
import { MatrixGlob } from "@the-draupnir-project/matrix-basic-types";
|
||||
import { createMock } from "ts-auto-mock";
|
||||
import expect from "expect";
|
||||
|
||||
describe("RedactionSynchronisation", function () {
|
||||
it("Attempts to retract invitations on permission requirements met", async function () {
|
||||
const room = randomRoomID([]);
|
||||
const targetUser = randomUserID();
|
||||
const { protectedRoomsSet } = await describeProtectedRoomsSet({
|
||||
rooms: [
|
||||
{
|
||||
room,
|
||||
membershipDescriptions: [
|
||||
{
|
||||
sender: targetUser,
|
||||
membership: Membership.Leave,
|
||||
},
|
||||
{
|
||||
sender: targetUser,
|
||||
membership: Membership.Invite,
|
||||
target: randomUserID(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
lists: [
|
||||
{
|
||||
policyDescriptions: [
|
||||
{
|
||||
recommendation: Recommendation.Ban,
|
||||
entity: targetUser,
|
||||
reason: "spam",
|
||||
type: PolicyRuleType.User,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
let mockMethodCalls = 0;
|
||||
const mockConsequences = createMock<RedactionSynchronisationConsequences>({
|
||||
async redactMessagesIn(userIDOrGlob, _reason, _roomIDs) {
|
||||
expect(userIDOrGlob).toBe(targetUser);
|
||||
mockMethodCalls += 1;
|
||||
return Ok(undefined);
|
||||
},
|
||||
async rejectInvite(roomID, sender, _reciever, _reason) {
|
||||
expect(roomID).toBe(room.toRoomIDOrAlias());
|
||||
expect(sender).toBe(targetUser);
|
||||
mockMethodCalls += 1;
|
||||
return Ok(undefined);
|
||||
},
|
||||
});
|
||||
const redactionSynronisationService = new StandardRedactionSynchronisation(
|
||||
[new MatrixGlob("spam")],
|
||||
mockConsequences,
|
||||
protectedRoomsSet.watchedPolicyRooms,
|
||||
protectedRoomsSet.setRoomMembership,
|
||||
protectedRoomsSet.setPoliciesMatchingMembership
|
||||
);
|
||||
redactionSynronisationService.handlePermissionRequirementsMet(
|
||||
room.toRoomIDOrAlias()
|
||||
);
|
||||
expect(mockMethodCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
16
yarn.lock
16
yarn.lock
@@ -2593,18 +2593,18 @@ matrix-appservice@^2.0.0:
|
||||
request-promise "^4.2.6"
|
||||
sanitize-html "^2.11.0"
|
||||
|
||||
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-3.0.0.tgz#eb4eaf07b040d223d18e949dfbe8d6712563974e"
|
||||
integrity sha512-Rv7Ouxc11z08RvJ6d+W1v8pTxWUsQoJztMR53YQaRV1Q2YBkRAPJxIm/xIAfAf15j32LYJR+2TKrs1HxwFMybQ==
|
||||
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-3.1.0.tgz#36ed640f6425f568e85c880b6e3da87019e06db5"
|
||||
integrity sha512-dgagHqAupTj7L/MAdXxwXFxNNceGALuMUr05WfnfBUfkbeI0xiC2rPtwKzh3xO2crjSYx52UE+59V73koHzDpQ==
|
||||
dependencies:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
await-lock "^2.2.2"
|
||||
|
||||
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-3.0.0.tgz#6e679f1f0e3b9a16f046c095d1ed1416a078e376"
|
||||
integrity sha512-X06uzOKN/fwOW2Vm3AKL2HKLbjU1PrqhUsYTzZGGQnEQYkAvoFnuK/TyKF8QaVoMJ/PF4Ybkkj6t5GbZGsGajg==
|
||||
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-3.1.0.tgz#5969adb791e28b5ed985cb82cf6d5bd0b2d2740c"
|
||||
integrity sha512-8w1uLsH1Q18qcl/vyGru+ckywEicg7JqPMgi9VGaHPhz9+JB5JmT+GoYXgyI4lEpqpiPv0uI+2bs2RkplWyseQ==
|
||||
dependencies:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
await-lock "^2.2.2"
|
||||
|
||||
Reference in New Issue
Block a user