feat(federation): process file_rejected relay events on sender instance
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user