From 0c804b82f1503a0797cfed8351d79abb1db35138 Mon Sep 17 00:00:00 2001 From: leon-morival Date: Wed, 8 Oct 2025 09:48:39 +0200 Subject: [PATCH] like & follow notifs --- firebase-debug.log | 118 ++++++++++++++++++++++++ functions/index.js | 1 + functions/src/notifications.js | 159 ++++++++++++++++++++++++++++++++- 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 firebase-debug.log diff --git a/firebase-debug.log b/firebase-debug.log new file mode 100644 index 0000000..4b6cacd --- /dev/null +++ b/firebase-debug.log @@ -0,0 +1,118 @@ +[debug] [2025-10-08T07:48:24.045Z] ---------------------------------------------------------------------- +[debug] [2025-10-08T07:48:24.046Z] Command: /usr/local/bin/firebase /home/leon/.cache/firebase/tools/lib/node_modules/firebase-tools/lib/bin/firebase deploy --only functions:notifications +[debug] [2025-10-08T07:48:24.047Z] CLI Version: 14.2.1 +[debug] [2025-10-08T07:48:24.047Z] Platform: linux +[debug] [2025-10-08T07:48:24.047Z] Node Version: v20.18.2 +[debug] [2025-10-08T07:48:24.047Z] Time: Wed Oct 08 2025 09:48:24 GMT+0200 (GMT+02:00) +[debug] [2025-10-08T07:48:24.047Z] ---------------------------------------------------------------------- +[debug] +[debug] [2025-10-08T07:48:24.184Z] Object "" in "firebase.json" has unknown property: {"additionalProperty":"react-native"} +[debug] [2025-10-08T07:48:24.187Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] +[debug] [2025-10-08T07:48:24.187Z] > authorizing via signed-in user (leon.morival@gmail.com) +[debug] [2025-10-08T07:48:24.187Z] [iam] checking project musicland-d33f9 for permissions ["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get"] +[debug] [2025-10-08T07:48:24.188Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:24.188Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:24.189Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9:testIamPermissions [none] +[debug] [2025-10-08T07:48:24.189Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9:testIamPermissions x-goog-quota-user=projects/musicland-d33f9 +[debug] [2025-10-08T07:48:24.189Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9:testIamPermissions {"permissions":["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get"]} +[debug] [2025-10-08T07:48:24.930Z] <<< [apiv2][status] POST https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9:testIamPermissions 200 +[debug] [2025-10-08T07:48:24.930Z] <<< [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9:testIamPermissions {"permissions":["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get"]} +[debug] [2025-10-08T07:48:24.931Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:24.931Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:24.931Z] >>> [apiv2][query] POST https://iam.googleapis.com/v1/projects/musicland-d33f9/serviceAccounts/musicland-d33f9@appspot.gserviceaccount.com:testIamPermissions [none] +[debug] [2025-10-08T07:48:24.931Z] >>> [apiv2][body] POST https://iam.googleapis.com/v1/projects/musicland-d33f9/serviceAccounts/musicland-d33f9@appspot.gserviceaccount.com:testIamPermissions {"permissions":["iam.serviceAccounts.actAs"]} +[debug] [2025-10-08T07:48:25.715Z] <<< [apiv2][status] POST https://iam.googleapis.com/v1/projects/musicland-d33f9/serviceAccounts/musicland-d33f9@appspot.gserviceaccount.com:testIamPermissions 404 +[debug] [2025-10-08T07:48:25.716Z] <<< [apiv2][body] POST https://iam.googleapis.com/v1/projects/musicland-d33f9/serviceAccounts/musicland-d33f9@appspot.gserviceaccount.com:testIamPermissions {"error":{"code":404,"message":"Unknown service account","status":"NOT_FOUND"}} +[debug] [2025-10-08T07:48:25.716Z] [functions] service account IAM check errored, deploy may fail: Request to https://iam.googleapis.com/v1/projects/musicland-d33f9/serviceAccounts/musicland-d33f9@appspot.gserviceaccount.com:testIamPermissions had HTTP Error: 404, Unknown service account {"name":"FirebaseError","children":[],"context":{"body":{"error":{"code":404,"message":"Unknown service account","status":"NOT_FOUND"}},"response":{"statusCode":404}},"exit":1,"message":"Request to https://iam.googleapis.com/v1/projects/musicland-d33f9/serviceAccounts/musicland-d33f9@appspot.gserviceaccount.com:testIamPermissions had HTTP Error: 404, Unknown service account","status":404} +[info] +[info] === Deploying to 'musicland-d33f9'... +[info] +[info] i deploying functions +[info] ✔ functions: Finished running predeploy script. +[debug] [2025-10-08T07:48:25.718Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.718Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.718Z] >>> [apiv2][query] GET https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9 [none] +[debug] [2025-10-08T07:48:25.938Z] <<< [apiv2][status] GET https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9 200 +[debug] [2025-10-08T07:48:25.938Z] <<< [apiv2][body] GET https://cloudresourcemanager.googleapis.com/v1/projects/musicland-d33f9 {"projectNumber":"305598753437","projectId":"musicland-d33f9","lifecycleState":"ACTIVE","name":"MusicLand","labels":{"firebase":"enabled","firebase-core":"disabled","generative-language":"enabled"},"createTime":"2025-08-01T09:52:18.735180Z"} +[info] i functions: preparing codebase default for deployment +[info] i functions: ensuring required API cloudfunctions.googleapis.com is enabled... +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[info] i functions: ensuring required API cloudbuild.googleapis.com is enabled... +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[info] i artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled... +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.939Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:25.939Z] >>> [apiv2][query] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudfunctions.googleapis.com [none] +[debug] [2025-10-08T07:48:25.939Z] >>> [apiv2][(partial)header] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudfunctions.googleapis.com x-goog-quota-user=projects/musicland-d33f9 +[debug] [2025-10-08T07:48:25.940Z] >>> [apiv2][query] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/runtimeconfig.googleapis.com [none] +[debug] [2025-10-08T07:48:25.940Z] >>> [apiv2][(partial)header] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/runtimeconfig.googleapis.com x-goog-quota-user=projects/musicland-d33f9 +[debug] [2025-10-08T07:48:25.941Z] >>> [apiv2][query] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudbuild.googleapis.com [none] +[debug] [2025-10-08T07:48:25.941Z] >>> [apiv2][(partial)header] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudbuild.googleapis.com x-goog-quota-user=projects/musicland-d33f9 +[debug] [2025-10-08T07:48:25.942Z] >>> [apiv2][query] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/artifactregistry.googleapis.com [none] +[debug] [2025-10-08T07:48:25.942Z] >>> [apiv2][(partial)header] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/artifactregistry.googleapis.com x-goog-quota-user=projects/musicland-d33f9 +[debug] [2025-10-08T07:48:26.526Z] <<< [apiv2][status] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudbuild.googleapis.com 200 +[debug] [2025-10-08T07:48:26.526Z] <<< [apiv2][body] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudbuild.googleapis.com [omitted] +[info] ✔ functions: required API cloudbuild.googleapis.com is enabled +[debug] [2025-10-08T07:48:26.882Z] <<< [apiv2][status] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/runtimeconfig.googleapis.com 200 +[debug] [2025-10-08T07:48:26.882Z] <<< [apiv2][body] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/runtimeconfig.googleapis.com [omitted] +[debug] [2025-10-08T07:48:26.892Z] <<< [apiv2][status] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudfunctions.googleapis.com 200 +[debug] [2025-10-08T07:48:26.892Z] <<< [apiv2][body] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/cloudfunctions.googleapis.com [omitted] +[info] ✔ functions: required API cloudfunctions.googleapis.com is enabled +[debug] [2025-10-08T07:48:26.903Z] <<< [apiv2][status] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/artifactregistry.googleapis.com 200 +[debug] [2025-10-08T07:48:26.904Z] <<< [apiv2][body] GET https://serviceusage.googleapis.com/v1/projects/musicland-d33f9/services/artifactregistry.googleapis.com [omitted] +[info] ✔ artifactregistry: required API artifactregistry.googleapis.com is enabled +[debug] [2025-10-08T07:48:26.904Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:26.904Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:26.904Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/musicland-d33f9/adminSdkConfig [none] +[debug] [2025-10-08T07:48:27.341Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/musicland-d33f9/adminSdkConfig 200 +[debug] [2025-10-08T07:48:27.342Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/musicland-d33f9/adminSdkConfig {"projectId":"musicland-d33f9","storageBucket":"musicland-d33f9.firebasestorage.app"} +[debug] [2025-10-08T07:48:27.342Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:27.342Z] Checked if tokens are valid: true, expires at: 1759912729066 +[debug] [2025-10-08T07:48:27.342Z] >>> [apiv2][query] GET https://runtimeconfig.googleapis.com/v1beta1/projects/musicland-d33f9/configs [none] +[debug] [2025-10-08T07:48:28.171Z] <<< [apiv2][status] GET https://runtimeconfig.googleapis.com/v1beta1/projects/musicland-d33f9/configs 200 +[debug] [2025-10-08T07:48:28.171Z] <<< [apiv2][body] GET https://runtimeconfig.googleapis.com/v1beta1/projects/musicland-d33f9/configs {} +[debug] [2025-10-08T07:48:28.171Z] Validating nodejs source +[debug] [2025-10-08T07:48:28.579Z] > [functions] package.json contents: { + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "lint": "eslint .", + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "20" + }, + "main": "index.js", + "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@genkit-ai/googleai": "^1.18.0", + "@google-cloud/storage": "^7.17.1", + "axios": "^1.11.0", + "expo-server-sdk": "^4.0.0", + "ffmpeg-static": "^5.2.0", + "firebase-admin": "^12.7.0", + "firebase-functions": "^6.4.0", + "fluent-ffmpeg": "^2.1.3", + "genkit": "^1.18.0" + }, + "devDependencies": { + "eslint": "^8.15.0", + "eslint-config-google": "^0.14.0" + }, + "private": true +} +[debug] [2025-10-08T07:48:28.579Z] Building nodejs source +[info] i functions: Loading and analyzing source code for codebase default to determine what to deploy +[debug] [2025-10-08T07:48:28.580Z] Could not find functions.yaml. Must use http discovery +[debug] [2025-10-08T07:48:28.583Z] Found firebase-functions binary at '/home/leon/Workspace/minuit/musicland/functions/node_modules/.bin/firebase-functions' +[info] Serving at port 8643 + +[debug] [2025-10-08T07:48:29.182Z] Got response from /__/functions.yaml {"endpoints":{"users-onUserDelete":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.deleted","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"users/{userID}"},"retry":false},"entryPoint":"users.onUserDelete"},"music-generateMusic":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"callableTrigger":{},"entryPoint":"music.generateMusic"},"music-getSunoStatus":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"callableTrigger":{},"entryPoint":"music.getSunoStatus"},"music-sunoCallback":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"httpsTrigger":{},"entryPoint":"music.sunoCallback"},"lyrics-generateLyrics":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"callableTrigger":{},"entryPoint":"lyrics.generateLyrics"},"lyrics-analyseLyricsToxicity":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"callableTrigger":{},"entryPoint":"lyrics.analyseLyricsToxicity"},"cover-onTaskCreateGenerateCover":{"availableMemoryMb":1024,"timeoutSeconds":540,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.created","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"tasks/{taskId}"},"retry":false},"entryPoint":"cover.onTaskCreateGenerateCover"},"projects-onProjectUpdate":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.written","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"projects/{projectId}"},"retry":false},"entryPoint":"projects.onProjectUpdate"},"thumbnail-generateVideoThumbnail":{"platform":"gcfv2","availableMemoryMb":1024,"timeoutSeconds":180,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"cpu":1,"region":["europe-west1"],"labels":{},"eventTrigger":{"eventType":"google.cloud.storage.object.v1.finalized","eventFilters":{"bucket":"musicland-d33f9.firebasestorage.app"},"retry":false},"entryPoint":"thumbnail.generateVideoThumbnail"},"upload-uploadProjectMedia":{"availableMemoryMb":2048,"timeoutSeconds":540,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","labels":{},"callableTrigger":{},"entryPoint":"upload.uploadProjectMedia"},"algolia-algoliaTransformProjectData":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","region":["europe-west1"],"labels":{},"httpsTrigger":{},"entryPoint":"algolia.algoliaTransformProjectData"},"notifications-sendNotificationWhenDocIsCreated":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","region":["europe-west1"],"labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.created","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"notifications/{notificationId}"},"retry":false},"entryPoint":"notifications.sendNotificationWhenDocIsCreated"},"notifications-createProjectCommentNotification":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","region":["europe-west1"],"labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.created","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"projects/{projectId}/comments/{commentId}"},"retry":false},"entryPoint":"notifications.createProjectCommentNotification"},"notifications-createProjectLikeNotification":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","region":["europe-west1"],"labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.written","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"projects/{projectId}"},"retry":false},"entryPoint":"notifications.createProjectLikeNotification"},"notifications-createNewFollowerNotification":{"availableMemoryMb":null,"timeoutSeconds":null,"minInstances":null,"maxInstances":null,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","region":["europe-west1"],"labels":{},"eventTrigger":{"eventType":"google.cloud.firestore.document.v1.written","eventFilters":{"database":"(default)","namespace":"(default)"},"eventFilterPathPatterns":{"document":"users/{userId}"},"retry":false},"entryPoint":"notifications.createNewFollowerNotification"}},"specVersion":"v1alpha1","requiredAPIs":[],"extensions":{}} diff --git a/functions/index.js b/functions/index.js index b75108c..1ae9b04 100644 --- a/functions/index.js +++ b/functions/index.js @@ -9,6 +9,7 @@ exports.refList = { playlists: admin.firestore().collection("playlists"), tasks: admin.firestore().collection("tasks"), notifications: admin.firestore().collection("notifications"), + users: admin.firestore().collection("users"), }; exports.ALERT_TYPE = { diff --git a/functions/src/notifications.js b/functions/src/notifications.js index 2d7f05a..9e0c2dc 100644 --- a/functions/src/notifications.js +++ b/functions/src/notifications.js @@ -1,4 +1,7 @@ -const { onDocumentCreated } = require("firebase-functions/v2/firestore"); +const { + onDocumentCreated, + onDocumentWritten, +} = require("firebase-functions/v2/firestore"); const admin = require("firebase-admin"); const { refList, ALERT_TYPE } = require("../index"); const { Expo } = require("expo-server-sdk"); @@ -315,3 +318,157 @@ exports.createProjectCommentNotification = onDocumentCreated( } } ); + +exports.createProjectLikeNotification = onDocumentWritten( + { + region: "europe-west1", + document: "projects/{projectId}", + }, + async (event) => { + try { + const { projectId } = event.params || {}; + const before = event?.data?.before?.data() || {}; + const after = event?.data?.after?.data() || {}; + + if (!projectId || !after) { + return null; + } + + const ownerId = after.userId || null; + if (!ownerId) { + return null; + } + + const beforeLikes = Array.isArray(before?.likedBy) ? before.likedBy : []; + const afterLikes = Array.isArray(after?.likedBy) ? after.likedBy : []; + + if (afterLikes.length <= beforeLikes.length) { + return null; + } + + const beforeSet = new Set(beforeLikes); + const newLikers = afterLikes.filter((uid) => !beforeSet.has(uid)); + + if (!newLikers.length) { + return null; + } + + const projectTitle = + typeof after.title === "string" && after.title.trim() + ? after.title.trim() + : "ton projet"; + + await Promise.all( + newLikers.map(async (likerId) => { + if (!likerId || likerId === ownerId) { + return null; + } + + const likerSnap = await refList.users.doc(likerId).get(); + const liker = likerSnap?.data() || {}; + const likerName = + typeof liker?.userName === "string" && liker.userName.trim() + ? liker.userName.trim() + : "Un utilisateur"; + + const message = `${likerName} a aimé ton projet "${projectTitle}"`; + + await sendNotification({ + sender: likerId, + receiver: ownerId, + receiverCollection: "users", + title: "Nouveau like", + message, + data: { + type: ALERT_TYPE?.NEW_LIKE, + projectId, + likerId, + likerName, + }, + }); + + return null; + }) + ); + + return null; + } catch (error) { + console.log("[createProjectLikeNotification] error:", error); + return error; + } + } +); + +exports.createNewFollowerNotification = onDocumentWritten( + { + region: "europe-west1", + document: "users/{userId}", + }, + async (event) => { + try { + const { userId } = event.params || {}; + const before = event?.data?.before?.data() || {}; + const after = event?.data?.after?.data() || {}; + + if (!userId || !after) { + return null; + } + + const beforeFollowers = Array.isArray(before?.followedBy) + ? before.followedBy + : []; + const afterFollowers = Array.isArray(after?.followedBy) + ? after.followedBy + : []; + + if (afterFollowers.length <= beforeFollowers.length) { + return null; + } + + const previousSet = new Set(beforeFollowers); + const newFollowers = afterFollowers.filter((uid) => !previousSet.has(uid)); + + if (!newFollowers.length) { + return null; + } + + await Promise.all( + newFollowers.map(async (followerId) => { + if (!followerId || followerId === userId) { + return null; + } + + const followerSnap = await refList.users.doc(followerId).get(); + const follower = followerSnap?.data() || {}; + const followerName = + typeof follower?.userName === "string" && follower.userName.trim() + ? follower.userName.trim() + : "Un utilisateur"; + + const message = `${followerName} te suit maintenant`; + + await sendNotification({ + sender: followerId, + receiver: userId, + receiverCollection: "users", + title: "Nouvel abonné", + message, + data: { + type: ALERT_TYPE?.NEW_FOLLOWER, + followerId, + followerName, + followerProfilePicture: follower?.profilePicture || "", + }, + }); + + return null; + }) + ); + + return null; + } catch (error) { + console.log("[createNewFollowerNotification] error:", error); + return error; + } + } +);