|
|
|
|
@@ -6,13 +6,54 @@ import {
|
|
|
|
|
import {
|
|
|
|
|
ReactionEntity,
|
|
|
|
|
RoomChannelEntity,
|
|
|
|
|
RoomMemberEntity
|
|
|
|
|
RoomMemberEntity,
|
|
|
|
|
RoomRoleEntity,
|
|
|
|
|
RoomUserRoleEntity,
|
|
|
|
|
RoomChannelPermissionEntity
|
|
|
|
|
} from '../entities';
|
|
|
|
|
import { ReactionPayload } from './types';
|
|
|
|
|
import {
|
|
|
|
|
AccessRolePayload,
|
|
|
|
|
ChannelPermissionPayload,
|
|
|
|
|
PermissionStatePayload,
|
|
|
|
|
ReactionPayload,
|
|
|
|
|
RoleAssignmentPayload,
|
|
|
|
|
RoomPayload,
|
|
|
|
|
RoomPermissionKeyPayload
|
|
|
|
|
} from './types';
|
|
|
|
|
|
|
|
|
|
type ChannelType = 'text' | 'voice';
|
|
|
|
|
type RoomMemberRole = 'host' | 'admin' | 'moderator' | 'member';
|
|
|
|
|
|
|
|
|
|
interface LegacyRoomPermissions {
|
|
|
|
|
adminsManageRooms?: boolean;
|
|
|
|
|
moderatorsManageRooms?: boolean;
|
|
|
|
|
adminsManageIcon?: boolean;
|
|
|
|
|
moderatorsManageIcon?: boolean;
|
|
|
|
|
allowVoice?: boolean;
|
|
|
|
|
allowScreenShare?: boolean;
|
|
|
|
|
allowFileUploads?: boolean;
|
|
|
|
|
slowModeInterval?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ROOM_PERMISSION_KEYS: RoomPermissionKeyPayload[] = [
|
|
|
|
|
'manageServer',
|
|
|
|
|
'manageRoles',
|
|
|
|
|
'manageChannels',
|
|
|
|
|
'manageIcon',
|
|
|
|
|
'kickMembers',
|
|
|
|
|
'banMembers',
|
|
|
|
|
'manageBans',
|
|
|
|
|
'deleteMessages',
|
|
|
|
|
'joinVoice',
|
|
|
|
|
'shareScreen',
|
|
|
|
|
'uploadFiles'
|
|
|
|
|
];
|
|
|
|
|
const SYSTEM_ROLE_IDS = {
|
|
|
|
|
everyone: 'system-everyone',
|
|
|
|
|
moderator: 'system-moderator',
|
|
|
|
|
admin: 'system-admin'
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
export interface RoomChannelRecord {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
@@ -27,13 +68,23 @@ export interface RoomMemberRecord {
|
|
|
|
|
displayName: string;
|
|
|
|
|
avatarUrl?: string;
|
|
|
|
|
role: RoomMemberRole;
|
|
|
|
|
roleIds?: string[];
|
|
|
|
|
joinedAt: number;
|
|
|
|
|
lastSeenAt: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type RoomRoleRecord = AccessRolePayload;
|
|
|
|
|
|
|
|
|
|
export type RoomRoleAssignmentRecord = RoleAssignmentPayload;
|
|
|
|
|
|
|
|
|
|
export type RoomChannelPermissionRecord = ChannelPermissionPayload;
|
|
|
|
|
|
|
|
|
|
interface RoomRelationRecord {
|
|
|
|
|
channels: RoomChannelRecord[];
|
|
|
|
|
members: RoomMemberRecord[];
|
|
|
|
|
roles: RoomRoleRecord[];
|
|
|
|
|
roleAssignments: RoomRoleAssignmentRecord[];
|
|
|
|
|
channelPermissions: RoomChannelPermissionRecord[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isFiniteNumber(value: unknown): value is number {
|
|
|
|
|
@@ -52,6 +103,56 @@ function memberKey(member: { id?: string; oderId?: string }): string {
|
|
|
|
|
return member.oderId?.trim() || member.id?.trim() || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compareText(firstValue: string, secondValue: string): number {
|
|
|
|
|
return firstValue.localeCompare(secondValue, undefined, { sensitivity: 'base' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uniqueStrings(values: readonly string[] | undefined): string[] {
|
|
|
|
|
return Array.from(new Set((values ?? [])
|
|
|
|
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
|
|
|
.map((value) => value.trim())));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trimmedString(record: Record<string, unknown>, key: string): string {
|
|
|
|
|
return typeof record[key] === 'string' ? record[key].trim() : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRoomMemberTimes(rawMember: Record<string, unknown>, now: number): Pick<RoomMemberRecord, 'joinedAt' | 'lastSeenAt'> {
|
|
|
|
|
const lastSeenAt = isFiniteNumber(rawMember['lastSeenAt'])
|
|
|
|
|
? rawMember['lastSeenAt']
|
|
|
|
|
: isFiniteNumber(rawMember['joinedAt'])
|
|
|
|
|
? rawMember['joinedAt']
|
|
|
|
|
: now;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
joinedAt: isFiniteNumber(rawMember['joinedAt']) ? rawMember['joinedAt'] : lastSeenAt,
|
|
|
|
|
lastSeenAt
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePermissionState(value: unknown): PermissionStatePayload {
|
|
|
|
|
return value === 'allow' || value === 'deny' || value === 'inherit'
|
|
|
|
|
? value
|
|
|
|
|
: 'inherit';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePermissionMatrix(rawMatrix: unknown): Partial<Record<RoomPermissionKeyPayload, PermissionStatePayload>> {
|
|
|
|
|
const matrix = rawMatrix && typeof rawMatrix === 'object'
|
|
|
|
|
? rawMatrix as Record<string, unknown>
|
|
|
|
|
: {};
|
|
|
|
|
const normalized: Partial<Record<RoomPermissionKeyPayload, PermissionStatePayload>> = {};
|
|
|
|
|
|
|
|
|
|
for (const key of ROOM_PERMISSION_KEYS) {
|
|
|
|
|
const value = normalizePermissionState(matrix[key]);
|
|
|
|
|
|
|
|
|
|
if (value !== 'inherit') {
|
|
|
|
|
normalized[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fallbackDisplayName(member: Partial<RoomMemberRecord>): string {
|
|
|
|
|
return member.displayName || member.username || member.oderId || member.id || 'User';
|
|
|
|
|
}
|
|
|
|
|
@@ -92,46 +193,161 @@ function mergeRoomMemberRole(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compareRoomMembers(firstMember: RoomMemberRecord, secondMember: RoomMemberRecord): number {
|
|
|
|
|
const displayNameCompare = firstMember.displayName.localeCompare(secondMember.displayName, undefined, { sensitivity: 'base' });
|
|
|
|
|
const displayNameCompare = compareText(firstMember.displayName, secondMember.displayName);
|
|
|
|
|
|
|
|
|
|
if (displayNameCompare !== 0) {
|
|
|
|
|
return displayNameCompare;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return memberKey(firstMember).localeCompare(memberKey(secondMember));
|
|
|
|
|
return compareText(memberKey(firstMember), memberKey(secondMember));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compareRoles(firstRole: RoomRoleRecord, secondRole: RoomRoleRecord): number {
|
|
|
|
|
if (firstRole.position !== secondRole.position) {
|
|
|
|
|
return firstRole.position - secondRole.position;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return compareText(firstRole.name, secondRole.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compareAssignments(firstAssignment: RoomRoleAssignmentRecord, secondAssignment: RoomRoleAssignmentRecord): number {
|
|
|
|
|
return compareText(firstAssignment.oderId || firstAssignment.userId, secondAssignment.oderId || secondAssignment.userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseLegacyPermissions(rawPermissions: unknown): LegacyRoomPermissions {
|
|
|
|
|
if (!rawPermissions || typeof rawPermissions !== 'object') {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const permissions = rawPermissions as Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
adminsManageRooms: permissions['adminsManageRooms'] === true,
|
|
|
|
|
moderatorsManageRooms: permissions['moderatorsManageRooms'] === true,
|
|
|
|
|
adminsManageIcon: permissions['adminsManageIcon'] === true,
|
|
|
|
|
moderatorsManageIcon: permissions['moderatorsManageIcon'] === true,
|
|
|
|
|
allowVoice: permissions['allowVoice'] !== false,
|
|
|
|
|
allowScreenShare: permissions['allowScreenShare'] !== false,
|
|
|
|
|
allowFileUploads: permissions['allowFileUploads'] !== false,
|
|
|
|
|
slowModeInterval: isFiniteNumber(permissions['slowModeInterval']) ? permissions['slowModeInterval'] : 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDefaultRoomRoles(legacyPermissions: LegacyRoomPermissions): RoomRoleRecord[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
id: SYSTEM_ROLE_IDS.everyone,
|
|
|
|
|
name: '@everyone',
|
|
|
|
|
color: '#6b7280',
|
|
|
|
|
position: 0,
|
|
|
|
|
isSystem: true,
|
|
|
|
|
permissions: {
|
|
|
|
|
joinVoice: legacyPermissions.allowVoice === false ? 'deny' : 'allow',
|
|
|
|
|
shareScreen: legacyPermissions.allowScreenShare === false ? 'deny' : 'allow',
|
|
|
|
|
uploadFiles: legacyPermissions.allowFileUploads === false ? 'deny' : 'allow'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: SYSTEM_ROLE_IDS.moderator,
|
|
|
|
|
name: 'Moderator',
|
|
|
|
|
color: '#10b981',
|
|
|
|
|
position: 200,
|
|
|
|
|
isSystem: true,
|
|
|
|
|
permissions: {
|
|
|
|
|
kickMembers: 'allow',
|
|
|
|
|
deleteMessages: 'allow',
|
|
|
|
|
manageChannels: legacyPermissions.moderatorsManageRooms ? 'allow' : 'inherit',
|
|
|
|
|
manageIcon: legacyPermissions.moderatorsManageIcon ? 'allow' : 'inherit'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: SYSTEM_ROLE_IDS.admin,
|
|
|
|
|
name: 'Admin',
|
|
|
|
|
color: '#60a5fa',
|
|
|
|
|
position: 300,
|
|
|
|
|
isSystem: true,
|
|
|
|
|
permissions: {
|
|
|
|
|
kickMembers: 'allow',
|
|
|
|
|
banMembers: 'allow',
|
|
|
|
|
manageBans: 'allow',
|
|
|
|
|
deleteMessages: 'allow',
|
|
|
|
|
manageChannels: legacyPermissions.adminsManageRooms ? 'allow' : 'inherit',
|
|
|
|
|
manageIcon: legacyPermissions.adminsManageIcon ? 'allow' : 'inherit'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoomRole(rawRole: Partial<RoomRoleRecord>, fallbackRole?: RoomRoleRecord): RoomRoleRecord | null {
|
|
|
|
|
const id = typeof rawRole.id === 'string' ? rawRole.id.trim() : fallbackRole?.id ?? '';
|
|
|
|
|
const name = typeof rawRole.name === 'string' ? rawRole.name.trim().replace(/\s+/g, ' ') : fallbackRole?.name ?? '';
|
|
|
|
|
|
|
|
|
|
if (!id || !name) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
name,
|
|
|
|
|
color: typeof rawRole.color === 'string' && rawRole.color.trim() ? rawRole.color.trim() : fallbackRole?.color,
|
|
|
|
|
position: isFiniteNumber(rawRole.position) ? rawRole.position : fallbackRole?.position ?? 0,
|
|
|
|
|
isSystem: typeof rawRole.isSystem === 'boolean' ? rawRole.isSystem : fallbackRole?.isSystem,
|
|
|
|
|
permissions: normalizePermissionMatrix(rawRole.permissions ?? fallbackRole?.permissions)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeRoomRoles(rawRoles: unknown, rawPermissions: unknown): RoomRoleRecord[] {
|
|
|
|
|
const defaults = buildDefaultRoomRoles(parseLegacyPermissions(rawPermissions));
|
|
|
|
|
const rolesById = new Map<string, RoomRoleRecord>();
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(rawRoles)) {
|
|
|
|
|
for (const rawRole of rawRoles) {
|
|
|
|
|
if (!rawRole || typeof rawRole !== 'object') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedRole = normalizeRoomRole(rawRole as Record<string, unknown>);
|
|
|
|
|
|
|
|
|
|
if (normalizedRole) {
|
|
|
|
|
rolesById.set(normalizedRole.id, normalizedRole);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const defaultRole of defaults) {
|
|
|
|
|
const mergedRole = normalizeRoomRole(rolesById.get(defaultRole.id) ?? defaultRole, defaultRole) ?? defaultRole;
|
|
|
|
|
|
|
|
|
|
rolesById.set(defaultRole.id, mergedRole);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(rolesById.values()).sort(compareRoles);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoomMember(rawMember: Record<string, unknown>, now: number): RoomMemberRecord | null {
|
|
|
|
|
const normalizedId = typeof rawMember['id'] === 'string' ? rawMember['id'].trim() : '';
|
|
|
|
|
const normalizedOderId = typeof rawMember['oderId'] === 'string' ? rawMember['oderId'].trim() : '';
|
|
|
|
|
const normalizedId = trimmedString(rawMember, 'id');
|
|
|
|
|
const normalizedOderId = trimmedString(rawMember, 'oderId');
|
|
|
|
|
const normalizedKey = normalizedOderId || normalizedId;
|
|
|
|
|
|
|
|
|
|
if (!normalizedKey) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lastSeenAt = isFiniteNumber(rawMember['lastSeenAt'])
|
|
|
|
|
? rawMember['lastSeenAt']
|
|
|
|
|
: isFiniteNumber(rawMember['joinedAt'])
|
|
|
|
|
? rawMember['joinedAt']
|
|
|
|
|
: now;
|
|
|
|
|
const joinedAt = isFiniteNumber(rawMember['joinedAt']) ? rawMember['joinedAt'] : lastSeenAt;
|
|
|
|
|
const username = typeof rawMember['username'] === 'string' ? rawMember['username'].trim() : '';
|
|
|
|
|
const displayName = typeof rawMember['displayName'] === 'string' ? rawMember['displayName'].trim() : '';
|
|
|
|
|
const avatarUrl = typeof rawMember['avatarUrl'] === 'string' ? rawMember['avatarUrl'].trim() : '';
|
|
|
|
|
const { joinedAt, lastSeenAt } = resolveRoomMemberTimes(rawMember, now);
|
|
|
|
|
const username = trimmedString(rawMember, 'username');
|
|
|
|
|
const displayName = trimmedString(rawMember, 'displayName');
|
|
|
|
|
const avatarUrl = trimmedString(rawMember, 'avatarUrl');
|
|
|
|
|
|
|
|
|
|
const member: RoomMemberRecord = {
|
|
|
|
|
return {
|
|
|
|
|
id: normalizedId || normalizedKey,
|
|
|
|
|
oderId: normalizedOderId || undefined,
|
|
|
|
|
username: username || fallbackUsername({ id: normalizedId || normalizedKey, oderId: normalizedOderId || undefined, displayName }),
|
|
|
|
|
displayName: displayName || fallbackDisplayName({ id: normalizedId || normalizedKey, oderId: normalizedOderId || undefined, username }),
|
|
|
|
|
avatarUrl: avatarUrl || undefined,
|
|
|
|
|
role: normalizeRoomMemberRole(rawMember['role']),
|
|
|
|
|
roleIds: uniqueStrings(Array.isArray(rawMember['roleIds']) ? rawMember['roleIds'] as string[] : undefined),
|
|
|
|
|
joinedAt,
|
|
|
|
|
lastSeenAt
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return member;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeRoomMembers(existingMember: RoomMemberRecord | undefined, incomingMember: RoomMemberRecord): RoomMemberRecord {
|
|
|
|
|
@@ -154,11 +370,176 @@ function mergeRoomMembers(existingMember: RoomMemberRecord | undefined, incoming
|
|
|
|
|
? (incomingMember.avatarUrl || existingMember.avatarUrl)
|
|
|
|
|
: (existingMember.avatarUrl || incomingMember.avatarUrl),
|
|
|
|
|
role: mergeRoomMemberRole(existingMember.role, incomingMember.role, preferIncoming),
|
|
|
|
|
roleIds: preferIncoming
|
|
|
|
|
? (incomingMember.roleIds || existingMember.roleIds)
|
|
|
|
|
: (existingMember.roleIds || incomingMember.roleIds),
|
|
|
|
|
joinedAt: Math.min(existingMember.joinedAt, incomingMember.joinedAt),
|
|
|
|
|
lastSeenAt: Math.max(existingMember.lastSeenAt, incomingMember.lastSeenAt)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeRoomMembers(rawMembers: unknown, now: number = Date.now()): RoomMemberRecord[] {
|
|
|
|
|
if (!Array.isArray(rawMembers)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const membersByKey = new Map<string, RoomMemberRecord>();
|
|
|
|
|
|
|
|
|
|
for (const rawMember of rawMembers) {
|
|
|
|
|
if (!rawMember || typeof rawMember !== 'object') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const member = normalizeRoomMember(rawMember as Record<string, unknown>, now);
|
|
|
|
|
|
|
|
|
|
if (!member) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const key = memberKey(member);
|
|
|
|
|
|
|
|
|
|
membersByKey.set(key, mergeRoomMembers(membersByKey.get(key), member));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(membersByKey.values()).sort(compareRoomMembers);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRoleAssignmentsFromMembers(members: readonly RoomMemberRecord[], roles: readonly RoomRoleRecord[]): RoomRoleAssignmentRecord[] {
|
|
|
|
|
const validRoleIds = new Set(roles.map((role) => role.id).filter((roleId) => roleId !== SYSTEM_ROLE_IDS.everyone));
|
|
|
|
|
|
|
|
|
|
return members.flatMap((member) => {
|
|
|
|
|
const roleIds = uniqueStrings(member.roleIds)
|
|
|
|
|
.filter((roleId) => validRoleIds.has(roleId));
|
|
|
|
|
const fallbackRoleIds = roleIds.length > 0
|
|
|
|
|
? roleIds
|
|
|
|
|
: member.role === 'admin'
|
|
|
|
|
? [SYSTEM_ROLE_IDS.admin]
|
|
|
|
|
: member.role === 'moderator'
|
|
|
|
|
? [SYSTEM_ROLE_IDS.moderator]
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
if (fallbackRoleIds.length === 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
userId: member.id,
|
|
|
|
|
oderId: member.oderId,
|
|
|
|
|
roleIds: fallbackRoleIds
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}).sort(compareAssignments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeRoomRoleAssignments(
|
|
|
|
|
rawAssignments: unknown,
|
|
|
|
|
members: readonly RoomMemberRecord[],
|
|
|
|
|
roles: readonly RoomRoleRecord[]
|
|
|
|
|
): RoomRoleAssignmentRecord[] {
|
|
|
|
|
const validRoleIds = new Set(roles.map((role) => role.id).filter((roleId) => roleId !== SYSTEM_ROLE_IDS.everyone));
|
|
|
|
|
const assignmentsByKey = new Map<string, RoomRoleAssignmentRecord>();
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(rawAssignments)) {
|
|
|
|
|
for (const rawAssignment of rawAssignments) {
|
|
|
|
|
if (!rawAssignment || typeof rawAssignment !== 'object') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const assignment = rawAssignment as Record<string, unknown>;
|
|
|
|
|
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(Array.isArray(assignment['roleIds']) ? assignment['roleIds'] as string[] : undefined)
|
|
|
|
|
.filter((roleId) => validRoleIds.has(roleId));
|
|
|
|
|
|
|
|
|
|
if (roleIds.length === 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assignmentsByKey.set(key, {
|
|
|
|
|
userId: userId || key,
|
|
|
|
|
oderId,
|
|
|
|
|
roleIds
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (assignmentsByKey.size === 0) {
|
|
|
|
|
return buildRoleAssignmentsFromMembers(members, roles);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(assignmentsByKey.values()).sort(compareAssignments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeRoomChannelPermissions(
|
|
|
|
|
rawChannelPermissions: unknown,
|
|
|
|
|
roles: readonly RoomRoleRecord[]
|
|
|
|
|
): RoomChannelPermissionRecord[] {
|
|
|
|
|
if (!Array.isArray(rawChannelPermissions)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validRoleIds = new Set(roles.map((role) => role.id));
|
|
|
|
|
const overridesByKey = new Map<string, RoomChannelPermissionRecord>();
|
|
|
|
|
|
|
|
|
|
for (const rawOverride of rawChannelPermissions) {
|
|
|
|
|
if (!rawOverride || typeof rawOverride !== 'object') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const override = rawOverride as Record<string, unknown>;
|
|
|
|
|
const channelId = typeof override['channelId'] === 'string' ? override['channelId'].trim() : '';
|
|
|
|
|
const targetType = override['targetType'] === 'role' || override['targetType'] === 'user' ? override['targetType'] : null;
|
|
|
|
|
const targetId = typeof override['targetId'] === 'string' ? override['targetId'].trim() : '';
|
|
|
|
|
const permission = ROOM_PERMISSION_KEYS.find((key) => key === override['permission']);
|
|
|
|
|
const value = normalizePermissionState(override['value']);
|
|
|
|
|
|
|
|
|
|
if (!channelId || !targetType || !targetId || !permission || value === 'inherit') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetType === 'role' && !validRoleIds.has(targetId)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const key = `${channelId}:${targetType}:${targetId}:${permission}`;
|
|
|
|
|
|
|
|
|
|
overridesByKey.set(key, {
|
|
|
|
|
channelId,
|
|
|
|
|
targetType,
|
|
|
|
|
targetId,
|
|
|
|
|
permission,
|
|
|
|
|
value
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(overridesByKey.values()).sort((firstOverride, secondOverride) => {
|
|
|
|
|
const channelCompare = compareText(firstOverride.channelId, secondOverride.channelId);
|
|
|
|
|
|
|
|
|
|
if (channelCompare !== 0) {
|
|
|
|
|
return channelCompare;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (firstOverride.targetType !== secondOverride.targetType) {
|
|
|
|
|
return compareText(firstOverride.targetType, secondOverride.targetType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetCompare = compareText(firstOverride.targetId, secondOverride.targetId);
|
|
|
|
|
|
|
|
|
|
if (targetCompare !== 0) {
|
|
|
|
|
return targetCompare;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return compareText(firstOverride.permission, secondOverride.permission);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeReactionPayloads(rawReactions: unknown, messageId: string): ReactionPayload[] {
|
|
|
|
|
if (!Array.isArray(rawReactions)) {
|
|
|
|
|
return [];
|
|
|
|
|
@@ -233,30 +614,34 @@ export function normalizeRoomChannels(rawChannels: unknown): RoomChannelRecord[]
|
|
|
|
|
return channels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeRoomMembers(rawMembers: unknown, now: number = Date.now()): RoomMemberRecord[] {
|
|
|
|
|
if (!Array.isArray(rawMembers)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
function deriveLegacyPermissions(roles: readonly RoomRoleRecord[], slowModeInterval: number): LegacyRoomPermissions {
|
|
|
|
|
const everyoneRole = roles.find((role) => role.id === SYSTEM_ROLE_IDS.everyone);
|
|
|
|
|
const moderatorRole = roles.find((role) => role.id === SYSTEM_ROLE_IDS.moderator);
|
|
|
|
|
const adminRole = roles.find((role) => role.id === SYSTEM_ROLE_IDS.admin);
|
|
|
|
|
const toBoolean = (role: RoomRoleRecord | undefined, permission: RoomPermissionKeyPayload, fallbackValue: boolean): boolean => {
|
|
|
|
|
const state = role?.permissions?.[permission];
|
|
|
|
|
|
|
|
|
|
const membersByKey = new Map<string, RoomMemberRecord>();
|
|
|
|
|
|
|
|
|
|
for (const rawMember of rawMembers) {
|
|
|
|
|
if (!rawMember || typeof rawMember !== 'object') {
|
|
|
|
|
continue;
|
|
|
|
|
if (state === 'allow') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const member = normalizeRoomMember(rawMember as Record<string, unknown>, now);
|
|
|
|
|
|
|
|
|
|
if (!member) {
|
|
|
|
|
continue;
|
|
|
|
|
if (state === 'deny') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const key = memberKey(member);
|
|
|
|
|
return fallbackValue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
membersByKey.set(key, mergeRoomMembers(membersByKey.get(key), member));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(membersByKey.values()).sort(compareRoomMembers);
|
|
|
|
|
return {
|
|
|
|
|
allowVoice: toBoolean(everyoneRole, 'joinVoice', true),
|
|
|
|
|
allowScreenShare: toBoolean(everyoneRole, 'shareScreen', true),
|
|
|
|
|
allowFileUploads: toBoolean(everyoneRole, 'uploadFiles', true),
|
|
|
|
|
adminsManageRooms: adminRole?.permissions?.manageChannels === 'allow',
|
|
|
|
|
moderatorsManageRooms: moderatorRole?.permissions?.manageChannels === 'allow',
|
|
|
|
|
adminsManageIcon: adminRole?.permissions?.manageIcon === 'allow',
|
|
|
|
|
moderatorsManageIcon: moderatorRole?.permissions?.manageIcon === 'allow',
|
|
|
|
|
slowModeInterval
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function replaceMessageReactions(
|
|
|
|
|
@@ -311,11 +696,12 @@ export async function loadMessageReactionsMap(
|
|
|
|
|
emoji: row.emoji,
|
|
|
|
|
timestamp: row.timestamp
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
groupedReactions.set(row.messageId, reactions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const reactions of groupedReactions.values()) {
|
|
|
|
|
reactions.sort((first, second) => first.timestamp - second.timestamp);
|
|
|
|
|
reactions.sort((firstReaction, secondReaction) => firstReaction.timestamp - secondReaction.timestamp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return groupedReactions;
|
|
|
|
|
@@ -324,8 +710,22 @@ export async function loadMessageReactionsMap(
|
|
|
|
|
export async function replaceRoomRelations(
|
|
|
|
|
manager: EntityManager,
|
|
|
|
|
roomId: string,
|
|
|
|
|
options: { channels?: unknown; members?: unknown }
|
|
|
|
|
options: {
|
|
|
|
|
channels?: unknown;
|
|
|
|
|
members?: unknown;
|
|
|
|
|
roles?: unknown;
|
|
|
|
|
roleAssignments?: unknown;
|
|
|
|
|
channelPermissions?: unknown;
|
|
|
|
|
permissions?: unknown;
|
|
|
|
|
}
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const normalizedMembers = options.members !== undefined
|
|
|
|
|
? normalizeRoomMembers(options.members)
|
|
|
|
|
: [];
|
|
|
|
|
const normalizedRoles = options.roles !== undefined
|
|
|
|
|
? normalizeRoomRoles(options.roles, options.permissions)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
if (options.channels !== undefined) {
|
|
|
|
|
const channelRepo = manager.getRepository(RoomChannelEntity);
|
|
|
|
|
const channels = normalizeRoomChannels(options.channels);
|
|
|
|
|
@@ -347,13 +747,12 @@ export async function replaceRoomRelations(
|
|
|
|
|
|
|
|
|
|
if (options.members !== undefined) {
|
|
|
|
|
const memberRepo = manager.getRepository(RoomMemberEntity);
|
|
|
|
|
const members = normalizeRoomMembers(options.members);
|
|
|
|
|
|
|
|
|
|
await memberRepo.delete({ roomId });
|
|
|
|
|
|
|
|
|
|
if (members.length > 0) {
|
|
|
|
|
if (normalizedMembers.length > 0) {
|
|
|
|
|
await memberRepo.insert(
|
|
|
|
|
members.map((member) => ({
|
|
|
|
|
normalizedMembers.map((member) => ({
|
|
|
|
|
roomId,
|
|
|
|
|
memberKey: memberKey(member),
|
|
|
|
|
id: member.id,
|
|
|
|
|
@@ -368,6 +767,84 @@ export async function replaceRoomRelations(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.roles !== undefined) {
|
|
|
|
|
const roleRepo = manager.getRepository(RoomRoleEntity);
|
|
|
|
|
|
|
|
|
|
await roleRepo.delete({ roomId });
|
|
|
|
|
|
|
|
|
|
if (normalizedRoles.length > 0) {
|
|
|
|
|
await roleRepo.insert(
|
|
|
|
|
normalizedRoles.map((role) => ({
|
|
|
|
|
roomId,
|
|
|
|
|
roleId: role.id,
|
|
|
|
|
name: role.name,
|
|
|
|
|
color: role.color ?? null,
|
|
|
|
|
position: role.position,
|
|
|
|
|
isSystem: role.isSystem ? 1 : 0,
|
|
|
|
|
manageServer: normalizePermissionState(role.permissions?.manageServer),
|
|
|
|
|
manageRoles: normalizePermissionState(role.permissions?.manageRoles),
|
|
|
|
|
manageChannels: normalizePermissionState(role.permissions?.manageChannels),
|
|
|
|
|
manageIcon: normalizePermissionState(role.permissions?.manageIcon),
|
|
|
|
|
kickMembers: normalizePermissionState(role.permissions?.kickMembers),
|
|
|
|
|
banMembers: normalizePermissionState(role.permissions?.banMembers),
|
|
|
|
|
manageBans: normalizePermissionState(role.permissions?.manageBans),
|
|
|
|
|
deleteMessages: normalizePermissionState(role.permissions?.deleteMessages),
|
|
|
|
|
joinVoice: normalizePermissionState(role.permissions?.joinVoice),
|
|
|
|
|
shareScreen: normalizePermissionState(role.permissions?.shareScreen),
|
|
|
|
|
uploadFiles: normalizePermissionState(role.permissions?.uploadFiles)
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.roleAssignments !== undefined) {
|
|
|
|
|
const assignmentRepo = manager.getRepository(RoomUserRoleEntity);
|
|
|
|
|
const assignments = normalizeRoomRoleAssignments(
|
|
|
|
|
options.roleAssignments,
|
|
|
|
|
normalizedMembers,
|
|
|
|
|
normalizedRoles.length > 0 ? normalizedRoles : normalizeRoomRoles([], options.permissions)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await assignmentRepo.delete({ roomId });
|
|
|
|
|
|
|
|
|
|
const rows = assignments.flatMap((assignment) =>
|
|
|
|
|
assignment.roleIds.map((roleId) => ({
|
|
|
|
|
roomId,
|
|
|
|
|
userKey: assignment.oderId?.trim() || assignment.userId.trim(),
|
|
|
|
|
roleId,
|
|
|
|
|
userId: assignment.userId,
|
|
|
|
|
oderId: assignment.oderId ?? null
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (rows.length > 0) {
|
|
|
|
|
await assignmentRepo.insert(rows);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.channelPermissions !== undefined) {
|
|
|
|
|
const channelPermissionRepo = manager.getRepository(RoomChannelPermissionEntity);
|
|
|
|
|
const channelPermissions = normalizeRoomChannelPermissions(
|
|
|
|
|
options.channelPermissions,
|
|
|
|
|
normalizedRoles.length > 0 ? normalizedRoles : normalizeRoomRoles([], options.permissions)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await channelPermissionRepo.delete({ roomId });
|
|
|
|
|
|
|
|
|
|
if (channelPermissions.length > 0) {
|
|
|
|
|
await channelPermissionRepo.insert(
|
|
|
|
|
channelPermissions.map((channelPermission) => ({
|
|
|
|
|
roomId,
|
|
|
|
|
channelId: channelPermission.channelId,
|
|
|
|
|
targetType: channelPermission.targetType,
|
|
|
|
|
targetId: channelPermission.targetId,
|
|
|
|
|
permission: channelPermission.permission,
|
|
|
|
|
value: channelPermission.value
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function loadRoomRelationsMap(
|
|
|
|
|
@@ -380,31 +857,51 @@ export async function loadRoomRelationsMap(
|
|
|
|
|
return groupedRelations;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [channelRows, memberRows] = await Promise.all([
|
|
|
|
|
const [
|
|
|
|
|
channelRows,
|
|
|
|
|
memberRows,
|
|
|
|
|
roleRows,
|
|
|
|
|
assignmentRows,
|
|
|
|
|
channelPermissionRows
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
dataSource.getRepository(RoomChannelEntity).find({
|
|
|
|
|
where: { roomId: In([...roomIds]) }
|
|
|
|
|
}),
|
|
|
|
|
dataSource.getRepository(RoomMemberEntity).find({
|
|
|
|
|
where: { roomId: In([...roomIds]) }
|
|
|
|
|
}),
|
|
|
|
|
dataSource.getRepository(RoomRoleEntity).find({
|
|
|
|
|
where: { roomId: In([...roomIds]) }
|
|
|
|
|
}),
|
|
|
|
|
dataSource.getRepository(RoomUserRoleEntity).find({
|
|
|
|
|
where: { roomId: In([...roomIds]) }
|
|
|
|
|
}),
|
|
|
|
|
dataSource.getRepository(RoomChannelPermissionEntity).find({
|
|
|
|
|
where: { roomId: In([...roomIds]) }
|
|
|
|
|
})
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
for (const row of channelRows) {
|
|
|
|
|
const relation = groupedRelations.get(row.roomId) ?? { channels: [], members: [] };
|
|
|
|
|
for (const roomId of roomIds) {
|
|
|
|
|
groupedRelations.set(roomId, {
|
|
|
|
|
channels: [],
|
|
|
|
|
members: [],
|
|
|
|
|
roles: [],
|
|
|
|
|
roleAssignments: [],
|
|
|
|
|
channelPermissions: []
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
relation.channels.push({
|
|
|
|
|
for (const row of channelRows) {
|
|
|
|
|
groupedRelations.get(row.roomId)?.channels.push({
|
|
|
|
|
id: row.channelId,
|
|
|
|
|
name: row.name,
|
|
|
|
|
type: row.type,
|
|
|
|
|
position: row.position
|
|
|
|
|
});
|
|
|
|
|
groupedRelations.set(row.roomId, relation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of memberRows) {
|
|
|
|
|
const relation = groupedRelations.get(row.roomId) ?? { channels: [], members: [] };
|
|
|
|
|
|
|
|
|
|
relation.members.push({
|
|
|
|
|
groupedRelations.get(row.roomId)?.members.push({
|
|
|
|
|
id: row.id,
|
|
|
|
|
oderId: row.oderId ?? undefined,
|
|
|
|
|
username: row.username,
|
|
|
|
|
@@ -414,13 +911,92 @@ export async function loadRoomRelationsMap(
|
|
|
|
|
joinedAt: row.joinedAt,
|
|
|
|
|
lastSeenAt: row.lastSeenAt
|
|
|
|
|
});
|
|
|
|
|
groupedRelations.set(row.roomId, relation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of roleRows) {
|
|
|
|
|
groupedRelations.get(row.roomId)?.roles.push({
|
|
|
|
|
id: row.roleId,
|
|
|
|
|
name: row.name,
|
|
|
|
|
color: row.color ?? undefined,
|
|
|
|
|
position: row.position,
|
|
|
|
|
isSystem: !!row.isSystem,
|
|
|
|
|
permissions: normalizePermissionMatrix({
|
|
|
|
|
manageServer: row.manageServer,
|
|
|
|
|
manageRoles: row.manageRoles,
|
|
|
|
|
manageChannels: row.manageChannels,
|
|
|
|
|
manageIcon: row.manageIcon,
|
|
|
|
|
kickMembers: row.kickMembers,
|
|
|
|
|
banMembers: row.banMembers,
|
|
|
|
|
manageBans: row.manageBans,
|
|
|
|
|
deleteMessages: row.deleteMessages,
|
|
|
|
|
joinVoice: row.joinVoice,
|
|
|
|
|
shareScreen: row.shareScreen,
|
|
|
|
|
uploadFiles: row.uploadFiles
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of assignmentRows) {
|
|
|
|
|
const relation = groupedRelations.get(row.roomId);
|
|
|
|
|
|
|
|
|
|
if (!relation) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existing = relation.roleAssignments.find((assignment) =>
|
|
|
|
|
assignment.userId === row.userId || assignment.oderId === row.oderId || (row.oderId == null && assignment.userId === row.userKey)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
existing.roleIds = uniqueStrings([...existing.roleIds, row.roleId]);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
relation.roleAssignments.push({
|
|
|
|
|
userId: row.userId,
|
|
|
|
|
oderId: row.oderId ?? undefined,
|
|
|
|
|
roleIds: [row.roleId]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of channelPermissionRows) {
|
|
|
|
|
groupedRelations.get(row.roomId)?.channelPermissions.push({
|
|
|
|
|
channelId: row.channelId,
|
|
|
|
|
targetType: row.targetType,
|
|
|
|
|
targetId: row.targetId,
|
|
|
|
|
permission: row.permission as RoomPermissionKeyPayload,
|
|
|
|
|
value: normalizePermissionState(row.value)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const relation of groupedRelations.values()) {
|
|
|
|
|
relation.channels.sort((first, second) => first.position - second.position || first.name.localeCompare(second.name));
|
|
|
|
|
relation.channels.sort(
|
|
|
|
|
(firstChannel, secondChannel) => firstChannel.position - secondChannel.position || compareText(firstChannel.name, secondChannel.name)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
relation.members.sort(compareRoomMembers);
|
|
|
|
|
relation.roles.sort(compareRoles);
|
|
|
|
|
relation.roleAssignments.sort(compareAssignments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return groupedRelations;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function relationRecordToRoomPayload(
|
|
|
|
|
row: Pick<RoomPayload, 'slowModeInterval'>,
|
|
|
|
|
relations: RoomRelationRecord
|
|
|
|
|
): Pick<RoomPayload, 'permissions' | 'channels' | 'members' | 'roles' | 'roleAssignments' | 'channelPermissions'> {
|
|
|
|
|
const roleAssignmentsByKey = new Map(relations.roleAssignments.map((assignment) => [assignment.oderId || assignment.userId, assignment.roleIds]));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
permissions: deriveLegacyPermissions(relations.roles, row.slowModeInterval ?? 0),
|
|
|
|
|
channels: relations.channels,
|
|
|
|
|
members: relations.members.map((member) => ({
|
|
|
|
|
...member,
|
|
|
|
|
roleIds: roleAssignmentsByKey.get(member.oderId || member.id)
|
|
|
|
|
})),
|
|
|
|
|
roles: relations.roles,
|
|
|
|
|
roleAssignments: relations.roleAssignments,
|
|
|
|
|
channelPermissions: relations.channelPermissions
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|