feat(federation): add room sync and deletion notification endpoints for federated instances fix(federation): handle room deletion and update settings during sync process feat(federation): enhance FederatedRoomCard and FederatedRoomDetail components to display deleted rooms i18n: add translations for room deletion messages in English and German
155 lines
4.7 KiB
JavaScript
155 lines
4.7 KiB
JavaScript
import { getDb } from '../config/database.js';
|
|
import { log, fmtDuration } from '../config/logger.js';
|
|
import {
|
|
isFederationEnabled,
|
|
getFederationDomain,
|
|
signPayload,
|
|
discoverInstance,
|
|
parseAddress,
|
|
} from '../config/federation.js';
|
|
|
|
const SYNC_INTERVAL_MS = 60_000; // 1 minute
|
|
|
|
let syncTimer = null;
|
|
|
|
/**
|
|
* Periodic federation sync job.
|
|
* Groups federated rooms by origin domain, then batch-queries each origin
|
|
* for current room settings. Updates local records if settings changed or
|
|
* if the room was deleted on the origin.
|
|
*/
|
|
async function runSync() {
|
|
if (!isFederationEnabled()) return;
|
|
|
|
const syncStart = Date.now();
|
|
let totalUpdated = 0;
|
|
let totalDeleted = 0;
|
|
let totalRooms = 0;
|
|
|
|
try {
|
|
const db = getDb();
|
|
|
|
// Fetch all non-deleted federated rooms
|
|
const rooms = await db.all(
|
|
'SELECT id, meet_id, from_user, room_name, max_participants, allow_recording FROM federated_rooms WHERE deleted = 0'
|
|
);
|
|
|
|
if (rooms.length === 0) return;
|
|
totalRooms = rooms.length;
|
|
|
|
// Group by origin domain
|
|
const byDomain = new Map();
|
|
for (const room of rooms) {
|
|
if (!room.meet_id) continue; // no room UID, can't sync
|
|
const { domain } = parseAddress(room.from_user);
|
|
if (!domain) continue;
|
|
if (!byDomain.has(domain)) byDomain.set(domain, []);
|
|
byDomain.get(domain).push(room);
|
|
}
|
|
|
|
// Query each origin domain
|
|
for (const [domain, domainRooms] of byDomain) {
|
|
try {
|
|
const roomUids = [...new Set(domainRooms.map(r => r.meet_id))];
|
|
|
|
const payload = {
|
|
room_uids: roomUids,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
const signature = signPayload(payload);
|
|
const { baseUrl: remoteApi } = await discoverInstance(domain);
|
|
|
|
const response = await fetch(`${remoteApi}/room-sync`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Federation-Signature': signature,
|
|
'X-Federation-Origin': getFederationDomain(),
|
|
},
|
|
body: JSON.stringify(payload),
|
|
signal: AbortSignal.timeout(15_000),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
log.fedSync.warn(`${domain} responded with status ${response.status}`);
|
|
continue;
|
|
}
|
|
|
|
const data = await response.json();
|
|
const remoteRooms = data.rooms || {};
|
|
|
|
// Update local records
|
|
for (const localRoom of domainRooms) {
|
|
const remote = remoteRooms[localRoom.meet_id];
|
|
if (!remote) continue; // UID not in response, skip
|
|
|
|
if (remote.deleted) {
|
|
// Room was deleted on origin
|
|
await db.run(
|
|
'UPDATE federated_rooms SET deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[localRoom.id]
|
|
);
|
|
totalDeleted++;
|
|
log.fedSync.info(`Room ${localRoom.meet_id} deleted on origin ${domain}`);
|
|
} else {
|
|
// Check if settings changed
|
|
const changed =
|
|
localRoom.room_name !== remote.room_name ||
|
|
(localRoom.max_participants ?? 0) !== (remote.max_participants ?? 0) ||
|
|
(localRoom.allow_recording ?? 1) !== (remote.allow_recording ?? 1);
|
|
|
|
if (changed) {
|
|
await db.run(
|
|
`UPDATE federated_rooms
|
|
SET room_name = ?, max_participants = ?, allow_recording = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?`,
|
|
[remote.room_name, remote.max_participants ?? 0, remote.allow_recording ?? 1, localRoom.id]
|
|
);
|
|
totalUpdated++;
|
|
log.fedSync.info(`Room ${localRoom.meet_id} settings updated from ${domain}`);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
log.fedSync.warn(`Sync with ${domain} failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Summary log (only if something happened)
|
|
if (totalUpdated > 0 || totalDeleted > 0) {
|
|
log.fedSync.info(
|
|
`Sync complete: ${totalRooms} rooms, ${totalUpdated} updated, ${totalDeleted} deleted (${fmtDuration(Date.now() - syncStart)})`
|
|
);
|
|
}
|
|
} catch (err) {
|
|
log.fedSync.error(`Sync job failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the periodic federation sync job.
|
|
*/
|
|
export function startFederationSync() {
|
|
if (!isFederationEnabled()) {
|
|
log.fedSync.info('Disabled (federation not configured)');
|
|
return;
|
|
}
|
|
|
|
// Run first sync after a short delay to let the server fully start
|
|
setTimeout(() => {
|
|
runSync();
|
|
syncTimer = setInterval(runSync, SYNC_INTERVAL_MS);
|
|
log.fedSync.info('Started (interval: 60s)');
|
|
}, 5_000);
|
|
}
|
|
|
|
/**
|
|
* Stop the periodic federation sync job.
|
|
*/
|
|
export function stopFederationSync() {
|
|
if (syncTimer) {
|
|
clearInterval(syncTimer);
|
|
syncTimer = null;
|
|
}
|
|
}
|