"use strict";
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// 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.JoinWaveShortCircuitProtection = exports.JoinWaveCommandTable = void 0;
const matrix_protection_suite_1 = require("matrix-protection-suite");
const matrix_bot_sdk_1 = require("@vector-im/matrix-bot-sdk");
const matrix_basic_types_1 = require("@the-draupnir-project/matrix-basic-types");
const typebox_1 = require("@sinclair/typebox");
const interface_manager_1 = require("@the-draupnir-project/interface-manager");
const DraupnirCommandPrerequisites_1 = require("../commands/DraupnirCommandPrerequisites");
const LeakyBucket_1 = require("../queues/LeakyBucket");
const mps_interface_adaptor_1 = require("@the-draupnir-project/mps-interface-adaptor");
const log = new matrix_protection_suite_1.Logger("JoinWaveShortCircuitProtection");
const DEFAULT_MAX_PER_TIMESCALE = 50;
const DEFAULT_TIMESCALE_MINUTES = 60;
const ONE_MINUTE = 60_000; // 1min in ms
const JoinWaveShortCircuitProtectionSettings = typebox_1.Type.Object({
    maxPer: typebox_1.Type.Integer({
        default: DEFAULT_MAX_PER_TIMESCALE,
        description: "The maximum number of users that can join a room in the timescaleMinutes timescale before the room is set to invite-only.",
    }),
    timescaleMinutes: typebox_1.Type.Integer({
        default: DEFAULT_TIMESCALE_MINUTES,
        description: "The timescale in minutes over which the maxPer users can join before the room is set to invite-only.",
    }),
}, { title: "JoinWaveShortCircuitProtectionSettings" });
exports.JoinWaveCommandTable = new interface_manager_1.StandardCommandTable("JoinWaveShortCircuitProtection");
(0, matrix_protection_suite_1.describeProtection)({
    name: "JoinWaveShortCircuitProtection",
    description: "If a wave of users join in a given time frame, then the protection can set the room to invite-only.",
    capabilityInterfaces: {},
    defaultCapabilities: {},
    configSchema: JoinWaveShortCircuitProtectionSettings,
    factory: async function (description, lifetime, protectedRoomsSet, draupnir, capabilities, settings) {
        const parsedSettings = description.protectionSettings.parseConfig(settings);
        if ((0, matrix_protection_suite_1.isError)(parsedSettings)) {
            return parsedSettings;
        }
        return (0, matrix_protection_suite_1.allocateProtection)(lifetime, new JoinWaveShortCircuitProtection(description, lifetime, capabilities, protectedRoomsSet, draupnir, parsedSettings.ok));
    },
});
class JoinWaveShortCircuitProtection extends matrix_protection_suite_1.AbstractProtection {
    draupnir;
    settings;
    joinBuckets;
    constructor(description, lifetime, capabilities, protectedRoomsSet, draupnir, settings) {
        super(description, lifetime, capabilities, protectedRoomsSet, {
            requiredStatePermissions: ["m.room.join_rules"],
        });
        this.draupnir = draupnir;
        this.settings = settings;
        this.joinBuckets = new LeakyBucket_1.LazyLeakyBucket(this.settings.maxPer, this.timescaleMilliseconds());
    }
    async handleMembershipChange(revision, changes) {
        const roomID = revision.room.toRoomIDOrAlias();
        for (const change of changes) {
            await this.handleMembership(roomID, change).catch((e) => {
                log.error(`Unexpected error handling memebership change`, e);
            });
        }
        return (0, matrix_protection_suite_1.Ok)(undefined);
    }
    async handleMembership(roomID, change) {
        if (change.membershipChangeType !== matrix_protection_suite_1.MembershipChangeType.Joined) {
            return;
        }
        const numberOfJoins = this.joinBuckets.addToken(roomID);
        if (numberOfJoins >= this.settings.maxPer) {
            // we should check that we haven't already set the room to invite only
            const revision = this.protectedRoomsSet.setRoomState.getRevision(roomID);
            if (revision === undefined) {
                throw new TypeError(`Shouldn't be possible to not have the room state revision for a protected room yet`);
            }
            const joinRules = revision.getStateEvent("m.room.join_rules", "");
            if ((joinRules?.content.join_rule ?? "public") !== "public") {
                log.info(`Room ${roomID} is already invite-only, not changing join rules`);
                return;
            }
            await this.draupnir.managementRoomOutput.logMessage(matrix_bot_sdk_1.LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomID} to invite-only as more than ${this.settings.maxPer} users have joined over the last ${this.settings.timescaleMinutes} minutes.`, roomID);
            if (!this.draupnir.config.noop) {
                await this.draupnir.client.sendStateEvent(roomID, "m.room.join_rules", "", { join_rule: "invite" });
            }
            else {
                await this.draupnir.managementRoomOutput.logMessage(matrix_bot_sdk_1.LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomID} to invite-only, but Draupnir is running in no-op mode`, roomID);
            }
        }
    }
    timescaleMilliseconds() {
        return this.settings.timescaleMinutes * ONE_MINUTE;
    }
    handleProtectionDisable() {
        this.joinBuckets.stop();
    }
}
exports.JoinWaveShortCircuitProtection = JoinWaveShortCircuitProtection;
const JoinWaveStatusCommand = (0, interface_manager_1.describeCommand)({
    summary: "Show the current status of the JoinWaveShortCircuitProtection",
    parameters: (0, interface_manager_1.tuple)(),
    async executor(draupnir, _info, _keywords, _rest) {
        return (0, matrix_protection_suite_1.Ok)(draupnir.protectedRoomsSet.protections.findEnabledProtection(JoinWaveShortCircuitProtection.name));
    },
});
DraupnirCommandPrerequisites_1.DraupnirInterfaceAdaptor.describeRenderer(JoinWaveStatusCommand, {
    JSXRenderer(result) {
        if ((0, matrix_protection_suite_1.isError)(result)) {
            return (0, matrix_protection_suite_1.Ok)(undefined);
        }
        if (result.ok === undefined) {
            return (0, matrix_protection_suite_1.Ok)(interface_manager_1.DeadDocumentJSX.JSXFactory("root", null, "The JoinWaveShortCircuitProtection has not been enabled."));
        }
        const joinBuckets = result.ok.joinBuckets.getAllTokens();
        return (0, matrix_protection_suite_1.Ok)(interface_manager_1.DeadDocumentJSX.JSXFactory("root", null,
            interface_manager_1.DeadDocumentJSX.JSXFactory("b", null,
                "Recent room joins (max ",
                result.ok.settings.maxPer,
                " per",
                " ",
                result.ok.settings.timescaleMinutes,
                " minutes):"),
            joinBuckets.size === 0 ? (interface_manager_1.DeadDocumentJSX.JSXFactory("p", null, "No rooms have had join events since the protection was enabled.")) : (interface_manager_1.DeadDocumentJSX.JSXFactory("fragment", null)),
            interface_manager_1.DeadDocumentJSX.JSXFactory("ul", null, [...joinBuckets.entries()].map(([roomID, joinCount]) => {
                return (interface_manager_1.DeadDocumentJSX.JSXFactory("li", null,
                    (0, mps_interface_adaptor_1.renderRoomPill)(matrix_basic_types_1.MatrixRoomReference.fromRoomID(roomID, [])),
                    " ",
                    joinCount,
                    " joins."));
            }))));
    },
});
exports.JoinWaveCommandTable.internCommand(JoinWaveStatusCommand, ["status"]);
//# sourceMappingURL=JoinWaveShortCircuit.js.map