Skip to content

Commit 5b13449

Browse files
authored
Add preview to watch command. (#1007)
the-draupnir-project/planning#2
1 parent e64c436 commit 5b13449

File tree

5 files changed

+273
-39
lines changed

5 files changed

+273
-39
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"jsdom": "^24.0.0",
6565
"matrix-appservice-bridge": "^10.3.1",
6666
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.1-element.6",
67-
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@5.0.0",
67+
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@5.1.0",
6868
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/[email protected]",
6969
"pg": "^8.8.0",
7070
"yaml": "^2.3.2"

src/commands/WatchPreview.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-FileCopyrightText: 2025 Gnuxie <[email protected]>
2+
//
3+
// SPDX-License-Identifier: AFL-3.0
4+
5+
import {
6+
DeadDocumentJSX,
7+
DocumentNode,
8+
} from "@the-draupnir-project/interface-manager";
9+
import {
10+
renderMentionPill,
11+
renderRoomPill,
12+
} from "@the-draupnir-project/mps-interface-adaptor";
13+
import {
14+
MemberBanSynchronisationProtection,
15+
PolicyRoomRevision,
16+
PolicyRuleChange,
17+
PolicyRuleChangeType,
18+
ProtectedRoomsSet,
19+
MemberBanIntentProjectionNode,
20+
ServerBanIntentProjectionNode,
21+
MemberBanIntentProjectionDelta,
22+
SetMembershipPolicyRevision,
23+
SetMembershipRevision,
24+
ServerBanSynchronisationProtection,
25+
ServerBanIntentProjectionDelta,
26+
} from "matrix-protection-suite";
27+
28+
function watchDeltaForMemberBanIntents(
29+
setMembershipPoliciesRevision: SetMembershipPolicyRevision,
30+
setMembershipRevision: SetMembershipRevision,
31+
watchedPoliciesDelta: PolicyRuleChange[],
32+
memberBanIntentProjectionNode: MemberBanIntentProjectionNode
33+
): MemberBanIntentProjectionDelta {
34+
const setMembershipPoliciesRevisionDelta =
35+
setMembershipPoliciesRevision.changesFromPolicyChanges(
36+
watchedPoliciesDelta,
37+
setMembershipRevision
38+
);
39+
return memberBanIntentProjectionNode.reduceInput(
40+
setMembershipPoliciesRevisionDelta
41+
);
42+
}
43+
44+
function watchDeltaForServerBanIntents(
45+
watchedPoliciesDelta: PolicyRuleChange[],
46+
serverBanIntentProjectionNode: ServerBanIntentProjectionNode
47+
) {
48+
return serverBanIntentProjectionNode.reduceInput(watchedPoliciesDelta);
49+
}
50+
51+
export type WatchPolicyRoomPreview = {
52+
memberBanIntentProjectionDelta: MemberBanIntentProjectionDelta | undefined;
53+
serverBanIntentProjectionDelta: ServerBanIntentProjectionDelta | undefined;
54+
revision: PolicyRoomRevision;
55+
};
56+
57+
export function generateWatchPreview(
58+
protectedRoomsSet: ProtectedRoomsSet,
59+
nextPolicyRoom: PolicyRoomRevision
60+
): WatchPolicyRoomPreview {
61+
const previewPoliciesRevisionChanges = nextPolicyRoom
62+
.allRules()
63+
.map((rule) => ({
64+
changeType: PolicyRuleChangeType.Added,
65+
rule,
66+
event: rule.sourceEvent,
67+
sender: rule.sourceEvent.sender,
68+
}));
69+
const currentMemberBanIntentProjectionNode = (
70+
protectedRoomsSet.protections.findEnabledProtection(
71+
MemberBanSynchronisationProtection.name
72+
) as MemberBanSynchronisationProtection | undefined
73+
)?.intentProjection.currentNode;
74+
const currentServerBanIntentProjectionNode = (
75+
protectedRoomsSet.protections.findEnabledProtection(
76+
ServerBanSynchronisationProtection.name
77+
) as ServerBanSynchronisationProtection | undefined
78+
)?.intentProjection.currentNode;
79+
return {
80+
memberBanIntentProjectionDelta: currentMemberBanIntentProjectionNode
81+
? watchDeltaForMemberBanIntents(
82+
protectedRoomsSet.setPoliciesMatchingMembership.currentRevision,
83+
protectedRoomsSet.setMembership.currentRevision,
84+
previewPoliciesRevisionChanges,
85+
currentMemberBanIntentProjectionNode
86+
)
87+
: undefined,
88+
serverBanIntentProjectionDelta: currentServerBanIntentProjectionNode
89+
? watchDeltaForServerBanIntents(
90+
previewPoliciesRevisionChanges,
91+
currentServerBanIntentProjectionNode
92+
)
93+
: undefined,
94+
revision: nextPolicyRoom,
95+
};
96+
}
97+
98+
export function renderMemberBanIntents(
99+
delta: MemberBanIntentProjectionDelta
100+
): DocumentNode {
101+
return (
102+
<details>
103+
<summary>Draupnir intends to ban {delta.ban.length} users</summary>
104+
<ul>
105+
{delta.ban.map((userID) => (
106+
<li>
107+
{renderMentionPill(userID, userID)} (<code>{userID}</code>)
108+
</li>
109+
))}
110+
</ul>
111+
</details>
112+
);
113+
}
114+
115+
export function renderServerBanIntents(
116+
delta: ServerBanIntentProjectionDelta
117+
): DocumentNode {
118+
return (
119+
<details>
120+
<summary>Draupnir intends to deny {delta.deny.length} servers</summary>
121+
<ul>
122+
{delta.deny.map((serverName) => (
123+
<li>
124+
<code>{serverName}</code>
125+
</li>
126+
))}
127+
</ul>
128+
</details>
129+
);
130+
}
131+
132+
export function renderWatchPreview(
133+
preview: WatchPolicyRoomPreview
134+
): DocumentNode {
135+
return (
136+
<fragment>
137+
<h4>
138+
Policy Room Subscription Preview for{" "}
139+
{renderRoomPill(preview.revision.room)}
140+
</h4>
141+
{preview.memberBanIntentProjectionDelta ? (
142+
renderMemberBanIntents(preview.memberBanIntentProjectionDelta)
143+
) : (
144+
<fragment></fragment>
145+
)}
146+
{preview.serverBanIntentProjectionDelta ? (
147+
renderServerBanIntents(preview.serverBanIntentProjectionDelta)
148+
) : (
149+
<fragment></fragment>
150+
)}
151+
</fragment>
152+
);
153+
}
154+
155+
export function renderWatchCommandPreview(preview: WatchPolicyRoomPreview) {
156+
return <root>{renderWatchPreview(preview)}</root>;
157+
}
Lines changed: 87 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 Gnuxie <[email protected]>
1+
// Copyright 2022, 2025 Gnuxie <[email protected]>
22
// Copyright 2019 The Matrix.org Foundation C.I.C.
33
//
44
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
@@ -9,7 +9,9 @@
99
// </text>
1010

1111
import {
12-
RoomResolver,
12+
PolicyRoomManager,
13+
ProtectedRoomsSet,
14+
RoomJoiner,
1315
WatchedPolicyRooms,
1416
isError,
1517
} from "matrix-protection-suite";
@@ -18,16 +20,23 @@ import {
1820
describeCommand,
1921
tuple,
2022
} from "@the-draupnir-project/interface-manager";
21-
import { Result, ResultError } from "@gnuxie/typescript-result";
23+
import { Ok, Result, ResultError } from "@gnuxie/typescript-result";
2224
import { Draupnir } from "../Draupnir";
2325
import {
2426
DraupnirContextToCommandContextTranslator,
2527
DraupnirInterfaceAdaptor,
2628
} from "./DraupnirCommandPrerequisites";
29+
import {
30+
generateWatchPreview,
31+
renderWatchCommandPreview,
32+
WatchPolicyRoomPreview,
33+
} from "./WatchPreview";
2734

2835
export type DraupnirWatchUnwatchCommandContext = {
2936
watchedPolicyRooms: WatchedPolicyRooms;
30-
roomResolver: RoomResolver;
37+
roomJoiner: RoomJoiner;
38+
policyRoomManager: PolicyRoomManager;
39+
protectedRoomsSet: ProtectedRoomsSet;
3140
};
3241

3342
export const DraupnirWatchPolicyRoomCommand = describeCommand({
@@ -37,16 +46,29 @@ export const DraupnirWatchPolicyRoomCommand = describeCommand({
3746
name: "policy room",
3847
acceptor: MatrixRoomReferencePresentationSchema,
3948
}),
49+
keywords: {
50+
keywordDescriptions: {
51+
"no-confirm": {
52+
isFlag: true,
53+
description: "Runs the command without the preview.",
54+
},
55+
},
56+
},
4057
async executor(
41-
{ watchedPolicyRooms, roomResolver }: DraupnirWatchUnwatchCommandContext,
58+
{
59+
watchedPolicyRooms,
60+
roomJoiner,
61+
policyRoomManager,
62+
protectedRoomsSet,
63+
}: DraupnirWatchUnwatchCommandContext,
4264
_info,
43-
_keywords,
65+
keywords,
4466
_rest,
4567
policyRoomReference
46-
): Promise<Result<void>> {
47-
const policyRoom = await roomResolver.resolveRoom(policyRoomReference);
68+
): Promise<Result<undefined | WatchPolicyRoomPreview>> {
69+
const policyRoom = await roomJoiner.joinRoom(policyRoomReference);
4870
if (isError(policyRoom)) {
49-
return policyRoom;
71+
return policyRoom.elaborate("Failed to resolve or join the room");
5072
}
5173
if (
5274
watchedPolicyRooms.allRooms.some(
@@ -56,10 +78,46 @@ export const DraupnirWatchPolicyRoomCommand = describeCommand({
5678
) {
5779
return ResultError.Result("We are already watching this list.");
5880
}
59-
return await watchedPolicyRooms.watchPolicyRoomDirectly(policyRoom.ok);
81+
if (keywords.getKeywordValue<boolean>("no-confirm", false)) {
82+
const watchResult = await watchedPolicyRooms.watchPolicyRoomDirectly(
83+
policyRoom.ok
84+
);
85+
if (isError(watchResult)) {
86+
return watchResult;
87+
}
88+
return Ok(undefined);
89+
}
90+
const revisionIssuer = await policyRoomManager.getPolicyRoomRevisionIssuer(
91+
policyRoom.ok
92+
);
93+
if (isError(revisionIssuer)) {
94+
return revisionIssuer.elaborate(
95+
"Failed to fetch policy room revision issuer"
96+
);
97+
}
98+
return Ok(
99+
generateWatchPreview(protectedRoomsSet, revisionIssuer.ok.currentRevision)
100+
);
60101
},
61102
});
62103

104+
DraupnirInterfaceAdaptor.describeRenderer(DraupnirWatchPolicyRoomCommand, {
105+
isAlwaysSupposedToUseDefaultRenderer: true,
106+
confirmationPromptJSXRenderer(commandResult) {
107+
if (isError(commandResult)) {
108+
return Ok(undefined);
109+
} else if (commandResult.ok === undefined) {
110+
return Ok(undefined);
111+
} else {
112+
return Ok(renderWatchCommandPreview(commandResult.ok));
113+
}
114+
},
115+
});
116+
DraupnirContextToCommandContextTranslator.registerTranslation(
117+
DraupnirWatchPolicyRoomCommand,
118+
buildWatchContext
119+
);
120+
63121
export const DraupnirUnwatchPolicyRoomCommand = describeCommand({
64122
summary:
65123
"Unwatches a list and stops applying the list's assocated policies to draupnir's protected rooms.",
@@ -68,32 +126,35 @@ export const DraupnirUnwatchPolicyRoomCommand = describeCommand({
68126
acceptor: MatrixRoomReferencePresentationSchema,
69127
}),
70128
async executor(
71-
{ watchedPolicyRooms, roomResolver }: DraupnirWatchUnwatchCommandContext,
129+
{ watchedPolicyRooms, roomJoiner }: DraupnirWatchUnwatchCommandContext,
72130
_info,
73131
_keywords,
74132
_rest,
75133
policyRoomReference
76134
): Promise<Result<void>> {
77-
const policyRoom = await roomResolver.resolveRoom(policyRoomReference);
135+
const policyRoom = await roomJoiner.resolveRoom(policyRoomReference);
78136
if (isError(policyRoom)) {
79137
return policyRoom;
80138
}
81139
return await watchedPolicyRooms.unwatchPolicyRoom(policyRoom.ok);
82140
},
83141
});
84142

85-
for (const command of [
86-
DraupnirWatchPolicyRoomCommand,
87-
DraupnirUnwatchPolicyRoomCommand,
88-
]) {
89-
DraupnirInterfaceAdaptor.describeRenderer(command, {
90-
isAlwaysSupposedToUseDefaultRenderer: true,
91-
});
92-
DraupnirContextToCommandContextTranslator.registerTranslation(
93-
command,
94-
(draupnir: Draupnir) => ({
95-
watchedPolicyRooms: draupnir.protectedRoomsSet.watchedPolicyRooms,
96-
roomResolver: draupnir.clientPlatform.toRoomResolver(),
97-
})
98-
);
143+
function buildWatchContext(
144+
draupnir: Draupnir
145+
): DraupnirWatchUnwatchCommandContext {
146+
return {
147+
watchedPolicyRooms: draupnir.protectedRoomsSet.watchedPolicyRooms,
148+
roomJoiner: draupnir.clientPlatform.toRoomJoiner(),
149+
policyRoomManager: draupnir.policyRoomManager,
150+
protectedRoomsSet: draupnir.protectedRoomsSet,
151+
};
99152
}
153+
154+
DraupnirInterfaceAdaptor.describeRenderer(DraupnirUnwatchPolicyRoomCommand, {
155+
isAlwaysSupposedToUseDefaultRenderer: true,
156+
});
157+
DraupnirContextToCommandContextTranslator.registerTranslation(
158+
DraupnirUnwatchPolicyRoomCommand,
159+
buildWatchContext
160+
);

0 commit comments

Comments
 (0)