feat(federation): update sync endpoint for friend event context type
This commit is contained in:
@@ -546,12 +546,37 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const sinceTimestamp = body.sinceTimestamp;
|
const sinceTimestamp = body.sinceTimestamp;
|
||||||
const dmChannelIdFilter = body.dmChannelId && typeof body.dmChannelId === 'string' ? body.dmChannelId : null;
|
const dmChannelIdFilter = body.dmChannelId && typeof body.dmChannelId === 'string' ? body.dmChannelId : null;
|
||||||
const federatedIdFilter = body.federatedId && typeof body.federatedId === 'string' ? body.federatedId : null;
|
const federatedIdFilter = body.federatedId && typeof body.federatedId === 'string' ? body.federatedId : null;
|
||||||
|
const contextTypeFilter = body.contextType && typeof body.contextType === 'string'
|
||||||
|
? body.contextType as 'dm' | 'friend'
|
||||||
|
: null;
|
||||||
|
|
||||||
// Clamp limit: min 1, max 500, default 100
|
// Clamp limit: min 1, max 500, default 100
|
||||||
let limit = typeof body.limit === 'number' ? body.limit : 100;
|
let limit = typeof body.limit === 'number' ? body.limit : 100;
|
||||||
limit = Math.max(1, Math.min(500, Math.floor(limit)));
|
limit = Math.max(1, Math.min(500, Math.floor(limit)));
|
||||||
|
|
||||||
// 3. Determine which DM channels to sync.
|
// 3. Query mutation log — branch by contextType
|
||||||
|
let mutationRows: Array<{
|
||||||
|
id: string;
|
||||||
|
entity_id: string;
|
||||||
|
context_id: string;
|
||||||
|
context_type: string;
|
||||||
|
mutation_type: string;
|
||||||
|
mutated_at: number;
|
||||||
|
payload: string | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
if (contextTypeFilter === 'friend') {
|
||||||
|
// ── Friend 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 = 'friend' AND mutated_at > ?
|
||||||
|
ORDER BY mutated_at ASC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(sinceTimestamp, limit) as typeof mutationRows;
|
||||||
|
} else {
|
||||||
|
// ── DM sync path ──
|
||||||
|
// Determine which DM channels to sync.
|
||||||
// Use federated_id: any channel with a federated ID is a federated DM
|
// Use federated_id: any channel with a federated ID is a federated DM
|
||||||
// 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.
|
||||||
@@ -586,15 +611,6 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// Return mutations for ALL locally-created messages (source_instance IS NULL).
|
// Return mutations for ALL locally-created messages (source_instance IS NULL).
|
||||||
// This includes messages by replicated users (e.g., Jannis browsing orbit)
|
// This includes messages by replicated users (e.g., Jannis browsing orbit)
|
||||||
// because they were created on THIS instance and need to be synced to the peer.
|
// because they were created on THIS instance and need to be synced to the peer.
|
||||||
let mutationRows: Array<{
|
|
||||||
id: string;
|
|
||||||
dm_message_id: string;
|
|
||||||
dm_channel_id: string;
|
|
||||||
mutation_type: string;
|
|
||||||
mutated_at: number;
|
|
||||||
payload: string | null;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
if (effectiveChannelFilter) {
|
if (effectiveChannelFilter) {
|
||||||
// Validate that the requested channel is actually shared with this peer
|
// Validate that the requested channel is actually shared with this peer
|
||||||
if (!sharedChannelIds.includes(effectiveChannelFilter)) {
|
if (!sharedChannelIds.includes(effectiveChannelFilter)) {
|
||||||
@@ -607,10 +623,11 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mutationRows = rawDb.prepare(`
|
mutationRows = rawDb.prepare(`
|
||||||
SELECT ml.id, ml.dm_message_id, ml.dm_channel_id, ml.mutation_type, ml.mutated_at, ml.payload
|
SELECT ml.id, ml.entity_id, ml.context_id, ml.context_type, ml.mutation_type, ml.mutated_at, ml.payload
|
||||||
FROM federation_mutation_log ml
|
FROM federation_mutation_log ml
|
||||||
LEFT JOIN dm_messages dm ON ml.dm_message_id = dm.id
|
LEFT JOIN dm_messages dm ON ml.entity_id = dm.id
|
||||||
WHERE ml.dm_channel_id = ?
|
WHERE ml.context_id = ?
|
||||||
|
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'))
|
||||||
ORDER BY ml.mutated_at ASC
|
ORDER BY ml.mutated_at ASC
|
||||||
@@ -619,12 +636,13 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
|
|
||||||
// For delete mutations, the dm_messages row won't exist — handle separately
|
// For delete mutations, the dm_messages row won't exist — handle separately
|
||||||
const deleteMutations = rawDb.prepare(`
|
const deleteMutations = rawDb.prepare(`
|
||||||
SELECT ml.id, ml.dm_message_id, ml.dm_channel_id, ml.mutation_type, ml.mutated_at, ml.payload
|
SELECT ml.id, ml.entity_id, ml.context_id, ml.context_type, ml.mutation_type, ml.mutated_at, ml.payload
|
||||||
FROM federation_mutation_log ml
|
FROM federation_mutation_log ml
|
||||||
WHERE ml.dm_channel_id = ?
|
WHERE ml.context_id = ?
|
||||||
|
AND ml.context_type = 'dm'
|
||||||
AND ml.mutated_at > ?
|
AND ml.mutated_at > ?
|
||||||
AND ml.mutation_type = 'delete'
|
AND ml.mutation_type = 'delete'
|
||||||
AND ml.dm_message_id NOT IN (SELECT dm.id FROM dm_messages dm WHERE dm.id = ml.dm_message_id)
|
AND ml.entity_id NOT IN (SELECT dm.id FROM dm_messages dm WHERE dm.id = ml.entity_id)
|
||||||
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;
|
||||||
@@ -646,10 +664,11 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const placeholders = sharedChannelIds.map(() => '?').join(',');
|
const placeholders = sharedChannelIds.map(() => '?').join(',');
|
||||||
|
|
||||||
mutationRows = rawDb.prepare(`
|
mutationRows = rawDb.prepare(`
|
||||||
SELECT ml.id, ml.dm_message_id, ml.dm_channel_id, ml.mutation_type, ml.mutated_at, ml.payload
|
SELECT ml.id, ml.entity_id, ml.context_id, ml.context_type, ml.mutation_type, ml.mutated_at, ml.payload
|
||||||
FROM federation_mutation_log ml
|
FROM federation_mutation_log ml
|
||||||
LEFT JOIN dm_messages dm ON ml.dm_message_id = dm.id
|
LEFT JOIN dm_messages dm ON ml.entity_id = dm.id
|
||||||
WHERE ml.dm_channel_id IN (${placeholders})
|
WHERE ml.context_id IN (${placeholders})
|
||||||
|
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'))
|
||||||
ORDER BY ml.mutated_at ASC
|
ORDER BY ml.mutated_at ASC
|
||||||
@@ -658,12 +677,13 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
|
|
||||||
// For delete mutations, the dm_messages row won't exist — handle separately
|
// For delete mutations, the dm_messages row won't exist — handle separately
|
||||||
const deleteMutations = rawDb.prepare(`
|
const deleteMutations = rawDb.prepare(`
|
||||||
SELECT ml.id, ml.dm_message_id, ml.dm_channel_id, ml.mutation_type, ml.mutated_at, ml.payload
|
SELECT ml.id, ml.entity_id, ml.context_id, ml.context_type, ml.mutation_type, ml.mutated_at, ml.payload
|
||||||
FROM federation_mutation_log ml
|
FROM federation_mutation_log ml
|
||||||
WHERE ml.dm_channel_id IN (${placeholders})
|
WHERE ml.context_id IN (${placeholders})
|
||||||
|
AND ml.context_type = 'dm'
|
||||||
AND ml.mutated_at > ?
|
AND ml.mutated_at > ?
|
||||||
AND ml.mutation_type = 'delete'
|
AND ml.mutation_type = 'delete'
|
||||||
AND ml.dm_message_id NOT IN (SELECT dm.id FROM dm_messages dm WHERE dm.id = ml.dm_message_id)
|
AND ml.entity_id NOT IN (SELECT dm.id FROM dm_messages dm WHERE dm.id = ml.entity_id)
|
||||||
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;
|
||||||
@@ -681,20 +701,27 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
mutationRows = mutationRows.slice(0, limit);
|
mutationRows = mutationRows.slice(0, limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Build response events from mutation log entries
|
// 5. Build response events from mutation log entries
|
||||||
const events: FederationRelayEvent[] = [];
|
const events: FederationRelayEvent[] = [];
|
||||||
|
|
||||||
for (const mutation of mutationRows) {
|
for (const mutation of mutationRows) {
|
||||||
const mutationType = mutation.mutation_type as 'create' | 'update' | 'delete' | 'reaction_add' | 'reaction_remove' | 'member_add' | 'member_remove' | 'ownership_transfer';
|
const mutationType = mutation.mutation_type as 'create' | 'update' | 'delete' | 'reaction_add' | 'reaction_remove'
|
||||||
|
| 'member_add' | 'member_remove' | 'ownership_transfer'
|
||||||
|
| 'friend_request_create' | 'friend_request_update' | 'friend_request_cancel'
|
||||||
|
| 'friend_add' | 'friend_remove';
|
||||||
|
|
||||||
if (['member_add', 'member_remove', 'ownership_transfer'].includes(mutationType)) {
|
if (['member_add', 'member_remove', 'ownership_transfer',
|
||||||
// Membership mutations store the full event in the payload
|
'friend_request_create', 'friend_request_update', 'friend_request_cancel',
|
||||||
|
'friend_add', 'friend_remove'].includes(mutationType)) {
|
||||||
|
// Membership and friend mutations store the full event in the payload
|
||||||
const payload = mutation.payload ? JSON.parse(mutation.payload) : {};
|
const payload = mutation.payload ? JSON.parse(mutation.payload) : {};
|
||||||
events.push({
|
events.push({
|
||||||
eventType: mutationType as FederationRelayEvent['eventType'],
|
eventType: mutationType as FederationRelayEvent['eventType'],
|
||||||
dmChannelId: mutation.dm_channel_id,
|
contextType: (mutation.context_type ?? 'dm') as 'dm' | 'friend',
|
||||||
messageId: mutation.dm_message_id,
|
...(mutation.context_type === 'dm' || !mutation.context_type ? { dmChannelId: mutation.context_id } : {}),
|
||||||
|
messageId: mutation.entity_id,
|
||||||
encryptionVersion: 0,
|
encryptionVersion: 0,
|
||||||
timestamp: mutation.mutated_at,
|
timestamp: mutation.mutated_at,
|
||||||
...payload,
|
...payload,
|
||||||
@@ -706,8 +733,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// For deletes, we don't need the message content — just the ID and channel
|
// For deletes, we don't need the message content — just the ID and channel
|
||||||
events.push({
|
events.push({
|
||||||
eventType: 'delete',
|
eventType: 'delete',
|
||||||
dmChannelId: mutation.dm_channel_id,
|
dmChannelId: mutation.context_id,
|
||||||
messageId: mutation.dm_message_id,
|
messageId: mutation.entity_id,
|
||||||
encryptionVersion: 0,
|
encryptionVersion: 0,
|
||||||
timestamp: mutation.mutated_at,
|
timestamp: mutation.mutated_at,
|
||||||
});
|
});
|
||||||
@@ -727,8 +754,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
eventType: mutationType,
|
eventType: mutationType,
|
||||||
dmChannelId: mutation.dm_channel_id,
|
dmChannelId: mutation.context_id,
|
||||||
messageId: mutation.dm_message_id,
|
messageId: mutation.entity_id,
|
||||||
encryptionVersion: 0,
|
encryptionVersion: 0,
|
||||||
timestamp: mutation.mutated_at,
|
timestamp: mutation.mutated_at,
|
||||||
reaction: {
|
reaction: {
|
||||||
@@ -746,7 +773,7 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const message = db
|
const message = db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.dmMessages)
|
.from(schema.dmMessages)
|
||||||
.where(eq(schema.dmMessages.id, mutation.dm_message_id))
|
.where(eq(schema.dmMessages.id, mutation.entity_id))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@@ -798,11 +825,11 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
eventType: mutationType,
|
eventType: mutationType,
|
||||||
dmChannelId: mutation.dm_channel_id,
|
dmChannelId: mutation.context_id,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
encryptionVersion: 0,
|
encryptionVersion: 0,
|
||||||
timestamp: mutation.mutated_at,
|
timestamp: mutation.mutated_at,
|
||||||
participants: getDmParticipants(mutation.dm_channel_id),
|
participants: getDmParticipants(mutation.context_id),
|
||||||
message: {
|
message: {
|
||||||
userId: message.userId,
|
userId: message.userId,
|
||||||
homeUserId,
|
homeUserId,
|
||||||
|
|||||||
Reference in New Issue
Block a user