Many improvments and bug fixes

This commit is contained in:
AskIt
2026-01-23 07:09:35 +01:00
parent 4b5a0a85f4
commit cfb558fff1
14 changed files with 906 additions and 536 deletions
+284 -21
View File
@@ -4,7 +4,7 @@
* Processes incoming activities from remote servers.
*/
import { db, users, remoteFollowers } from '@/db';
import { db, users, remoteFollowers, remotePosts, posts, remoteFollows } from '@/db';
import { eq, and } from 'drizzle-orm';
import { verifySignature, fetchActorPublicKey } from './signatures';
import { createAcceptActivity } from './activities';
@@ -107,16 +107,100 @@ export async function processIncomingActivity(
* Handle Create activities (new posts)
*/
async function handleCreate(activity: IncomingActivity): Promise<{ success: boolean; error?: string }> {
const object = activity.object as { type: string; content?: string; id?: string; attributedTo?: string };
const object = activity.object as {
type: string;
content?: string;
id?: string;
url?: string;
attributedTo?: string;
published?: string;
attachment?: Array<{
type: string;
mediaType?: string;
url?: string;
name?: string;
}>;
};
if (object.type !== 'Note') {
return { success: true }; // We only handle Notes for now
}
// TODO: Store remote posts in database for caching/display
console.log('[Inbox] Received remote post:', object.id);
if (!object.id || !object.attributedTo) {
console.warn('[Inbox] Create activity missing id or attributedTo');
return { success: false, error: 'Missing required fields' };
}
return { success: true };
try {
// Check if we already have this post cached
const existingPost = await db.query.remotePosts.findFirst({
where: eq(remotePosts.apId, object.id),
});
if (existingPost) {
console.log('[Inbox] Post already cached:', object.id);
return { success: true };
}
// Parse author info from attributedTo URL
const authorUrl = new URL(object.attributedTo);
const authorPathParts = authorUrl.pathname.split('/').filter(Boolean);
const authorHandle = authorPathParts[authorPathParts.length - 1] || 'unknown';
const authorDomain = authorUrl.hostname;
const fullHandle = `${authorHandle}@${authorDomain}`;
// Fetch author profile for display name and avatar
let displayName: string | null = null;
let avatarUrl: string | null = null;
try {
const actorResponse = await fetch(object.attributedTo, {
headers: {
'Accept': 'application/activity+json, application/ld+json',
},
});
if (actorResponse.ok) {
const actorData = await actorResponse.json();
displayName = actorData.name || actorData.preferredUsername || null;
avatarUrl = actorData.icon?.url || actorData.icon || null;
}
} catch (e) {
console.warn('[Inbox] Could not fetch actor profile:', e);
}
// Parse media attachments
let mediaJson: string | null = null;
if (object.attachment && object.attachment.length > 0) {
const mediaItems = object.attachment
.filter(att => att.type === 'Document' || att.type === 'Image' || att.type === 'Video')
.map(att => ({
url: att.url,
altText: att.name || null,
mediaType: att.mediaType,
}));
if (mediaItems.length > 0) {
mediaJson = JSON.stringify(mediaItems);
}
}
// Store the remote post
await db.insert(remotePosts).values({
apId: object.id,
authorHandle: fullHandle,
authorActorUrl: object.attributedTo,
authorDisplayName: displayName,
authorAvatarUrl: avatarUrl,
content: object.content || '',
publishedAt: object.published ? new Date(object.published) : new Date(),
mediaJson: mediaJson,
fetchedAt: new Date(),
});
console.log(`[Inbox] Cached remote post from ${fullHandle}:`, object.id);
return { success: true };
} catch (error) {
console.error('[Inbox] Error caching remote post:', error);
return { success: false, error: 'Failed to cache post' };
}
}
/**
@@ -244,9 +328,27 @@ async function handleLike(activity: IncomingActivity): Promise<{ success: boolea
return { success: false, error: 'Invalid like target' };
}
// TODO: Update like count on local post
console.log('[Inbox] Received like for:', targetUrl, 'from:', activity.actor);
try {
// Find the local post by its apId or apUrl
const post = await db.query.posts.findFirst({
where: eq(posts.apId, targetUrl),
});
if (post) {
// Increment like count
await db.update(posts)
.set({ likesCount: post.likesCount + 1 })
.where(eq(posts.id, post.id));
console.log(`[Inbox] Updated like count for post ${post.id}: ${post.likesCount + 1}`);
} else {
console.log('[Inbox] Like target not found locally:', targetUrl);
}
} catch (error) {
console.error('[Inbox] Error updating like count:', error);
}
return { success: true };
}
@@ -260,9 +362,27 @@ async function handleAnnounce(activity: IncomingActivity): Promise<{ success: bo
return { success: false, error: 'Invalid announce target' };
}
// TODO: Update repost count on local post
console.log('[Inbox] Received announce for:', targetUrl, 'from:', activity.actor);
try {
// Find the local post by its apId or apUrl
const post = await db.query.posts.findFirst({
where: eq(posts.apId, targetUrl),
});
if (post) {
// Increment repost count
await db.update(posts)
.set({ repostsCount: post.repostsCount + 1 })
.where(eq(posts.id, post.id));
console.log(`[Inbox] Updated repost count for post ${post.id}: ${post.repostsCount + 1}`);
} else {
console.log('[Inbox] Announce target not found locally:', targetUrl);
}
} catch (error) {
console.error('[Inbox] Error updating repost count:', error);
}
return { success: true };
}
@@ -330,7 +450,36 @@ async function handleUndo(
async function handleDelete(activity: IncomingActivity): Promise<{ success: boolean; error?: string }> {
console.log('[Inbox] Received delete from:', activity.actor);
// TODO: Remove cached remote content
try {
// The object can be the deleted item's URL or an object with an id
const deletedId = typeof activity.object === 'string'
? activity.object
: (activity.object as { id?: string })?.id;
if (!deletedId) {
console.log('[Inbox] Delete activity missing object id');
return { success: true };
}
// Try to find and remove cached remote post
const cachedPost = await db.query.remotePosts.findFirst({
where: eq(remotePosts.apId, deletedId),
});
if (cachedPost) {
// Verify the delete is from the original author
if (cachedPost.authorActorUrl === activity.actor) {
await db.delete(remotePosts).where(eq(remotePosts.id, cachedPost.id));
console.log(`[Inbox] Deleted cached remote post: ${deletedId}`);
} else {
console.warn('[Inbox] Delete actor mismatch - ignoring');
}
} else {
console.log('[Inbox] Deleted content not found in cache:', deletedId);
}
} catch (error) {
console.error('[Inbox] Error handling delete:', error);
}
return { success: true };
}
@@ -341,7 +490,51 @@ async function handleDelete(activity: IncomingActivity): Promise<{ success: bool
async function handleAccept(activity: IncomingActivity): Promise<{ success: boolean; error?: string }> {
console.log('[Inbox] Follow accepted by:', activity.actor);
// TODO: Update follow status in remoteFollows table
try {
// The object should be the original Follow activity
const followActivity = activity.object as { type?: string; actor?: string; object?: string };
if (followActivity?.type !== 'Follow') {
console.log('[Inbox] Accept is not for a Follow activity');
return { success: true };
}
// Find the local user who sent the follow
const localActorUrl = followActivity.actor;
if (!localActorUrl) {
return { success: true };
}
// Extract handle from our actor URL
const handleMatch = localActorUrl.match(/\/users\/([^\/]+)$/);
if (!handleMatch) {
return { success: true };
}
const localUser = await db.query.users.findFirst({
where: eq(users.handle, handleMatch[1].toLowerCase()),
});
if (!localUser) {
return { success: true };
}
// Find and update the remote follow record
const remoteFollow = await db.query.remoteFollows.findFirst({
where: and(
eq(remoteFollows.followerId, localUser.id),
eq(remoteFollows.targetActorUrl, activity.actor)
),
});
if (remoteFollow) {
// The follow is now confirmed - we could add an 'accepted' flag if needed
// For now, just log it since the follow is already stored
console.log(`[Inbox] Follow to ${activity.actor} confirmed for @${localUser.handle}`);
}
} catch (error) {
console.error('[Inbox] Error handling accept:', error);
}
return { success: true };
}
@@ -379,22 +572,92 @@ async function handleMove(activity: IncomingActivity): Promise<{ success: boolea
if (did) {
console.log(`[Inbox] Move includes DID: ${did} - attempting automatic migration`);
// Find any local follows that match this DID
// This would require querying by the remote user's DID
// For now, we'll log the DID and handle it
try {
// Find all local users following the old actor URL
const affectedFollows = await db.query.remoteFollows.findMany({
where: eq(remoteFollows.targetActorUrl, oldActorUrl),
});
// In a full implementation, we would:
// 1. Find all local users following the old actor URL
// 2. Update their follow relationship to point to the new actor URL
// 3. Automatically send a Follow to the new actor
if (affectedFollows.length === 0) {
console.log('[Inbox] No local users following the migrating account');
return { success: true };
}
// For Synapsis-to-Synapsis migrations, we can auto-follow
// because we trust the DID verification
console.log(`[Inbox] Found ${affectedFollows.length} local users to migrate`);
console.log(`[Inbox] DID-based migration supported. Followers will be auto-migrated.`);
// Fetch the new actor's info to get their inbox
const newActorResponse = await fetch(newActorUrl, {
headers: {
'Accept': 'application/activity+json, application/ld+json',
},
});
// TODO: Implement automatic follow migration
// await migrateFollowersByDid(did, oldActorUrl, newActorUrl);
if (!newActorResponse.ok) {
console.error('[Inbox] Failed to fetch new actor profile');
return { success: true }; // Don't fail, just log
}
const newActor = await newActorResponse.json();
const newInbox = newActor.endpoints?.sharedInbox || newActor.inbox;
const newHandle = newActor.preferredUsername
? `${newActor.preferredUsername}@${new URL(newActorUrl).hostname}`
: null;
if (!newInbox) {
console.error('[Inbox] New actor has no inbox');
return { success: true };
}
// Update each follow relationship and send new Follow activities
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const { createFollowActivity } = await import('./activities');
const { deliverActivity } = await import('./outbox');
for (const follow of affectedFollows) {
try {
// Get the local user who was following
const localUser = await db.query.users.findFirst({
where: eq(users.id, follow.followerId),
});
if (!localUser || !localUser.privateKeyEncrypted) {
continue;
}
// Update the remoteFollows record with new actor info
const newActivityId = crypto.randomUUID();
await db.update(remoteFollows)
.set({
targetActorUrl: newActorUrl,
targetHandle: newHandle || follow.targetHandle,
inboxUrl: newInbox,
activityId: newActivityId,
displayName: newActor.name || follow.displayName,
avatarUrl: newActor.icon?.url || newActor.icon || follow.avatarUrl,
})
.where(eq(remoteFollows.id, follow.id));
// Send a Follow activity to the new actor
const followActivity = createFollowActivity(
localUser,
newActorUrl,
nodeDomain,
newActivityId
);
const keyId = `https://${nodeDomain}/users/${localUser.handle}#main-key`;
await deliverActivity(followActivity, newInbox, localUser.privateKeyEncrypted, keyId);
console.log(`[Inbox] Auto-migrated @${localUser.handle}'s follow to ${newActorUrl}`);
} catch (err) {
console.error(`[Inbox] Error migrating follow ${follow.id}:`, err);
}
}
console.log(`[Inbox] DID-based migration complete. ${affectedFollows.length} followers migrated.`);
} catch (error) {
console.error('[Inbox] Error during DID-based migration:', error);
}
} else {
// Standard Fediverse Move - just log it
// Users will need to manually re-follow
+25 -5
View File
@@ -95,11 +95,31 @@ export async function deliverToFollowers(
/**
* Get followers' inboxes for delivery
* This would query the database for follower inbox URLs
* Queries the remoteFollowers table for inbox URLs of remote users following this user
*/
export async function getFollowerInboxes(userId: string): Promise<string[]> {
// TODO: Query database for followers and their inbox URLs
// For local followers: use their local inbox
// For remote followers: use their remote inbox (stored when they followed)
return [];
try {
const { db, remoteFollowers } = await import('@/db');
const { eq } = await import('drizzle-orm');
if (!db) {
console.warn('[Outbox] Database not available for follower query');
return [];
}
// Get all remote followers of this user
const followers = await db.query.remoteFollowers.findMany({
where: eq(remoteFollowers.userId, userId),
});
// Prefer shared inbox when available (more efficient)
const inboxes = followers.map(f => f.sharedInboxUrl || f.inboxUrl);
// Deduplicate (shared inboxes may appear multiple times)
return [...new Set(inboxes)];
} catch (error) {
console.error('[Outbox] Error fetching follower inboxes:', error);
return [];
}
}