"use strict";
// Copyright 2025 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.MembershipChangeProtection = void 0;
const matrix_protection_suite_1 = require("matrix-protection-suite");
const typebox_1 = require("@sinclair/typebox");
const LeakyBucket_1 = require("../queues/LeakyBucket");
const typescript_result_1 = require("@gnuxie/typescript-result");
const interface_manager_1 = require("@the-draupnir-project/interface-manager");
const mps_interface_adaptor_1 = require("@the-draupnir-project/mps-interface-adaptor");
const DEFAULT_MAX_PER_TIMESCALE = 7;
const DEFAULT_TIMESCALE_MINUTES = 60;
const ONE_MINUTE = 60_000; // 1min in ms
const log = new matrix_protection_suite_1.Logger("MembershipChangeProtection");
const MembershipChangeProtectionSettings = typebox_1.Type.Object({
    maxChangesPerUser: typebox_1.Type.Integer({
        default: DEFAULT_MAX_PER_TIMESCALE,
        description: "The maximum number of membership changes that a single user can perform within the timescaleMinutes before the consequence is enacted.",
    }),
    timescaleMinutes: typebox_1.Type.Integer({
        default: DEFAULT_TIMESCALE_MINUTES,
        description: "The timescale in minutes over which the maxChangesPerUser is relevant before the consequence is enacted.",
    }),
    finalConsequenceReason: typebox_1.Type.String({
        default: "You are changing your membership too frequently and have been removed as a precaution.",
        description: "The reason given to the user when they are rate limited.",
    }),
    warningText: typebox_1.Type.String({
        default: "Hi, you are changing your room membership too frequently, and may be temporarily banned as an automated precaution if you continue.",
        description: "The message to send to the user when they are nearing the rate limit.",
    }),
}, { title: "MembershipChangeProtectionSettings" });
function makeBucketKey(roomID, userID) {
    return roomID + userID;
}
class MembershipChangeProtection extends matrix_protection_suite_1.AbstractProtection {
    messageSender;
    settings;
    finalConsequences;
    changeBucket;
    // just a crap attempt to stop consequences being spammed
    consequenceBucket;
    warningThreshold;
    constructor(description, lifetime, capabilities, protectedRoomsSet, messageSender, settings) {
        super(description, lifetime, capabilities, protectedRoomsSet, {});
        this.messageSender = messageSender;
        this.settings = settings;
        this.finalConsequences = capabilities.finalConsequences;
        this.consequenceBucket = new LeakyBucket_1.LazyLeakyBucket(1, this.timescaleMilliseconds());
        this.warningThreshold = Math.floor(this.settings.maxChangesPerUser * 0.6);
        this.changeBucket = new LeakyBucket_1.LazyLeakyBucket(this.settings.maxChangesPerUser, this.timescaleMilliseconds());
    }
    async handleTimelineEvent(room, event) {
        if (!matrix_protection_suite_1.SafeMembershipEventMirror.isSafeContent(event.content)) {
            return (0, matrix_protection_suite_1.Ok)(undefined);
        }
        const safeEvent = event;
        if (safeEvent.sender !== safeEvent.state_key) {
            return (0, matrix_protection_suite_1.Ok)(undefined); // they're being banned or kicked.
        }
        const key = makeBucketKey(event.room_id, safeEvent.state_key);
        const numberOfChanges = this.changeBucket.addToken(key);
        if (numberOfChanges >= this.warningThreshold &&
            this.consequenceBucket.getTokenCount(key) === 0) {
            this.consequenceBucket.addToken(key);
            const warningResult = await (0, mps_interface_adaptor_1.sendMatrixEventsFromDeadDocument)(this.messageSender, safeEvent.room_id, interface_manager_1.DeadDocumentJSX.JSXFactory("root", null,
                (0, mps_interface_adaptor_1.renderMentionPill)(safeEvent.state_key, safeEvent.content.displayname ?? safeEvent.state_key),
                " ",
                this.settings.warningText), { replyToEvent: safeEvent });
            if ((0, typescript_result_1.isError)(warningResult)) {
                log.error("Failed to send warning message to user", safeEvent.state_key, warningResult.error);
            }
        }
        if (numberOfChanges > this.settings.maxChangesPerUser &&
            this.consequenceBucket.getTokenCount(key) === 1) {
            this.consequenceBucket.addToken(key);
            const consequenceResult = await this.finalConsequences.consequenceForUserInRoom(room.toRoomIDOrAlias(), safeEvent.state_key, this.settings.finalConsequenceReason);
            if ((0, typescript_result_1.isError)(consequenceResult)) {
                log.error("Failed to enact consequence for user", safeEvent.state_key, consequenceResult.error);
            }
        }
        return (0, matrix_protection_suite_1.Ok)(undefined);
    }
    handleProtectionDisable() {
        this.changeBucket.stop();
        this.consequenceBucket.stop();
    }
    timescaleMilliseconds() {
        return this.settings.timescaleMinutes * ONE_MINUTE;
    }
}
exports.MembershipChangeProtection = MembershipChangeProtection;
(0, matrix_protection_suite_1.describeProtection)({
    name: MembershipChangeProtection.name,
    description: `A protection that will rate limit the number of changes a single user can make to their membership event. Experimental.`,
    capabilityInterfaces: {
        finalConsequences: "UserConsequences",
    },
    defaultCapabilities: {
        finalConsequences: "StandardUserConsequences",
    },
    configSchema: MembershipChangeProtectionSettings,
    factory: async (decription, lifetime, protectedRoomsSet, draupnir, capabilitySet, settings) => (0, matrix_protection_suite_1.allocateProtection)(lifetime, new MembershipChangeProtection(decription, lifetime, capabilitySet, protectedRoomsSet, draupnir.clientPlatform.toRoomMessageSender(), settings)),
});
//# sourceMappingURL=MembershipChangeProtection.js.map