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 try { await this.prisma.workspace.update({ where: { id: workspaceId }, data: { matrixRoomId: roomId }, }); } catch (dbError: unknown) { this.logger.error( `Failed to store room mapping for workspace ${workspaceId}, room ${roomId} may be orphaned: ${dbError instanceof Error ? dbError.message : "unknown"}` ); throw dbError; } 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; } /** * Reverse lookup: find the workspace that owns a given Matrix room. * * @param roomId - The Matrix room ID (e.g. "!abc:example.com") * @returns The workspace ID, or null if the room is not mapped to any workspace */ async getWorkspaceForRoom(roomId: string): Promise { const workspace = await this.prisma.workspace.findFirst({ where: { matrixRoomId: roomId }, select: { id: true }, }); return workspace?.id ?? 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 * via the public getClient() accessor. */ private getMatrixClient(): MatrixClient | null { if (!this.matrixService) return null; return this.matrixService.getClient(); } }