feat(logging): implement centralized logging system and replace console errors with structured logs
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
This commit is contained in:
154
server/jobs/federationSync.js
Normal file
154
server/jobs/federationSync.js
Normal file
@@ -0,0 +1,154 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user