Remote follow fix
This commit is contained in:
@@ -1,19 +1,115 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, posts, users } from '@/db';
|
||||
import { eq, desc, and } from 'drizzle-orm';
|
||||
import { resolveRemoteUser } from '@/lib/activitypub/fetch';
|
||||
|
||||
type RouteContext = { params: Promise<{ handle: string }> };
|
||||
|
||||
const decodeEntities = (value: string) =>
|
||||
value
|
||||
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
||||
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(Number(num)))
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
|
||||
const sanitizeText = (value?: string | null) => {
|
||||
if (!value) return null;
|
||||
const withoutTags = value.replace(/<[^>]*>/g, ' ');
|
||||
const decoded = decodeEntities(withoutTags);
|
||||
return decoded.replace(/\s+/g, ' ').trim() || null;
|
||||
};
|
||||
|
||||
const parseRemoteHandle = (handle: string) => {
|
||||
const clean = handle.toLowerCase().replace(/^@/, '');
|
||||
const parts = clean.split('@').filter(Boolean);
|
||||
if (parts.length === 2) {
|
||||
return { handle: parts[0], domain: parts[1] };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fetchOutboxItems = async (outboxUrl: string, limit: number) => {
|
||||
const res = await fetch(outboxUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
},
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
const first = data?.first;
|
||||
if (first) {
|
||||
if (typeof first === 'string') {
|
||||
const pageRes = await fetch(first, {
|
||||
headers: {
|
||||
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
},
|
||||
});
|
||||
if (!pageRes.ok) return [];
|
||||
const page = await pageRes.json();
|
||||
return page?.orderedItems || page?.items || [];
|
||||
}
|
||||
return first?.orderedItems || first?.items || [];
|
||||
}
|
||||
const items = data?.orderedItems || data?.items || [];
|
||||
return items.slice(0, limit);
|
||||
};
|
||||
|
||||
export async function GET(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const { handle } = await context.params;
|
||||
const cleanHandle = handle.toLowerCase().replace(/^@/, '');
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50);
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
||||
|
||||
const remote = parseRemoteHandle(handle);
|
||||
|
||||
// Return empty if no database
|
||||
if (!db) {
|
||||
return NextResponse.json({ posts: [], nextCursor: null });
|
||||
if (!remote) {
|
||||
return NextResponse.json({ posts: [], nextCursor: null });
|
||||
}
|
||||
const remoteProfile = await resolveRemoteUser(remote.handle, remote.domain);
|
||||
if (!remoteProfile?.outbox) {
|
||||
return NextResponse.json({ posts: [] });
|
||||
}
|
||||
const outboxItems = await fetchOutboxItems(remoteProfile.outbox, limit);
|
||||
const authorHandle = `${remoteProfile.preferredUsername || remote.handle}@${remote.domain}`;
|
||||
const author = {
|
||||
id: remoteProfile.id || `remote:${authorHandle}`,
|
||||
handle: authorHandle,
|
||||
displayName: sanitizeText(remoteProfile.name) || remoteProfile.preferredUsername || remote.handle,
|
||||
avatarUrl: typeof remoteProfile.icon === 'string' ? remoteProfile.icon : remoteProfile.icon?.url,
|
||||
bio: sanitizeText(remoteProfile.summary),
|
||||
};
|
||||
const posts = outboxItems
|
||||
.map((item: any) => {
|
||||
const activity = item?.type === 'Create' ? item : null;
|
||||
const object = activity?.object;
|
||||
if (!object || typeof object === 'string' || object.type !== 'Note') {
|
||||
return null;
|
||||
}
|
||||
const attachments = Array.isArray(object.attachment) ? object.attachment : [];
|
||||
return {
|
||||
id: object.id || activity.id,
|
||||
content: sanitizeText(object.content) || '',
|
||||
createdAt: object.published || activity.published || new Date().toISOString(),
|
||||
likesCount: 0,
|
||||
repostsCount: 0,
|
||||
repliesCount: 0,
|
||||
author,
|
||||
media: attachments
|
||||
.filter((attachment: any) => attachment?.url)
|
||||
.map((attachment: any, index: number) => ({
|
||||
id: `${object.id || activity.id || 'media'}-${index}`,
|
||||
url: attachment.url,
|
||||
altText: sanitizeText(attachment.name) || null,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
return NextResponse.json({ posts, nextCursor: null });
|
||||
}
|
||||
|
||||
// Find the user
|
||||
@@ -22,7 +118,55 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
if (!remote) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
const remoteProfile = await resolveRemoteUser(remote.handle, remote.domain);
|
||||
if (!remoteProfile?.outbox) {
|
||||
return NextResponse.json({ posts: [] });
|
||||
}
|
||||
|
||||
const outboxItems = await fetchOutboxItems(remoteProfile.outbox, limit);
|
||||
const authorHandle = `${remoteProfile.preferredUsername || remote.handle}@${remote.domain}`;
|
||||
const author = {
|
||||
id: remoteProfile.id || `remote:${authorHandle}`,
|
||||
handle: authorHandle,
|
||||
displayName: sanitizeText(remoteProfile.name) || remoteProfile.preferredUsername || remote.handle,
|
||||
avatarUrl: typeof remoteProfile.icon === 'string' ? remoteProfile.icon : remoteProfile.icon?.url,
|
||||
bio: sanitizeText(remoteProfile.summary),
|
||||
};
|
||||
|
||||
const posts = outboxItems
|
||||
.map((item: any) => {
|
||||
const activity = item?.type === 'Create' ? item : null;
|
||||
const object = activity?.object;
|
||||
if (!object || typeof object === 'string' || object.type !== 'Note') {
|
||||
return null;
|
||||
}
|
||||
const attachments = Array.isArray(object.attachment) ? object.attachment : [];
|
||||
return {
|
||||
id: object.id || activity.id,
|
||||
content: sanitizeText(object.content) || '',
|
||||
createdAt: object.published || activity.published || new Date().toISOString(),
|
||||
likesCount: 0,
|
||||
repostsCount: 0,
|
||||
repliesCount: 0,
|
||||
author,
|
||||
media: attachments
|
||||
.filter((attachment: any) => attachment?.url)
|
||||
.map((attachment: any, index: number) => ({
|
||||
id: `${object.id || activity.id || 'media'}-${index}`,
|
||||
url: attachment.url,
|
||||
altText: sanitizeText(attachment.name) || null,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return NextResponse.json({
|
||||
posts,
|
||||
nextCursor: null,
|
||||
});
|
||||
}
|
||||
if (user.isSuspended) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
|
||||
Reference in New Issue
Block a user