feat(federation): /sync serializers for 5 new event types
Adds contextType='profile' query branch. Extends the DM-pass mutation_type IN-clause to include dm_close, dm_reopen, read_state_update, file_rejected (events with no associated dm_messages row). Builds channelFederatedIdMap for O(1) federatedId resolution. Adds serializer branches for all five new event types (dm_close/dm_reopen, read_state_update, file_rejected, profile_update) that emit FederationRelayEvent objects compatible with the existing inbound processors. Inbound processors (processDmCloseEvent, processDmReopenEvent, processReadStateUpdateEvent, processProfileUpdateEvent, processFileRejectedEvent) already exist; sync replay feeds events through processRelayEvents without any new receive-side code.
This commit is contained in:
@@ -1634,6 +1634,10 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
payload: string | null;
|
payload: string | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
// Maps local DM channel ID → federatedId for O(1) lookup in serializers.
|
||||||
|
// Only populated in the DM branch (friend/profile branches don't need it).
|
||||||
|
let channelFederatedIdMap = new Map<string, string>();
|
||||||
|
|
||||||
if (contextTypeFilter === 'friend') {
|
if (contextTypeFilter === 'friend') {
|
||||||
// ── Friend event sync: no DM channel logic needed ──
|
// ── Friend event sync: no DM channel logic needed ──
|
||||||
mutationRows = rawDb.prepare(`
|
mutationRows = rawDb.prepare(`
|
||||||
@@ -1643,6 +1647,15 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
ORDER BY mutated_at ASC
|
ORDER BY mutated_at ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(sinceTimestamp, limit) as typeof mutationRows;
|
`).all(sinceTimestamp, limit) as typeof mutationRows;
|
||||||
|
} else if (contextTypeFilter === 'profile') {
|
||||||
|
// ── Profile event sync: no DM channel logic needed ──
|
||||||
|
mutationRows = rawDb.prepare(`
|
||||||
|
SELECT id, entity_id, context_id, context_type, mutation_type, mutated_at, payload
|
||||||
|
FROM federation_mutation_log
|
||||||
|
WHERE context_type = 'profile' AND mutated_at > ?
|
||||||
|
ORDER BY mutated_at ASC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(sinceTimestamp, limit) as typeof mutationRows;
|
||||||
} else {
|
} else {
|
||||||
// ── DM sync path ──
|
// ── DM sync path ──
|
||||||
// Determine which DM channels to sync.
|
// Determine which DM channels to sync.
|
||||||
@@ -1650,11 +1663,14 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// that should be synced. The peer's relay endpoint will create the channel
|
// that should be synced. The peer's relay endpoint will create the channel
|
||||||
// if it doesn't exist, or match by federated_id if it does.
|
// if it doesn't exist, or match by federated_id if it does.
|
||||||
const sharedChannelRows = rawDb.prepare(`
|
const sharedChannelRows = rawDb.prepare(`
|
||||||
SELECT id as dm_channel_id FROM dm_channels
|
SELECT id as dm_channel_id, federated_id FROM dm_channels
|
||||||
WHERE federated_id IS NOT NULL AND deleted_at IS NULL
|
WHERE federated_id IS NOT NULL AND deleted_at IS NULL
|
||||||
`).all() as Array<{ dm_channel_id: string }>;
|
`).all() as Array<{ dm_channel_id: string; federated_id: string }>;
|
||||||
|
|
||||||
const sharedChannelIds = sharedChannelRows.map(r => r.dm_channel_id);
|
const sharedChannelIds = sharedChannelRows.map(r => r.dm_channel_id);
|
||||||
|
channelFederatedIdMap = new Map<string, string>(
|
||||||
|
sharedChannelRows.map(r => [r.dm_channel_id, r.federated_id])
|
||||||
|
);
|
||||||
|
|
||||||
// If filtering by federatedId, resolve to local channel ID
|
// If filtering by federatedId, resolve to local channel ID
|
||||||
let effectiveChannelFilter = dmChannelIdFilter;
|
let effectiveChannelFilter = dmChannelIdFilter;
|
||||||
@@ -1698,7 +1714,10 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
WHERE ml.context_id = ?
|
WHERE ml.context_id = ?
|
||||||
AND ml.context_type = 'dm'
|
AND ml.context_type = 'dm'
|
||||||
AND ml.mutated_at > ?
|
AND ml.mutated_at > ?
|
||||||
AND (dm.id IS NOT NULL OR ml.mutation_type IN ('delete', 'member_add', 'member_remove', 'ownership_transfer'))
|
AND (dm.id IS NOT NULL OR ml.mutation_type IN (
|
||||||
|
'delete', 'member_add', 'member_remove', 'ownership_transfer',
|
||||||
|
'dm_close', 'dm_reopen', 'read_state_update', 'file_rejected'
|
||||||
|
))
|
||||||
ORDER BY ml.mutated_at ASC
|
ORDER BY ml.mutated_at ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(effectiveChannelFilter, sinceTimestamp, limit) as typeof mutationRows;
|
`).all(effectiveChannelFilter, sinceTimestamp, limit) as typeof mutationRows;
|
||||||
@@ -1739,7 +1758,10 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
WHERE ml.context_id IN (${placeholders})
|
WHERE ml.context_id IN (${placeholders})
|
||||||
AND ml.context_type = 'dm'
|
AND ml.context_type = 'dm'
|
||||||
AND ml.mutated_at > ?
|
AND ml.mutated_at > ?
|
||||||
AND (dm.id IS NOT NULL OR ml.mutation_type IN ('delete', 'member_add', 'member_remove', 'ownership_transfer'))
|
AND (dm.id IS NOT NULL OR ml.mutation_type IN (
|
||||||
|
'delete', 'member_add', 'member_remove', 'ownership_transfer',
|
||||||
|
'dm_close', 'dm_reopen', 'read_state_update', 'file_rejected'
|
||||||
|
))
|
||||||
ORDER BY ml.mutated_at ASC
|
ORDER BY ml.mutated_at ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(...sharedChannelIds, sinceTimestamp, limit) as typeof mutationRows;
|
`).all(...sharedChannelIds, sinceTimestamp, limit) as typeof mutationRows;
|
||||||
@@ -1779,7 +1801,9 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const mutationType = mutation.mutation_type as 'create' | 'update' | 'delete' | 'reaction_add' | 'reaction_remove'
|
const mutationType = mutation.mutation_type as 'create' | 'update' | 'delete' | 'reaction_add' | 'reaction_remove'
|
||||||
| 'member_add' | 'member_remove' | 'ownership_transfer'
|
| 'member_add' | 'member_remove' | 'ownership_transfer'
|
||||||
| 'friend_request_create' | 'friend_request_update' | 'friend_request_cancel'
|
| 'friend_request_create' | 'friend_request_update' | 'friend_request_cancel'
|
||||||
| 'friend_add' | 'friend_remove';
|
| 'friend_add' | 'friend_remove'
|
||||||
|
| 'dm_close' | 'dm_reopen' | 'read_state_update' | 'file_rejected'
|
||||||
|
| 'profile_update';
|
||||||
|
|
||||||
if (['member_add', 'member_remove', 'ownership_transfer',
|
if (['member_add', 'member_remove', 'ownership_transfer',
|
||||||
'friend_request_create', 'friend_request_update', 'friend_request_cancel',
|
'friend_request_create', 'friend_request_update', 'friend_request_cancel',
|
||||||
@@ -1839,6 +1863,86 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mutationType === 'dm_close' || mutationType === 'dm_reopen') {
|
||||||
|
if (!mutation.payload) continue;
|
||||||
|
let dmCloseReopenPayload: { homeUserId: string; homeInstance: string } | null = null;
|
||||||
|
try { dmCloseReopenPayload = JSON.parse(mutation.payload); } catch { continue; }
|
||||||
|
if (!dmCloseReopenPayload) continue;
|
||||||
|
const fedIdCloseReopen = channelFederatedIdMap.get(mutation.context_id);
|
||||||
|
if (!fedIdCloseReopen) continue;
|
||||||
|
events.push({
|
||||||
|
eventType: mutationType,
|
||||||
|
dmChannelId: mutation.context_id,
|
||||||
|
messageId: mutation.entity_id,
|
||||||
|
federatedId: fedIdCloseReopen,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: mutation.mutated_at,
|
||||||
|
dmCloseReopen: dmCloseReopenPayload,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutationType === 'read_state_update') {
|
||||||
|
if (!mutation.payload) continue;
|
||||||
|
let readState: NonNullable<FederationRelayEvent['readState']> | null = null;
|
||||||
|
try { readState = JSON.parse(mutation.payload) as NonNullable<FederationRelayEvent['readState']>; } catch { continue; }
|
||||||
|
if (!readState) continue;
|
||||||
|
const fedIdReadState = channelFederatedIdMap.get(mutation.context_id);
|
||||||
|
if (!fedIdReadState) continue;
|
||||||
|
events.push({
|
||||||
|
eventType: 'read_state_update',
|
||||||
|
dmChannelId: mutation.context_id,
|
||||||
|
messageId: mutation.entity_id,
|
||||||
|
federatedId: fedIdReadState,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: mutation.mutated_at,
|
||||||
|
readState,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutationType === 'file_rejected') {
|
||||||
|
if (!mutation.payload) continue;
|
||||||
|
let fileRejectedPayload: {
|
||||||
|
attachmentId: string;
|
||||||
|
sourceFilename: string;
|
||||||
|
rejectionReason: string;
|
||||||
|
rejectionLimit: number;
|
||||||
|
affectedUserIds: string[];
|
||||||
|
} | null = null;
|
||||||
|
try { fileRejectedPayload = JSON.parse(mutation.payload); } catch { continue; }
|
||||||
|
if (!fileRejectedPayload) continue;
|
||||||
|
events.push({
|
||||||
|
eventType: 'file_rejected',
|
||||||
|
dmChannelId: mutation.context_id,
|
||||||
|
messageId: mutation.entity_id,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: mutation.mutated_at,
|
||||||
|
attachmentId: fileRejectedPayload.attachmentId,
|
||||||
|
sourceFilename: fileRejectedPayload.sourceFilename,
|
||||||
|
rejectionReason: fileRejectedPayload.rejectionReason,
|
||||||
|
rejectionLimit: fileRejectedPayload.rejectionLimit,
|
||||||
|
affectedUserIds: fileRejectedPayload.affectedUserIds,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutationType === 'profile_update') {
|
||||||
|
if (!mutation.payload) continue;
|
||||||
|
let profileOuter: { profileUpdate?: NonNullable<FederationRelayEvent['profileUpdate']> } | null = null;
|
||||||
|
try { profileOuter = JSON.parse(mutation.payload); } catch { continue; }
|
||||||
|
if (!profileOuter?.profileUpdate) continue;
|
||||||
|
events.push({
|
||||||
|
eventType: 'profile_update',
|
||||||
|
contextType: 'profile',
|
||||||
|
messageId: mutation.entity_id,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: mutation.mutated_at,
|
||||||
|
profileUpdate: profileOuter.profileUpdate,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// For create and update: fetch the current message state
|
// For create and update: fetch the current message state
|
||||||
const message = db
|
const message = db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
Reference in New Issue
Block a user