import { Room, RoomMember, RoomRole, RoomRoleAssignment } from '../../../../shared-kernel'; import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants'; import { localizeSystemRoleDisplayName } from './role-display.rules'; import type { MemberIdentity } from '../models/access-control.model'; import { buildRoleLookup, compareText, memberIdentityKey, matchesIdentity, roleSortDescending, uniqueStrings } from '../util/access-control.util'; import { getRoomRoleById, normalizeRoomRoles } from './role.rules'; function sortAssignments(assignments: readonly RoomRoleAssignment[]): RoomRoleAssignment[] { return [...assignments].sort((firstAssignment, secondAssignment) => compareText(firstAssignment.oderId || firstAssignment.userId, secondAssignment.oderId || secondAssignment.userId) ); } function resolveLegacyMemberRoleIds(member: RoomMember, validRoleIds: Set): string[] { if (Array.isArray(member.roleIds) && member.roleIds.length > 0) { return uniqueStrings(member.roleIds).filter((roleId) => validRoleIds.has(roleId)); } if (member.role === 'admin') { return [SYSTEM_ROLE_IDS.admin]; } if (member.role === 'moderator') { return [SYSTEM_ROLE_IDS.moderator]; } return []; } export function normalizeRoomRoleAssignments( assignments: readonly RoomRoleAssignment[] | undefined, members: readonly RoomMember[] | undefined, roles: readonly RoomRole[] ): RoomRoleAssignment[] { const validRoleIds = new Set(roles.map((role) => role.id).filter((roleId) => roleId !== SYSTEM_ROLE_IDS.everyone)); const normalizedByUserKey = new Map(); const hasExplicitAssignments = Array.isArray(assignments); for (const assignment of assignments ?? []) { if (!assignment || typeof assignment !== 'object') { continue; } const userId = typeof assignment.userId === 'string' ? assignment.userId.trim() : ''; const oderId = typeof assignment.oderId === 'string' ? assignment.oderId.trim() : undefined; const key = oderId || userId; if (!key) { continue; } const roleIds = uniqueStrings(assignment.roleIds ?? []).filter((roleId) => validRoleIds.has(roleId)); if (roleIds.length === 0) { continue; } normalizedByUserKey.set(key, { userId: userId || key, oderId, roleIds }); } if (hasExplicitAssignments) { return sortAssignments(Array.from(normalizedByUserKey.values())); } for (const member of members ?? []) { const key = memberIdentityKey(member); if (!key) { continue; } const roleIds = resolveLegacyMemberRoleIds(member, validRoleIds); if (roleIds.length === 0) { continue; } normalizedByUserKey.set(key, { userId: member.id || key, oderId: member.oderId || undefined, roleIds }); } return sortAssignments(Array.from(normalizedByUserKey.values())); } export function getAssignedRoleIds(assignments: readonly RoomRoleAssignment[] | undefined, identity: MemberIdentity | null | undefined): string[] { const assignment = (assignments ?? []).find((candidateAssignment) => matchesIdentity(identity, candidateAssignment)); return uniqueStrings(assignment?.roleIds ?? []); } export function getDisplayRoleName( room: Room, member: MemberIdentity | null | undefined, translate?: (key: string) => string ): string { const translateOr = (key: string, fallback: string) => translate?.(key) ?? fallback; if (!member) { return translateOr('shared.leaveServer.roles.member', 'Member'); } if (room.hostId === member.id || room.hostId === member.oderId) { return translateOr('shared.leaveServer.roles.owner', 'Owner'); } const roles = normalizeRoomRoles(room.roles, room.permissions); const roleLookup = buildRoleLookup(roles); const assignedRoles = getAssignedRoleIds(room.roleAssignments, member) .map((roleId) => roleLookup.get(roleId)) .filter((role): role is RoomRole => !!role) .sort(roleSortDescending); const roleName = assignedRoles[0]?.name; if (!roleName) { return translateOr('shared.accessControl.roles.everyone', '@everyone'); } return translate ? localizeSystemRoleDisplayName(roleName, translate) : roleName; } export function getAssignedRoles(room: Room, identity: MemberIdentity | null | undefined): RoomRole[] { const roles = normalizeRoomRoles(room.roles, room.permissions); const roleLookup = buildRoleLookup(roles); return getAssignedRoleIds(room.roleAssignments, identity) .map((roleId) => roleLookup.get(roleId)) .filter((role): role is RoomRole => !!role) .sort(roleSortDescending); } export function getHighestAssignedRole(room: Room, identity: MemberIdentity | null | undefined): RoomRole | null { if (!identity) { return null; } if (room.hostId === identity.id || room.hostId === identity.oderId) { return null; } return getAssignedRoles(room, identity)[0] ?? getRoomRoleById(normalizeRoomRoles(room.roles, room.permissions), SYSTEM_ROLE_IDS.everyone) ?? null; } export function setRoleAssignmentsForMember( assignments: readonly RoomRoleAssignment[] | undefined, member: MemberIdentity, roleIds: readonly string[] ): RoomRoleAssignment[] { const nextAssignments = new Map(); const memberKey = memberIdentityKey(member); for (const assignment of assignments ?? []) { const key = memberIdentityKey({ id: assignment.userId, oderId: assignment.oderId }); if (!key || key === memberKey) { continue; } nextAssignments.set(key, { userId: assignment.userId, oderId: assignment.oderId, roleIds: uniqueStrings(assignment.roleIds) }); } const normalizedRoleIds = uniqueStrings(roleIds); if (memberKey && normalizedRoleIds.length > 0) { nextAssignments.set(memberKey, { userId: member.id || member.oderId || memberKey, oderId: member.oderId || undefined, roleIds: normalizedRoleIds }); } return sortAssignments(Array.from(nextAssignments.values())); } export function removeRoleFromAssignments(assignments: readonly RoomRoleAssignment[] | undefined, roleId: string): RoomRoleAssignment[] { return (assignments ?? []) .map((assignment) => ({ ...assignment, roleIds: assignment.roleIds.filter((candidateRoleId) => candidateRoleId !== roleId) })) .filter((assignment) => assignment.roleIds.length > 0); } export function getRoleIdsForMember(room: Room, member: MemberIdentity | null | undefined): string[] { return getAssignedRoleIds( normalizeRoomRoleAssignments(room.roleAssignments, room.members, normalizeRoomRoles(room.roles, room.permissions)), member ); }