"use strict";
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.WatchReplacementPolicyRooms = void 0;
const matrix_protection_suite_1 = require("matrix-protection-suite");
const StatusCommand_1 = require("../../commands/StatusCommand");
const interface_manager_1 = require("@the-draupnir-project/interface-manager");
const matrix_basic_types_1 = require("@the-draupnir-project/matrix-basic-types");
const typescript_result_1 = require("@gnuxie/typescript-result");
const mps_interface_adaptor_1 = require("@the-draupnir-project/mps-interface-adaptor");
const log = new matrix_protection_suite_1.Logger("WatchReplacementPolicyRooms");
const WatchReplacementPolicyRoomsPromptListenerName = "space.draupnir.watch_replacement_policy_rooms";
const WatchReplacementPolicyRoomsPromptReactionMap = {
    "Watch replacement only": "Watch replacement only",
    "Watch both": "Watch both",
    Cancel: "Cancel",
};
function renderPrivilegedUsers(revision) {
    const powerLevels = revision.getStateEvent("m.room.power_levels", "");
    if (powerLevels === undefined) {
        throw new TypeError("Mate can't find power levels within a room this is awful");
    }
    const poweredUsers = powerLevels.content.users
        ? Object.entries(powerLevels.content.users)
            .filter(([userID]) => matrix_protection_suite_1.PowerLevelsEventMirror.isUserAbleToSendEvent(userID, matrix_protection_suite_1.PolicyRuleType.User, powerLevels.content))
            // Descending order.
            .sort(([_u1, a], [_u2, b]) => b - a)
        : [];
    const createEvent = revision.getStateEvent("m.room.create", "");
    if (createEvent === undefined) {
        throw new TypeError("Mate can't find create event in the room");
    }
    const privilegedCreators = matrix_protection_suite_1.RoomVersionMirror.privilegedCreators(createEvent);
    return (interface_manager_1.DeadDocumentJSX.JSXFactory("fragment", null,
        interface_manager_1.DeadDocumentJSX.JSXFactory("details", null,
            interface_manager_1.DeadDocumentJSX.JSXFactory("summary", null,
                "Privileged users (",
                poweredUsers.length + privilegedCreators.length,
                ")"),
            interface_manager_1.DeadDocumentJSX.JSXFactory("h6", null, "Creators"),
            interface_manager_1.DeadDocumentJSX.JSXFactory("ul", null, privilegedCreators.map((creator) => (interface_manager_1.DeadDocumentJSX.JSXFactory("li", null, (0, mps_interface_adaptor_1.renderMentionPill)(creator, creator))))),
            interface_manager_1.DeadDocumentJSX.JSXFactory("h6", null, "Powered Users"),
            interface_manager_1.DeadDocumentJSX.JSXFactory("ul", null,
                " ",
                poweredUsers.map(([poweredUser, powerLevel]) => (interface_manager_1.DeadDocumentJSX.JSXFactory("li", null,
                    (0, mps_interface_adaptor_1.renderMentionPill)(poweredUser, poweredUser),
                    ":",
                    " ",
                    interface_manager_1.DeadDocumentJSX.JSXFactory("code", null, powerLevel))))))));
}
function renderReplacementPrompt(originalWatchProfile, originalRoomStateRevision, replacementWatchProfile, replacementRoomStateRevision) {
    return (interface_manager_1.DeadDocumentJSX.JSXFactory("root", null,
        interface_manager_1.DeadDocumentJSX.JSXFactory("h4", null, "Policy room replaced"),
        "The following policy room has been replaced, would you like Draupnir to watch the new policy room?",
        interface_manager_1.DeadDocumentJSX.JSXFactory("h5", null, "Old room"),
        interface_manager_1.DeadDocumentJSX.JSXFactory("ul", null, (0, StatusCommand_1.renderPolicyList)(originalWatchProfile)),
        renderPrivilegedUsers(originalRoomStateRevision),
        interface_manager_1.DeadDocumentJSX.JSXFactory("h5", null, "Replacement room"),
        interface_manager_1.DeadDocumentJSX.JSXFactory("ul", null, (0, StatusCommand_1.renderPolicyList)(replacementWatchProfile)),
        renderPrivilegedUsers(replacementRoomStateRevision),
        interface_manager_1.DeadDocumentJSX.JSXFactory("h4", null, "Prompt"),
        "Unfortunately we are unable to determine the policy list curator's intent with this upgrade, and whether or not they want you to still watch the old room."));
}
async function sendPromptForReplacementRoom(roomMessageSender, managementRoomID, tombstoneSender, reactionHandler, originalWatchProfile, originalRoomStateRevision, replacementWatchProfile, replacementRoomStateRevision) {
    const reactionMap = new Map(Object.entries(WatchReplacementPolicyRoomsPromptReactionMap));
    const sendResult = await (0, mps_interface_adaptor_1.sendMatrixEventsFromDeadDocument)(roomMessageSender, managementRoomID, renderReplacementPrompt(originalWatchProfile, originalRoomStateRevision, replacementWatchProfile, replacementRoomStateRevision), {
        additionalContent: reactionHandler.createAnnotation(WatchReplacementPolicyRoomsPromptListenerName, reactionMap, {
            tombstoneSender,
            original_room_id: originalWatchProfile.room.toRoomIDOrAlias(),
            replacement_room_id: replacementWatchProfile.room.toRoomIDOrAlias(),
        }),
    });
    if ((0, typescript_result_1.isError)(sendResult)) {
        return sendResult;
    }
    return await reactionHandler.addReactionsToEvent(managementRoomID, sendResult.ok[0], reactionMap);
}
/**
 * This class sends prompts to the management room to watch upgraded policy rooms.
 *
 * Lifecycle:
 * - `unregisterListeners` must be called to dispose.
 * - `handleRoomStateChange` must be called if you want it to detect active tombstones.
 * - `syncTombstonedPolicyRooms` must be called if you want it to detect stale tombstones at startup.
 */
class WatchReplacementPolicyRooms {
    managementRoomID;
    roomJoiner;
    roomMessageSender;
    roomReactionSender;
    watchedPolicyRooms;
    roomStateManager;
    policyRoomManager;
    reactionHandler;
    watchReplacementPolicyRoomsPromptListener = (...args) => {
        void (0, matrix_protection_suite_1.Task)(this.handlePromptReaction(...args), {
            log,
        });
    };
    constructor(managementRoomID, roomJoiner, roomMessageSender, roomReactionSender, watchedPolicyRooms, roomStateManager, policyRoomManager, reactionHandler) {
        this.managementRoomID = managementRoomID;
        this.roomJoiner = roomJoiner;
        this.roomMessageSender = roomMessageSender;
        this.roomReactionSender = roomReactionSender;
        this.watchedPolicyRooms = watchedPolicyRooms;
        this.roomStateManager = roomStateManager;
        this.policyRoomManager = policyRoomManager;
        this.reactionHandler = reactionHandler;
        this.reactionHandler.addListener(WatchReplacementPolicyRoomsPromptListenerName, this.watchReplacementPolicyRoomsPromptListener);
    }
    handleRoomStateChange(changes) {
        for (const change of changes) {
            if (change.eventType !== "m.room.tombstone") {
                continue;
            }
            switch (change.changeType) {
                case matrix_protection_suite_1.StateChangeType.Introduced:
                case matrix_protection_suite_1.StateChangeType.PartiallyRedacted:
                case matrix_protection_suite_1.StateChangeType.Reintroduced:
                case matrix_protection_suite_1.StateChangeType.SupersededContent:
                    break;
                default:
                    continue;
            }
            if (matrix_protection_suite_1.Value.Check(matrix_protection_suite_1.TombstoneEvent, change.state)) {
                void this.handleTombstoneAndPhoneManagementRoom(change.state);
            }
        }
    }
    async syncTombstonedPolicyRooms() {
        for (const profile of this.watchedPolicyRooms.allRooms) {
            const roomStateRevision = (await this.roomStateManager.getRoomStateRevisionIssuer(profile.room)).expect("Should always be able to get the room state revision issuer for a watched policy room").currentRevision;
            const tombstoneEvent = roomStateRevision.getStateEvent("m.room.tombstone", "");
            if (tombstoneEvent === undefined ||
                tombstoneEvent.content.replacement_room === undefined) {
                continue; // room hasn't been upgraded yet.
            }
            const replacementWatchProfile = this.watchedPolicyRooms.allRooms.find((profile) => profile.room.toRoomIDOrAlias() ===
                tombstoneEvent.content.replacement_room);
            if (replacementWatchProfile !== undefined) {
                continue; // room upgrade has been handled
                // TODO: in MSC4321 if the state is move then we should still send the prompt.
            }
            void this.handleTombstoneAndPhoneManagementRoom(tombstoneEvent);
        }
    }
    async handleTombstoneAndPhoneManagementRoom(event) {
        const handleTombstoneResult = await this.handleTombstone(event);
        if ((0, typescript_result_1.isOk)(handleTombstoneResult)) {
            return;
        }
        const error = handleTombstoneResult.error;
        void (0, matrix_protection_suite_1.Task)((0, mps_interface_adaptor_1.sendMatrixEventsFromDeadDocument)(this.roomMessageSender, this.managementRoomID, interface_manager_1.DeadDocumentJSX.JSXFactory("root", null,
            "A policy room was replaced, but there was an error during the upgrade process.",
            (0, mps_interface_adaptor_1.renderErrorDetails)(error)), {}), { log });
    }
    async handleTombstone(event) {
        const findWatchProfile = (roomID) => this.watchedPolicyRooms.allRooms.find((profile) => profile.room.toRoomIDOrAlias() === roomID);
        const oldRoomWatchProfile = findWatchProfile(event.room_id);
        if (!oldRoomWatchProfile ||
            event.content.replacement_room === undefined ||
            // Make sure that we aren't already watching the replacement room.
            findWatchProfile(event.content.replacement_room)) {
            return (0, typescript_result_1.Ok)(undefined); // already watching the replacement.
        }
        const originalRoomStateRevisionIssuer = await this.roomStateManager.getRoomStateRevisionIssuer(oldRoomWatchProfile.room);
        if ((0, typescript_result_1.isError)(originalRoomStateRevisionIssuer)) {
            return originalRoomStateRevisionIssuer.elaborate("Unable to fetch room state revision for original policy room");
        }
        const replacementRoom = new matrix_basic_types_1.MatrixRoomID(event.content.replacement_room, [
            (0, matrix_basic_types_1.userServerName)(event.sender),
        ]);
        const joinAttempt = await this.roomJoiner.joinRoom(replacementRoom);
        if ((0, typescript_result_1.isError)(joinAttempt)) {
            return joinAttempt.elaborate("Unable to join the replacement room");
        }
        const replacementRoomStateRevisionIssuer = await this.roomStateManager.getRoomStateRevisionIssuer(replacementRoom);
        if ((0, typescript_result_1.isError)(replacementRoomStateRevisionIssuer)) {
            return replacementRoomStateRevisionIssuer.elaborate("Unable to fetch room state revision for an upgraded policy room");
        }
        const policyRoomRevisionIssuer = await this.policyRoomManager.getPolicyRoomRevisionIssuer(replacementRoom);
        if ((0, typescript_result_1.isError)(policyRoomRevisionIssuer)) {
            return policyRoomRevisionIssuer.elaborate("Unable to fetch a policy room revision for an upgraded policy room");
        }
        const replacementRoomWatchProfile = {
            room: replacementRoom,
            propagation: oldRoomWatchProfile.propagation,
            revision: policyRoomRevisionIssuer.ok.currentRevision,
        };
        // Shouldn't this be a prompt to watch the new room?
        // Yes. it shouldn't happen automatically because it could be a hostile
        // takeover or something.
        // It is important to prompt specifically because new replacement rooms
        // are always prone to configuration errors. Ie room admins may misconfigure
        // the permissions in the new room and need to make yet another replacement.
        // This could allow a hypothetical attack where the tombstone permission
        // can be insecure in a new room and with automatic following they can
        // cause absolute havoc.
        const sendResult = await sendPromptForReplacementRoom(this.roomMessageSender, this.managementRoomID, event.sender, this.reactionHandler, oldRoomWatchProfile, originalRoomStateRevisionIssuer.ok.currentRevision, replacementRoomWatchProfile, replacementRoomStateRevisionIssuer.ok.currentRevision);
        if ((0, typescript_result_1.isError)(sendResult)) {
            return sendResult.elaborate("Unable to send prompt to the management room");
        }
        return (0, typescript_result_1.Ok)(undefined);
    }
    async handlePromptReaction(key, item, context, _reactionMap, promptEvent) {
        const renderResultToPromptEvent = (result) => {
            (0, mps_interface_adaptor_1.renderActionResultToEvent)(this.roomMessageSender, this.roomReactionSender, promptEvent, result);
            if ((0, typescript_result_1.isOk)(result)) {
                void (0, matrix_protection_suite_1.Task)(this.reactionHandler.completePrompt(promptEvent.room_id, promptEvent.event_id));
            }
        };
        if (key === WatchReplacementPolicyRoomsPromptReactionMap.Cancel) {
            void (0, matrix_protection_suite_1.Task)(this.reactionHandler.cancelPrompt(promptEvent));
            return;
        }
        if (key === WatchReplacementPolicyRoomsPromptReactionMap["Watch both"]) {
            const watchReplacementPolicyRoomResult = await this.watchedPolicyRooms.watchPolicyRoomDirectly(new matrix_basic_types_1.MatrixRoomID(context.replacement_room_id, [
                (0, matrix_basic_types_1.userServerName)(context.tombstoneSender),
            ]));
            renderResultToPromptEvent(watchReplacementPolicyRoomResult);
            return;
        }
        if (key ===
            WatchReplacementPolicyRoomsPromptReactionMap["Watch replacement only"]) {
            const watchReplacementPolicyRoomResult = await this.watchedPolicyRooms.watchPolicyRoomDirectly(new matrix_basic_types_1.MatrixRoomID(context.replacement_room_id, [
                (0, matrix_basic_types_1.userServerName)(context.tombstoneSender),
            ]));
            if ((0, typescript_result_1.isError)(watchReplacementPolicyRoomResult)) {
                renderResultToPromptEvent(watchReplacementPolicyRoomResult);
                return;
            }
            const unwatchOriginalPolicyRoomResult = await this.watchedPolicyRooms.unwatchPolicyRoom(new matrix_basic_types_1.MatrixRoomID(context.original_room_id, [
                (0, matrix_basic_types_1.userServerName)(context.tombstoneSender),
            ]));
            renderResultToPromptEvent(unwatchOriginalPolicyRoomResult);
            return;
        }
        log.error("Could not handle the prompt to upgrade the room", context.original_room_id);
    }
    unregisterListeners() {
        this.reactionHandler.removeListener(WatchReplacementPolicyRoomsPromptListenerName, this.watchReplacementPolicyRoomsPromptListener);
    }
}
exports.WatchReplacementPolicyRooms = WatchReplacementPolicyRooms;
//# sourceMappingURL=WatchReplacementPolicyRooms.js.map