feat(notifications): Refactor notification system and consolidate interaction handlers
- Remove dedicated bot owner notification utility in favor of unified notification system - Update all interaction endpoints (follow, like, mention, repost) to use consolidated notification flow - Refactor notification route to handle all interaction types through single endpoint - Update posts endpoints to trigger notifications through unified system - Add database migration snapshot for notification schema changes - Simplify notification logic by removing redundant bot owner notification module - Improve notification consistency across all user interactions and post operations
This commit is contained in:
@@ -53,7 +53,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Construct the remote follower's actor URL (swarm-style)
|
||||
const remoteHandle = `${data.follow.followerHandle}@${data.follow.followerNodeDomain}`;
|
||||
const actorUrl = `swarm://${data.follow.followerNodeDomain}/${data.follow.followerHandle}`;
|
||||
const inboxUrl = `https://${data.follow.followerNodeDomain}/api/swarm/interactions/inbox`;
|
||||
|
||||
@@ -77,7 +76,7 @@ export async function POST(request: NextRequest) {
|
||||
userId: targetUser.id,
|
||||
actorUrl,
|
||||
inboxUrl,
|
||||
handle: remoteHandle,
|
||||
handle: `${data.follow.followerHandle}@${data.follow.followerNodeDomain}`,
|
||||
activityId: data.follow.interactionId,
|
||||
});
|
||||
|
||||
@@ -86,40 +85,38 @@ export async function POST(request: NextRequest) {
|
||||
.set({ followersCount: targetUser.followersCount + 1 })
|
||||
.where(eq(users.id, targetUser.id));
|
||||
|
||||
// Get or create placeholder user for the remote follower (for notifications)
|
||||
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.follow.followerNodeDomain}:${data.follow.followerHandle}`,
|
||||
handle: remoteHandle,
|
||||
displayName: data.follow.followerDisplayName,
|
||||
avatarUrl: data.follow.followerAvatarUrl || null,
|
||||
bio: data.follow.followerBio || null,
|
||||
publicKey: 'swarm-remote-user',
|
||||
}).returning();
|
||||
remoteUser = newUser;
|
||||
}
|
||||
|
||||
// Create notification
|
||||
// Create notification with actor info stored directly
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: targetUser.id,
|
||||
actorId: remoteUser.id,
|
||||
actorHandle: data.follow.followerHandle,
|
||||
actorDisplayName: data.follow.followerDisplayName,
|
||||
actorAvatarUrl: data.follow.followerAvatarUrl || null,
|
||||
actorNodeDomain: data.follow.followerNodeDomain,
|
||||
type: 'follow',
|
||||
});
|
||||
console.log(`[Swarm] Created follow notification for @${data.targetHandle} from ${remoteHandle}`);
|
||||
console.log(`[Swarm] Created follow notification for @${data.targetHandle} from ${data.follow.followerHandle}@${data.follow.followerNodeDomain}`);
|
||||
} catch (notifError) {
|
||||
console.error(`[Swarm] Failed to create notification:`, notifError);
|
||||
}
|
||||
|
||||
// Also notify bot owner if this is a bot being followed
|
||||
const { notifyBotOwnerForFollow } = await import('@/lib/notifications/botOwnerNotify');
|
||||
await notifyBotOwnerForFollow(targetUser.id, remoteUser.id);
|
||||
if (targetUser.isBot && targetUser.botOwnerId) {
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: targetUser.botOwnerId,
|
||||
actorHandle: data.follow.followerHandle,
|
||||
actorDisplayName: data.follow.followerDisplayName,
|
||||
actorAvatarUrl: data.follow.followerAvatarUrl || null,
|
||||
actorNodeDomain: data.follow.followerNodeDomain,
|
||||
type: 'follow',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Failed to notify bot owner:', err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Swarm] Received follow from ${remoteHandle} for @${data.targetHandle}`);
|
||||
console.log(`[Swarm] Received follow from ${data.follow.followerHandle}@${data.follow.followerNodeDomain} for @${data.targetHandle}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
* Swarm Like Endpoint
|
||||
*
|
||||
* POST: Receive a like from another swarm node
|
||||
*
|
||||
* This is the swarm-first approach - direct node-to-node communication
|
||||
* for likes, bypassing ActivityPub for Synapsis nodes.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
@@ -57,43 +54,43 @@ export async function POST(request: NextRequest) {
|
||||
.set({ likesCount: post.likesCount + 1 })
|
||||
.where(eq(posts.id, data.postId));
|
||||
|
||||
// Create a notification for the post author
|
||||
// First, get or create a placeholder user for the remote liker
|
||||
const remoteHandle = `${data.like.actorHandle}@${data.like.actorNodeDomain}`;
|
||||
let remoteUser = await db.query.users.findFirst({
|
||||
where: eq(users.handle, remoteHandle),
|
||||
});
|
||||
|
||||
if (!remoteUser) {
|
||||
// Create a placeholder user for the remote actor
|
||||
const [newUser] = await db.insert(users).values({
|
||||
did: `did:swarm:${data.like.actorNodeDomain}:${data.like.actorHandle}`,
|
||||
handle: remoteHandle,
|
||||
displayName: data.like.actorDisplayName,
|
||||
avatarUrl: data.like.actorAvatarUrl || null,
|
||||
publicKey: 'swarm-remote-user',
|
||||
}).returning();
|
||||
remoteUser = newUser;
|
||||
}
|
||||
|
||||
// Create notification
|
||||
// Create notification with actor info stored directly
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: post.userId,
|
||||
actorId: remoteUser.id,
|
||||
actorHandle: data.like.actorHandle,
|
||||
actorDisplayName: data.like.actorDisplayName,
|
||||
actorAvatarUrl: data.like.actorAvatarUrl || null,
|
||||
actorNodeDomain: data.like.actorNodeDomain,
|
||||
postId: data.postId,
|
||||
postContent: post.content?.slice(0, 200) || null,
|
||||
type: 'like',
|
||||
});
|
||||
console.log(`[Swarm] Created like notification for post ${data.postId} from ${remoteHandle}`);
|
||||
console.log(`[Swarm] Created like notification for post ${data.postId} from ${data.like.actorHandle}@${data.like.actorNodeDomain}`);
|
||||
} catch (notifError) {
|
||||
console.error(`[Swarm] Failed to create like notification:`, notifError);
|
||||
}
|
||||
|
||||
// Also notify bot owner if this is a bot's post
|
||||
const { notifyBotOwnerForPost } = await import('@/lib/notifications/botOwnerNotify');
|
||||
await notifyBotOwnerForPost(post.userId, remoteUser.id, 'like', data.postId);
|
||||
const author = post.author as { isBot?: boolean; botOwnerId?: string } | null;
|
||||
if (author?.isBot && author.botOwnerId) {
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: author.botOwnerId,
|
||||
actorHandle: data.like.actorHandle,
|
||||
actorDisplayName: data.like.actorDisplayName,
|
||||
actorAvatarUrl: data.like.actorAvatarUrl || null,
|
||||
actorNodeDomain: data.like.actorNodeDomain,
|
||||
postId: data.postId,
|
||||
postContent: post.content?.slice(0, 200) || null,
|
||||
type: 'like',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Failed to notify bot owner:', err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Swarm] Received like from ${remoteHandle} on post ${data.postId}`);
|
||||
console.log(`[Swarm] Received like from ${data.like.actorHandle}@${data.like.actorNodeDomain} on post ${data.postId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, users, notifications, posts } from '@/db';
|
||||
import { db, users, notifications } from '@/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -50,55 +50,40 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get or create placeholder user for the remote actor
|
||||
const remoteHandle = `${data.mention.actorHandle}@${data.mention.actorNodeDomain}`;
|
||||
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.mention.actorNodeDomain}:${data.mention.actorHandle}`,
|
||||
handle: remoteHandle,
|
||||
displayName: data.mention.actorDisplayName,
|
||||
avatarUrl: data.mention.actorAvatarUrl || null,
|
||||
publicKey: 'swarm-remote-user',
|
||||
}).returning();
|
||||
remoteUser = newUser;
|
||||
// Create notification with actor info stored directly
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: mentionedUser.id,
|
||||
actorHandle: data.mention.actorHandle,
|
||||
actorDisplayName: data.mention.actorDisplayName,
|
||||
actorAvatarUrl: data.mention.actorAvatarUrl || null,
|
||||
actorNodeDomain: data.mention.actorNodeDomain,
|
||||
postContent: data.mention.postContent.slice(0, 200),
|
||||
type: 'mention',
|
||||
});
|
||||
console.log(`[Swarm] Created mention notification for @${data.mentionedHandle} from ${data.mention.actorHandle}@${data.mention.actorNodeDomain}`);
|
||||
} catch (notifError) {
|
||||
console.error(`[Swarm] Failed to create mention notification:`, notifError);
|
||||
}
|
||||
|
||||
// Check if we already have this post cached (from swarm timeline)
|
||||
// If not, create a placeholder post for the notification
|
||||
const swarmPostId = `swarm:${data.mention.actorNodeDomain}:${data.mention.postId}`;
|
||||
let post = await db.query.posts.findFirst({
|
||||
where: eq(posts.apId, swarmPostId),
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
// Create a placeholder post for the mention
|
||||
const [newPost] = await db.insert(posts).values({
|
||||
userId: remoteUser.id,
|
||||
content: data.mention.postContent,
|
||||
apId: swarmPostId,
|
||||
apUrl: `https://${data.mention.actorNodeDomain}/${data.mention.actorHandle}/posts/${data.mention.postId}`,
|
||||
createdAt: new Date(data.mention.timestamp),
|
||||
}).returning();
|
||||
post = newPost;
|
||||
}
|
||||
|
||||
// Create notification
|
||||
await db.insert(notifications).values({
|
||||
userId: mentionedUser.id,
|
||||
actorId: remoteUser.id,
|
||||
postId: post.id,
|
||||
type: 'mention',
|
||||
});
|
||||
|
||||
// Also notify bot owner if this is a bot being mentioned
|
||||
const { notifyBotOwnerForPost } = await import('@/lib/notifications/botOwnerNotify');
|
||||
await notifyBotOwnerForPost(mentionedUser.id, remoteUser.id, 'mention', post.id);
|
||||
if (mentionedUser.isBot && mentionedUser.botOwnerId) {
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: mentionedUser.botOwnerId,
|
||||
actorHandle: data.mention.actorHandle,
|
||||
actorDisplayName: data.mention.actorDisplayName,
|
||||
actorAvatarUrl: data.mention.actorAvatarUrl || null,
|
||||
actorNodeDomain: data.mention.actorNodeDomain,
|
||||
postContent: data.mention.postContent.slice(0, 200),
|
||||
type: 'mention',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Failed to notify bot owner:', err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Swarm] Received mention from ${remoteHandle} for @${data.mentionedHandle}`);
|
||||
console.log(`[Swarm] Received mention from ${data.mention.actorHandle}@${data.mention.actorNodeDomain} for @${data.mentionedHandle}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -16,7 +16,7 @@ const swarmRepostSchema = z.object({
|
||||
actorDisplayName: z.string(),
|
||||
actorAvatarUrl: z.string().optional(),
|
||||
actorNodeDomain: z.string(),
|
||||
repostId: z.string(), // The ID of the repost on the actor's node
|
||||
repostId: z.string(),
|
||||
interactionId: z.string(),
|
||||
timestamp: z.string(),
|
||||
}),
|
||||
@@ -39,6 +39,7 @@ export async function POST(request: NextRequest) {
|
||||
// Find the target post
|
||||
const post = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, data.postId),
|
||||
with: { author: true },
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
@@ -54,41 +55,43 @@ export async function POST(request: NextRequest) {
|
||||
.set({ repostsCount: post.repostsCount + 1 })
|
||||
.where(eq(posts.id, data.postId));
|
||||
|
||||
// Get or create placeholder user for the remote reposter
|
||||
const remoteHandle = `${data.repost.actorHandle}@${data.repost.actorNodeDomain}`;
|
||||
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.repost.actorNodeDomain}:${data.repost.actorHandle}`,
|
||||
handle: remoteHandle,
|
||||
displayName: data.repost.actorDisplayName,
|
||||
avatarUrl: data.repost.actorAvatarUrl || null,
|
||||
publicKey: 'swarm-remote-user',
|
||||
}).returning();
|
||||
remoteUser = newUser;
|
||||
}
|
||||
|
||||
// Create notification
|
||||
// Create notification with actor info stored directly
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: post.userId,
|
||||
actorId: remoteUser.id,
|
||||
actorHandle: data.repost.actorHandle,
|
||||
actorDisplayName: data.repost.actorDisplayName,
|
||||
actorAvatarUrl: data.repost.actorAvatarUrl || null,
|
||||
actorNodeDomain: data.repost.actorNodeDomain,
|
||||
postId: data.postId,
|
||||
postContent: post.content?.slice(0, 200) || null,
|
||||
type: 'repost',
|
||||
});
|
||||
console.log(`[Swarm] Created repost notification for post ${data.postId} from ${remoteHandle}`);
|
||||
console.log(`[Swarm] Created repost notification for post ${data.postId} from ${data.repost.actorHandle}@${data.repost.actorNodeDomain}`);
|
||||
} catch (notifError) {
|
||||
console.error(`[Swarm] Failed to create repost notification:`, notifError);
|
||||
}
|
||||
|
||||
// Also notify bot owner if this is a bot's post
|
||||
const { notifyBotOwnerForPost } = await import('@/lib/notifications/botOwnerNotify');
|
||||
await notifyBotOwnerForPost(post.userId, remoteUser.id, 'repost', data.postId);
|
||||
const author = post.author as { isBot?: boolean; botOwnerId?: string } | null;
|
||||
if (author?.isBot && author.botOwnerId) {
|
||||
try {
|
||||
await db.insert(notifications).values({
|
||||
userId: author.botOwnerId,
|
||||
actorHandle: data.repost.actorHandle,
|
||||
actorDisplayName: data.repost.actorDisplayName,
|
||||
actorAvatarUrl: data.repost.actorAvatarUrl || null,
|
||||
actorNodeDomain: data.repost.actorNodeDomain,
|
||||
postId: data.postId,
|
||||
postContent: post.content?.slice(0, 200) || null,
|
||||
type: 'repost',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Swarm] Failed to notify bot owner:', err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Swarm] Received repost from ${remoteHandle} on post ${data.postId}`);
|
||||
console.log(`[Swarm] Received repost from ${data.repost.actorHandle}@${data.repost.actorNodeDomain} on post ${data.postId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user