Add access control rework
This commit is contained in:
@@ -5,6 +5,9 @@ import {
|
||||
RoomEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity,
|
||||
RoomRoleEntity,
|
||||
RoomUserRoleEntity,
|
||||
RoomChannelPermissionEntity,
|
||||
ReactionEntity,
|
||||
BanEntity,
|
||||
AttachmentEntity,
|
||||
@@ -17,6 +20,9 @@ export async function handleClearAllData(dataSource: DataSource): Promise<void>
|
||||
await dataSource.getRepository(RoomEntity).clear();
|
||||
await dataSource.getRepository(RoomChannelEntity).clear();
|
||||
await dataSource.getRepository(RoomMemberEntity).clear();
|
||||
await dataSource.getRepository(RoomRoleEntity).clear();
|
||||
await dataSource.getRepository(RoomUserRoleEntity).clear();
|
||||
await dataSource.getRepository(RoomChannelPermissionEntity).clear();
|
||||
await dataSource.getRepository(ReactionEntity).clear();
|
||||
await dataSource.getRepository(BanEntity).clear();
|
||||
await dataSource.getRepository(AttachmentEntity).clear();
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
RoomChannelPermissionEntity,
|
||||
RoomChannelEntity,
|
||||
RoomEntity,
|
||||
RoomMemberEntity,
|
||||
RoomRoleEntity,
|
||||
RoomUserRoleEntity,
|
||||
MessageEntity
|
||||
} from '../../../entities';
|
||||
import { DeleteRoomCommand } from '../../types';
|
||||
@@ -11,8 +14,11 @@ export async function handleDeleteRoom(command: DeleteRoomCommand, dataSource: D
|
||||
const { roomId } = command.payload;
|
||||
|
||||
await dataSource.transaction(async (manager) => {
|
||||
await manager.getRepository(RoomChannelPermissionEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomChannelEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomMemberEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomRoleEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomUserRoleEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomEntity).delete({ id: roomId });
|
||||
await manager.getRepository(MessageEntity).delete({ roomId });
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SaveMessageCommand } from '../../types';
|
||||
|
||||
export async function handleSaveMessage(command: SaveMessageCommand, dataSource: DataSource): Promise<void> {
|
||||
const { message } = command.payload;
|
||||
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(MessageEntity);
|
||||
const entity = repo.create({
|
||||
|
||||
@@ -3,8 +3,23 @@ import { RoomEntity } from '../../../entities';
|
||||
import { replaceRoomRelations } from '../../relations';
|
||||
import { SaveRoomCommand } from '../../types';
|
||||
|
||||
function extractSlowModeInterval(room: SaveRoomCommand['payload']['room']): number {
|
||||
if (typeof room.slowModeInterval === 'number' && Number.isFinite(room.slowModeInterval)) {
|
||||
return room.slowModeInterval;
|
||||
}
|
||||
|
||||
const permissions = room.permissions && typeof room.permissions === 'object'
|
||||
? room.permissions as { slowModeInterval?: unknown }
|
||||
: null;
|
||||
|
||||
return typeof permissions?.slowModeInterval === 'number' && Number.isFinite(permissions.slowModeInterval)
|
||||
? permissions.slowModeInterval
|
||||
: 0;
|
||||
}
|
||||
|
||||
export async function handleSaveRoom(command: SaveRoomCommand, dataSource: DataSource): Promise<void> {
|
||||
const { room } = command.payload;
|
||||
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(RoomEntity);
|
||||
const entity = repo.create({
|
||||
@@ -21,7 +36,7 @@ export async function handleSaveRoom(command: SaveRoomCommand, dataSource: DataS
|
||||
maxUsers: room.maxUsers ?? null,
|
||||
icon: room.icon ?? null,
|
||||
iconUpdatedAt: room.iconUpdatedAt ?? null,
|
||||
permissions: room.permissions != null ? JSON.stringify(room.permissions) : null,
|
||||
slowModeInterval: extractSlowModeInterval(room),
|
||||
sourceId: room.sourceId ?? null,
|
||||
sourceName: room.sourceName ?? null,
|
||||
sourceUrl: room.sourceUrl ?? null
|
||||
@@ -30,7 +45,11 @@ export async function handleSaveRoom(command: SaveRoomCommand, dataSource: DataS
|
||||
await repo.save(entity);
|
||||
await replaceRoomRelations(manager, room.id, {
|
||||
channels: room.channels ?? [],
|
||||
members: room.members ?? []
|
||||
members: room.members ?? [],
|
||||
roles: room.roles ?? [],
|
||||
roleAssignments: room.roleAssignments ?? [],
|
||||
channelPermissions: room.channelPermissions ?? [],
|
||||
permissions: room.permissions
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UpdateMessageCommand } from '../../types';
|
||||
|
||||
export async function handleUpdateMessage(command: UpdateMessageCommand, dataSource: DataSource): Promise<void> {
|
||||
const { messageId, updates } = command.payload;
|
||||
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(MessageEntity);
|
||||
const existing = await repo.findOne({ where: { id: messageId } });
|
||||
|
||||
@@ -5,19 +5,32 @@ import { UpdateRoomCommand } from '../../types';
|
||||
import {
|
||||
applyUpdates,
|
||||
boolToInt,
|
||||
jsonOrNull,
|
||||
TransformMap
|
||||
} from './utils/applyUpdates';
|
||||
|
||||
const ROOM_TRANSFORMS: TransformMap = {
|
||||
hasPassword: boolToInt,
|
||||
isPrivate: boolToInt,
|
||||
userCount: (val) => (val ?? 0),
|
||||
permissions: jsonOrNull
|
||||
userCount: (val) => (val ?? 0)
|
||||
};
|
||||
|
||||
function extractSlowModeInterval(updates: UpdateRoomCommand['payload']['updates']): number | undefined {
|
||||
if (typeof updates.slowModeInterval === 'number' && Number.isFinite(updates.slowModeInterval)) {
|
||||
return updates.slowModeInterval;
|
||||
}
|
||||
|
||||
const permissions = updates.permissions && typeof updates.permissions === 'object'
|
||||
? updates.permissions as { slowModeInterval?: unknown }
|
||||
: null;
|
||||
|
||||
return typeof permissions?.slowModeInterval === 'number' && Number.isFinite(permissions.slowModeInterval)
|
||||
? permissions.slowModeInterval
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export async function handleUpdateRoom(command: UpdateRoomCommand, dataSource: DataSource): Promise<void> {
|
||||
const { roomId, updates } = command.payload;
|
||||
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(RoomEntity);
|
||||
const existing = await repo.findOne({ where: { id: roomId } });
|
||||
@@ -25,13 +38,30 @@ export async function handleUpdateRoom(command: UpdateRoomCommand, dataSource: D
|
||||
if (!existing)
|
||||
return;
|
||||
|
||||
const { channels, members, ...entityUpdates } = updates;
|
||||
const {
|
||||
channels,
|
||||
members,
|
||||
roles,
|
||||
roleAssignments,
|
||||
channelPermissions,
|
||||
permissions: rawPermissions,
|
||||
...entityUpdates
|
||||
} = updates;
|
||||
const slowModeInterval = extractSlowModeInterval(updates);
|
||||
|
||||
if (slowModeInterval !== undefined) {
|
||||
entityUpdates.slowModeInterval = slowModeInterval;
|
||||
}
|
||||
|
||||
applyUpdates(existing, entityUpdates, ROOM_TRANSFORMS);
|
||||
await repo.save(existing);
|
||||
await replaceRoomRelations(manager, roomId, {
|
||||
channels,
|
||||
members
|
||||
members,
|
||||
roles,
|
||||
roleAssignments,
|
||||
channelPermissions,
|
||||
permissions: rawPermissions
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ import { RoomEntity } from '../entities/RoomEntity';
|
||||
import { ReactionEntity } from '../entities/ReactionEntity';
|
||||
import { BanEntity } from '../entities/BanEntity';
|
||||
import { AttachmentEntity } from '../entities/AttachmentEntity';
|
||||
import { ReactionPayload } from './types';
|
||||
import {
|
||||
ReactionPayload,
|
||||
RoomPayload
|
||||
} from './types';
|
||||
relationRecordToRoomPayload,
|
||||
RoomChannelPermissionRecord,
|
||||
RoomChannelRecord,
|
||||
RoomMemberRecord,
|
||||
RoomRoleAssignmentRecord,
|
||||
RoomRoleRecord
|
||||
} from './relations';
|
||||
|
||||
const DELETED_MESSAGE_CONTENT = '[Message deleted]';
|
||||
|
||||
@@ -55,8 +60,28 @@ export function rowToUser(row: UserEntity) {
|
||||
|
||||
export function rowToRoom(
|
||||
row: RoomEntity,
|
||||
relations: Pick<RoomPayload, 'channels' | 'members'> = { channels: [], members: [] }
|
||||
relations: {
|
||||
channels?: RoomChannelRecord[];
|
||||
members?: RoomMemberRecord[];
|
||||
roles?: RoomRoleRecord[];
|
||||
roleAssignments?: RoomRoleAssignmentRecord[];
|
||||
channelPermissions?: RoomChannelPermissionRecord[];
|
||||
} = {
|
||||
channels: [],
|
||||
members: [],
|
||||
roles: [],
|
||||
roleAssignments: [],
|
||||
channelPermissions: []
|
||||
}
|
||||
) {
|
||||
const relationPayload = relationRecordToRoomPayload({ slowModeInterval: row.slowModeInterval }, {
|
||||
channels: relations.channels ?? [],
|
||||
members: relations.members ?? [],
|
||||
roles: relations.roles ?? [],
|
||||
roleAssignments: relations.roleAssignments ?? [],
|
||||
channelPermissions: relations.channelPermissions ?? []
|
||||
});
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -71,9 +96,13 @@ export function rowToRoom(
|
||||
maxUsers: row.maxUsers ?? undefined,
|
||||
icon: row.icon ?? undefined,
|
||||
iconUpdatedAt: row.iconUpdatedAt ?? undefined,
|
||||
permissions: row.permissions ? JSON.parse(row.permissions) : undefined,
|
||||
channels: relations.channels ?? [],
|
||||
members: relations.members ?? [],
|
||||
slowModeInterval: row.slowModeInterval,
|
||||
permissions: relationPayload.permissions,
|
||||
channels: relationPayload.channels,
|
||||
members: relationPayload.members,
|
||||
roles: relationPayload.roles,
|
||||
roleAssignments: relationPayload.roleAssignments,
|
||||
channelPermissions: relationPayload.channelPermissions,
|
||||
sourceId: row.sourceId ?? undefined,
|
||||
sourceName: row.sourceName ?? undefined,
|
||||
sourceUrl: row.sourceUrl ?? undefined
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +61,44 @@ export interface ReactionPayload {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type PermissionStatePayload = 'allow' | 'deny' | 'inherit';
|
||||
|
||||
export type RoomPermissionKeyPayload =
|
||||
| 'manageServer'
|
||||
| 'manageRoles'
|
||||
| 'manageChannels'
|
||||
| 'manageIcon'
|
||||
| 'kickMembers'
|
||||
| 'banMembers'
|
||||
| 'manageBans'
|
||||
| 'deleteMessages'
|
||||
| 'joinVoice'
|
||||
| 'shareScreen'
|
||||
| 'uploadFiles';
|
||||
|
||||
export interface AccessRolePayload {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
position: number;
|
||||
isSystem?: boolean;
|
||||
permissions?: Partial<Record<RoomPermissionKeyPayload, PermissionStatePayload>>;
|
||||
}
|
||||
|
||||
export interface RoleAssignmentPayload {
|
||||
userId: string;
|
||||
oderId?: string;
|
||||
roleIds: string[];
|
||||
}
|
||||
|
||||
export interface ChannelPermissionPayload {
|
||||
channelId: string;
|
||||
targetType: 'role' | 'user';
|
||||
targetId: string;
|
||||
permission: RoomPermissionKeyPayload;
|
||||
value: PermissionStatePayload;
|
||||
}
|
||||
|
||||
export interface UserPayload {
|
||||
id: string;
|
||||
oderId?: string;
|
||||
@@ -92,9 +130,13 @@ export interface RoomPayload {
|
||||
maxUsers?: number;
|
||||
icon?: string;
|
||||
iconUpdatedAt?: number;
|
||||
slowModeInterval?: number;
|
||||
permissions?: unknown;
|
||||
channels?: unknown[];
|
||||
members?: unknown[];
|
||||
roles?: AccessRolePayload[];
|
||||
roleAssignments?: RoleAssignmentPayload[];
|
||||
channelPermissions?: ChannelPermissionPayload[];
|
||||
sourceId?: string;
|
||||
sourceName?: string;
|
||||
sourceUrl?: string;
|
||||
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
MessageEntity,
|
||||
UserEntity,
|
||||
RoomEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity,
|
||||
RoomRoleEntity,
|
||||
RoomUserRoleEntity,
|
||||
RoomChannelPermissionEntity,
|
||||
ReactionEntity,
|
||||
BanEntity,
|
||||
AttachmentEntity,
|
||||
@@ -38,6 +43,11 @@ export const AppDataSource = new DataSource({
|
||||
MessageEntity,
|
||||
UserEntity,
|
||||
RoomEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity,
|
||||
RoomRoleEntity,
|
||||
RoomUserRoleEntity,
|
||||
RoomChannelPermissionEntity,
|
||||
ReactionEntity,
|
||||
BanEntity,
|
||||
AttachmentEntity,
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
RoomEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity,
|
||||
RoomRoleEntity,
|
||||
RoomUserRoleEntity,
|
||||
RoomChannelPermissionEntity,
|
||||
ReactionEntity,
|
||||
BanEntity,
|
||||
AttachmentEntity,
|
||||
@@ -44,6 +47,9 @@ export async function initializeDatabase(): Promise<void> {
|
||||
RoomEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity,
|
||||
RoomRoleEntity,
|
||||
RoomUserRoleEntity,
|
||||
RoomChannelPermissionEntity,
|
||||
ReactionEntity,
|
||||
BanEntity,
|
||||
AttachmentEntity,
|
||||
|
||||
@@ -20,4 +20,4 @@ export class RoomChannelEntity {
|
||||
|
||||
@Column('integer')
|
||||
position!: number;
|
||||
}
|
||||
}
|
||||
|
||||
26
electron/entities/RoomChannelPermissionEntity.ts
Normal file
26
electron/entities/RoomChannelPermissionEntity.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryColumn
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('room_channel_permissions')
|
||||
export class RoomChannelPermissionEntity {
|
||||
@PrimaryColumn('text')
|
||||
roomId!: string;
|
||||
|
||||
@PrimaryColumn('text')
|
||||
channelId!: string;
|
||||
|
||||
@PrimaryColumn('text')
|
||||
targetType!: 'role' | 'user';
|
||||
|
||||
@PrimaryColumn('text')
|
||||
targetId!: string;
|
||||
|
||||
@PrimaryColumn('text')
|
||||
permission!: string;
|
||||
|
||||
@Column('text')
|
||||
value!: 'allow' | 'deny' | 'inherit';
|
||||
}
|
||||
@@ -45,8 +45,8 @@ export class RoomEntity {
|
||||
@Column('integer', { nullable: true })
|
||||
iconUpdatedAt!: number | null;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
permissions!: string | null;
|
||||
@Column('integer', { default: 0 })
|
||||
slowModeInterval!: number;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
sourceId!: string | null;
|
||||
|
||||
@@ -35,4 +35,4 @@ export class RoomMemberEntity {
|
||||
|
||||
@Column('integer')
|
||||
lastSeenAt!: number;
|
||||
}
|
||||
}
|
||||
|
||||
59
electron/entities/RoomRoleEntity.ts
Normal file
59
electron/entities/RoomRoleEntity.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryColumn
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('room_roles')
|
||||
export class RoomRoleEntity {
|
||||
@PrimaryColumn('text')
|
||||
roomId!: string;
|
||||
|
||||
@PrimaryColumn('text')
|
||||
roleId!: string;
|
||||
|
||||
@Column('text')
|
||||
name!: string;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
color!: string | null;
|
||||
|
||||
@Column('integer')
|
||||
position!: number;
|
||||
|
||||
@Column('integer', { default: 0 })
|
||||
isSystem!: number;
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
manageServer!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
manageRoles!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
manageChannels!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
manageIcon!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
kickMembers!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
banMembers!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
manageBans!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
deleteMessages!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
joinVoice!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
shareScreen!: 'allow' | 'deny' | 'inherit';
|
||||
|
||||
@Column('text', { default: 'inherit' })
|
||||
uploadFiles!: 'allow' | 'deny' | 'inherit';
|
||||
}
|
||||
23
electron/entities/RoomUserRoleEntity.ts
Normal file
23
electron/entities/RoomUserRoleEntity.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryColumn
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('room_user_roles')
|
||||
export class RoomUserRoleEntity {
|
||||
@PrimaryColumn('text')
|
||||
roomId!: string;
|
||||
|
||||
@PrimaryColumn('text')
|
||||
userKey!: string;
|
||||
|
||||
@PrimaryColumn('text')
|
||||
roleId!: string;
|
||||
|
||||
@Column('text')
|
||||
userId!: string;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
oderId!: string | null;
|
||||
}
|
||||
@@ -3,6 +3,9 @@ export { UserEntity } from './UserEntity';
|
||||
export { RoomEntity } from './RoomEntity';
|
||||
export { RoomChannelEntity } from './RoomChannelEntity';
|
||||
export { RoomMemberEntity } from './RoomMemberEntity';
|
||||
export { RoomRoleEntity } from './RoomRoleEntity';
|
||||
export { RoomUserRoleEntity } from './RoomUserRoleEntity';
|
||||
export { RoomChannelPermissionEntity } from './RoomChannelPermissionEntity';
|
||||
export { ReactionEntity } from './ReactionEntity';
|
||||
export { BanEntity } from './BanEntity';
|
||||
export { AttachmentEntity } from './AttachmentEntity';
|
||||
|
||||
@@ -338,6 +338,7 @@ export function setupSystemHandlers(): void {
|
||||
ipcMain.handle('write-saved-theme', async (_event, fileName: string, text: string) => {
|
||||
return await writeSavedTheme(fileName, text);
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-saved-theme', async (_event, fileName: string) => {
|
||||
return await deleteSavedTheme(fileName);
|
||||
});
|
||||
|
||||
310
electron/migrations/1000000000004-NormalizeRoomAccessControl.ts
Normal file
310
electron/migrations/1000000000004-NormalizeRoomAccessControl.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
type LegacyRoomRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
topic: string | null;
|
||||
hostId: string;
|
||||
password: string | null;
|
||||
hasPassword: number;
|
||||
isPrivate: number;
|
||||
createdAt: number;
|
||||
userCount: number;
|
||||
maxUsers: number | null;
|
||||
icon: string | null;
|
||||
iconUpdatedAt: number | null;
|
||||
permissions: string | null;
|
||||
sourceId: string | null;
|
||||
sourceName: string | null;
|
||||
sourceUrl: string | null;
|
||||
};
|
||||
|
||||
type RoomMemberRow = {
|
||||
roomId: string;
|
||||
memberKey: string;
|
||||
id: string;
|
||||
oderId: string | null;
|
||||
role: string;
|
||||
};
|
||||
|
||||
type LegacyRoomPermissions = {
|
||||
adminsManageRooms?: boolean;
|
||||
moderatorsManageRooms?: boolean;
|
||||
adminsManageIcon?: boolean;
|
||||
moderatorsManageIcon?: boolean;
|
||||
allowVoice?: boolean;
|
||||
allowScreenShare?: boolean;
|
||||
allowFileUploads?: boolean;
|
||||
slowModeInterval?: number;
|
||||
};
|
||||
|
||||
const SYSTEM_ROLE_IDS = {
|
||||
everyone: 'system-everyone',
|
||||
moderator: 'system-moderator',
|
||||
admin: 'system-admin'
|
||||
} as const;
|
||||
|
||||
function parseLegacyPermissions(rawPermissions: string | null): LegacyRoomPermissions {
|
||||
try {
|
||||
const parsed = JSON.parse(rawPermissions || '{}') as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
adminsManageRooms: parsed['adminsManageRooms'] === true,
|
||||
moderatorsManageRooms: parsed['moderatorsManageRooms'] === true,
|
||||
adminsManageIcon: parsed['adminsManageIcon'] === true,
|
||||
moderatorsManageIcon: parsed['moderatorsManageIcon'] === true,
|
||||
allowVoice: parsed['allowVoice'] !== false,
|
||||
allowScreenShare: parsed['allowScreenShare'] !== false,
|
||||
allowFileUploads: parsed['allowFileUploads'] !== false,
|
||||
slowModeInterval: typeof parsed['slowModeInterval'] === 'number' && Number.isFinite(parsed['slowModeInterval'])
|
||||
? parsed['slowModeInterval']
|
||||
: 0
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
allowVoice: true,
|
||||
allowScreenShare: true,
|
||||
allowFileUploads: true,
|
||||
slowModeInterval: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildDefaultRoomRoles(legacyPermissions: LegacyRoomPermissions) {
|
||||
return [
|
||||
{
|
||||
roleId: SYSTEM_ROLE_IDS.everyone,
|
||||
name: '@everyone',
|
||||
color: '#6b7280',
|
||||
position: 0,
|
||||
isSystem: 1,
|
||||
manageServer: 'inherit',
|
||||
manageRoles: 'inherit',
|
||||
manageChannels: 'inherit',
|
||||
manageIcon: 'inherit',
|
||||
kickMembers: 'inherit',
|
||||
banMembers: 'inherit',
|
||||
manageBans: 'inherit',
|
||||
deleteMessages: 'inherit',
|
||||
joinVoice: legacyPermissions.allowVoice === false ? 'deny' : 'allow',
|
||||
shareScreen: legacyPermissions.allowScreenShare === false ? 'deny' : 'allow',
|
||||
uploadFiles: legacyPermissions.allowFileUploads === false ? 'deny' : 'allow'
|
||||
},
|
||||
{
|
||||
roleId: SYSTEM_ROLE_IDS.moderator,
|
||||
name: 'Moderator',
|
||||
color: '#10b981',
|
||||
position: 200,
|
||||
isSystem: 1,
|
||||
manageServer: 'inherit',
|
||||
manageRoles: 'inherit',
|
||||
manageChannels: legacyPermissions.moderatorsManageRooms ? 'allow' : 'inherit',
|
||||
manageIcon: legacyPermissions.moderatorsManageIcon ? 'allow' : 'inherit',
|
||||
kickMembers: 'allow',
|
||||
banMembers: 'inherit',
|
||||
manageBans: 'inherit',
|
||||
deleteMessages: 'allow',
|
||||
joinVoice: 'inherit',
|
||||
shareScreen: 'inherit',
|
||||
uploadFiles: 'inherit'
|
||||
},
|
||||
{
|
||||
roleId: SYSTEM_ROLE_IDS.admin,
|
||||
name: 'Admin',
|
||||
color: '#60a5fa',
|
||||
position: 300,
|
||||
isSystem: 1,
|
||||
manageServer: 'inherit',
|
||||
manageRoles: 'inherit',
|
||||
manageChannels: legacyPermissions.adminsManageRooms ? 'allow' : 'inherit',
|
||||
manageIcon: legacyPermissions.adminsManageIcon ? 'allow' : 'inherit',
|
||||
kickMembers: 'allow',
|
||||
banMembers: 'allow',
|
||||
manageBans: 'allow',
|
||||
deleteMessages: 'allow',
|
||||
joinVoice: 'inherit',
|
||||
shareScreen: 'inherit',
|
||||
uploadFiles: 'inherit'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function roleIdsForMemberRole(role: string): string[] {
|
||||
if (role === 'admin') {
|
||||
return [SYSTEM_ROLE_IDS.admin];
|
||||
}
|
||||
|
||||
if (role === 'moderator') {
|
||||
return [SYSTEM_ROLE_IDS.moderator];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export class NormalizeRoomAccessControl1000000000004 implements MigrationInterface {
|
||||
name = 'NormalizeRoomAccessControl1000000000004';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "room_roles" (
|
||||
"roomId" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
"position" INTEGER NOT NULL,
|
||||
"isSystem" INTEGER NOT NULL DEFAULT 0,
|
||||
"manageServer" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"manageRoles" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"manageChannels" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"manageIcon" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"kickMembers" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"banMembers" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"manageBans" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"deleteMessages" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"joinVoice" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"shareScreen" TEXT NOT NULL DEFAULT 'inherit',
|
||||
"uploadFiles" TEXT NOT NULL DEFAULT 'inherit',
|
||||
PRIMARY KEY ("roomId", "roleId")
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_room_roles_roomId" ON "room_roles" ("roomId")`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "room_user_roles" (
|
||||
"roomId" TEXT NOT NULL,
|
||||
"userKey" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"oderId" TEXT,
|
||||
PRIMARY KEY ("roomId", "userKey", "roleId")
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_room_user_roles_roomId" ON "room_user_roles" ("roomId")`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "room_channel_permissions" (
|
||||
"roomId" TEXT NOT NULL,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"targetType" TEXT NOT NULL,
|
||||
"targetId" TEXT NOT NULL,
|
||||
"permission" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
PRIMARY KEY ("roomId", "channelId", "targetType", "targetId", "permission")
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_room_channel_permissions_roomId" ON "room_channel_permissions" ("roomId")`);
|
||||
|
||||
const rooms = await queryRunner.query(`
|
||||
SELECT "id", "name", "description", "topic", "hostId", "password", "hasPassword", "isPrivate", "createdAt", "userCount", "maxUsers", "icon", "iconUpdatedAt", "permissions", "sourceId", "sourceName", "sourceUrl"
|
||||
FROM "rooms"
|
||||
`) as LegacyRoomRow[];
|
||||
const members = await queryRunner.query(`
|
||||
SELECT "roomId", "memberKey", "id", "oderId", "role"
|
||||
FROM "room_members"
|
||||
`) as RoomMemberRow[];
|
||||
|
||||
for (const room of rooms) {
|
||||
const legacyPermissions = parseLegacyPermissions(room.permissions);
|
||||
const roles = buildDefaultRoomRoles(legacyPermissions);
|
||||
|
||||
for (const role of roles) {
|
||||
await queryRunner.query(
|
||||
`INSERT OR REPLACE INTO "room_roles" ("roomId", "roleId", "name", "color", "position", "isSystem", "manageServer", "manageRoles", "manageChannels", "manageIcon", "kickMembers", "banMembers", "manageBans", "deleteMessages", "joinVoice", "shareScreen", "uploadFiles") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
room.id,
|
||||
role.roleId,
|
||||
role.name,
|
||||
role.color,
|
||||
role.position,
|
||||
role.isSystem,
|
||||
role.manageServer,
|
||||
role.manageRoles,
|
||||
role.manageChannels,
|
||||
role.manageIcon,
|
||||
role.kickMembers,
|
||||
role.banMembers,
|
||||
role.manageBans,
|
||||
role.deleteMessages,
|
||||
role.joinVoice,
|
||||
role.shareScreen,
|
||||
role.uploadFiles
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
for (const member of members.filter((candidateMember) => candidateMember.roomId === room.id)) {
|
||||
for (const roleId of roleIdsForMemberRole(member.role)) {
|
||||
await queryRunner.query(
|
||||
`INSERT OR REPLACE INTO "room_user_roles" ("roomId", "userKey", "roleId", "userId", "oderId") VALUES (?, ?, ?, ?, ?)`,
|
||||
[
|
||||
room.id,
|
||||
member.memberKey,
|
||||
roleId,
|
||||
member.id,
|
||||
member.oderId
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "rooms_next" (
|
||||
"id" TEXT PRIMARY KEY NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"topic" TEXT,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"hasPassword" INTEGER NOT NULL DEFAULT 0,
|
||||
"isPrivate" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" INTEGER NOT NULL,
|
||||
"userCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxUsers" INTEGER,
|
||||
"icon" TEXT,
|
||||
"iconUpdatedAt" INTEGER,
|
||||
"slowModeInterval" INTEGER NOT NULL DEFAULT 0,
|
||||
"sourceId" TEXT,
|
||||
"sourceName" TEXT,
|
||||
"sourceUrl" TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
for (const room of rooms) {
|
||||
const legacyPermissions = parseLegacyPermissions(room.permissions);
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "rooms_next" ("id", "name", "description", "topic", "hostId", "password", "hasPassword", "isPrivate", "createdAt", "userCount", "maxUsers", "icon", "iconUpdatedAt", "slowModeInterval", "sourceId", "sourceName", "sourceUrl") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
room.id,
|
||||
room.name,
|
||||
room.description,
|
||||
room.topic,
|
||||
room.hostId,
|
||||
room.password,
|
||||
room.hasPassword,
|
||||
room.isPrivate,
|
||||
room.createdAt,
|
||||
room.userCount,
|
||||
room.maxUsers,
|
||||
room.icon,
|
||||
room.iconUpdatedAt,
|
||||
legacyPermissions.slowModeInterval ?? 0,
|
||||
room.sourceId,
|
||||
room.sourceName,
|
||||
room.sourceUrl
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query(`DROP TABLE "rooms"`);
|
||||
await queryRunner.query(`ALTER TABLE "rooms_next" RENAME TO "rooms"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "room_channel_permissions"`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "room_user_roles"`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "room_roles"`);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ export async function listSavedThemes(): Promise<SavedThemeFileDescriptor[]> {
|
||||
const themesPath = await ensureSavedThemesPath();
|
||||
const entries = await fsp.readdir(themesPath, { withFileTypes: true });
|
||||
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.json'));
|
||||
|
||||
const descriptors = await Promise.all(files.map(async (entry) => {
|
||||
const filePath = path.join(themesPath, entry.name);
|
||||
const stats = await fsp.stat(filePath);
|
||||
@@ -89,4 +88,4 @@ export async function deleteSavedTheme(fileName: string): Promise<boolean> {
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user