feat(federation): relay friend requests and friendships to peer instances
This commit is contained in:
@@ -4,6 +4,9 @@ import { getDb, schema } from '../db/index.js';
|
|||||||
import { authenticate } from '../utils/auth.js';
|
import { authenticate } from '../utils/auth.js';
|
||||||
import { generateSnowflake } from '../utils/snowflake.js';
|
import { generateSnowflake } from '../utils/snowflake.js';
|
||||||
import { connectionManager } from '../ws/handler.js';
|
import { connectionManager } from '../ws/handler.js';
|
||||||
|
import { appendMutationLog, queueOutboxEvent, buildFriendContextId, getFriendEventTargets } from '../utils/federationOutbox.js';
|
||||||
|
import { getOurOrigin } from '../utils/federationAuth.js';
|
||||||
|
import type { FederationRelayEvent } from '@backspace/shared';
|
||||||
import type {
|
import type {
|
||||||
Friend,
|
Friend,
|
||||||
FriendRequest,
|
FriendRequest,
|
||||||
@@ -171,6 +174,42 @@ export async function socialRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
request: friendRequestPayload,
|
request: friendRequestPayload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Federation relay: notify the target user's home instance
|
||||||
|
const domainOrigin = getOurOrigin();
|
||||||
|
|
||||||
|
const fromIdentity = {
|
||||||
|
homeUserId: senderUser?.homeUserId || request.userId,
|
||||||
|
homeInstance: senderUser?.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
const toIdentity = {
|
||||||
|
homeUserId: targetUser.homeUserId || targetUser.id,
|
||||||
|
homeInstance: targetUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
|
||||||
|
const targets = getFriendEventTargets(fromIdentity.homeInstance, toIdentity.homeInstance);
|
||||||
|
if (targets.length > 0) {
|
||||||
|
const contextId = buildFriendContextId(fromIdentity.homeUserId, toIdentity.homeUserId);
|
||||||
|
const entityId = `friend_req:${[fromIdentity.homeUserId, toIdentity.homeUserId].sort().join(':')}:${now}`;
|
||||||
|
|
||||||
|
const payload: FederationRelayEvent = {
|
||||||
|
eventType: 'friend_request_create',
|
||||||
|
contextType: 'friend',
|
||||||
|
messageId: entityId,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: now,
|
||||||
|
friendship: {
|
||||||
|
from: fromIdentity,
|
||||||
|
to: toIdentity,
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: now,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const payloadStr = JSON.stringify(payload);
|
||||||
|
appendMutationLog(entityId, contextId, 'friend_request_create', payloadStr, 'friend');
|
||||||
|
queueOutboxEvent(entityId, contextId, 'friend_request_create', payloadStr, targets, 'friend');
|
||||||
|
}
|
||||||
|
|
||||||
return reply.code(201).send({ success: true, requestId: id });
|
return reply.code(201).send({ success: true, requestId: id });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,6 +278,68 @@ export async function socialRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Federation relay: notify the other user's home instance
|
||||||
|
const domainOrigin = getOurOrigin();
|
||||||
|
|
||||||
|
const fromUser = db.select().from(schema.users).where(eq(schema.users.id, friendRequest.fromId)).get();
|
||||||
|
const toUser = db.select().from(schema.users).where(eq(schema.users.id, friendRequest.toId)).get();
|
||||||
|
|
||||||
|
if (fromUser && toUser) {
|
||||||
|
const fromIdentity = {
|
||||||
|
homeUserId: fromUser.homeUserId || fromUser.id,
|
||||||
|
homeInstance: fromUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
const toIdentity = {
|
||||||
|
homeUserId: toUser.homeUserId || toUser.id,
|
||||||
|
homeInstance: toUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
|
||||||
|
const targets = getFriendEventTargets(fromIdentity.homeInstance, toIdentity.homeInstance);
|
||||||
|
if (targets.length > 0) {
|
||||||
|
const contextId = buildFriendContextId(fromIdentity.homeUserId, toIdentity.homeUserId);
|
||||||
|
const now2 = Date.now();
|
||||||
|
|
||||||
|
// Relay request status update
|
||||||
|
const updateEntityId = `friend_req:${[fromIdentity.homeUserId, toIdentity.homeUserId].sort().join(':')}:${now2}`;
|
||||||
|
const updatePayload: FederationRelayEvent = {
|
||||||
|
eventType: 'friend_request_update',
|
||||||
|
contextType: 'friend',
|
||||||
|
messageId: updateEntityId,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: now2,
|
||||||
|
friendship: {
|
||||||
|
from: fromIdentity,
|
||||||
|
to: toIdentity,
|
||||||
|
status: status as 'accepted' | 'declined',
|
||||||
|
createdAt: friendRequest.createdAt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const updateStr = JSON.stringify(updatePayload);
|
||||||
|
appendMutationLog(updateEntityId, contextId, 'friend_request_update', updateStr, 'friend');
|
||||||
|
queueOutboxEvent(updateEntityId, contextId, 'friend_request_update', updateStr, targets, 'friend');
|
||||||
|
|
||||||
|
// If accepted, also relay friend_add
|
||||||
|
if (status === 'accepted') {
|
||||||
|
const addEntityId = `friend:${[fromIdentity.homeUserId, toIdentity.homeUserId].sort().join(':')}:${now2}`;
|
||||||
|
const addPayload: FederationRelayEvent = {
|
||||||
|
eventType: 'friend_add',
|
||||||
|
contextType: 'friend',
|
||||||
|
messageId: addEntityId,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: now2,
|
||||||
|
friendship: {
|
||||||
|
from: fromIdentity,
|
||||||
|
to: toIdentity,
|
||||||
|
createdAt: now2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const addStr = JSON.stringify(addPayload);
|
||||||
|
appendMutationLog(addEntityId, contextId, 'friend_add', addStr, 'friend');
|
||||||
|
queueOutboxEvent(addEntityId, contextId, 'friend_add', addStr, targets, 'friend');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return reply.code(200).send({ success: true });
|
return reply.code(200).send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,6 +375,45 @@ export async function socialRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
userId: request.userId,
|
userId: request.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Federation relay: notify the recipient's home instance
|
||||||
|
const domainOrigin = getOurOrigin();
|
||||||
|
const callerUser = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
||||||
|
const recipientUser = db.select().from(schema.users).where(eq(schema.users.id, friendRequest.toId)).get();
|
||||||
|
|
||||||
|
if (callerUser && recipientUser) {
|
||||||
|
const fromIdentity = {
|
||||||
|
homeUserId: callerUser.homeUserId || callerUser.id,
|
||||||
|
homeInstance: callerUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
const toIdentity = {
|
||||||
|
homeUserId: recipientUser.homeUserId || recipientUser.id,
|
||||||
|
homeInstance: recipientUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
|
||||||
|
const targets = getFriendEventTargets(fromIdentity.homeInstance, toIdentity.homeInstance);
|
||||||
|
if (targets.length > 0) {
|
||||||
|
const contextId = buildFriendContextId(fromIdentity.homeUserId, toIdentity.homeUserId);
|
||||||
|
const now = Date.now();
|
||||||
|
const entityId = `friend_req:${[fromIdentity.homeUserId, toIdentity.homeUserId].sort().join(':')}:${now}`;
|
||||||
|
|
||||||
|
const payload: FederationRelayEvent = {
|
||||||
|
eventType: 'friend_request_cancel',
|
||||||
|
contextType: 'friend',
|
||||||
|
messageId: entityId,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: now,
|
||||||
|
friendship: {
|
||||||
|
from: fromIdentity,
|
||||||
|
to: toIdentity,
|
||||||
|
createdAt: friendRequest.createdAt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const payloadStr = JSON.stringify(payload);
|
||||||
|
appendMutationLog(entityId, contextId, 'friend_request_cancel', payloadStr, 'friend');
|
||||||
|
queueOutboxEvent(entityId, contextId, 'friend_request_cancel', payloadStr, targets, 'friend');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return reply.code(200).send({ success: true });
|
return reply.code(200).send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,6 +445,45 @@ export async function socialRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
userId: request.userId,
|
userId: request.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Federation relay: notify the other user's home instance
|
||||||
|
const domainOrigin = getOurOrigin();
|
||||||
|
const callerUser = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
||||||
|
const otherUser = db.select().from(schema.users).where(eq(schema.users.id, id)).get();
|
||||||
|
|
||||||
|
if (callerUser && otherUser) {
|
||||||
|
const callerIdentity = {
|
||||||
|
homeUserId: callerUser.homeUserId || callerUser.id,
|
||||||
|
homeInstance: callerUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
const otherIdentity = {
|
||||||
|
homeUserId: otherUser.homeUserId || otherUser.id,
|
||||||
|
homeInstance: otherUser.homeInstance || domainOrigin,
|
||||||
|
};
|
||||||
|
|
||||||
|
const targets = getFriendEventTargets(callerIdentity.homeInstance, otherIdentity.homeInstance);
|
||||||
|
if (targets.length > 0) {
|
||||||
|
const contextId = buildFriendContextId(callerIdentity.homeUserId, otherIdentity.homeUserId);
|
||||||
|
const now = Date.now();
|
||||||
|
const entityId = `friend:${[callerIdentity.homeUserId, otherIdentity.homeUserId].sort().join(':')}:${now}`;
|
||||||
|
|
||||||
|
const payload: FederationRelayEvent = {
|
||||||
|
eventType: 'friend_remove',
|
||||||
|
contextType: 'friend',
|
||||||
|
messageId: entityId,
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: now,
|
||||||
|
friendship: {
|
||||||
|
from: callerIdentity,
|
||||||
|
to: otherIdentity,
|
||||||
|
createdAt: now,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const payloadStr = JSON.stringify(payload);
|
||||||
|
appendMutationLog(entityId, contextId, 'friend_remove', payloadStr, 'friend');
|
||||||
|
queueOutboxEvent(entityId, contextId, 'friend_remove', payloadStr, targets, 'friend');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return reply.code(200).send({ success: true });
|
return reply.code(200).send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user