From 1c235e202783fabfc77dd1e15e1797f5724172b5 Mon Sep 17 00:00:00 2001 From: Christomatt Date: Mon, 26 Jan 2026 10:57:32 +0100 Subject: [PATCH] feat(notifications): Add bot owner notifications for interactions - Create new botOwnerNotify module with notifyBotOwnerForPost and notifyBotOwnerForFollow functions - Add bot owner notifications to local post interactions (likes, reposts) - Add bot owner notifications to swarm post interactions (likes, reposts, mentions) - Add bot owner notifications to follow interactions (local and swarm) - Implement mention notification creation for local mentions in post creation - Notify bot owners when their bots receive interactions from other users - Ensure bot owners are alerted to engagement on their bot accounts across federated network --- src/app/api/posts/[id]/like/route.ts | 4 + src/app/api/posts/[id]/repost/route.ts | 4 + src/app/api/posts/route.ts | 38 +++++- .../api/swarm/interactions/follow/route.ts | 4 + src/app/api/swarm/interactions/like/route.ts | 4 + .../api/swarm/interactions/mention/route.ts | 4 + .../api/swarm/interactions/repost/route.ts | 4 + src/app/api/users/[handle]/follow/route.ts | 4 + src/lib/notifications/botOwnerNotify.ts | 112 ++++++++++++++++++ 9 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/lib/notifications/botOwnerNotify.ts diff --git a/src/app/api/posts/[id]/like/route.ts b/src/app/api/posts/[id]/like/route.ts index 49f7d95..bb4bd66 100644 --- a/src/app/api/posts/[id]/like/route.ts +++ b/src/app/api/posts/[id]/like/route.ts @@ -83,6 +83,10 @@ export async function POST(request: Request, context: RouteContext) { postId, type: 'like', }); + + // Also notify bot owner if this is a bot's post + const { notifyBotOwnerForPost } = await import('@/lib/notifications/botOwnerNotify'); + await notifyBotOwnerForPost(post.userId, user.id, 'like', postId); } // SWARM-FIRST: Check if this is a swarm post and deliver directly diff --git a/src/app/api/posts/[id]/repost/route.ts b/src/app/api/posts/[id]/repost/route.ts index 1f1cbe1..2fb79b4 100644 --- a/src/app/api/posts/[id]/repost/route.ts +++ b/src/app/api/posts/[id]/repost/route.ts @@ -93,6 +93,10 @@ export async function POST(request: Request, context: RouteContext) { postId, type: 'repost', }); + + // Also notify bot owner if this is a bot's post + const { notifyBotOwnerForPost } = await import('@/lib/notifications/botOwnerNotify'); + await notifyBotOwnerForPost(originalPost.userId, user.id, 'repost', postId); } // SWARM-FIRST: Deliver repost to swarm node diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index a7e32f7..5a01486 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { db, posts, users, media, follows, mutes, blocks, likes, remoteFollows, remotePosts } from '@/db'; +import { db, posts, users, media, follows, mutes, blocks, likes, remoteFollows, remotePosts, notifications } from '@/db'; import { requireAuth } from '@/lib/auth'; import { eq, desc, and, inArray, isNull, notInArray, or } from 'drizzle-orm'; import type { SQL } from 'drizzle-orm'; @@ -134,6 +134,42 @@ export async function POST(request: Request) { })(); } + // Handle local mentions (create notifications for users on this node) + (async () => { + try { + const { extractMentions } = await import('@/lib/swarm/interactions'); + const { notifications } = await import('@/db'); + + const mentions = extractMentions(data.content); + + for (const mention of mentions) { + // Only handle local mentions (no domain) + if (mention.domain) continue; + + // Find the mentioned user + const mentionedUser = await db.query.users.findFirst({ + where: eq(users.handle, mention.handle.toLowerCase()), + }); + + if (mentionedUser && mentionedUser.id !== user.id && !mentionedUser.isSuspended) { + // Create notification for the mentioned user + await db.insert(notifications).values({ + userId: mentionedUser.id, + actorId: user.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, user.id, 'mention', post.id); + } + } + } catch (err) { + console.error('[Local] Error creating mention notifications:', err); + } + })(); + // SWARM-FIRST: Deliver mentions to swarm nodes (async () => { try { diff --git a/src/app/api/swarm/interactions/follow/route.ts b/src/app/api/swarm/interactions/follow/route.ts index 6459f56..efb746f 100644 --- a/src/app/api/swarm/interactions/follow/route.ts +++ b/src/app/api/swarm/interactions/follow/route.ts @@ -110,6 +110,10 @@ export async function POST(request: NextRequest) { type: 'follow', }); + // Also notify bot owner if this is a bot being followed + const { notifyBotOwnerForFollow } = await import('@/lib/notifications/botOwnerNotify'); + await notifyBotOwnerForFollow(targetUser.id, remoteUser.id); + console.log(`[Swarm] Received follow from ${remoteHandle} for @${data.targetHandle}`); return NextResponse.json({ diff --git a/src/app/api/swarm/interactions/like/route.ts b/src/app/api/swarm/interactions/like/route.ts index 41bc1e1..ed2d317 100644 --- a/src/app/api/swarm/interactions/like/route.ts +++ b/src/app/api/swarm/interactions/like/route.ts @@ -84,6 +84,10 @@ export async function POST(request: NextRequest) { type: 'like', }); + // 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); + console.log(`[Swarm] Received like from ${remoteHandle} on post ${data.postId}`); return NextResponse.json({ diff --git a/src/app/api/swarm/interactions/mention/route.ts b/src/app/api/swarm/interactions/mention/route.ts index e69e591..19148e3 100644 --- a/src/app/api/swarm/interactions/mention/route.ts +++ b/src/app/api/swarm/interactions/mention/route.ts @@ -94,6 +94,10 @@ export async function POST(request: NextRequest) { 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); + console.log(`[Swarm] Received mention from ${remoteHandle} for @${data.mentionedHandle}`); return NextResponse.json({ diff --git a/src/app/api/swarm/interactions/repost/route.ts b/src/app/api/swarm/interactions/repost/route.ts index 15a3e38..be330f5 100644 --- a/src/app/api/swarm/interactions/repost/route.ts +++ b/src/app/api/swarm/interactions/repost/route.ts @@ -79,6 +79,10 @@ export async function POST(request: NextRequest) { type: 'repost', }); + // 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); + console.log(`[Swarm] Received repost from ${remoteHandle} on post ${data.postId}`); return NextResponse.json({ diff --git a/src/app/api/users/[handle]/follow/route.ts b/src/app/api/users/[handle]/follow/route.ts index 1705e6d..594e727 100644 --- a/src/app/api/users/[handle]/follow/route.ts +++ b/src/app/api/users/[handle]/follow/route.ts @@ -268,6 +268,10 @@ export async function POST(request: Request, context: RouteContext) { actorId: currentUser.id, type: 'follow', }); + + // Also notify bot owner if this is a bot being followed + const { notifyBotOwnerForFollow } = await import('@/lib/notifications/botOwnerNotify'); + await notifyBotOwnerForFollow(targetUser.id, currentUser.id); } // Update counts diff --git a/src/lib/notifications/botOwnerNotify.ts b/src/lib/notifications/botOwnerNotify.ts new file mode 100644 index 0000000..2d5101c --- /dev/null +++ b/src/lib/notifications/botOwnerNotify.ts @@ -0,0 +1,112 @@ +/** + * Bot Owner Notification Helper + * + * When someone interacts with a bot (likes, reposts, follows, mentions), + * this helper creates a notification for the bot's owner so they can + * see engagement on their bots. + */ + +import { db, notifications, users } from '@/db'; +import { eq } from 'drizzle-orm'; + +export type BotInteractionType = 'like' | 'repost' | 'follow' | 'mention'; + +/** + * Check if a user is a bot and get their owner's ID. + * + * @param userId - The user ID to check + * @returns The bot owner's ID if this is a bot, null otherwise + */ +export async function getBotOwnerId(userId: string): Promise { + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + columns: { + isBot: true, + botOwnerId: true, + }, + }); + + if (user?.isBot && user.botOwnerId) { + return user.botOwnerId; + } + + return null; +} + +/** + * Create a notification for a bot's owner when someone interacts with the bot. + * + * This is called in addition to the normal notification (which goes to the bot's + * user account). The owner gets notified so they can see engagement on their bots. + * + * @param botUserId - The bot's user ID (the one receiving the interaction) + * @param actorId - The user who performed the interaction + * @param type - The type of interaction + * @param postId - Optional post ID (for likes, reposts, mentions) + * @returns True if a notification was created, false otherwise + */ +export async function notifyBotOwner( + botUserId: string, + actorId: string, + type: BotInteractionType, + postId?: string +): Promise { + try { + const ownerId = await getBotOwnerId(botUserId); + + if (!ownerId) { + // Not a bot, no owner to notify + return false; + } + + // Don't notify owner if they're the one doing the interaction + if (ownerId === actorId) { + return false; + } + + // Create notification for the bot owner + await db.insert(notifications).values({ + userId: ownerId, + actorId, + postId: postId || null, + type, + }); + + return true; + } catch (error) { + console.error('[BotOwnerNotify] Error creating notification:', error); + return false; + } +} + +/** + * Notify bot owner about an interaction on a bot's post. + * + * Checks if the post author is a bot and notifies the owner. + * + * @param postAuthorId - The post author's user ID + * @param actorId - The user who performed the interaction + * @param type - The type of interaction (like, repost, mention) + * @param postId - The post ID + */ +export async function notifyBotOwnerForPost( + postAuthorId: string, + actorId: string, + type: 'like' | 'repost' | 'mention', + postId: string +): Promise { + return notifyBotOwner(postAuthorId, actorId, type, postId); +} + +/** + * Notify bot owner about a new follower. + * + * @param botUserId - The bot's user ID being followed + * @param followerId - The user who followed the bot + */ +export async function notifyBotOwnerForFollow( + botUserId: string, + followerId: string +): Promise { + return notifyBotOwner(botUserId, followerId, 'follow'); +}