From ae53aac5796771768309611dfa372cc8d3f7b860 Mon Sep 17 00:00:00 2001 From: Ritam Nandi Date: Wed, 17 Sep 2025 16:14:29 -0500 Subject: [PATCH 1/2] Enrolling tags into notifications --- src/services/attendee/attendee-router.ts | 57 ++++++++++++++++--- .../notifications/notifications-router.ts | 21 +++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/services/attendee/attendee-router.ts b/src/services/attendee/attendee-router.ts index ebfd7309..8e3b0a8d 100644 --- a/src/services/attendee/attendee-router.ts +++ b/src/services/attendee/attendee-router.ts @@ -433,13 +433,23 @@ attendeeRouter.patch( const payload = res.locals.payload; const userId = payload.userId; - const { tags } = AttendeeTagsUpdateValidator.parse(req.body); + const { tags: newTags } = AttendeeTagsUpdateValidator.parse(req.body); // Check if the user exists in the database - const { data: user } = await SupabaseDB.ATTENDEES.select("tags") - .eq("userId", userId) - .maybeSingle() - .throwOnError(); + + const [userData, notificationData] = await Promise.all([ + SupabaseDB.ATTENDEES.select("tags") + .eq("userId", userId) + .maybeSingle() + .throwOnError(), + SupabaseDB.NOTIFICATIONS.select("deviceId") + .eq("userId", userId) + .maybeSingle() + .throwOnError(), + ]); + + const user = userData.data; + const deviceId = notificationData.data?.deviceId; if (!user) { return res @@ -447,12 +457,45 @@ attendeeRouter.patch( .json({ error: "UserNotFound" }); } + const oldTags = user.tags || []; + + if (deviceId) { + const tagsToSubscribe = newTags.filter( + (tag) => !oldTags.includes(tag) + ); + const tagsToUnsubscribe = oldTags.filter( + (tag) => !newTags.includes(tag) + ); + const subscribePromises = tagsToSubscribe.map((tag) => { + const topicName = `tag_${tag.replace(/[^a-zA-Z0-9-_.~%]/g, "_")}`; + return getFirebaseAdmin() + .messaging() + .subscribeToTopic(deviceId, topicName); + }); + + const unsubscribePromises = tagsToUnsubscribe.map((tag) => { + const topicName = `tag_${tag.replace(/[^a-zA-Z0-9-_.~%]/g, "_")}`; + return getFirebaseAdmin() + .messaging() + .unsubscribeFromTopic(deviceId, topicName); + }); + + Promise.all([...subscribePromises, ...unsubscribePromises]).catch( + (error) => { + console.error( + `Failed to sync FCM topics for user ${userId}:`, + error + ); + } + ); + } + // Update the tags - await SupabaseDB.ATTENDEES.update({ tags }) + await SupabaseDB.ATTENDEES.update({ tags: newTags }) .eq("userId", userId) .throwOnError(); - return res.status(StatusCodes.OK).json({ tags }); + return res.status(StatusCodes.OK).json({ tags: newTags }); } ); diff --git a/src/services/notifications/notifications-router.ts b/src/services/notifications/notifications-router.ts index b01544c8..16b4be1b 100644 --- a/src/services/notifications/notifications-router.ts +++ b/src/services/notifications/notifications-router.ts @@ -34,6 +34,27 @@ notificationsRouter.post( .messaging() .subscribeToTopic(notificationEnrollmentData.deviceId, "allUsers"); + // Get their tags + const { data: attendee } = await SupabaseDB.ATTENDEES.select("tags") + .eq("userId", userId) + .maybeSingle() + .throwOnError(); + + // enroll them in a topic for the tags + if (attendee?.tags && attendee.tags.length > 0) { + const userTags = attendee.tags; + const subscriptionPromises = userTags.map((tag) => { + const topicName = `tag_${tag.replace(/[^a-zA-Z0-9-_.~%]/g, "_")}`; + return getFirebaseAdmin() + .messaging() + .subscribeToTopic( + notificationEnrollmentData.deviceId, + topicName + ); + }); + await Promise.all(subscriptionPromises); + } + return res.status(StatusCodes.CREATED).json(notificationEnrollmentData); } ); From 450f23fa46dd2b6c1384703c98fd3fa9995d5c90 Mon Sep 17 00:00:00 2001 From: Ritam Nandi Date: Wed, 17 Sep 2025 16:25:26 -0500 Subject: [PATCH 2/2] Add to custom topics --- .../notifications-router.test.ts | 11 ++++++++ .../notifications/notifications-router.ts | 26 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/services/notifications/notifications-router.test.ts b/src/services/notifications/notifications-router.test.ts index 30243066..4619d3e0 100644 --- a/src/services/notifications/notifications-router.test.ts +++ b/src/services/notifications/notifications-router.test.ts @@ -221,6 +221,17 @@ describe("/notifications", () => { `event_Test_Event`, currentDayTopic, "food_wave_1", + "tag_AI", + "tag_Art_Media", + "tag_Autonomous_Vehicles", + "tag_Career_Readiness", + "tag_Company_Talk", + "tag_Cybersecurity", + "tag_Ethics", + "tag_HCI", + "tag_Interactive_Events", + "tag_Networking", + "tag_Research", ].sort(); expect(response.body.topics).toEqual(expectedTopics); diff --git a/src/services/notifications/notifications-router.ts b/src/services/notifications/notifications-router.ts index 16b4be1b..3f999e98 100644 --- a/src/services/notifications/notifications-router.ts +++ b/src/services/notifications/notifications-router.ts @@ -185,8 +185,32 @@ notificationsRouter.get( await SupabaseDB.CUSTOM_TOPICS.select("topicName").throwOnError(); const customTopics = customTopicsData.map((topic) => topic.topicName) ?? []; + + const hardcodedTags = [ + "Career Readiness", + "AI", + "Research", + "Interactive Events", + "HCI", + "Ethics", + "Art/Media", + "Autonomous Vehicles", + "Networking", + "Company Talk", + "Cybersecurity", + ]; + + const tagTopics = hardcodedTags.map( + (tag) => `tag_${tag.replace(/[^a-zA-Z0-9-_.~%]/g, "_")}` + ); + const allTopics = [ - ...new Set([...staticTopics, ...eventTopics, ...customTopics]), + ...new Set([ + ...staticTopics, + ...eventTopics, + ...customTopics, + ...tagTopics, + ]), ]; return res.status(StatusCodes.OK).send({ topics: allTopics.sort() }); }