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 { 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 { 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 { 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 { 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; } }