mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-03-29 02:19:51 +00:00
Add Explicit support for allocating multiple Bots per user to AS mode.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
||||
//
|
||||
@@ -185,6 +186,7 @@ export class MjolnirAppService {
|
||||
const serverName = config.homeserver.domain;
|
||||
const mjolnirManager = await AppServiceDraupnirManager.makeDraupnirManager(
|
||||
serverName,
|
||||
config.maxDraupnirsPerUser ?? 1,
|
||||
dataStore,
|
||||
bridge,
|
||||
accessControl,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 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: AFL-3.0 AND Apache-2.0
|
||||
//
|
||||
@@ -66,6 +67,7 @@ export class AppServiceDraupnirManager {
|
||||
|
||||
private constructor(
|
||||
private readonly serverName: string,
|
||||
private readonly maxDraupnirsPerUser: number,
|
||||
private readonly dataStore: DataStore,
|
||||
private readonly bridge: Bridge,
|
||||
private readonly accessControl: AccessControl,
|
||||
@@ -103,6 +105,7 @@ export class AppServiceDraupnirManager {
|
||||
*/
|
||||
public static async makeDraupnirManager(
|
||||
serverName: string,
|
||||
maxDraupnirsPerUser: number,
|
||||
dataStore: DataStore,
|
||||
bridge: Bridge,
|
||||
accessControl: AccessControl,
|
||||
@@ -115,6 +118,7 @@ export class AppServiceDraupnirManager {
|
||||
): Promise<AppServiceDraupnirManager> {
|
||||
const draupnirManager = new AppServiceDraupnirManager(
|
||||
serverName,
|
||||
maxDraupnirsPerUser,
|
||||
dataStore,
|
||||
bridge,
|
||||
accessControl,
|
||||
@@ -199,6 +203,19 @@ export class AppServiceDraupnirManager {
|
||||
*/
|
||||
public async provisionNewDraupnir(
|
||||
requestingUserID: StringUserID
|
||||
): Promise<ActionResult<MjolnirRecord>> {
|
||||
return await this.provisionNewDraupnirInternal(requestingUserID, false);
|
||||
}
|
||||
|
||||
public async provisionNewDraupnirBypassingUserLimit(
|
||||
requestingUserID: StringUserID
|
||||
): Promise<ActionResult<MjolnirRecord>> {
|
||||
return await this.provisionNewDraupnirInternal(requestingUserID, true);
|
||||
}
|
||||
|
||||
private async provisionNewDraupnirInternal(
|
||||
requestingUserID: StringUserID,
|
||||
bypassUserLimit: boolean
|
||||
): Promise<ActionResult<MjolnirRecord>> {
|
||||
const access = this.accessControl.getUserAccess(requestingUserID);
|
||||
if (access.outcome !== Access.Allowed) {
|
||||
@@ -208,7 +225,10 @@ export class AppServiceDraupnirManager {
|
||||
}
|
||||
const provisionedMjolnirs =
|
||||
await this.dataStore.lookupByOwner(requestingUserID);
|
||||
if (provisionedMjolnirs.length === 0) {
|
||||
if (
|
||||
bypassUserLimit ||
|
||||
provisionedMjolnirs.length < this.maxDraupnirsPerUser
|
||||
) {
|
||||
const mjolnirLocalPart = `draupnir_${randomUUID()}`;
|
||||
const mjIntent = await this.makeMatrixIntent(mjolnirLocalPart);
|
||||
const draupnirUserID = StringUserID(mjIntent.userId);
|
||||
@@ -263,7 +283,7 @@ export class AppServiceDraupnirManager {
|
||||
return Ok(record);
|
||||
} else {
|
||||
return ActionError.Result(
|
||||
`User: ${requestingUserID} has already provisioned ${provisionedMjolnirs.length} draupnirs.`
|
||||
`User: ${requestingUserID} has already provisioned ${provisionedMjolnirs.length} draupnirs, which meets the configured limit of ${this.maxDraupnirsPerUser}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
@@ -17,12 +18,14 @@ import {
|
||||
AppserviceListUnstartedCommand,
|
||||
AppserviceRestartDraupnirCommand,
|
||||
} from "./ListCommand";
|
||||
import { AppserviceProvisionForUserCommand } from "./ProvisionCommand";
|
||||
import { AppserviceVersionCommand } from "./VersionCommand";
|
||||
|
||||
AppserviceBotCommands.internCommand(AppserviceBotHelpCommand, ["admin", "help"])
|
||||
.internCommand(AppserviceAllowCommand, ["admin", "allow"])
|
||||
.internCommand(AppserviceRemoveCommand, ["admin", "remove"])
|
||||
.internCommand(AppserviceVersionCommand, ["admin", "version"])
|
||||
.internCommand(AppserviceProvisionForUserCommand, ["admin", "provision"])
|
||||
.internCommand(AppserviceRestartDraupnirCommand, ["admin", "restart"])
|
||||
.internCommand(AppserviceListUnstartedCommand, [
|
||||
"admin",
|
||||
|
||||
42
apps/draupnir/src/appservice/bot/ProvisionCommand.tsx
Normal file
42
apps/draupnir/src/appservice/bot/ProvisionCommand.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { AppserviceAdaptorContext } from "./AppserviceBotPrerequisite";
|
||||
import { ActionResult, isError, Ok } from "matrix-protection-suite";
|
||||
import {
|
||||
MatrixUserIDPresentationType,
|
||||
describeCommand,
|
||||
tuple,
|
||||
} from "@the-draupnir-project/interface-manager";
|
||||
import { AppserviceBotInterfaceAdaptor } from "./AppserviceBotInterfaceAdaptor";
|
||||
|
||||
export const AppserviceProvisionForUserCommand = describeCommand({
|
||||
parameters: tuple({
|
||||
name: "user",
|
||||
acceptor: MatrixUserIDPresentationType,
|
||||
description: "The user to provision a bot for, bypassing user allocation limit",
|
||||
}),
|
||||
summary:
|
||||
"Provision a new Draupnir for a user while bypassing the per-user allocation limit.",
|
||||
async executor(
|
||||
context: AppserviceAdaptorContext,
|
||||
_info,
|
||||
_keywords,
|
||||
_rest,
|
||||
user
|
||||
): Promise<ActionResult<void>> {
|
||||
const result =
|
||||
await context.appservice.draupnirManager.provisionNewDraupnirBypassingUserLimit(
|
||||
user.toString()
|
||||
);
|
||||
if (isError(result)) {
|
||||
return result;
|
||||
}
|
||||
return Ok(undefined);
|
||||
},
|
||||
});
|
||||
|
||||
AppserviceBotInterfaceAdaptor.describeRenderer(AppserviceProvisionForUserCommand, {
|
||||
isAlwaysSupposedToUseDefaultRenderer: true,
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
|
||||
homeserver:
|
||||
# The Matrix server name, this will be the name of the server in your matrix id.
|
||||
domain: "localhost:9999"
|
||||
@@ -16,5 +18,9 @@ adminRoom: "#draupnir-admin:localhost:9999"
|
||||
# The directory the bot should store various bits of information in
|
||||
dataPath: "/data/storage"
|
||||
|
||||
# Maximum number of bots each user can provision.
|
||||
# Defaults to 1 when omitted.
|
||||
maxDraupnirsPerUser: 1
|
||||
|
||||
roomStateBackingStore:
|
||||
enabled: false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
||||
//
|
||||
@@ -100,5 +101,13 @@ export const AppserviceConfig = Type.Object({
|
||||
"A directory where the appservice can storestore persistent data.",
|
||||
default: "/data/storage",
|
||||
}),
|
||||
maxDraupnirsPerUser: Type.Optional(
|
||||
Type.Integer({
|
||||
description:
|
||||
"Maximum number of Draupnir instances a single user may provision.",
|
||||
minimum: 1,
|
||||
default: 1,
|
||||
})
|
||||
),
|
||||
logging: Type.Optional(LoggingOptsSchema),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
||||
//
|
||||
@@ -8,7 +9,11 @@
|
||||
// https://github.com/matrix-org/mjolnir
|
||||
// </text>
|
||||
|
||||
import { readTestConfig, setupHarness } from "../utils/harness";
|
||||
import {
|
||||
readTestConfig,
|
||||
setupHarness,
|
||||
setupHarnessWithConfig,
|
||||
} from "../utils/harness";
|
||||
import { newTestUser } from "../../integration/clientHelper";
|
||||
import { getFirstReply } from "../../integration/commands/commandUtils";
|
||||
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
@@ -118,4 +123,72 @@ describe("Test that the app service can provision a draupnir on invite of the ap
|
||||
allowedUser.stop();
|
||||
blockedUser.stop();
|
||||
});
|
||||
|
||||
it("Allows provisioning multiple bots per user up to maxDraupnirsPerUser", async function (this: Context) {
|
||||
const config = readTestConfig();
|
||||
config.maxDraupnirsPerUser = 2;
|
||||
this.appservice = await setupHarnessWithConfig(config);
|
||||
const appservice = this.appservice;
|
||||
const allowedUser = await newTestUser(config.homeserver.url, {
|
||||
name: { contains: "multi-allowed" },
|
||||
});
|
||||
const allowedUserID = (await allowedUser.getUserId()) as StringUserID;
|
||||
const allowResult = await appservice.accessControl.allow(allowedUserID);
|
||||
if (isError(allowResult)) {
|
||||
throw allowResult.error;
|
||||
}
|
||||
|
||||
const firstProvisionResult =
|
||||
await appservice.draupnirManager.provisionNewDraupnir(allowedUserID);
|
||||
if (isError(firstProvisionResult)) {
|
||||
throw firstProvisionResult.error;
|
||||
}
|
||||
|
||||
const secondProvisionResult =
|
||||
await appservice.draupnirManager.provisionNewDraupnir(allowedUserID);
|
||||
if (isError(secondProvisionResult)) {
|
||||
throw secondProvisionResult.error;
|
||||
}
|
||||
|
||||
const thirdProvisionResult =
|
||||
await appservice.draupnirManager.provisionNewDraupnir(allowedUserID);
|
||||
if (!isError(thirdProvisionResult)) {
|
||||
throw new TypeError(
|
||||
`Expected provisioning to fail after reaching limit for user ${allowedUserID}`
|
||||
);
|
||||
}
|
||||
|
||||
allowedUser.stop();
|
||||
});
|
||||
|
||||
it("Admin provisioning path can bypass per-user allocation limit", async function (this: Context) {
|
||||
const config = readTestConfig();
|
||||
config.maxDraupnirsPerUser = 1;
|
||||
this.appservice = await setupHarnessWithConfig(config);
|
||||
const appservice = this.appservice;
|
||||
const allowedUser = await newTestUser(config.homeserver.url, {
|
||||
name: { contains: "admin-bypass-allowed" },
|
||||
});
|
||||
const allowedUserID = (await allowedUser.getUserId()) as StringUserID;
|
||||
const allowResult = await appservice.accessControl.allow(allowedUserID);
|
||||
if (isError(allowResult)) {
|
||||
throw allowResult.error;
|
||||
}
|
||||
|
||||
const firstProvisionResult =
|
||||
await appservice.draupnirManager.provisionNewDraupnir(allowedUserID);
|
||||
if (isError(firstProvisionResult)) {
|
||||
throw firstProvisionResult.error;
|
||||
}
|
||||
|
||||
const secondProvisionResult =
|
||||
await appservice.draupnirManager.provisionNewDraupnirBypassingUserLimit(
|
||||
allowedUserID
|
||||
);
|
||||
if (isError(secondProvisionResult)) {
|
||||
throw secondProvisionResult.error;
|
||||
}
|
||||
|
||||
allowedUser.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
||||
//
|
||||
@@ -28,6 +29,12 @@ export function readTestConfig(): AppserviceConfig {
|
||||
|
||||
export async function setupHarness(): Promise<MjolnirAppService> {
|
||||
const config = readTestConfig();
|
||||
return await setupHarnessWithConfig(config);
|
||||
}
|
||||
|
||||
export async function setupHarnessWithConfig(
|
||||
config: AppserviceConfig
|
||||
): Promise<MjolnirAppService> {
|
||||
const utilityUser = await newTestUser(config.homeserver.url, {
|
||||
name: { contains: "utility" },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user