475 lines
12 KiB
JavaScript
475 lines
12 KiB
JavaScript
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");
|
|
|
|
// Initialisation de Expo SDK
|
|
let expo = new Expo();
|
|
const db = admin.firestore();
|
|
|
|
exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
|
|
{ region: "europe-west1", document: "notifications/{notificationId}" },
|
|
async (event) => {
|
|
try {
|
|
const {
|
|
receiver = null,
|
|
title = "MusicLand",
|
|
receiverCollection = "users",
|
|
message = "",
|
|
data: notifData = {},
|
|
} = event.data.data();
|
|
|
|
if (!receiver || !message) {
|
|
throw new Error("Receiver and message are required");
|
|
}
|
|
|
|
const userSnap = await db.collection(receiverCollection).doc(receiver).get();
|
|
const { pushToken = null, pushTokens = [] } = userSnap?.data() || {};
|
|
|
|
const tokensSet = new Set(
|
|
[]
|
|
.concat(Array.isArray(pushTokens) ? pushTokens : [])
|
|
.concat(pushToken ? [pushToken] : [])
|
|
.filter(Boolean)
|
|
);
|
|
const tokens = Array.from(tokensSet);
|
|
|
|
if (!tokens?.length) {
|
|
console.warn("User push token not found");
|
|
return null;
|
|
}
|
|
|
|
await sendExpoNotification({
|
|
tokens,
|
|
receiverId: receiver,
|
|
receiverCollection,
|
|
title: title || "MusicLand",
|
|
message: message,
|
|
data: notifData || {},
|
|
});
|
|
} catch (e) {
|
|
console.log(e);
|
|
return e;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Fonction pour envoyer la notification via Expo SDK
|
|
async function sendExpoNotification({
|
|
tokens = [],
|
|
title = "",
|
|
message = "",
|
|
data = {},
|
|
receiverId = null,
|
|
receiverCollection = "users",
|
|
}) {
|
|
try {
|
|
const candidateTokens = Array.isArray(tokens) ? tokens : [tokens];
|
|
const validTokens = [];
|
|
const invalidTokens = [];
|
|
|
|
candidateTokens.forEach((token) => {
|
|
if (Expo.isExpoPushToken(token)) {
|
|
validTokens.push(token);
|
|
} else if (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 };
|
|
}
|
|
|
|
const messages = validTokens.map((token) => ({
|
|
to: token,
|
|
sound: "default",
|
|
title: title,
|
|
body: message,
|
|
data: data || {},
|
|
priority: "high",
|
|
badge: 1,
|
|
channelId: "default",
|
|
}));
|
|
|
|
const chunks = expo.chunkPushNotifications(messages);
|
|
const receipts = [];
|
|
const tokensToPrune = new Set();
|
|
|
|
for (const chunk of chunks) {
|
|
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 (token) {
|
|
tokensToPrune.add(token);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
receipts.push(...chunkReceipts);
|
|
}
|
|
|
|
if (tokensToPrune.size && receiverId) {
|
|
await removeInvalidTokens({
|
|
tokens: Array.from(tokensToPrune),
|
|
receiverId,
|
|
receiverCollection,
|
|
});
|
|
}
|
|
|
|
console.log("Sent push notifications:", receipts);
|
|
|
|
return { sent: true };
|
|
} catch (e) {
|
|
console.log("Error sending notification:", e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function removeInvalidTokens({ tokens = [], receiverId, receiverCollection }) {
|
|
try {
|
|
if (!receiverId || !tokens.length) {
|
|
return;
|
|
}
|
|
|
|
const uniqueTokens = Array.from(new Set(tokens.filter(Boolean)));
|
|
if (!uniqueTokens.length) {
|
|
return;
|
|
}
|
|
|
|
const docRef = db.collection(receiverCollection).doc(receiverId);
|
|
const userSnap = await docRef.get();
|
|
const userData = userSnap?.data() || {};
|
|
|
|
const updates = {
|
|
pushTokens: admin.firestore.FieldValue.arrayRemove(...uniqueTokens),
|
|
};
|
|
|
|
if (uniqueTokens.includes(userData?.pushToken)) {
|
|
updates.pushToken = admin.firestore.FieldValue.delete();
|
|
}
|
|
|
|
await docRef.set(updates, { merge: true });
|
|
console.log(
|
|
"Pruned invalid push tokens",
|
|
JSON.stringify({ receiverId, tokens: uniqueTokens }, null, 2)
|
|
);
|
|
} catch (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",
|
|
receiver = null,
|
|
receiverCollection = "users",
|
|
title = "",
|
|
message = null,
|
|
data = {},
|
|
}) => {
|
|
try {
|
|
if (!receiver || !message) {
|
|
throw new Error("Receiver and message are required");
|
|
}
|
|
const payload = {
|
|
sender,
|
|
receiver,
|
|
receiverCollection,
|
|
title,
|
|
message,
|
|
time: admin.firestore.FieldValue.serverTimestamp(),
|
|
read: false,
|
|
readAt: null,
|
|
data,
|
|
};
|
|
const { id } = await refList.notifications.add(payload);
|
|
console.log(
|
|
"[sendNotification] Notification created",
|
|
JSON.stringify({ id, receiver, receiverCollection }, null, 2)
|
|
);
|
|
|
|
return id;
|
|
} catch (e) {
|
|
console.log("[sendNotification] Error creating notification:", e);
|
|
}
|
|
};
|
|
|
|
exports.sendNotification = sendNotification;
|
|
|
|
exports.createProjectCommentNotification = onDocumentCreated(
|
|
{
|
|
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();
|
|
|
|
if (!projectId || !comment) {
|
|
console.log(
|
|
"[createProjectCommentNotification] Missing project/comment data",
|
|
{ hasProjectId: !!projectId, hasComment: !!comment }
|
|
);
|
|
return null;
|
|
}
|
|
|
|
console.log(
|
|
"[createProjectCommentNotification] Comment payload",
|
|
JSON.stringify(comment, null, 2)
|
|
);
|
|
|
|
const projectSnap = await refList.projects.doc(projectId).get();
|
|
if (!projectSnap.exists) {
|
|
console.log(
|
|
"[createProjectCommentNotification] Project not found",
|
|
projectId
|
|
);
|
|
return 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;
|
|
}
|
|
|
|
const commenterName =
|
|
typeof comment?.userName === "string" && comment.userName.trim()
|
|
? comment.userName.trim()
|
|
: "Un utilisateur";
|
|
const projectTitle =
|
|
typeof project.title === "string" && project.title.trim()
|
|
? project.title.trim()
|
|
: "ton projet";
|
|
const message = `${commenterName} a commenté ton projet "${projectTitle}"`;
|
|
|
|
console.log(
|
|
"[createProjectCommentNotification] Creating notification",
|
|
JSON.stringify(
|
|
{
|
|
receiver,
|
|
message,
|
|
commentId,
|
|
projectId,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
|
|
await sendNotification({
|
|
sender: comment.userId || "SYSTEM",
|
|
receiver,
|
|
receiverCollection: "users",
|
|
title: "Nouveau commentaire",
|
|
message,
|
|
data: {
|
|
type: ALERT_TYPE?.NEW_COMMENT,
|
|
projectId,
|
|
commentId,
|
|
commenterId: comment.userId || null,
|
|
commenterName: commenterName,
|
|
commenterProfilePicture: comment?.profilePicture || "",
|
|
text:
|
|
typeof comment?.text === "string" && comment.text.trim()
|
|
? comment.text.trim()
|
|
: "",
|
|
},
|
|
});
|
|
|
|
console.log(
|
|
"[createProjectCommentNotification] Notification creation complete"
|
|
);
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.log("createProjectCommentNotification error:", error);
|
|
return error;
|
|
}
|
|
}
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|
|
);
|