mirror of
https://github.com/urosran/cally.git
synced 2025-07-10 15:17:17 +00:00
Notification changes
This commit is contained in:
@ -18,23 +18,190 @@ const GOOGLE_CALENDAR_ID = "primary";
|
||||
const CHANNEL_ID = "cally-family-calendar";
|
||||
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
||||
|
||||
async function getPushTokensForFamily(familyId, excludeUserId = null) {
|
||||
const usersRef = db.collection('Profiles');
|
||||
const snapshot = await usersRef.where('familyId', '==', familyId).get();
|
||||
let pushTokens = [];
|
||||
|
||||
snapshot.forEach(doc => {
|
||||
const data = doc.data();
|
||||
if ((!excludeUserId || data.uid !== excludeUserId) && data.pushToken) {
|
||||
pushTokens.push(data.pushToken);
|
||||
}
|
||||
});
|
||||
|
||||
return pushTokens;
|
||||
}
|
||||
|
||||
exports.sendOverviews = functions.pubsub
|
||||
.schedule('0 20 * * *')
|
||||
.onRun(async (context) => {
|
||||
const familiesSnapshot = await admin.firestore().collection('Families').get();
|
||||
|
||||
for (const familyDoc of familiesSnapshot.docs) {
|
||||
const familyId = familyDoc.id;
|
||||
const familySettings = familyDoc.data()?.settings || {};
|
||||
const overviewTime = familySettings.overviewTime || '20:00';
|
||||
|
||||
const [hours, minutes] = overviewTime.split(':');
|
||||
const now = new Date();
|
||||
if (now.getHours() !== parseInt(hours) || now.getMinutes() !== parseInt(minutes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
|
||||
const tomorrowEnd = new Date(tomorrow);
|
||||
tomorrowEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
const weekEnd = new Date(tomorrow);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
weekEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
const [tomorrowEvents, weekEvents] = await Promise.all([
|
||||
admin.firestore()
|
||||
.collection('Events')
|
||||
.where('familyId', '==', familyId)
|
||||
.where('startDate', '>=', tomorrow)
|
||||
.where('startDate', '<=', tomorrowEnd)
|
||||
.orderBy('startDate')
|
||||
.limit(3)
|
||||
.get(),
|
||||
|
||||
admin.firestore()
|
||||
.collection('Events')
|
||||
.where('familyId', '==', familyId)
|
||||
.where('startDate', '>', tomorrowEnd)
|
||||
.where('startDate', '<=', weekEnd)
|
||||
.orderBy('startDate')
|
||||
.limit(3)
|
||||
.get()
|
||||
]);
|
||||
|
||||
if (tomorrowEvents.empty && weekEvents.empty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let notificationBody = '';
|
||||
|
||||
if (!tomorrowEvents.empty) {
|
||||
notificationBody += 'Tomorrow: ';
|
||||
const tomorrowTitles = tomorrowEvents.docs.map(doc => doc.data().title);
|
||||
notificationBody += tomorrowTitles.join(', ');
|
||||
if (tomorrowEvents.size === 3) notificationBody += ' and more...';
|
||||
}
|
||||
|
||||
if (!weekEvents.empty) {
|
||||
if (notificationBody) notificationBody += '\n\n';
|
||||
notificationBody += 'This week: ';
|
||||
const weekTitles = weekEvents.docs.map(doc => doc.data().title);
|
||||
notificationBody += weekTitles.join(', ');
|
||||
if (weekEvents.size === 3) notificationBody += ' and more...';
|
||||
}
|
||||
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Family Calendar Overview",
|
||||
body: notificationBody,
|
||||
data: {
|
||||
type: 'calendar_overview',
|
||||
date: tomorrow.toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
await storeNotification({
|
||||
type: 'calendar_overview',
|
||||
familyId,
|
||||
content: notificationBody,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error sending overview for family ${familyId}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.sendWeeklyOverview = functions.pubsub
|
||||
.schedule('0 20 * * 0')
|
||||
.onRun(async (context) => {
|
||||
const familiesSnapshot = await admin.firestore().collection('Families').get();
|
||||
|
||||
for (const familyDoc of familiesSnapshot.docs) {
|
||||
const familyId = familyDoc.id;
|
||||
const familySettings = familyDoc.data()?.settings || {};
|
||||
const overviewTime = familySettings.weeklyOverviewTime || '20:00';
|
||||
|
||||
const [hours, minutes] = overviewTime.split(':');
|
||||
const now = new Date();
|
||||
if (now.getHours() !== parseInt(hours) || now.getMinutes() !== parseInt(minutes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const weekStart = new Date();
|
||||
weekStart.setDate(weekStart.getDate() + 1);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
|
||||
const weekEvents = await admin.firestore()
|
||||
.collection('Events')
|
||||
.where('familyId', '==', familyId)
|
||||
.where('startDate', '>=', weekStart)
|
||||
.where('startDate', '<=', weekEnd)
|
||||
.orderBy('startDate')
|
||||
.limit(3)
|
||||
.get();
|
||||
|
||||
if (weekEvents.empty) continue;
|
||||
|
||||
const eventTitles = weekEvents.docs.map(doc => doc.data().title);
|
||||
const notificationBody = `Next week: ${eventTitles.join(', ')}${weekEvents.size === 3 ? ' and more...' : ''}`;
|
||||
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Weekly Calendar Overview",
|
||||
body: notificationBody,
|
||||
data: {
|
||||
type: 'weekly_overview',
|
||||
weekStart: weekStart.toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
await storeNotification({
|
||||
type: 'weekly_overview',
|
||||
familyId,
|
||||
content: notificationBody,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error sending weekly overview for family ${familyId}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.sendNotificationOnEventCreation = functions.firestore
|
||||
.document('Events/{eventId}')
|
||||
.onCreate(async (snapshot, context) => {
|
||||
const eventData = snapshot.data();
|
||||
const { familyId, creatorId, email, title } = eventData;
|
||||
|
||||
if (!!eventData?.externalOrigin) {
|
||||
console.log('Externally synced event, ignoring.')
|
||||
return;
|
||||
}
|
||||
const {familyId, creatorId, email, title, externalOrigin} = eventData;
|
||||
|
||||
if (!familyId || !creatorId) {
|
||||
console.error('Missing familyId or creatorId in event data');
|
||||
return;
|
||||
}
|
||||
|
||||
let pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
|
||||
// Get push tokens - exclude creator for manual events, include everyone for synced events
|
||||
let pushTokens = await getPushTokensForFamily(
|
||||
familyId,
|
||||
externalOrigin ? null : creatorId // Only exclude creator for manual events
|
||||
);
|
||||
|
||||
if (!pushTokens.length) {
|
||||
console.log('No push tokens available for the event.');
|
||||
@ -48,9 +215,16 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
notificationInProgress = true;
|
||||
|
||||
notificationTimeout = setTimeout(async () => {
|
||||
const eventMessage = eventCount === 1
|
||||
? `An event "${title}" has been added. Check it out!`
|
||||
: `${eventCount} new events have been added.`;
|
||||
let eventMessage;
|
||||
if (externalOrigin) {
|
||||
eventMessage = eventCount === 1
|
||||
? `Calendar sync completed: "${title}" has been added.`
|
||||
: `Calendar sync completed: ${eventCount} new events have been added.`;
|
||||
} else {
|
||||
eventMessage = eventCount === 1
|
||||
? `New event "${title}" has been added to the family calendar.`
|
||||
: `${eventCount} new events have been added to the family calendar.`;
|
||||
}
|
||||
|
||||
let messages = pushTokens.map(pushToken => {
|
||||
if (!Expo.isExpoPushToken(pushToken)) {
|
||||
@ -61,9 +235,12 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
return {
|
||||
to: pushToken,
|
||||
sound: 'default',
|
||||
title: 'New Events Added!',
|
||||
title: externalOrigin ? 'Calendar Sync Complete' : 'New Family Calendar Events',
|
||||
body: eventMessage,
|
||||
data: { eventId: context.params.eventId },
|
||||
data: {
|
||||
eventId: context.params.eventId,
|
||||
type: externalOrigin ? 'sync' : 'manual'
|
||||
},
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
@ -90,13 +267,15 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
}
|
||||
}
|
||||
|
||||
// Save the notification in Firestore for record-keeping
|
||||
// Save the notification in Firestore
|
||||
const notificationData = {
|
||||
creatorId,
|
||||
familyId,
|
||||
content: eventMessage,
|
||||
eventId: context.params.eventId,
|
||||
type: externalOrigin ? 'sync' : 'manual',
|
||||
timestamp: Timestamp.now(),
|
||||
date: eventData.startDate
|
||||
};
|
||||
|
||||
try {
|
||||
@ -106,15 +285,159 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
console.error("Error saving notification to Firestore:", error);
|
||||
}
|
||||
|
||||
// Reset state variables after notifications are sent
|
||||
// Reset state variables
|
||||
eventCount = 0;
|
||||
pushTokens = [];
|
||||
notificationInProgress = false;
|
||||
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
exports.onEventUpdate = functions.firestore
|
||||
.document('Events/{eventId}')
|
||||
.onUpdate(async (change, context) => {
|
||||
const beforeData = change.before.data();
|
||||
const afterData = change.after.data();
|
||||
const {familyId, title, lastModifiedBy} = afterData;
|
||||
|
||||
// Skip if no meaningful changes
|
||||
if (JSON.stringify(beforeData) === JSON.stringify(afterData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get push tokens excluding the user who made the change
|
||||
const pushTokens = await getPushTokensForFamily(familyId, lastModifiedBy);
|
||||
|
||||
const message = `Event "${title}" has been updated`;
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Event Updated",
|
||||
body: message,
|
||||
data: {
|
||||
type: 'event_update',
|
||||
eventId: context.params.eventId
|
||||
}
|
||||
});
|
||||
|
||||
// Store notification in Firestore
|
||||
await storeNotification({
|
||||
type: 'event_update',
|
||||
familyId,
|
||||
content: message,
|
||||
eventId: context.params.eventId,
|
||||
excludedUser: lastModifiedBy,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
date: eventData.startDate
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending event update notification:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Upcoming Event Reminders
|
||||
exports.checkUpcomingEvents = functions.pubsub
|
||||
.schedule('every 5 minutes')
|
||||
.onRun(async (context) => {
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const eventsSnapshot = await admin.firestore().collection('Events').get();
|
||||
|
||||
for (const doc of eventsSnapshot.docs) {
|
||||
const event = doc.data();
|
||||
const {startDate, familyId, title, allDay, creatorId} = event;
|
||||
|
||||
if (startDate.toDate() < now.toDate()) continue;
|
||||
|
||||
try {
|
||||
const familyDoc = await admin.firestore().collection('Families').doc(familyId).get();
|
||||
const familySettings = familyDoc.data()?.settings || {};
|
||||
const reminderTime = familySettings.defaultReminderTime || 15; // minutes
|
||||
|
||||
const eventTime = startDate.toDate();
|
||||
const reminderThreshold = new Date(now.toDate().getTime() + (reminderTime * 60 * 1000));
|
||||
|
||||
// For all-day events, send reminder the evening before
|
||||
if (allDay) {
|
||||
const eveningBefore = new Date(eventTime);
|
||||
eveningBefore.setDate(eveningBefore.getDate() - 1);
|
||||
eveningBefore.setHours(20, 0, 0, 0);
|
||||
|
||||
if (now.toDate() >= eveningBefore && !event.eveningReminderSent) {
|
||||
// Get all family members' tokens (including creator for reminders)
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Tomorrow's All-Day Event",
|
||||
body: `Tomorrow: ${title}`,
|
||||
data: {
|
||||
type: 'event_reminder',
|
||||
eventId: doc.id
|
||||
}
|
||||
});
|
||||
|
||||
await doc.ref.update({eveningReminderSent: true});
|
||||
}
|
||||
}
|
||||
// For regular events, check if within reminder threshold
|
||||
else if (eventTime <= reminderThreshold && !event.reminderSent) {
|
||||
// Include creator for reminders
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Upcoming Event",
|
||||
body: `In ${reminderTime} minutes: ${title}`,
|
||||
data: {
|
||||
type: 'event_reminder',
|
||||
eventId: doc.id
|
||||
}
|
||||
});
|
||||
|
||||
await doc.ref.update({reminderSent: true});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing reminder for event ${doc.id}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function storeNotification(notificationData) {
|
||||
try {
|
||||
await admin.firestore()
|
||||
.collection('Notifications')
|
||||
.add(notificationData);
|
||||
} catch (error) {
|
||||
console.error('Error storing notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNotifications(pushTokens, notification) {
|
||||
if (!pushTokens.length) return;
|
||||
|
||||
const messages = pushTokens
|
||||
.filter(token => Expo.isExpoPushToken(token))
|
||||
.map(pushToken => ({
|
||||
to: pushToken,
|
||||
sound: 'default',
|
||||
priority: 'high',
|
||||
...notification
|
||||
}));
|
||||
|
||||
const chunks = expo.chunkPushNotifications(messages);
|
||||
|
||||
for (let chunk of chunks) {
|
||||
try {
|
||||
const tickets = await expo.sendPushNotificationsAsync(chunk);
|
||||
for (let ticket of tickets) {
|
||||
if (ticket.status === "error") {
|
||||
if (ticket.details?.error === "DeviceNotRegistered") {
|
||||
await removeInvalidPushToken(ticket.to);
|
||||
}
|
||||
console.error('Push notification error:', ticket.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending notifications:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.createSubUser = onRequest(async (request, response) => {
|
||||
const authHeader = request.get('Authorization');
|
||||
|
||||
@ -209,7 +532,7 @@ exports.removeSubUser = onRequest(async (request, response) => {
|
||||
|
||||
logger.info("Processing user removal", {requestBody: request.body.data});
|
||||
|
||||
const { userId, familyId } = request.body.data;
|
||||
const {userId, familyId} = request.body.data;
|
||||
|
||||
if (!userId || !familyId) {
|
||||
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
|
||||
@ -397,7 +720,22 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
|
||||
}
|
||||
|
||||
async function removeInvalidPushToken(pushToken) {
|
||||
// TODO
|
||||
try {
|
||||
const profilesRef = db.collection('Profiles');
|
||||
const snapshot = await profilesRef.where('pushToken', '==', pushToken).get();
|
||||
|
||||
const batch = db.batch();
|
||||
snapshot.forEach(doc => {
|
||||
batch.update(doc.ref, {
|
||||
pushToken: admin.firestore.FieldValue.delete()
|
||||
});
|
||||
});
|
||||
|
||||
await batch.commit();
|
||||
console.log(`Removed invalid push token: ${pushToken}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing invalid push token:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = require("node-fetch");
|
||||
@ -667,7 +1005,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId }) {
|
||||
async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId}) {
|
||||
const baseDate = new Date();
|
||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||
@ -700,7 +1038,7 @@ async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId,
|
||||
token = refreshedToken;
|
||||
|
||||
if (token) {
|
||||
return fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId });
|
||||
return fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId});
|
||||
} else {
|
||||
console.error(`Failed to refresh token for user: ${email}`);
|
||||
await clearToken(email);
|
||||
@ -717,8 +1055,12 @@ async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId,
|
||||
const googleEvent = {
|
||||
id: item.id,
|
||||
title: item.summary || "",
|
||||
startDate: item.start?.dateTime ? new Date(item.start.dateTime) : new Date(item.start.date),
|
||||
endDate: item.end?.dateTime ? new Date(item.end.dateTime) : new Date(item.end.date),
|
||||
startDate: item.start?.dateTime
|
||||
? new Date(item.start.dateTime)
|
||||
: new Date(item.start.date + 'T00:00:00'),
|
||||
endDate: item.end?.dateTime
|
||||
? new Date(item.end.dateTime)
|
||||
: new Date(new Date(item.end.date + 'T00:00:00').setDate(new Date(item.end.date).getDate() - 1)),
|
||||
allDay: !item.start?.dateTime,
|
||||
familyId,
|
||||
email,
|
||||
@ -743,12 +1085,12 @@ async function saveEventsToFirestore(events) {
|
||||
const batch = db.batch();
|
||||
events.forEach((event) => {
|
||||
const eventRef = db.collection("Events").doc(event.id);
|
||||
batch.set(eventRef, event, { merge: true });
|
||||
batch.set(eventRef, event, {merge: true});
|
||||
});
|
||||
await batch.commit();
|
||||
}
|
||||
|
||||
async function calendarSync({ userId, email, token, refreshToken, familyId }) {
|
||||
async function calendarSync({userId, email, token, refreshToken, familyId}) {
|
||||
console.log(`Starting calendar sync for user ${userId} with email ${email}`);
|
||||
try {
|
||||
await fetchAndSaveGoogleEvents({
|
||||
@ -787,7 +1129,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { googleAccounts } = userData;
|
||||
const {googleAccounts} = userData;
|
||||
const email = Object.keys(googleAccounts || {})[0];
|
||||
const accountData = googleAccounts[email] || {};
|
||||
const token = accountData.accessToken;
|
||||
@ -795,7 +1137,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
const familyId = userData.familyId;
|
||||
|
||||
console.log("Starting calendar sync...");
|
||||
await calendarSync({ userId, email, token, refreshToken, familyId });
|
||||
await calendarSync({userId, email, token, refreshToken, familyId});
|
||||
console.log("Calendar sync completed.");
|
||||
|
||||
res.status(200).send("Sync notification sent.");
|
||||
@ -803,4 +1145,202 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||
res.status(500).send("Failed to send sync notification.");
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshMicrosoftToken(refreshToken) {
|
||||
try {
|
||||
console.log("Refreshing Microsoft token...");
|
||||
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af",
|
||||
scope: "openid profile email offline_access Calendars.ReadWrite User.Read",
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error("Error refreshing Microsoft token:", errorData);
|
||||
throw new Error(`Failed to refresh Microsoft token: ${errorData.error || response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Microsoft token refreshed successfully");
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token || refreshToken
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error refreshing Microsoft token:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndSaveMicrosoftEvents({token, refreshToken, email, familyId, creatorId}) {
|
||||
const baseDate = new Date();
|
||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||
|
||||
try {
|
||||
console.log(`Fetching Microsoft calendar events for user: ${email}`);
|
||||
|
||||
const url = `https://graph.microsoft.com/v1.0/me/calendar/events`;
|
||||
const queryParams = new URLSearchParams({
|
||||
$select: 'subject,start,end,id',
|
||||
$filter: `start/dateTime ge '${timeMin}' and end/dateTime le '${timeMax}'`
|
||||
});
|
||||
|
||||
const response = await fetch(`${url}?${queryParams}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 401 && refreshToken) {
|
||||
console.log(`Token expired for user: ${email}, attempting to refresh`);
|
||||
const {accessToken: newToken} = await refreshMicrosoftToken(refreshToken);
|
||||
if (newToken) {
|
||||
return fetchAndSaveMicrosoftEvents({
|
||||
token: newToken,
|
||||
refreshToken,
|
||||
email,
|
||||
familyId,
|
||||
creatorId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||
}
|
||||
|
||||
const events = data.value.map(item => ({
|
||||
id: item.id,
|
||||
title: item.subject || "",
|
||||
startDate: new Date(item.start.dateTime + 'Z'),
|
||||
endDate: new Date(item.end.dateTime + 'Z'),
|
||||
allDay: false, // Microsoft Graph API handles all-day events differently
|
||||
familyId,
|
||||
email,
|
||||
creatorId,
|
||||
externalOrigin: "microsoft"
|
||||
}));
|
||||
|
||||
console.log(`Saving Microsoft calendar events to Firestore for user: ${email}`);
|
||||
await saveEventsToFirestore(events);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Microsoft Calendar events for ${email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribeMicrosoftCalendar(accessToken, userId) {
|
||||
try {
|
||||
console.log(`Setting up Microsoft calendar subscription for user ${userId}`);
|
||||
|
||||
const subscription = {
|
||||
changeType: "created,updated,deleted",
|
||||
notificationUrl: `https://us-central1-cally-family-calendar.cloudfunctions.net/microsoftCalendarWebhook?userId=${userId}`,
|
||||
resource: "/me/calendar/events",
|
||||
expirationDateTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days
|
||||
clientState: userId
|
||||
};
|
||||
|
||||
const response = await fetch('https://graph.microsoft.com/v1.0/subscriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create subscription: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const subscriptionData = await response.json();
|
||||
|
||||
// Store subscription details in Firestore
|
||||
await admin.firestore().collection('MicrosoftSubscriptions').doc(userId).set({
|
||||
subscriptionId: subscriptionData.id,
|
||||
expirationDateTime: subscriptionData.expirationDateTime,
|
||||
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
|
||||
console.log(`Microsoft calendar subscription created for user ${userId}`);
|
||||
return subscriptionData;
|
||||
} catch (error) {
|
||||
console.error(`Error creating Microsoft calendar subscription for user ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
exports.microsoftCalendarWebhook = functions.https.onRequest(async (req, res) => {
|
||||
const userId = req.query.userId;
|
||||
console.log(`Received Microsoft calendar webhook for user ${userId}`, req.body);
|
||||
|
||||
try {
|
||||
const userDoc = await admin.firestore().collection("Profiles").doc(userId).get();
|
||||
const userData = userDoc.data();
|
||||
|
||||
if (!userData?.microsoftAccounts) {
|
||||
console.log(`No Microsoft account found for user ${userId}`);
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
const email = Object.keys(userData.microsoftAccounts)[0];
|
||||
const token = userData.microsoftAccounts[email];
|
||||
|
||||
await fetchAndSaveMicrosoftEvents({
|
||||
token,
|
||||
email,
|
||||
familyId: userData.familyId,
|
||||
creatorId: userId
|
||||
});
|
||||
|
||||
res.status(200).send();
|
||||
} catch (error) {
|
||||
console.error(`Error processing Microsoft calendar webhook for user ${userId}:`, error);
|
||||
res.status(500).send();
|
||||
}
|
||||
});
|
||||
exports.renewMicrosoftSubscriptions = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
|
||||
console.log('Starting Microsoft subscription renewal process');
|
||||
|
||||
try {
|
||||
const subscriptionsSnapshot = await admin.firestore()
|
||||
.collection('MicrosoftSubscriptions')
|
||||
.get();
|
||||
|
||||
for (const doc of subscriptionsSnapshot.docs) {
|
||||
const userId = doc.id;
|
||||
const userDoc = await admin.firestore().collection('Profiles').doc(userId).get();
|
||||
const userData = userDoc.data();
|
||||
|
||||
if (userData?.microsoftAccounts) {
|
||||
const email = Object.keys(userData.microsoftAccounts)[0];
|
||||
const token = userData.microsoftAccounts[email];
|
||||
|
||||
try {
|
||||
await subscribeMicrosoftCalendar(token, userId);
|
||||
console.log(`Renewed Microsoft subscription for user ${userId}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to renew Microsoft subscription for user ${userId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in Microsoft subscription renewal process:', error);
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user