Initial commit
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, posts, likes, users, notifications } from '@/db';
|
||||
import { requireAuth } from '@/lib/auth';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import crypto from 'crypto';
|
||||
|
||||
type RouteContext = { params: Promise<{ id: string }> };
|
||||
|
||||
/**
|
||||
* Extract domain from a swarm post ID (swarm:domain:postId)
|
||||
*/
|
||||
function extractSwarmDomain(apId: string | null): string | null {
|
||||
if (!apId?.startsWith('swarm:')) return null;
|
||||
const parts = apId.split(':');
|
||||
return parts.length >= 2 ? parts[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post is from a swarm node (has swarm: prefix in apId)
|
||||
*/
|
||||
function isSwarmPost(apId: string | null): boolean {
|
||||
return apId?.startsWith('swarm:') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the original post ID from a swarm apId
|
||||
*/
|
||||
function extractSwarmPostId(apId: string): string | null {
|
||||
const parts = apId.split(':');
|
||||
return parts.length >= 3 ? parts[2] : null;
|
||||
}
|
||||
|
||||
// Like a post
|
||||
export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const user = await requireAuth();
|
||||
const { id: postId } = await context.params;
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
|
||||
if (user.isSuspended || user.isSilenced) {
|
||||
return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if post exists
|
||||
const post = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, postId),
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
if (post.isRemoved) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if already liked
|
||||
const existingLike = await db.query.likes.findFirst({
|
||||
where: and(
|
||||
eq(likes.userId, user.id),
|
||||
eq(likes.postId, postId)
|
||||
),
|
||||
});
|
||||
|
||||
if (existingLike) {
|
||||
return NextResponse.json({ error: 'Already liked' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Create like
|
||||
await db.insert(likes).values({
|
||||
userId: user.id,
|
||||
postId,
|
||||
});
|
||||
|
||||
// Update post's like count
|
||||
await db.update(posts)
|
||||
.set({ likesCount: post.likesCount + 1 })
|
||||
.where(eq(posts.id, postId));
|
||||
|
||||
if (post.userId !== user.id) {
|
||||
await db.insert(notifications).values({
|
||||
userId: post.userId,
|
||||
actorId: user.id,
|
||||
postId,
|
||||
type: 'like',
|
||||
});
|
||||
}
|
||||
|
||||
// SWARM-FIRST: Check if this is a swarm post and deliver directly
|
||||
if (isSwarmPost(post.apId)) {
|
||||
const targetDomain = extractSwarmDomain(post.apId);
|
||||
const originalPostId = extractSwarmPostId(post.apId!);
|
||||
|
||||
if (targetDomain && originalPostId) {
|
||||
(async () => {
|
||||
try {
|
||||
const { deliverSwarmLike } = await import('@/lib/swarm/interactions');
|
||||
|
||||
const result = await deliverSwarmLike(targetDomain, {
|
||||
postId: originalPostId,
|
||||
like: {
|
||||
actorHandle: user.handle,
|
||||
actorDisplayName: user.displayName || user.handle,
|
||||
actorAvatarUrl: user.avatarUrl || undefined,
|
||||
actorNodeDomain: nodeDomain,
|
||||
interactionId: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`);
|
||||
} else {
|
||||
console.warn(`[Swarm] Like delivery failed: ${result.error}`);
|
||||
// Could fall back to ActivityPub here if needed
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Error delivering like:', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} else if (post.apId) {
|
||||
// FALLBACK: Use ActivityPub for non-swarm posts
|
||||
(async () => {
|
||||
try {
|
||||
const { createLikeActivity } = await import('@/lib/activitypub/activities');
|
||||
const { deliverActivity } = await import('@/lib/activitypub/outbox');
|
||||
|
||||
// Get the post author's actor URL
|
||||
const postWithAuthor = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, postId),
|
||||
with: { author: true },
|
||||
});
|
||||
|
||||
if (!postWithAuthor?.author) return;
|
||||
|
||||
const author = postWithAuthor.author as { handle: string };
|
||||
console.log(`[Federation] Like activity for post ${post.apId} from @${user.handle}`);
|
||||
} catch (err) {
|
||||
console.error('[Federation] Error federating like:', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, liked: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Authentication required') {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.json({ error: 'Failed to like post' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Unlike a post
|
||||
export async function DELETE(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const user = await requireAuth();
|
||||
const { id: postId } = await context.params;
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
|
||||
if (user.isSuspended || user.isSilenced) {
|
||||
return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if post exists
|
||||
const post = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, postId),
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
if (post.isRemoved) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Find the like
|
||||
const existingLike = await db.query.likes.findFirst({
|
||||
where: and(
|
||||
eq(likes.userId, user.id),
|
||||
eq(likes.postId, postId)
|
||||
),
|
||||
});
|
||||
|
||||
if (!existingLike) {
|
||||
return NextResponse.json({ error: 'Not liked' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Remove like
|
||||
await db.delete(likes).where(eq(likes.id, existingLike.id));
|
||||
|
||||
// Update post's like count
|
||||
await db.update(posts)
|
||||
.set({ likesCount: Math.max(0, post.likesCount - 1) })
|
||||
.where(eq(posts.id, postId));
|
||||
|
||||
// SWARM-FIRST: Deliver unlike to swarm node
|
||||
if (isSwarmPost(post.apId)) {
|
||||
const targetDomain = extractSwarmDomain(post.apId);
|
||||
const originalPostId = extractSwarmPostId(post.apId!);
|
||||
|
||||
if (targetDomain && originalPostId) {
|
||||
(async () => {
|
||||
try {
|
||||
const { deliverSwarmUnlike } = await import('@/lib/swarm/interactions');
|
||||
|
||||
const result = await deliverSwarmUnlike(targetDomain, {
|
||||
postId: originalPostId,
|
||||
unlike: {
|
||||
actorHandle: user.handle,
|
||||
actorNodeDomain: nodeDomain,
|
||||
interactionId: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[Swarm] Unlike delivered to ${targetDomain}`);
|
||||
} else {
|
||||
console.warn(`[Swarm] Unlike delivery failed: ${result.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Error delivering unlike:', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, liked: false });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Authentication required') {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.json({ error: 'Failed to unlike post' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, posts, users, notifications } from '@/db';
|
||||
import { requireAuth } from '@/lib/auth';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import crypto from 'crypto';
|
||||
|
||||
type RouteContext = { params: Promise<{ id: string }> };
|
||||
|
||||
/**
|
||||
* Extract domain from a swarm post ID (swarm:domain:postId)
|
||||
*/
|
||||
function extractSwarmDomain(apId: string | null): string | null {
|
||||
if (!apId?.startsWith('swarm:')) return null;
|
||||
const parts = apId.split(':');
|
||||
return parts.length >= 2 ? parts[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post is from a swarm node
|
||||
*/
|
||||
function isSwarmPost(apId: string | null): boolean {
|
||||
return apId?.startsWith('swarm:') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the original post ID from a swarm apId
|
||||
*/
|
||||
function extractSwarmPostId(apId: string): string | null {
|
||||
const parts = apId.split(':');
|
||||
return parts.length >= 3 ? parts[2] : null;
|
||||
}
|
||||
|
||||
// Repost a post
|
||||
export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const user = await requireAuth();
|
||||
const { id: postId } = await context.params;
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
|
||||
if (user.isSuspended || user.isSilenced) {
|
||||
return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if post exists
|
||||
const originalPost = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, postId),
|
||||
});
|
||||
|
||||
if (!originalPost) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
if (originalPost.isRemoved) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if already reposted by this user
|
||||
const existingRepost = await db.query.posts.findFirst({
|
||||
where: and(
|
||||
eq(posts.userId, user.id),
|
||||
eq(posts.repostOfId, postId),
|
||||
eq(posts.isRemoved, false)
|
||||
),
|
||||
});
|
||||
|
||||
if (existingRepost) {
|
||||
return NextResponse.json({ error: 'Already reposted' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Create repost
|
||||
const repostId = crypto.randomUUID();
|
||||
const [repost] = await db.insert(posts).values({
|
||||
userId: user.id,
|
||||
content: '', // Reposts don't have their own content
|
||||
repostOfId: postId,
|
||||
apId: `https://${nodeDomain}/posts/${repostId}`,
|
||||
apUrl: `https://${nodeDomain}/posts/${repostId}`,
|
||||
}).returning();
|
||||
|
||||
// Update original post's repost count
|
||||
await db.update(posts)
|
||||
.set({ repostsCount: originalPost.repostsCount + 1 })
|
||||
.where(eq(posts.id, postId));
|
||||
|
||||
// Update user's post count
|
||||
await db.update(users)
|
||||
.set({ postsCount: user.postsCount + 1 })
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
if (originalPost.userId !== user.id) {
|
||||
await db.insert(notifications).values({
|
||||
userId: originalPost.userId,
|
||||
actorId: user.id,
|
||||
postId,
|
||||
type: 'repost',
|
||||
});
|
||||
}
|
||||
|
||||
// SWARM-FIRST: Deliver repost to swarm node
|
||||
if (isSwarmPost(originalPost.apId)) {
|
||||
const targetDomain = extractSwarmDomain(originalPost.apId);
|
||||
const originalPostIdOnRemote = extractSwarmPostId(originalPost.apId!);
|
||||
|
||||
if (targetDomain && originalPostIdOnRemote) {
|
||||
(async () => {
|
||||
try {
|
||||
const { deliverSwarmRepost } = await import('@/lib/swarm/interactions');
|
||||
|
||||
const result = await deliverSwarmRepost(targetDomain, {
|
||||
postId: originalPostIdOnRemote,
|
||||
repost: {
|
||||
actorHandle: user.handle,
|
||||
actorDisplayName: user.displayName || user.handle,
|
||||
actorAvatarUrl: user.avatarUrl || undefined,
|
||||
actorNodeDomain: nodeDomain,
|
||||
repostId: repost.id,
|
||||
interactionId: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[Swarm] Repost delivered to ${targetDomain}`);
|
||||
} else {
|
||||
console.warn(`[Swarm] Repost delivery failed: ${result.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Error delivering repost:', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} else if (originalPost.apId) {
|
||||
// FALLBACK: Use ActivityPub for non-swarm posts
|
||||
(async () => {
|
||||
try {
|
||||
const { createAnnounceActivity } = await import('@/lib/activitypub/activities');
|
||||
const { getFollowerInboxes, deliverToFollowers } = await import('@/lib/activitypub/outbox');
|
||||
|
||||
// Send Announce to our followers
|
||||
const followerInboxes = await getFollowerInboxes(user.id);
|
||||
if (followerInboxes.length > 0) {
|
||||
const announceActivity = createAnnounceActivity(
|
||||
user,
|
||||
originalPost.apId!,
|
||||
nodeDomain,
|
||||
repost.id
|
||||
);
|
||||
|
||||
const privateKey = user.privateKeyEncrypted;
|
||||
if (privateKey) {
|
||||
const keyId = `https://${nodeDomain}/users/${user.handle}#main-key`;
|
||||
const result = await deliverToFollowers(announceActivity, followerInboxes, privateKey, keyId);
|
||||
console.log(`[Federation] Announce for ${originalPost.apId} delivered to ${result.delivered}/${followerInboxes.length} inboxes`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Federation] Error federating repost:', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, repost, reposted: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Authentication required') {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.json({ error: 'Failed to repost' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Unrepost a post
|
||||
export async function DELETE(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const user = await requireAuth();
|
||||
const { id: postId } = await context.params;
|
||||
|
||||
if (user.isSuspended || user.isSilenced) {
|
||||
return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if original post exists
|
||||
const originalPost = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, postId),
|
||||
});
|
||||
|
||||
// Find the repost by this user
|
||||
const repost = await db.query.posts.findFirst({
|
||||
where: and(
|
||||
eq(posts.userId, user.id),
|
||||
eq(posts.repostOfId, postId),
|
||||
eq(posts.isRemoved, false)
|
||||
),
|
||||
});
|
||||
|
||||
if (!repost) {
|
||||
return NextResponse.json({ error: 'Not reposted' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Mark repost as removed
|
||||
await db.update(posts)
|
||||
.set({ isRemoved: true })
|
||||
.where(eq(posts.id, repost.id));
|
||||
|
||||
// Update original post's repost count
|
||||
if (originalPost) {
|
||||
await db.update(posts)
|
||||
.set({ repostsCount: Math.max(0, originalPost.repostsCount - 1) })
|
||||
.where(eq(posts.id, postId));
|
||||
}
|
||||
|
||||
// Update user's post count
|
||||
await db.update(users)
|
||||
.set({ postsCount: Math.max(0, user.postsCount - 1) })
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
return NextResponse.json({ success: true, reposted: false });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Authentication required') {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.json({ error: 'Failed to unrepost' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, posts, users, media, remotePosts } from '@/db';
|
||||
import { eq, desc, and } from 'drizzle-orm';
|
||||
import { fetchRemotePost } from '@/lib/activitypub/fetchRemotePost';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
|
||||
let mainPost: any = null;
|
||||
let replyPosts: any[] = [];
|
||||
|
||||
const post = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, id),
|
||||
with: {
|
||||
author: true,
|
||||
media: true,
|
||||
replyTo: {
|
||||
with: { author: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (post) {
|
||||
mainPost = post;
|
||||
|
||||
const replies = await db.query.posts.findMany({
|
||||
where: and(
|
||||
eq(posts.replyToId, id),
|
||||
eq(posts.isRemoved, false)
|
||||
),
|
||||
with: {
|
||||
author: true,
|
||||
media: true,
|
||||
},
|
||||
orderBy: [desc(posts.createdAt)],
|
||||
});
|
||||
|
||||
let allPostIds = [post.id, ...replies.map(r => r.id)];
|
||||
|
||||
try {
|
||||
const { requireAuth } = await import('@/lib/auth');
|
||||
const { likes } = await import('@/db');
|
||||
const { inArray } = await import('drizzle-orm');
|
||||
|
||||
const viewer = await requireAuth();
|
||||
allPostIds = [post.id, ...replies.map(r => r.id)];
|
||||
|
||||
if (allPostIds.length > 0) {
|
||||
const viewerLikes = await db.query.likes.findMany({
|
||||
where: and(
|
||||
eq(likes.userId, viewer.id),
|
||||
inArray(likes.postId, allPostIds)
|
||||
),
|
||||
});
|
||||
const likedPostIds = new Set(viewerLikes.map(l => l.postId));
|
||||
|
||||
const viewerReposts = await db.query.posts.findMany({
|
||||
where: and(
|
||||
eq(posts.userId, viewer.id),
|
||||
inArray(posts.repostOfId, allPostIds)
|
||||
),
|
||||
});
|
||||
const repostedPostIds = new Set(viewerReposts.map(r => r.repostOfId));
|
||||
|
||||
mainPost = {
|
||||
...post,
|
||||
isLiked: likedPostIds.has(post.id),
|
||||
isReposted: repostedPostIds.has(post.id),
|
||||
} as any;
|
||||
|
||||
replyPosts = replies.map(r => ({
|
||||
...r,
|
||||
isLiked: likedPostIds.has(r.id),
|
||||
isReposted: repostedPostIds.has(r.id),
|
||||
})) as any[];
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} else {
|
||||
const cached = await db.query.remotePosts.findFirst({
|
||||
where: eq(remotePosts.apId, id),
|
||||
});
|
||||
|
||||
if (cached) {
|
||||
mainPost = {
|
||||
id: cached.id,
|
||||
content: cached.content,
|
||||
createdAt: cached.publishedAt.toISOString(),
|
||||
likesCount: 0,
|
||||
repostsCount: 0,
|
||||
repliesCount: 0,
|
||||
author: {
|
||||
id: cached.authorHandle,
|
||||
handle: cached.authorHandle,
|
||||
displayName: cached.authorDisplayName || cached.authorHandle,
|
||||
avatarUrl: cached.authorAvatarUrl,
|
||||
bio: null,
|
||||
isRemote: true,
|
||||
},
|
||||
media: cached.mediaJson ? JSON.parse(cached.mediaJson) : null,
|
||||
linkPreviewUrl: cached.linkPreviewUrl,
|
||||
linkPreviewTitle: cached.linkPreviewTitle,
|
||||
linkPreviewDescription: cached.linkPreviewDescription,
|
||||
linkPreviewImage: cached.linkPreviewImage,
|
||||
isLiked: false,
|
||||
isReposted: false,
|
||||
};
|
||||
} else {
|
||||
const postUrl = `https://${nodeDomain}/posts/${id}`;
|
||||
const result = await fetchRemotePost(postUrl, nodeDomain);
|
||||
|
||||
if (result.post) {
|
||||
mainPost = result.post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mainPost) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
post: mainPost,
|
||||
replies: replyPosts,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get post detail error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get post detail' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { requireAuth } = await import('@/lib/auth');
|
||||
const { bots } = await import('@/db');
|
||||
const user = await requireAuth();
|
||||
const { id } = await params;
|
||||
|
||||
const post = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, id),
|
||||
with: {
|
||||
bot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Allow deletion if user owns the post OR if user owns the bot that made the post
|
||||
const isPostOwner = post.userId === user.id;
|
||||
const isBotOwner = post.bot && post.bot.ownerId === user.id;
|
||||
|
||||
if (!isPostOwner && !isBotOwner) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. If it's a reply, decrement parent's repliesCount
|
||||
if (post.replyToId) {
|
||||
const parentPost = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, post.replyToId),
|
||||
});
|
||||
if (parentPost && parentPost.repliesCount > 0) {
|
||||
await db.update(posts)
|
||||
.set({ repliesCount: parentPost.repliesCount - 1 })
|
||||
.where(eq(posts.id, post.replyToId));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete the post (cascades to media, likes, notifications)
|
||||
await db.delete(posts).where(eq(posts.id, id));
|
||||
|
||||
// 3. Decrement the post author's postsCount
|
||||
const postAuthor = await db.query.users.findFirst({
|
||||
where: eq(users.id, post.userId),
|
||||
});
|
||||
if (postAuthor && postAuthor.postsCount > 0) {
|
||||
await db.update(users)
|
||||
.set({ postsCount: postAuthor.postsCount - 1 })
|
||||
.where(eq(users.id, post.userId));
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete post error:', error);
|
||||
if (error instanceof Error && error.message === 'Authentication required') {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete post' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user