diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index cbd8ebe..56b93b3 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -91,13 +91,11 @@ model Notification { } model Achievement { - id String @id + id String @id @default(cuid()) type AchievementType name String level Int @default(0) - bannerUrl String condition Int - progress Int // progress to condition 1/10 progress/condition promotionPoints Int // promotionPoints a user receives for reaching the achievement userToAchievement UserToAchievement[] } @@ -107,7 +105,9 @@ model UserToAchievement { userId String achievement Achievement @relation(fields: [achievementId], references: [id], onDelete: Cascade, onUpdate: Cascade) achievementId String + progress Int @default(0) // progress to condition 1/10 progress/condition created_at DateTime @default(now()) + updated_at DateTime @updatedAt @@id([userId, achievementId]) } @@ -137,6 +137,7 @@ enum RewardType { enum AchievementType { NTH_STREAM + MINUTES_STREAMED NTH_VIEWERS } diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 41e4fea..c513fab 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -26,18 +26,20 @@ import { initRabbitMQ } from "./utils/initRabbitMQ"; import { rewards } from "./utils/rewards"; import { getNotificationsMessage } from "./utils/notificationsMessages"; import isAuth from "./middleware/isAuth"; +import { handleAchievements } from "./service/achievments.service"; type Stream = { id: string; created_at: string; streamer: SocketUser; viewerCount: number; - viewers: Array; + viewers: Array; // viewers that are currently in the livestream + mostViewers: number; }; // Holds state of active streams // eslint-disable-next-line @typescript-eslint/no-unused-vars -const streams: Map = new Map(); // key is the id of the stream +export const streams: Map = new Map(); // key is the id of the stream // eslint-disable-next-line prefer-const const app: Express = express(); @@ -282,7 +284,8 @@ io.on("connection", (socket) => { created_at: checkStream.created_at.toString(), streamer: socket.data.user, viewerCount: 0, - viewers: [] + viewers: [], + mostViewers: 0 }); logger.info(`Created stream: ${streamId}`); @@ -375,6 +378,21 @@ io.on("connection", (socket) => { stream.viewerCount++; stream.viewers.push(socket.data.user.id); + if (stream.viewerCount > stream.mostViewers) { + stream.mostViewers = stream.viewerCount; + } + + await prismaClient.stream.update({ + where: { + id: streamId + }, + data: { + viewerCount: { + increment: 1 + } + } + }); + io.to(streamId).emit("user_joined", { user: socket.data.user }); @@ -567,7 +585,8 @@ io.on("connection", (socket) => { data: { ended_at, duration, - active: false + active: false, + mostViewers: stream.mostViewers } }); @@ -583,6 +602,8 @@ io.on("connection", (socket) => { } }); + await handleAchievements(checkStream.streamerId); + // broadcast stream end to all clients io.to(streamId).emit("stream_ended", { duration, @@ -629,6 +650,17 @@ io.on("connection", (socket) => { socket.emit("you-left-stream"); + await prismaClient.stream.update({ + where: { + id: streamId + }, + data: { + viewerCount: { + decrement: 1 + } + } + }); + socket.to(streamId).emit("user_leaved", { user: socket.data.user }); @@ -898,6 +930,7 @@ io.on("connection", (socket) => { data: { ended_at, duration, + mostViewers: stream.mostViewers, active: false } }); @@ -914,6 +947,8 @@ io.on("connection", (socket) => { } }); + await handleAchievements(checkStream.streamerId); + // broadcast stream end to all clients io.to(streamId).emit("stream_ended", { duration, @@ -957,6 +992,18 @@ io.on("connection", (socket) => { stream.viewers = stream.viewers.filter( (id) => id !== socket.data.user.id ); + + await prismaClient.stream.update({ + where: { + id: streamId + }, + data: { + viewerCount: { + decrement: 1 + } + } + }); + delete socket.data.streamId; logger.info( diff --git a/apps/api/src/controller/.gitkeep b/apps/api/src/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/src/controller/stream.controller.ts b/apps/api/src/controller/stream.controller.ts index 4dbcf19..902f150 100644 --- a/apps/api/src/controller/stream.controller.ts +++ b/apps/api/src/controller/stream.controller.ts @@ -85,6 +85,7 @@ export const getStreamInfo = async ( followed = !!followsStreamer; } + return res.status(httpStatus.OK).json({ success: true, data: { diff --git a/apps/api/src/controller/user.controller.ts b/apps/api/src/controller/user.controller.ts index 79c1f9a..3bb1bc8 100644 --- a/apps/api/src/controller/user.controller.ts +++ b/apps/api/src/controller/user.controller.ts @@ -13,7 +13,8 @@ import { Follows, Notification, Stream, - User + User, + UserToAchievement } from "@prisma/client"; import { createErrorObject } from "../utils/createErrorObject"; import { getNotificationsMessage } from "../utils/notificationsMessages"; @@ -27,9 +28,10 @@ export const getUserInfo = async ( TypedResponse<{ user: User & { subscribed: boolean; - userToAchievement: { - achievement: Achievement; - }[]; + userToAchievement: (Pick< + UserToAchievement, + "progress" | "created_at" | "updated_at" + > & { achievement: Achievement })[]; streams: Stream[]; }; }> @@ -41,6 +43,9 @@ export const getUserInfo = async ( streams: true, userToAchievement: { select: { + progress: true, + created_at: true, + updated_at: true, achievement: true } } @@ -120,12 +125,31 @@ export const createUser = async ( }); } + // Get all achievements a user can collect + const getAllAchievements = await prismaClient.achievement.findMany({ + select: { + id: true + } + }); + + // create a relationship to all achievements + const userToAchievementsData: { userId: string; achievementId: string }[] = + getAllAchievements.map((v) => ({ + userId: id, + achievementId: v.id + })); + const newUser = await prismaClient.user.create({ data: { id, username, dispname: username, - email + email, + userToAchievement: { + createMany: { + data: userToAchievementsData + } + } }, select: { id: true, diff --git a/apps/api/src/service/achievments.service.ts b/apps/api/src/service/achievments.service.ts new file mode 100644 index 0000000..7bede3a --- /dev/null +++ b/apps/api/src/service/achievments.service.ts @@ -0,0 +1,147 @@ +import logger from "../middleware/logger"; +import prismaClient from "../config/prisma"; +import { createAchievementNotification } from "./notification.service"; + +/** + * Checks and updates the progress of the achievements for a specific user + * @param userId UserID + * @returns Promise + */ +export const handleAchievements = async (userId: string) => { + try { + const userData = await prismaClient.user.findUnique({ + where: { + id: userId + }, + select: { + id: true, + numStreams: true, + num10minStreams: true, + minStreamed: true, + userToAchievement: { + include: { + achievement: true + } + }, + streams: { + select: { + id: true, + mostViewers: true + } + } + } + }); + + if (!userData || !userData.streams) return; + + const mostViewers = Math.max( + ...userData.streams.map((stream) => stream.mostViewers) + ); + + for (const uta of userData.userToAchievement) { + const achievement = uta.achievement; + + // if progress equals condition we dont need to update something + if (uta.progress === achievement.condition) continue; + + switch (achievement.type) { + case "NTH_STREAM": { + if ( + userData.num10minStreams > uta.progress && + userData.num10minStreams <= achievement.condition + ) { + await updateProgress( + uta.userId, + uta.achievementId, + userData.num10minStreams + ); + } else if ( + userData.num10minStreams > uta.progress && + userData.num10minStreams > achievement.condition + ) { + await updateProgress( + uta.userId, + uta.achievementId, + achievement.condition + ); + } + + // user has achieved the achievement + if (userData.num10minStreams >= achievement.condition) { + await createAchievementNotification(userId, achievement); + } + break; + } + case "MINUTES_STREAMED": { + if ( + userData.minStreamed > uta.progress && + userData.minStreamed <= achievement.condition + ) { + await updateProgress( + uta.userId, + uta.achievementId, + userData.minStreamed + ); + } else if ( + userData.minStreamed > uta.progress && + userData.minStreamed > achievement.condition + ) { + await updateProgress( + uta.userId, + uta.achievementId, + achievement.condition + ); + } + + // user has achieved the achievement + if (userData.minStreamed >= achievement.condition) { + await createAchievementNotification(userId, achievement); + } + break; + } + case "NTH_VIEWERS": { + if ( + mostViewers > uta.progress && + mostViewers <= achievement.condition + ) { + await updateProgress(uta.userId, uta.achievementId, mostViewers); + } else if ( + mostViewers > uta.progress && + mostViewers > achievement.condition + ) { + await updateProgress( + uta.userId, + uta.achievementId, + achievement.condition + ); + } + + // user has achieved the achievement + if (mostViewers >= achievement.condition) { + await createAchievementNotification(userId, achievement); + } + break; + } + default: + break; + } + } + } catch (error) { + logger.error("Error on handleAchievments: ", error); + } +}; + +export const updateProgress = async ( + userId: string, + achievementId: string, + progress: number +): Promise => { + try { + await prismaClient.userToAchievement.update({ + where: { userId_achievementId: { userId, achievementId } }, + data: { progress } + }); + } catch (error) { + logger.error("Error on updateProgress: ", error); + } +}; diff --git a/apps/api/src/service/notification.service.ts b/apps/api/src/service/notification.service.ts new file mode 100644 index 0000000..d144743 --- /dev/null +++ b/apps/api/src/service/notification.service.ts @@ -0,0 +1,20 @@ +import { Achievement } from "@prisma/client"; +import { getNotificationsMessage } from "../utils/notificationsMessages"; +import prismaClient from "../config/prisma"; + +export const createAchievementNotification = async ( + userId: string, + achievement: Achievement +) => { + await prismaClient.notification.create({ + data: { + type: "ACHIEVEMENT_RECEIVED", + achievemntId: achievement.id, + message: getNotificationsMessage( + "ACHIEVEMENT_RECEIVED", + achievement.name + ), + recipientId: userId + } + }); +};