Migrate user/profile URLs to /u/[handle] and remove legacy scripts
All user and post profile URLs have been updated from /[handle] to /u/[handle] for consistency. Added a redirect page for /posts/[id] to the canonical /u/[handle]/posts/[id] route. Deprecated and removed legacy scripts and documentation related to chat and federation endpoints. Push-based federation endpoints are now disabled in favor of real-time pull-based federation.
This commit is contained in:
@@ -102,6 +102,7 @@ export async function GET(request: Request) {
|
||||
post: row.postId ? {
|
||||
id: row.postId,
|
||||
content: row.postContent,
|
||||
authorHandle: row.actorHandle, // The actor is the post author for likes/reposts
|
||||
} : null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -115,7 +115,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a reply to a swarm post, deliver it to the origin node
|
||||
// DEPRECATED: Push-based federation disabled
|
||||
// Swarm now uses real-time pull-based federation
|
||||
// Replies are fetched in real-time from the origin node
|
||||
/*
|
||||
if (data.swarmReplyTo) {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -153,6 +156,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
})();
|
||||
}
|
||||
*/
|
||||
|
||||
// Handle local mentions (create notifications for users on this node)
|
||||
(async () => {
|
||||
|
||||
@@ -43,114 +43,12 @@ const swarmPostSchema = z.object({
|
||||
/**
|
||||
* POST /api/swarm/inbox
|
||||
*
|
||||
* Receives a post from another swarm node.
|
||||
* Stores it for local users who follow the author.
|
||||
* DEPRECATED: This endpoint is disabled.
|
||||
* We now use real-time pull-based federation via /api/swarm/timeline
|
||||
* instead of push-based caching.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!db) {
|
||||
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const data = swarmPostSchema.parse(body);
|
||||
|
||||
// Construct the swarm post ID
|
||||
const swarmPostId = `swarm:${data.nodeDomain}:${data.post.id}`;
|
||||
|
||||
// Check if we already have this post
|
||||
const existingPost = await db.query.posts.findFirst({
|
||||
where: eq(posts.apId, swarmPostId),
|
||||
});
|
||||
|
||||
if (existingPost) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Post already exists',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if anyone on this node follows the author
|
||||
const authorActorUrl = `swarm://${data.nodeDomain}/${data.author.handle}`;
|
||||
const hasFollowers = await db.query.remoteFollowers.findFirst({
|
||||
where: eq(remoteFollowers.actorUrl, authorActorUrl),
|
||||
});
|
||||
|
||||
// Even if no one follows, we might want to cache for timeline
|
||||
// For now, only store if someone follows
|
||||
if (!hasFollowers) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'No local followers',
|
||||
});
|
||||
}
|
||||
|
||||
// Get or create placeholder user for the remote author
|
||||
const remoteHandle = `${data.author.handle}@${data.nodeDomain}`;
|
||||
let remoteUser = await db.query.users.findFirst({
|
||||
where: eq(users.handle, remoteHandle),
|
||||
});
|
||||
|
||||
if (!remoteUser) {
|
||||
const [newUser] = await db.insert(users).values({
|
||||
did: `did:swarm:${data.nodeDomain}:${data.author.handle}`,
|
||||
handle: remoteHandle,
|
||||
displayName: data.author.displayName,
|
||||
avatarUrl: data.author.avatarUrl || null,
|
||||
isNsfw: data.author.isNsfw,
|
||||
publicKey: 'swarm-remote-user',
|
||||
}).returning();
|
||||
remoteUser = newUser;
|
||||
} else {
|
||||
// Update profile info if changed
|
||||
await db.update(users)
|
||||
.set({
|
||||
displayName: data.author.displayName,
|
||||
avatarUrl: data.author.avatarUrl || remoteUser.avatarUrl,
|
||||
isNsfw: data.author.isNsfw,
|
||||
})
|
||||
.where(eq(users.id, remoteUser.id));
|
||||
}
|
||||
|
||||
// Create the post
|
||||
const [newPost] = await db.insert(posts).values({
|
||||
userId: remoteUser.id,
|
||||
content: data.post.content,
|
||||
isNsfw: data.post.isNsfw || data.author.isNsfw,
|
||||
apId: swarmPostId,
|
||||
apUrl: `https://${data.nodeDomain}/${data.author.handle}/posts/${data.post.id}`,
|
||||
createdAt: new Date(data.post.createdAt),
|
||||
linkPreviewUrl: data.post.linkPreviewUrl || null,
|
||||
linkPreviewTitle: data.post.linkPreviewTitle || null,
|
||||
linkPreviewDescription: data.post.linkPreviewDescription || null,
|
||||
linkPreviewImage: data.post.linkPreviewImage || null,
|
||||
}).returning();
|
||||
|
||||
// Store media attachments
|
||||
if (data.post.media && data.post.media.length > 0) {
|
||||
for (const m of data.post.media) {
|
||||
await db.insert(media).values({
|
||||
userId: remoteUser.id,
|
||||
postId: newPost.id,
|
||||
url: m.url,
|
||||
mimeType: m.mimeType || null,
|
||||
altText: m.altText || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Swarm] Received post from ${remoteHandle}: ${newPost.id}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Post received',
|
||||
postId: newPost.id,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 });
|
||||
}
|
||||
console.error('[Swarm] Inbox error:', error);
|
||||
return NextResponse.json({ error: 'Failed to process post' }, { status: 500 });
|
||||
}
|
||||
return NextResponse.json({
|
||||
error: 'This endpoint is deprecated. Swarm uses real-time pull-based federation.',
|
||||
}, { status: 410 }); // 410 Gone
|
||||
}
|
||||
|
||||
@@ -30,86 +30,13 @@ const swarmReplySchema = z.object({
|
||||
/**
|
||||
* POST /api/swarm/replies
|
||||
*
|
||||
* Receives a reply from another node in the swarm.
|
||||
* The reply is stored as a remote reply linked to the local post.
|
||||
* DEPRECATED: This endpoint is disabled.
|
||||
* We now use real-time pull-based federation instead of push-based caching.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!db) {
|
||||
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const data = swarmReplySchema.parse(body);
|
||||
|
||||
// Verify the target post exists on this node
|
||||
const targetPost = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, data.postId),
|
||||
});
|
||||
|
||||
if (!targetPost) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if we already have this reply (by swarm ID)
|
||||
const swarmReplyId = `swarm:${data.reply.nodeDomain}:${data.reply.id}`;
|
||||
const existingReply = await db.query.posts.findFirst({
|
||||
where: eq(posts.apId, swarmReplyId),
|
||||
});
|
||||
|
||||
if (existingReply) {
|
||||
return NextResponse.json({ success: true, message: 'Reply already exists' });
|
||||
}
|
||||
|
||||
// We need a system user to attribute swarm replies to
|
||||
// For now, we'll store them with metadata in the apId/apUrl fields
|
||||
// and create a virtual representation
|
||||
|
||||
// Get or create a placeholder user for this remote author
|
||||
let remoteUser = await db.query.users.findFirst({
|
||||
where: eq(users.handle, `${data.reply.author.handle}@${data.reply.nodeDomain}`),
|
||||
});
|
||||
|
||||
if (!remoteUser) {
|
||||
// Create a placeholder user for the remote author
|
||||
const [newUser] = await db.insert(users).values({
|
||||
did: `did:swarm:${data.reply.nodeDomain}:${data.reply.author.handle}`,
|
||||
handle: `${data.reply.author.handle}@${data.reply.nodeDomain}`,
|
||||
displayName: data.reply.author.displayName,
|
||||
avatarUrl: data.reply.author.avatarUrl || null,
|
||||
publicKey: 'swarm-remote-user', // Placeholder
|
||||
}).returning();
|
||||
remoteUser = newUser;
|
||||
}
|
||||
|
||||
// Create the reply post
|
||||
const [replyPost] = await db.insert(posts).values({
|
||||
userId: remoteUser.id,
|
||||
content: data.reply.content,
|
||||
replyToId: data.postId,
|
||||
apId: swarmReplyId,
|
||||
apUrl: `https://${data.reply.nodeDomain}/${data.reply.author.handle}/posts/${data.reply.id}`,
|
||||
createdAt: new Date(data.reply.createdAt),
|
||||
}).returning();
|
||||
|
||||
// Update the parent post's reply count
|
||||
await db.update(posts)
|
||||
.set({ repliesCount: targetPost.repliesCount + 1 })
|
||||
.where(eq(posts.id, data.postId));
|
||||
|
||||
console.log(`[Swarm] Received reply from ${data.reply.nodeDomain} to post ${data.postId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
replyId: replyPost.id,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 });
|
||||
}
|
||||
console.error('[Swarm] Reply error:', error);
|
||||
return NextResponse.json({ error: 'Failed to process reply' }, { status: 500 });
|
||||
}
|
||||
return NextResponse.json({
|
||||
error: 'This endpoint is deprecated. Swarm uses real-time pull-based federation.',
|
||||
}, { status: 410 }); // 410 Gone
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+13
-7
@@ -489,15 +489,21 @@ export default function ChatPage() {
|
||||
marginLeft: msg.isSentByMe ? 'auto' : '0',
|
||||
flexDirection: msg.isSentByMe ? 'row-reverse' : 'row'
|
||||
}}>
|
||||
{!msg.isSentByMe && (
|
||||
<div className="avatar avatar-sm" style={{ flexShrink: 0 }}>
|
||||
{msg.senderAvatarUrl ? (
|
||||
<div className="avatar avatar-sm" style={{ flexShrink: 0 }}>
|
||||
{msg.isSentByMe ? (
|
||||
user.avatarUrl ? (
|
||||
<img src={user.avatarUrl} alt="" />
|
||||
) : (
|
||||
user.displayName[0]
|
||||
)
|
||||
) : (
|
||||
msg.senderAvatarUrl ? (
|
||||
<img src={msg.senderAvatarUrl} alt="" />
|
||||
) : (
|
||||
msg.senderDisplayName?.[0]
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: msg.isSentByMe ? 'flex-end' : 'flex-start' }}>
|
||||
<div style={{
|
||||
@@ -682,7 +688,7 @@ export default function ChatPage() {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{conv.lastMessagePreview}
|
||||
{conv.lastMessagePreview === 'New message' ? 'Encrypted Message' : conv.lastMessagePreview}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ interface User {
|
||||
|
||||
function UserCard({ user }: { user: User }) {
|
||||
return (
|
||||
<Link href={`/${user.handle}`} className="user-card">
|
||||
<Link href={`/u/${user.handle}`} className="user-card">
|
||||
<div className="avatar">
|
||||
{user.avatarUrl ? (
|
||||
<img src={user.avatarUrl} alt={user.displayName} />
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
export default function PostRedirect() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAndRedirect = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/posts/${id}`);
|
||||
if (!res.ok) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
router.push(`/u/${data.post.author.handle}/posts/${id}`);
|
||||
} catch {
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
fetchAndRedirect();
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--foreground-tertiary)',
|
||||
}}>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export default function BotsPage() {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '12px' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', flex: 1, minWidth: 0 }}>
|
||||
<Link
|
||||
href={`/${bot.handle}`}
|
||||
href={`/u/${bot.handle}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="avatar"
|
||||
style={{
|
||||
@@ -147,7 +147,7 @@ export default function BotsPage() {
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href={`/${bot.handle}`}
|
||||
href={`/u/${bot.handle}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ fontSize: '13px', color: 'var(--foreground-tertiary)' }}
|
||||
>
|
||||
|
||||
@@ -35,7 +35,7 @@ const stripHtml = (html: string | null | undefined): string | null => {
|
||||
|
||||
function UserRow({ user }: { user: UserSummary }) {
|
||||
return (
|
||||
<Link href={`/${user.handle}`} className="user-row">
|
||||
<Link href={`/u/${user.handle}`} className="user-row">
|
||||
<div className="avatar">
|
||||
{user.avatarUrl ? (
|
||||
<img src={user.avatarUrl} alt={user.displayName || user.handle} />
|
||||
@@ -215,7 +215,7 @@ export default function ProfilePage() {
|
||||
const handleComment = (post: Post) => {
|
||||
// Navigation is handled by the PostCard overlay,
|
||||
// but we can also use router.push if they explicitly click the comment button.
|
||||
router.push(`/${post.author.handle}/posts/${post.id}`);
|
||||
router.push(`/u/${post.author.handle}/posts/${post.id}`);
|
||||
};
|
||||
|
||||
const handleDelete = (postId: string) => {
|
||||
@@ -624,7 +624,7 @@ export default function ProfilePage() {
|
||||
<>
|
||||
{' · Managed by '}
|
||||
<Link
|
||||
href={`/${(user as any).botOwner.handle}`}
|
||||
href={`/u/${(user as any).botOwner.handle}`}
|
||||
style={{ color: 'var(--accent)', fontWeight: 500 }}
|
||||
>
|
||||
@{(user as any).botOwner.handle}
|
||||
@@ -85,7 +85,7 @@ export default function PostDetailPage() {
|
||||
|
||||
const handleDelete = (postId: string) => {
|
||||
if (postId === id) {
|
||||
router.push(`/${handle}`);
|
||||
router.push(`/u/${handle}`);
|
||||
} else {
|
||||
setReplies(prev => prev.filter(r => r.id !== postId));
|
||||
if (post) {
|
||||
Reference in New Issue
Block a user