/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import type { Activity, ActivityFeedResponse, ActivityUser, SuggestionActivity, LikeActivity, CommentActivity, AchievementActivity, } from "@library/shared-types"; import { ACHIEVEMENTS, ActivityType } from "@library/shared-types"; import { prisma } from "../lib/prisma"; export class ActivityService { private prisma = prisma; constructor() {} /** * Get activity feed with pagination. */ async getActivityFeed( limit = 50, offset = 0, userId?: string ): Promise { // Fetch suggestions, likes, comments, and achievements const [suggestions, likes, comments, achievements] = await Promise.all([ this.getSuggestionActivities(limit, offset, userId), this.getLikeActivities(limit, offset, userId), this.getCommentActivities(limit, offset, userId), this.getAchievementActivities(limit, offset, userId), ]); // Combine and sort by createdAt const activities: Activity[] = [ ...suggestions, ...likes, ...comments, ...achievements, ].sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); }); // Apply pagination to combined results const paginatedActivities = activities.slice(offset, offset + limit); const hasMore = activities.length > offset + limit; return { activities: paginatedActivities, total: activities.length, hasMore, }; } /** * Get suggestion activities. */ private async getSuggestionActivities( limit: number, offset: number, userId?: string ): Promise { const where = userId ? { userId, user: { profilePublic: true, isBanned: false } } : { user: { profilePublic: true, isBanned: false } }; const suggestions = await this.prisma.suggestion.findMany({ where, include: { user: { select: { id: true, username: true, slug: true, avatar: true, primaryBadge: true, isVip: true, isMod: true, isStaff: true, }, }, }, orderBy: { createdAt: "desc" }, take: limit * 2, // Get more since we're combining }); return suggestions.map((suggestion) => ({ id: `suggestion-${suggestion.id}`, type: ActivityType.suggestion, user: suggestion.user as ActivityUser, entityType: suggestion.entityType, suggestionTitle: suggestion.title, status: suggestion.status, createdAt: suggestion.createdAt, })); } /** * Get like activities. */ private async getLikeActivities( limit: number, offset: number, userId?: string ): Promise { const where = userId ? { userId, user: { profilePublic: true, isBanned: false } } : { user: { profilePublic: true, isBanned: false } }; const likes = await this.prisma.like.findMany({ where, include: { user: { select: { id: true, username: true, slug: true, avatar: true, primaryBadge: true, isVip: true, isMod: true, isStaff: true, }, }, }, orderBy: { createdAt: "desc" }, take: limit * 2, }); // For each like, fetch the entity title const likesWithTitles = await Promise.all( likes.map(async (like): Promise => { const entityTitle = await this.getEntityTitle( like.entityType, like.entityId ); return { id: `like-${like.id}`, type: ActivityType.like, user: like.user as ActivityUser, entityType: like.entityType, entityId: like.entityId, entityTitle, createdAt: like.createdAt, }; }) ); return likesWithTitles; } /** * Get comment activities. */ private async getCommentActivities( limit: number, offset: number, userId?: string ): Promise { const where = userId ? { userId, user: { profilePublic: true, isBanned: false } } : { user: { profilePublic: true, isBanned: false } }; const comments = await this.prisma.comment.findMany({ where, include: { user: { select: { id: true, username: true, slug: true, avatar: true, primaryBadge: true, isVip: true, isMod: true, isStaff: true, }, }, game: { select: { id: true, title: true } }, book: { select: { id: true, title: true } }, music: { select: { id: true, title: true } }, art: { select: { id: true, title: true } }, show: { select: { id: true, title: true } }, manga: { select: { id: true, title: true } }, }, orderBy: { createdAt: "desc" }, take: limit * 2, }); return comments.map((comment) => { let entityType = ""; let entityId = ""; let entityTitle = ""; if (comment.game) { entityType = "game"; entityId = comment.game.id; entityTitle = comment.game.title; } else if (comment.book) { entityType = "book"; entityId = comment.book.id; entityTitle = comment.book.title; } else if (comment.music) { entityType = "music"; entityId = comment.music.id; entityTitle = comment.music.title; } else if (comment.art) { entityType = "art"; entityId = comment.art.id; entityTitle = comment.art.title; } else if (comment.show) { entityType = "show"; entityId = comment.show.id; entityTitle = comment.show.title; } else if (comment.manga) { entityType = "manga"; entityId = comment.manga.id; entityTitle = comment.manga.title; } // Get first 100 characters of comment const commentPreview = comment.content.length > 100 ? `${comment.content.slice(0, 100)}...` : comment.content; return { id: `comment-${comment.id}`, type: ActivityType.comment, user: comment.user as ActivityUser, entityType, entityId, entityTitle, commentPreview, createdAt: comment.createdAt, }; }); } /** * Get achievement activities. */ private async getAchievementActivities( limit: number, offset: number, userId?: string ): Promise { const where = userId ? { userId, earned: true, user: { profilePublic: true, isBanned: false }, } : { earned: true, user: { profilePublic: true, isBanned: false } }; const userAchievements = await this.prisma.userAchievement.findMany({ where, include: { user: { select: { id: true, username: true, slug: true, avatar: true, primaryBadge: true, isVip: true, isMod: true, isStaff: true, }, }, }, orderBy: { earnedAt: "desc" }, take: limit * 2, }); return userAchievements .filter((ua) => ua.earnedAt) // Only show earned achievements .map((ua) => { const achievement = ACHIEVEMENTS[ua.achievementKey]; return { id: `achievement-${ua.id}`, type: ActivityType.achievement, user: ua.user as ActivityUser, achievementKey: ua.achievementKey, achievementName: achievement.title, achievementIcon: achievement.icon, achievementPoints: achievement.points, createdAt: ua.earnedAt!, }; }); } /** * Helper to get entity title by type and ID. */ private async getEntityTitle( entityType: string, entityId: string ): Promise { switch (entityType) { case "game": { const game = await this.prisma.game.findUnique({ where: { id: entityId }, select: { title: true }, }); return game?.title || "Unknown Game"; } case "book": { const book = await this.prisma.book.findUnique({ where: { id: entityId }, select: { title: true }, }); return book?.title || "Unknown Book"; } case "music": { const music = await this.prisma.music.findUnique({ where: { id: entityId }, select: { title: true }, }); return music?.title || "Unknown Music"; } case "art": { const art = await this.prisma.art.findUnique({ where: { id: entityId }, select: { title: true }, }); return art?.title || "Unknown Art"; } case "show": { const show = await this.prisma.show.findUnique({ where: { id: entityId }, select: { title: true }, }); return show?.title || "Unknown Show"; } case "manga": { const manga = await this.prisma.manga.findUnique({ where: { id: entityId }, select: { title: true }, }); return manga?.title || "Unknown Manga"; } default: return "Unknown Item"; } } }