"use strict";
// Copyright 2022 - 2024 Gnuxie <Gnuxie@protonmail.com>
// Copyright 2022 The Matrix.org Foundation C.I.C.
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
//
// SPDX-FileAttributionText: <text>
// This modified file incorporates work from mjolnir
// https://github.com/matrix-org/mjolnir
// </text>
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppServiceDraupnirManager = void 0;
const matrix_appservice_bridge_1 = require("matrix-appservice-bridge");
const config_1 = require("../config");
const crypto_1 = require("crypto");
const utils_1 = require("../utils");
const matrix_protection_suite_1 = require("matrix-protection-suite");
const matrix_protection_suite_for_matrix_bot_sdk_1 = require("matrix-protection-suite-for-matrix-bot-sdk");
const StandardDraupnirManager_1 = require("../draupnirfactory/StandardDraupnirManager");
const DraupnirFactory_1 = require("../draupnirfactory/DraupnirFactory");
const matrix_basic_types_1 = require("@the-draupnir-project/matrix-basic-types");
const managedRoomAccountData_1 = require("../managedRoomAccountData");
const log = new matrix_appservice_bridge_1.Logger("AppServiceDraupnirManager");
/**
 * The DraupnirManager is responsible for:
 * * Provisioning new draupnir instances.
 * * Starting draupnir when the appservice is brought online.
 * * Informing draupnir about new events.
 */
class AppServiceDraupnirManager {
    serverName;
    maxDraupnirsPerUser;
    dataStore;
    bridge;
    accessControl;
    roomStateManagerFactory;
    clientCapabilityFactory;
    clientsInRoomMap;
    instanceCountGauge;
    baseManager;
    constructor(serverName, maxDraupnirsPerUser, dataStore, bridge, accessControl, roomStateManagerFactory, stores, clientCapabilityFactory, clientsInRoomMap, clientProvider, instanceCountGauge) {
        this.serverName = serverName;
        this.maxDraupnirsPerUser = maxDraupnirsPerUser;
        this.dataStore = dataStore;
        this.bridge = bridge;
        this.accessControl = accessControl;
        this.roomStateManagerFactory = roomStateManagerFactory;
        this.clientCapabilityFactory = clientCapabilityFactory;
        this.clientsInRoomMap = clientsInRoomMap;
        this.instanceCountGauge = instanceCountGauge;
        const draupnirFactory = new DraupnirFactory_1.DraupnirFactory(this.roomStateManagerFactory.clientsInRoomMap, this.clientCapabilityFactory, clientProvider, this.roomStateManagerFactory, stores);
        this.baseManager = new StandardDraupnirManager_1.StandardDraupnirManager(draupnirFactory);
    }
    draupnirMXID(mjolnirRecord) {
        return `@${mjolnirRecord.local_part}:${this.serverName}`;
    }
    unregisterListeners() {
        this.baseManager.unregisterListeners();
    }
    /**
     * Create the draupnir manager from the datastore and the access control.
     * @param dataStore The data store interface that has the details for provisioned draupnirs.
     * @param bridge The bridge abstraction that encapsulates details about the appservice.
     * @param accessControl Who has access to the bridge.
     * @returns A new Draupnir manager.
     */
    static async makeDraupnirManager(serverName, maxDraupnirsPerUser, dataStore, bridge, accessControl, roomStateManagerFactory, stores, clientCapabilityFactory, clientsInRoomMap, clientProvider, instanceCountGauge) {
        const draupnirManager = new AppServiceDraupnirManager(serverName, maxDraupnirsPerUser, dataStore, bridge, accessControl, roomStateManagerFactory, stores, clientCapabilityFactory, clientsInRoomMap, clientProvider, instanceCountGauge);
        await draupnirManager.startDraupnirs(await dataStore.list());
        return draupnirManager;
    }
    /**
     * Creates a new draupnir for a user.
     * @param requestingUserID The user that is requesting this draupnir and who will own it.
     * @param managementRoomId An existing matrix room to act as the management room.
     * @param client A client for the appservice virtual user that the new draupnir should use.
     * @returns A new managed draupnir.
     */
    async makeInstance(localPart, requestingUserID, managementRoomID, client) {
        const mxid = (await client.getUserId());
        const managedDraupnir = await this.baseManager.makeDraupnir(mxid, matrix_basic_types_1.MatrixRoomReference.fromRoomID(managementRoomID), (0, config_1.getProvisionedMjolnirConfig)(managementRoomID));
        if ((0, matrix_protection_suite_1.isError)(managedDraupnir)) {
            return managedDraupnir;
        }
        (0, utils_1.incrementGaugeValue)(this.instanceCountGauge, "offline", localPart);
        (0, utils_1.decrementGaugeValue)(this.instanceCountGauge, "disabled", localPart);
        (0, utils_1.incrementGaugeValue)(this.instanceCountGauge, "online", localPart);
        return managedDraupnir;
    }
    /**
     * Gets a draupnir for the corresponding mxid that is owned by a specific user.
     * @param draupnirID The mxid of the draupnir we are trying to get.
     * @param ownerID The owner of the draupnir. We ask for it explicitly to not leak access to another user's draupnir.
     * @returns The matching managed draupnir instance.
     */
    async getRunningDraupnir(draupnirClientID, ownerID) {
        const records = await this.dataStore.lookupByOwner(ownerID);
        if (records.length === 0) {
            return undefined;
        }
        const associatedRecord = records.find((record) => record.local_part === (0, matrix_basic_types_1.userLocalpart)(draupnirClientID));
        if (associatedRecord === undefined || associatedRecord.owner !== ownerID) {
            return undefined;
        }
        return this.baseManager.findRunningDraupnir(draupnirClientID);
    }
    /**
     * Find all of the running Draupnir that are owned by this specific user.
     * @param ownerID An owner of multiple draupnir.
     * @returns Any draupnir that they own.
     */
    async getOwnedDraupnir(ownerID) {
        const records = await this.dataStore.lookupByOwner(ownerID);
        return records.map((record) => this.draupnirMXID(record));
    }
    /**
     * provision a new Draupnir for a matrix user.
     * @param requestingUserID The mxid of the user we are creating a Draupnir for.
     * @returns The matrix id of the new Draupnir and its management room.
     */
    async provisionNewDraupnir(requestingUserID) {
        return await this.provisionNewDraupnirInternal(requestingUserID, false);
    }
    async provisionNewDraupnirBypassingUserLimit(requestingUserID) {
        return await this.provisionNewDraupnirInternal(requestingUserID, true);
    }
    async provisionNewDraupnirInternal(requestingUserID, bypassUserLimit) {
        const access = this.accessControl.getUserAccess(requestingUserID);
        if (access.outcome !== matrix_protection_suite_1.Access.Allowed) {
            return matrix_protection_suite_1.ActionError.Result(`${requestingUserID} tried to provision a draupnir when they do not have access ${access.outcome} ${access.rule?.reason ?? "no reason specified"}`);
        }
        const provisionedMjolnirs = await this.dataStore.lookupByOwner(requestingUserID);
        if (bypassUserLimit ||
            provisionedMjolnirs.length < this.maxDraupnirsPerUser) {
            const mjolnirLocalPart = `draupnir_${(0, crypto_1.randomUUID)()}`;
            const mjIntent = await this.makeMatrixIntent(mjolnirLocalPart);
            const draupnirUserID = (0, matrix_basic_types_1.StringUserID)(mjIntent.userId);
            // we need to make sure the client rooms are available for the capability factory
            const clientRooms = await this.clientsInRoomMap.makeClientRooms(draupnirUserID, async () => (0, matrix_protection_suite_for_matrix_bot_sdk_1.joinedRoomsSafe)(mjIntent.matrixClient));
            if ((0, matrix_protection_suite_1.isError)(clientRooms)) {
                return clientRooms.elaborate("Unable to make client rooms for draupnir");
            }
            const clientPlatform = this.clientCapabilityFactory.makeClientPlatform(draupnirUserID, mjIntent.matrixClient);
            const managementRoom = await (0, managedRoomAccountData_1.loadOrCreateZeroTouchDeployRoom)(new matrix_protection_suite_for_matrix_bot_sdk_1.BotSDKMatrixAccountData(managedRoomAccountData_1.ZERO_TOUCH_DEPLOY_ROOM_ACCOUNT_DATA_TYPE, managedRoomAccountData_1.ZeroTouchDeployRoomAccountDataSchema, mjIntent.matrixClient), clientPlatform.toRoomCreator(), clientPlatform.toClientCapabilitiesNegotiation(), requestingUserID, draupnirUserID);
            if ((0, matrix_protection_suite_1.isError)(managementRoom)) {
                return managementRoom.elaborate("Failed to create management room for draupnir");
            }
            const draupnir = await this.makeInstance(mjolnirLocalPart, requestingUserID, managementRoom.ok.toRoomIDOrAlias(), mjIntent.matrixClient);
            if ((0, matrix_protection_suite_1.isError)(draupnir)) {
                return draupnir;
            }
            const policyListResult = await createFirstList(draupnir.ok, requestingUserID, "list");
            if ((0, matrix_protection_suite_1.isError)(policyListResult)) {
                return policyListResult;
            }
            const record = {
                local_part: mjolnirLocalPart,
                owner: requestingUserID,
                management_room: managementRoom.ok.toRoomIDOrAlias(),
            };
            await this.dataStore.store(record);
            return (0, matrix_protection_suite_1.Ok)(record);
        }
        else {
            return matrix_protection_suite_1.ActionError.Result(`User: ${requestingUserID} has already provisioned ${provisionedMjolnirs.length} draupnirs, which meets the configured limit of ${this.maxDraupnirsPerUser}.`);
        }
    }
    getUnstartedDraupnirs() {
        return this.baseManager.getUnstartedDraupnirs();
    }
    findUnstartedDraupnir(clientUserID) {
        return this.baseManager.findUnstartedDraupnir(clientUserID);
    }
    /**
     * Utility that creates a matrix client for a virtual user on our homeserver with the specified loclapart.
     * @param localPart The localpart of the virtual user we need a client for.
     * @returns A bridge intent with the complete mxid of the virtual user and a MatrixClient.
     */
    async makeMatrixIntent(localPart) {
        const mjIntent = this.bridge.getIntentFromLocalpart(localPart);
        await mjIntent.ensureRegistered();
        return mjIntent;
    }
    async startDraupnirFromMXID(draupnirClientID) {
        const records = await this.dataStore.lookupByLocalPart((0, matrix_basic_types_1.userLocalpart)(draupnirClientID));
        const firstRecord = records[0];
        if (firstRecord === undefined) {
            return matrix_protection_suite_1.ActionError.Result(`There is no record of a draupnir with the mxid ${draupnirClientID}`);
        }
        else {
            return await this.startDraupnirFromRecord(firstRecord);
        }
    }
    /**
     * Attempt to start a draupnir, and notify its management room of any failure to start.
     * Will be added to `this.unstartedMjolnirs` if we fail to start it AND it is not already running.
     * @param mjolnirRecord The record for the draupnir that we want to start.
     */
    async startDraupnirFromRecord(mjolnirRecord) {
        const clientUserID = this.draupnirMXID(mjolnirRecord);
        if (this.baseManager.isDraupnirAvailable(clientUserID)) {
            throw new TypeError(`${mjolnirRecord.local_part} is already running, we cannot start it.`);
        }
        const mjIntent = await this.makeMatrixIntent(mjolnirRecord.local_part);
        const access = this.accessControl.getUserAccess(mjolnirRecord.owner);
        if (access.outcome !== matrix_protection_suite_1.Access.Allowed) {
            // Don't await, we don't want to clobber initialization just because we can't tell someone they're no longer allowed.
            void (0, matrix_protection_suite_1.Task)((async () => {
                await mjIntent.matrixClient.sendNotice(mjolnirRecord.management_room, `Your draupnir has been disabled by the administrator: ${access.rule?.reason ?? "no reason supplied"}`);
            })());
            this.baseManager.reportUnstartedDraupnir(StandardDraupnirManager_1.DraupnirFailType.Unauthorized, access.outcome, clientUserID);
            (0, utils_1.decrementGaugeValue)(this.instanceCountGauge, "online", mjolnirRecord.local_part);
            (0, utils_1.incrementGaugeValue)(this.instanceCountGauge, "disabled", mjolnirRecord.local_part);
            return matrix_protection_suite_1.ActionError.Result(`Tried to start a draupnir that has been disabled by the administrator: ${access.rule?.reason ?? "no reason supplied"}`);
        }
        else {
            const startResult = await this.makeInstance(mjolnirRecord.local_part, mjolnirRecord.owner, mjolnirRecord.management_room, mjIntent.matrixClient).catch((e) => {
                log.error(`Could not start draupnir ${mjolnirRecord.local_part} for ${mjolnirRecord.owner}:`, e);
                this.baseManager.reportUnstartedDraupnir(StandardDraupnirManager_1.DraupnirFailType.StartError, e, clientUserID);
                return matrix_protection_suite_1.ActionException.Result(`Could not start draupnir ${clientUserID} for owner ${mjolnirRecord.owner}`, {
                    exception: (0, matrix_protection_suite_1.assertThrowableIsError)(e),
                    exceptionKind: matrix_protection_suite_1.ActionExceptionKind.Unknown,
                });
            });
            if ((0, matrix_protection_suite_1.isError)(startResult)) {
                // Don't await, we don't want to clobber initialization if this fails.
                void (0, matrix_protection_suite_1.Task)((async () => {
                    await mjIntent.matrixClient.sendNotice(mjolnirRecord.management_room, `Your draupnir could not be started. Please alert the administrator`);
                })());
                (0, utils_1.decrementGaugeValue)(this.instanceCountGauge, "online", mjolnirRecord.local_part);
                (0, utils_1.incrementGaugeValue)(this.instanceCountGauge, "offline", mjolnirRecord.local_part);
                return startResult;
            }
            return (0, matrix_protection_suite_1.Ok)(undefined);
        }
    }
    // TODO: We need to check that an owner still has access to the appservice each time they send a command to the mjolnir or use the web api.
    // https://github.com/matrix-org/mjolnir/issues/410
    /**
     * Used at startup to create all the ManagedMjolnir instances and start them so that they will respond to users.
     */
    async startDraupnirs(mjolnirRecords) {
        // Start the bots in small batches instead of sequentially.
        // This is to avoid a thundering herd of bots all starting at once.
        // It also is to avoid that others have to wait for a single bot to start.
        const chunkSize = 5;
        for (let i = 0; i < mjolnirRecords.length; i += chunkSize) {
            const batch = mjolnirRecords.slice(i, i + chunkSize);
            await Promise.all(
            // `startDraupnirFromRecord` handles errors for us and adds the draupnir to the list of unstarted draupnir.
            batch.map((record) => this.startDraupnirFromRecord(record)));
        }
    }
}
exports.AppServiceDraupnirManager = AppServiceDraupnirManager;
async function createFirstList(draupnir, draupnirOwnerID, shortcode) {
    const policyRoom = await draupnir.policyRoomManager.createPolicyRoom(shortcode, [draupnirOwnerID], { name: `${draupnirOwnerID}'s policy room` });
    if ((0, matrix_protection_suite_1.isError)(policyRoom)) {
        return policyRoom;
    }
    const addRoomResult = await draupnir.protectedRoomsSet.protectedRoomsManager.addRoom(policyRoom.ok);
    if ((0, matrix_protection_suite_1.isError)(addRoomResult)) {
        return addRoomResult;
    }
    return await draupnir.protectedRoomsSet.watchedPolicyRooms.watchPolicyRoomDirectly(policyRoom.ok);
}
//# sourceMappingURL=AppServiceDraupnirManager.js.map