feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+239 -284
View File
@@ -1,100 +1,88 @@
const {
onDocumentCreated,
onDocumentWritten,
} = require("firebase-functions/v2/firestore");
const { FieldValue } = require("firebase-admin/firestore");
const { refList, ALERT_TYPE } = require("../index");
const { Expo } = require("expo-server-sdk");
const { Resend } = require("resend");
const { basicTemplate } = require("../helpers/email");
const { RESEND_API_KEY } = require("../config/keys");
const { onDocumentCreated, onDocumentWritten } = require('firebase-functions/v2/firestore')
const { FieldValue } = require('firebase-admin/firestore')
const { refList, ALERT_TYPE } = require('../index')
const { Expo } = require('expo-server-sdk')
const { Resend } = require('resend')
const { basicTemplate } = require('../helpers/email')
const { RESEND_API_KEY } = require('../config/keys')
const resendInstance = RESEND_API_KEY ? new Resend(RESEND_API_KEY) : null;
const resendInstance = RESEND_API_KEY ? new Resend(RESEND_API_KEY) : null
// Initialisation de Expo SDK
let expo = new Expo();
const EMAIL_FROM = "MusicLand <musicland@musicland.ai>";
const DEFAULT_EMAIL_TITLE = "MusicLand";
let expo = new Expo()
const EMAIL_FROM = 'MusicLand <musicland@musicland.ai>'
const DEFAULT_EMAIL_TITLE = 'MusicLand'
function getCollectionRef(collectionName = "") {
const ref = refList?.[collectionName];
function getCollectionRef(collectionName = '') {
const ref = refList?.[collectionName]
if (!ref) {
throw new Error(`Unknown collection "${collectionName}"`);
throw new Error(`Unknown collection "${collectionName}"`)
}
return ref;
return ref
}
function cleanString(value) {
return typeof value === "string" && value.trim() ? value.trim() : null;
return typeof value === 'string' && value.trim() ? value.trim() : null
}
function buildNotificationEmailPayload({
title = "",
message = "",
template = {},
} = {}) {
const fallbackTitle = cleanString(title) || DEFAULT_EMAIL_TITLE;
const fallbackContent = cleanString(message) || "";
const overrides = template && typeof template === "object" ? template : {};
function buildNotificationEmailPayload({ title = '', message = '', template = {} } = {}) {
const fallbackTitle = cleanString(title) || DEFAULT_EMAIL_TITLE
const fallbackContent = cleanString(message) || ''
const overrides = template && typeof template === 'object' ? template : {}
const subject = cleanString(overrides.subject) || fallbackTitle;
const emailTitle = cleanString(overrides.title) || fallbackTitle;
const content = cleanString(overrides.content) || fallbackContent;
const subject = cleanString(overrides.subject) || fallbackTitle
const emailTitle = cleanString(overrides.title) || fallbackTitle
const content = cleanString(overrides.content) || fallbackContent
let button = null;
if (overrides.button && typeof overrides.button === "object") {
const buttonUrl =
cleanString(overrides.button.url) || cleanString(overrides.button.href);
let button = null
if (overrides.button && typeof overrides.button === 'object') {
const buttonUrl = cleanString(overrides.button.url) || cleanString(overrides.button.href)
if (buttonUrl) {
button = {
url: buttonUrl,
label:
cleanString(overrides.button.label) ||
cleanString(overrides.button.text) ||
undefined,
};
cleanString(overrides.button.label) || cleanString(overrides.button.text) || undefined,
}
}
}
const templatePayload = { title: emailTitle, content };
const templatePayload = { title: emailTitle, content }
if (button) {
templatePayload.button = button;
templatePayload.button = button
}
return {
subject,
html: basicTemplate(templatePayload),
};
}
}
exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
{ region: "europe-west1", document: "notifications/{notificationId}" },
{ region: 'europe-west1', document: 'notifications/{notificationId}' },
async (event) => {
try {
const {
receiver = null,
title = "MusicLand",
receiverCollection = "users",
message = "",
title = 'MusicLand',
receiverCollection = 'users',
message = '',
data: notifData = {},
mailOnly = false,
} = event.data.data();
} = event.data.data()
if (!receiver || !message) {
throw new Error("Receiver and message are required");
throw new Error('Receiver and message are required')
}
const receiverSnap = await getCollectionRef(receiverCollection)
.doc(receiver)
.get();
const receiverData = receiverSnap.exists ? receiverSnap.data() : {};
const receiverSnap = await getCollectionRef(receiverCollection).doc(receiver).get()
const receiverData = receiverSnap.exists ? receiverSnap.data() : {}
const {
pushToken = null,
pushTokens = [],
email: receiverEmail = "",
email: receiverEmail = '',
emailNotifications = false,
} = receiverData;
} = receiverData
if (!mailOnly) {
try {
@@ -102,129 +90,124 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
[]
.concat(Array.isArray(pushTokens) ? pushTokens : [])
.concat(pushToken ? [pushToken] : [])
.filter(Boolean),
);
const tokens = Array.from(tokensSet);
.filter(Boolean)
)
const tokens = Array.from(tokensSet)
if (!tokens?.length) {
await sendExpoNotification({
tokens,
receiverId: receiver,
receiverCollection,
title: title || "MusicLand",
title: title || 'MusicLand',
message: message,
data: notifData || {},
});
})
} else {
console.log("User push token not found");
console.log('User push token not found')
}
} catch (e) {
console.log("Error sending notif:", e);
console.log('Error sending notif:', e)
}
}
if ((emailNotifications || mailOnly) && !!receiverEmail) {
if (!resendInstance) {
console.warn(
"Resend client not configured; unable to send notification email.",
);
console.warn('Resend client not configured; unable to send notification email.')
} else {
try {
const { subject, html } = buildNotificationEmailPayload({
title,
message,
template: notifData?.email,
});
})
await resendInstance.emails.send({
from: EMAIL_FROM,
to: [receiverEmail],
subject,
html,
});
})
} catch (e) {
console.log("Error sending email:", e);
console.log('Error sending email:', e)
}
}
} else {
console.log(
`[sendNotificationWhenDocIsCreated] Email not sent (${receiverEmail ? "user preference" : "missing email"}) for user ${receiver} and type ${notifData?.type || "UNKNOWN"}`,
);
`[sendNotificationWhenDocIsCreated] Email not sent (${receiverEmail ? 'user preference' : 'missing email'}) for user ${receiver} and type ${notifData?.type || 'UNKNOWN'}`
)
}
} catch (e) {
console.log(e);
return e;
console.log(e)
return e
}
},
);
}
)
// Fonction pour envoyer la notification via Expo SDK
async function sendExpoNotification({
tokens = [],
title = "",
message = "",
title = '',
message = '',
data = {},
receiverId = null,
receiverCollection = "users",
receiverCollection = 'users',
}) {
try {
const candidateTokens = Array.isArray(tokens) ? tokens : [tokens];
const validTokens = [];
const invalidTokens = [];
const candidateTokens = Array.isArray(tokens) ? tokens : [tokens]
const validTokens = []
const invalidTokens = []
candidateTokens.forEach((token) => {
if (Expo.isExpoPushToken(token)) {
validTokens.push(token);
validTokens.push(token)
} else if (token) {
invalidTokens.push(token);
invalidTokens.push(token)
}
});
})
if (invalidTokens.length && receiverId) {
await removeInvalidTokens({
tokens: invalidTokens,
receiverId,
receiverCollection,
});
})
}
if (!validTokens.length) {
console.warn("No valid Expo push tokens to send notification");
return { sent: false };
console.warn('No valid Expo push tokens to send notification')
return { sent: false }
}
const messages = validTokens.map((token) => ({
to: token,
sound: "default",
sound: 'default',
title: title,
body: message,
data: data || {},
priority: "high",
priority: 'high',
badge: 1,
channelId: "default",
}));
channelId: 'default',
}))
const chunks = expo.chunkPushNotifications(messages);
const receipts = [];
const tokensToPrune = new Set();
const chunks = expo.chunkPushNotifications(messages)
const receipts = []
const tokensToPrune = new Set()
for (const chunk of chunks) {
const chunkReceipts = await expo.sendPushNotificationsAsync(chunk);
const chunkReceipts = await expo.sendPushNotificationsAsync(chunk)
chunkReceipts.forEach((receipt, index) => {
if (receipt?.status === "error") {
const errorCode = receipt?.details?.error || receipt?.details?.code;
console.log("Error sending notification:", receipt);
if (
errorCode === "DeviceNotRegistered" ||
errorCode === "PushTokenNotRegistered"
) {
const token = chunk[index]?.to;
if (receipt?.status === 'error') {
const errorCode = receipt?.details?.error || receipt?.details?.code
console.log('Error sending notification:', receipt)
if (errorCode === 'DeviceNotRegistered' || errorCode === 'PushTokenNotRegistered') {
const token = chunk[index]?.to
if (token) {
tokensToPrune.add(token);
tokensToPrune.add(token)
}
}
}
});
receipts.push(...chunkReceipts);
})
receipts.push(...chunkReceipts)
}
if (tokensToPrune.size && receiverId) {
@@ -232,68 +215,64 @@ async function sendExpoNotification({
tokens: Array.from(tokensToPrune),
receiverId,
receiverCollection,
});
})
}
console.log("Sent push notifications:", receipts);
console.log('Sent push notifications:', receipts)
return { sent: true };
return { sent: true }
} catch (e) {
console.log("Error sending notification:", e);
throw e;
console.log('Error sending notification:', e)
throw e
}
}
async function removeInvalidTokens({
tokens = [],
receiverId,
receiverCollection,
}) {
async function removeInvalidTokens({ tokens = [], receiverId, receiverCollection }) {
try {
if (!receiverId || !tokens.length) {
return;
return
}
const uniqueTokens = Array.from(new Set(tokens.filter(Boolean)));
const uniqueTokens = Array.from(new Set(tokens.filter(Boolean)))
if (!uniqueTokens.length) {
return;
return
}
const docRef = getCollectionRef(receiverCollection).doc(receiverId);
const userSnap = await docRef.get();
const userData = userSnap?.data() || {};
const docRef = getCollectionRef(receiverCollection).doc(receiverId)
const userSnap = await docRef.get()
const userData = userSnap?.data() || {}
const updates = {
pushTokens: FieldValue.arrayRemove(...uniqueTokens),
};
if (uniqueTokens.includes(userData?.pushToken)) {
updates.pushToken = FieldValue.delete();
}
await docRef.set(updates, { merge: true });
if (uniqueTokens.includes(userData?.pushToken)) {
updates.pushToken = FieldValue.delete()
}
await docRef.set(updates, { merge: true })
console.log(
"Pruned invalid push tokens",
JSON.stringify({ receiverId, tokens: uniqueTokens }, null, 2),
);
'Pruned invalid push tokens',
JSON.stringify({ receiverId, tokens: uniqueTokens }, null, 2)
)
} catch (error) {
console.log("Failed to prune invalid push tokens:", error);
console.log('Failed to prune invalid push tokens:', error)
}
}
// Fonction pour ajouter une notification à la base de données
const sendNotification = async ({
sender = "SYSTEM",
sender = 'SYSTEM',
receiver = null,
receiverCollection = "users",
title = "",
receiverCollection = 'users',
title = '',
message = null,
mailOnly = false,
data = {},
}) => {
try {
if (!receiver || !message) {
throw new Error("Receiver and message are required");
throw new Error('Receiver and message are required')
}
const payload = {
sender,
@@ -306,81 +285,78 @@ const sendNotification = async ({
readAt: null,
mailOnly,
data,
};
const { id } = await refList.notifications.add(payload);
}
const { id } = await refList.notifications.add(payload)
console.log(
"[sendNotification] Notification created",
JSON.stringify({ id, receiver, receiverCollection }, null, 2),
);
'[sendNotification] Notification created',
JSON.stringify({ id, receiver, receiverCollection }, null, 2)
)
return id;
return id
} catch (e) {
console.log("[sendNotification] Error creating notification:", e);
console.log('[sendNotification] Error creating notification:', e)
}
};
}
exports.sendNotification = sendNotification;
exports.sendNotification = sendNotification
exports.createProjectCommentNotification = onDocumentCreated(
{
region: "europe-west1",
document: "projects/{projectId}/comments/{commentId}",
region: 'europe-west1',
document: 'projects/{projectId}/comments/{commentId}',
},
async (event) => {
try {
console.log(
"[createProjectCommentNotification] Trigger received",
JSON.stringify(event.params || {}, null, 2),
);
const { data: snap } = event;
const { projectId, commentId } = event.params || {};
const comment = snap?.data();
'[createProjectCommentNotification] Trigger received',
JSON.stringify(event.params || {}, null, 2)
)
const { data: snap } = event
const { projectId, commentId } = event.params || {}
const comment = snap?.data()
if (!projectId || !comment) {
console.log(
"[createProjectCommentNotification] Missing project/comment data",
{ hasProjectId: !!projectId, hasComment: !!comment },
);
return null;
console.log('[createProjectCommentNotification] Missing project/comment data', {
hasProjectId: !!projectId,
hasComment: !!comment,
})
return null
}
console.log(
"[createProjectCommentNotification] Comment payload",
JSON.stringify(comment, null, 2),
);
'[createProjectCommentNotification] Comment payload',
JSON.stringify(comment, null, 2)
)
const projectSnap = await refList.projects.doc(projectId).get();
const projectSnap = await refList.projects.doc(projectId).get()
if (!projectSnap.exists) {
console.log(
"[createProjectCommentNotification] Project not found",
projectId,
);
return null;
console.log('[createProjectCommentNotification] Project not found', projectId)
return null
}
const project = projectSnap.data() || {};
const receiver = project.userId || null;
const project = projectSnap.data() || {}
const receiver = project.userId || null
if (!receiver || receiver === comment.userId) {
console.log(
"[createProjectCommentNotification] Invalid receiver",
JSON.stringify({ receiver, commentUserId: comment.userId }),
);
return null;
'[createProjectCommentNotification] Invalid receiver',
JSON.stringify({ receiver, commentUserId: comment.userId })
)
return null
}
const commenterName =
typeof comment?.userName === "string" && comment.userName.trim()
typeof comment?.userName === 'string' && comment.userName.trim()
? comment.userName.trim()
: "Un utilisateur";
: 'Un utilisateur'
const projectTitle =
typeof project.title === "string" && project.title.trim()
typeof project.title === 'string' && project.title.trim()
? project.title.trim()
: "ton projet";
const message = `${commenterName} a commenté ton projet "${projectTitle}"`;
: 'ton projet'
const message = `${commenterName} a commenté ton projet "${projectTitle}"`
console.log(
"[createProjectCommentNotification] Creating notification",
'[createProjectCommentNotification] Creating notification',
JSON.stringify(
{
receiver,
@@ -389,15 +365,15 @@ exports.createProjectCommentNotification = onDocumentCreated(
projectId,
},
null,
2,
),
);
2
)
)
await sendNotification({
sender: comment.userId || "SYSTEM",
sender: comment.userId || 'SYSTEM',
receiver,
receiverCollection: "users",
title: "Nouveau commentaire",
receiverCollection: 'users',
title: 'Nouveau commentaire',
message,
data: {
type: ALERT_TYPE?.NEW_COMMENT,
@@ -405,109 +381,94 @@ exports.createProjectCommentNotification = onDocumentCreated(
commentId,
commenterId: comment.userId || null,
commenterName: commenterName,
commenterProfilePicture: comment?.profilePicture || "",
text:
typeof comment?.text === "string" && comment.text.trim()
? comment.text.trim()
: "",
commenterProfilePicture: comment?.profilePicture || '',
text: typeof comment?.text === 'string' && comment.text.trim() ? comment.text.trim() : '',
},
});
})
console.log(
"[createProjectCommentNotification] Notification creation complete",
);
console.log('[createProjectCommentNotification] Notification creation complete')
return null;
return null
} catch (error) {
console.log("createProjectCommentNotification error:", error);
return error;
console.log('createProjectCommentNotification error:', error)
return error
}
},
);
}
)
exports.createProjectLikeNotification = onDocumentWritten(
{
region: "europe-west1",
document: "projects/{projectId}",
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() || {};
const { projectId } = event.params || {}
const before = event?.data?.before?.data() || {}
const after = event?.data?.after?.data() || {}
if (!projectId || !after) {
return null;
return null
}
const ownerId = after.userId || null;
const ownerId = after.userId || null
if (!ownerId) {
return null;
return null
}
const beforeSongLikes = Array.isArray(before?.likes?.song)
? before.likes.song
: [];
const afterSongLikes = Array.isArray(after?.likes?.song)
? after.likes.song
: [];
const beforeSongLikes = Array.isArray(before?.likes?.song) ? before.likes.song : []
const afterSongLikes = Array.isArray(after?.likes?.song) ? after.likes.song : []
const beforePlaybackLikes = Array.isArray(before?.likes?.playback)
? before.likes.playback
: [];
const afterPlaybackLikes = Array.isArray(after?.likes?.playback)
? after.likes.playback
: [];
: []
const afterPlaybackLikes = Array.isArray(after?.likes?.playback) ? after.likes.playback : []
const beforeSongSet = new Set(beforeSongLikes);
const beforePlaybackSet = new Set(beforePlaybackLikes);
const beforeSongSet = new Set(beforeSongLikes)
const beforePlaybackSet = new Set(beforePlaybackLikes)
const newSongLikers = afterSongLikes.filter(
(uid) => uid && !beforeSongSet.has(uid),
);
const newSongLikers = afterSongLikes.filter((uid) => uid && !beforeSongSet.has(uid))
const newPlaybackLikers = afterPlaybackLikes.filter(
(uid) => uid && !beforePlaybackSet.has(uid),
);
(uid) => uid && !beforePlaybackSet.has(uid)
)
const newLikers = [];
const newLikers = []
newSongLikers.forEach((uid) => {
newLikers.push({ likerId: uid, likeType: "song" });
});
newLikers.push({ likerId: uid, likeType: 'song' })
})
newPlaybackLikers.forEach((uid) => {
newLikers.push({ likerId: uid, likeType: "playback" });
});
newLikers.push({ likerId: uid, likeType: 'playback' })
})
if (!newLikers.length) {
return null;
return null
}
const projectTitle =
typeof after.title === "string" && after.title.trim()
? after.title.trim()
: "ton projet";
typeof after.title === 'string' && after.title.trim() ? after.title.trim() : 'ton projet'
await Promise.all(
newLikers.map(async ({ likerId, likeType }) => {
if (!likerId || likerId === ownerId) {
return null;
return null
}
const likerSnap = await refList.users.doc(likerId).get();
const liker = likerSnap?.data() || {};
const likerSnap = await refList.users.doc(likerId).get()
const liker = likerSnap?.data() || {}
const likerName =
typeof liker?.userName === "string" && liker.userName.trim()
typeof liker?.userName === 'string' && liker.userName.trim()
? liker.userName.trim()
: "Un utilisateur";
: 'Un utilisateur'
const isPlaybackLike = likeType === "playback";
const assetLabel = isPlaybackLike ? "ton playback" : "ta musique";
const message = `${likerName} a aimé ${assetLabel} "${projectTitle}"`;
const isPlaybackLike = likeType === 'playback'
const assetLabel = isPlaybackLike ? 'ton playback' : 'ta musique'
const message = `${likerName} a aimé ${assetLabel} "${projectTitle}"`
await sendNotification({
sender: likerId,
receiver: ownerId,
receiverCollection: "users",
title: "Nouveau like",
receiverCollection: 'users',
title: 'Nouveau like',
message,
data: {
type: ALERT_TYPE?.NEW_LIKE,
@@ -516,92 +477,86 @@ exports.createProjectLikeNotification = onDocumentWritten(
likerName,
likeType,
},
});
})
return null;
}),
);
return null
})
)
return null;
return null
} catch (error) {
console.log("[createProjectLikeNotification] error:", error);
return error;
console.log('[createProjectLikeNotification] error:', error)
return error
}
},
);
}
)
exports.createNewFollowerNotification = onDocumentWritten(
{
region: "europe-west1",
document: "users/{userId}",
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() || {};
const { userId } = event.params || {}
const before = event?.data?.before?.data() || {}
const after = event?.data?.after?.data() || {}
if (!userId || !after) {
return null;
return null
}
const beforeFollowers = Array.isArray(before?.followedBy)
? before.followedBy
: [];
const afterFollowers = Array.isArray(after?.followedBy)
? after.followedBy
: [];
const beforeFollowers = Array.isArray(before?.followedBy) ? before.followedBy : []
const afterFollowers = Array.isArray(after?.followedBy) ? after.followedBy : []
if (afterFollowers.length <= beforeFollowers.length) {
return null;
return null
}
const previousSet = new Set(beforeFollowers);
const newFollowers = afterFollowers.filter(
(uid) => !previousSet.has(uid),
);
const previousSet = new Set(beforeFollowers)
const newFollowers = afterFollowers.filter((uid) => !previousSet.has(uid))
if (!newFollowers.length) {
return null;
return null
}
await Promise.all(
newFollowers.map(async (followerId) => {
if (!followerId || followerId === userId) {
return null;
return null
}
const followerSnap = await refList.users.doc(followerId).get();
const follower = followerSnap?.data() || {};
const followerSnap = await refList.users.doc(followerId).get()
const follower = followerSnap?.data() || {}
const followerName =
typeof follower?.userName === "string" && follower.userName.trim()
typeof follower?.userName === 'string' && follower.userName.trim()
? follower.userName.trim()
: "Un utilisateur";
: 'Un utilisateur'
const message = `${followerName} te suit maintenant`;
const message = `${followerName} te suit maintenant`
await sendNotification({
sender: followerId,
receiver: userId,
receiverCollection: "users",
title: "Nouvel abonné",
receiverCollection: 'users',
title: 'Nouvel abonné',
message,
data: {
type: ALERT_TYPE?.NEW_FOLLOWER,
followerId,
followerName,
followerProfilePicture: follower?.profilePicture || "",
followerProfilePicture: follower?.profilePicture || '',
},
});
})
return null;
}),
);
return null
})
)
return null;
return null
} catch (error) {
console.log("[createNewFollowerNotification] error:", error);
return error;
console.log('[createNewFollowerNotification] error:', error)
return error
}
},
);
}
)