Some checks failed
ci/woodpecker/push/api Pipeline failed
- Add matrix_room_id column to workspace table (migration) - Create MatrixRoomService for room provisioning and mapping - Auto-create Matrix room on workspace provisioning (when configured) - Support manual room linking for existing workspaces - Unit tests for all mapping operations Refs #380
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
import { Injectable, Logger, Optional, Inject } from "@nestjs/common";
|
|
import { PrismaService } from "../../prisma/prisma.service";
|
|
import { MatrixService } from "./matrix.service";
|
|
import type { MatrixClient, RoomCreateOptions } from "matrix-bot-sdk";
|
|
|
|
/**
|
|
* MatrixRoomService - Workspace-to-Matrix-Room mapping and provisioning
|
|
*
|
|
* Responsibilities:
|
|
* - Provision Matrix rooms for Mosaic workspaces
|
|
* - Map workspaces to Matrix room IDs
|
|
* - Link/unlink existing rooms to workspaces
|
|
*
|
|
* Room provisioning creates a private Matrix room with:
|
|
* - Name: "Mosaic: {workspace_name}"
|
|
* - Alias: #mosaic-{workspace_slug}:{server_name}
|
|
* - Room ID stored in workspace.matrixRoomId
|
|
*/
|
|
@Injectable()
|
|
export class MatrixRoomService {
|
|
private readonly logger = new Logger(MatrixRoomService.name);
|
|
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
@Optional() @Inject(MatrixService) private readonly matrixService: MatrixService | null
|
|
) {}
|
|
|
|
/**
|
|
* Provision a Matrix room for a workspace and store the mapping.
|
|
*
|
|
* @param workspaceId - The workspace UUID
|
|
* @param workspaceName - Human-readable workspace name
|
|
* @param workspaceSlug - URL-safe workspace identifier for the room alias
|
|
* @returns The Matrix room ID, or null if Matrix is not configured
|
|
*/
|
|
async provisionRoom(
|
|
workspaceId: string,
|
|
workspaceName: string,
|
|
workspaceSlug: string
|
|
): Promise<string | null> {
|
|
if (!this.matrixService?.isConnected()) {
|
|
this.logger.warn("Matrix is not configured or not connected; skipping room provisioning");
|
|
return null;
|
|
}
|
|
|
|
const client = this.getMatrixClient();
|
|
if (!client) {
|
|
this.logger.warn("Matrix client is not available; skipping room provisioning");
|
|
return null;
|
|
}
|
|
|
|
const roomOptions: RoomCreateOptions = {
|
|
name: `Mosaic: ${workspaceName}`,
|
|
room_alias_name: `mosaic-${workspaceSlug}`,
|
|
topic: `Mosaic workspace: ${workspaceName}`,
|
|
preset: "private_chat",
|
|
visibility: "private",
|
|
};
|
|
|
|
this.logger.log(
|
|
`Provisioning Matrix room for workspace "${workspaceName}" (${workspaceId})...`
|
|
);
|
|
|
|
const roomId = await client.createRoom(roomOptions);
|
|
|
|
// Store the room mapping
|
|
await this.prisma.workspace.update({
|
|
where: { id: workspaceId },
|
|
data: { matrixRoomId: roomId },
|
|
});
|
|
|
|
this.logger.log(`Matrix room ${roomId} provisioned and linked to workspace ${workspaceId}`);
|
|
|
|
return roomId;
|
|
}
|
|
|
|
/**
|
|
* Look up the Matrix room ID mapped to a workspace.
|
|
*
|
|
* @param workspaceId - The workspace UUID
|
|
* @returns The Matrix room ID, or null if no room is mapped
|
|
*/
|
|
async getRoomForWorkspace(workspaceId: string): Promise<string | null> {
|
|
const workspace = await this.prisma.workspace.findUnique({
|
|
where: { id: workspaceId },
|
|
select: { matrixRoomId: true },
|
|
});
|
|
|
|
return workspace?.matrixRoomId ?? null;
|
|
}
|
|
|
|
/**
|
|
* Manually link an existing Matrix room to a workspace.
|
|
*
|
|
* @param workspaceId - The workspace UUID
|
|
* @param roomId - The Matrix room ID to link
|
|
*/
|
|
async linkWorkspaceToRoom(workspaceId: string, roomId: string): Promise<void> {
|
|
await this.prisma.workspace.update({
|
|
where: { id: workspaceId },
|
|
data: { matrixRoomId: roomId },
|
|
});
|
|
|
|
this.logger.log(`Linked workspace ${workspaceId} to Matrix room ${roomId}`);
|
|
}
|
|
|
|
/**
|
|
* Remove the Matrix room mapping from a workspace.
|
|
*
|
|
* @param workspaceId - The workspace UUID
|
|
*/
|
|
async unlinkWorkspace(workspaceId: string): Promise<void> {
|
|
await this.prisma.workspace.update({
|
|
where: { id: workspaceId },
|
|
data: { matrixRoomId: null },
|
|
});
|
|
|
|
this.logger.log(`Unlinked Matrix room from workspace ${workspaceId}`);
|
|
}
|
|
|
|
/**
|
|
* Access the underlying MatrixClient from the MatrixService.
|
|
*
|
|
* The MatrixService stores the client as a private field, so we
|
|
* access it via a known private property name. This is intentional
|
|
* to avoid exposing the client publicly on the service interface.
|
|
*/
|
|
private getMatrixClient(): MatrixClient | null {
|
|
if (!this.matrixService) return null;
|
|
|
|
// Access the private client field from MatrixService.
|
|
// MatrixService stores `client` as a private property; we use a type assertion
|
|
// to access it since exposing it publicly is not appropriate for the service API.
|
|
const service = this.matrixService as unknown as { client: MatrixClient | null };
|
|
return service.client;
|
|
}
|
|
}
|