feat(federation): process file_rejected relay events on sender instance

This commit is contained in:
Jannis Braun
2026-03-27 04:39:48 +01:00
parent 1990b90022
commit b99cbea268
+119
View File
@@ -468,6 +468,9 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
case 'friend_remove': case 'friend_remove':
processFriendRemoveEvent(event, sourceInstance, db, accepted, rejected); processFriendRemoveEvent(event, sourceInstance, db, accepted, rejected);
break; break;
case 'file_rejected':
processFileRejectedEvent(event, sourceInstance, db, accepted, rejected);
break;
default: default:
rejected.push({ messageId: event.messageId, reason: 'unknown_event_type' }); rejected.push({ messageId: event.messageId, reason: 'unknown_event_type' });
break; break;
@@ -2279,3 +2282,119 @@ function processFriendRemoveEvent(
accepted.push(event.messageId); accepted.push(event.messageId);
} }
function processFileRejectedEvent(
event: FederationRelayEvent,
sourceInstance: string,
db: ReturnType<typeof getDb>,
accepted: string[],
rejected: Array<{ messageId: string; reason: string }>,
): void {
if (!event.attachmentId || !event.rejectionReason) {
rejected.push({ messageId: event.messageId, reason: 'missing_file_rejected_payload' });
return;
}
// event.messageId is the original local message ID on THIS (sender) instance
const localMsg = db.select()
.from(schema.dmMessages)
.where(eq(schema.dmMessages.id, event.messageId))
.get();
if (!localMsg) {
rejected.push({ messageId: event.messageId, reason: 'message_not_found' });
return;
}
// Find the attachment — try by sourceUrl matching, then by checking all attachments on the message
const messageAttachments = db.select()
.from(schema.attachments)
.where(eq(schema.attachments.dmMessageId, localMsg.id))
.all();
// The attachmentId from the remote is their local attachment ID, not ours.
// Match by sourceUrl instead — the remote's sourceUrl points to our upload endpoint.
const ourOrigin = getOurOrigin();
let matchedAttachment = messageAttachments.find(a =>
a.sourceUrl && a.sourceUrl.startsWith(ourOrigin),
);
// If only one attachment, use it directly
if (!matchedAttachment && messageAttachments.length === 1) {
matchedAttachment = messageAttachments[0];
}
if (!matchedAttachment) {
rejected.push({ messageId: event.messageId, reason: 'attachment_not_found' });
return;
}
// Resolve affected user IDs to local usernames
const affectedUsers: Array<{ userId: string; username: string; limit: number }> = [];
for (const remoteUserId of (event.affectedUserIds ?? [])) {
// These are homeUserIds — find the replicated user stub
const user = db.select()
.from(schema.users)
.where(eq(schema.users.homeUserId, remoteUserId))
.get();
if (user) {
affectedUsers.push({
userId: user.id,
username: user.displayName || user.username,
limit: event.rejectionLimit ?? 0,
});
}
}
if (affectedUsers.length === 0) {
// Fallback: if we can't resolve usernames, still accept the event
// but skip the UI update since we can't show meaningful info
accepted.push(event.messageId);
return;
}
// Merge into federation_meta — accumulate rejections from multiple peers
let existingMeta: Array<{ userId: string; username: string; limit: number }> = [];
if (matchedAttachment.federationMeta) {
try {
existingMeta = JSON.parse(matchedAttachment.federationMeta);
} catch { /* ignore parse errors */ }
}
// Add new affected users, avoiding duplicates by userId
const existingUserIds = new Set(existingMeta.map(u => u.userId));
for (const user of affectedUsers) {
if (!existingUserIds.has(user.userId)) {
existingMeta.push(user);
}
}
// Update attachment
db.update(schema.attachments)
.set({
federationStatus: 'remote_partial',
federationMeta: JSON.stringify(existingMeta),
})
.where(eq(schema.attachments.id, matchedAttachment.id))
.run();
// Broadcast dm_message_updated to all DM members (persistent indicator)
const updatedMsg = getDmMessageWithUser(localMsg.id);
if (updatedMsg) {
connectionManager.sendToDmMembers(updatedMsg.dmChannelId, {
type: 'dm_message_updated',
message: updatedMsg,
});
// Send targeted toast event to the message author only
connectionManager.sendToUser(localMsg.userId, {
type: 'federation_file_rejected',
messageId: localMsg.id,
dmChannelId: localMsg.dmChannelId,
attachmentId: matchedAttachment.id,
affectedUsers,
});
}
accepted.push(event.messageId);
}