mirror of
https://github.com/urosran/cally.git
synced 2025-11-26 08:24:55 +00:00
cal sync improvements
This commit is contained in:
@ -9,27 +9,21 @@ const {Expo} = require('expo-server-sdk');
|
|||||||
admin.initializeApp();
|
admin.initializeApp();
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
|
|
||||||
let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN });
|
let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN});
|
||||||
let notificationTimeout = null;
|
let notificationTimeout = null;
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
let pushTokens = [];
|
let notificationInProgress = false;
|
||||||
|
|
||||||
const GOOGLE_CALENDAR_ID = "primary";
|
const GOOGLE_CALENDAR_ID = "primary";
|
||||||
const CHANNEL_ID = "cally-family-calendar";
|
const CHANNEL_ID = "cally-family-calendar";
|
||||||
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
||||||
|
|
||||||
|
|
||||||
exports.sendNotificationOnEventCreation = functions.firestore
|
exports.sendNotificationOnEventCreation = functions.firestore
|
||||||
.document('Events/{eventId}')
|
.document('Events/{eventId}')
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
const eventData = snapshot.data();
|
const eventData = snapshot.data();
|
||||||
const { familyId, creatorId, email } = eventData;
|
const { familyId, creatorId, email } = eventData;
|
||||||
|
|
||||||
// if (email) {
|
|
||||||
// console.log('Event has an email field. Skipping notification.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!familyId || !creatorId) {
|
if (!familyId || !creatorId) {
|
||||||
console.error('Missing familyId or creatorId in event data');
|
console.error('Missing familyId or creatorId in event data');
|
||||||
return;
|
return;
|
||||||
@ -44,60 +38,62 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
|
|
||||||
eventCount++;
|
eventCount++;
|
||||||
|
|
||||||
if (notificationTimeout) {
|
// Only set up the notification timeout if it's not already in progress
|
||||||
clearTimeout(notificationTimeout);
|
if (!notificationInProgress) {
|
||||||
}
|
notificationInProgress = true;
|
||||||
|
|
||||||
notificationTimeout = setTimeout(async () => {
|
notificationTimeout = setTimeout(async () => {
|
||||||
const eventMessage = eventCount === 1
|
const eventMessage = eventCount === 1
|
||||||
? `An event "${eventData.title}" has been added. Check it out!`
|
? `An event "${eventData.title}" has been added. Check it out!`
|
||||||
: `${eventCount} new events have been added.`;
|
: `${eventCount} new events have been added.`;
|
||||||
|
|
||||||
let messages = pushTokens.map(pushToken => {
|
let messages = pushTokens.map(pushToken => {
|
||||||
if (!Expo.isExpoPushToken(pushToken)) {
|
if (!Expo.isExpoPushToken(pushToken)) {
|
||||||
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
to: pushToken,
|
to: pushToken,
|
||||||
sound: 'default',
|
sound: 'default',
|
||||||
title: 'New Events Added!',
|
title: 'New Events Added!',
|
||||||
body: eventMessage,
|
body: eventMessage,
|
||||||
data: { eventId: context.params.eventId },
|
data: { eventId: context.params.eventId },
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
let chunks = expo.chunkPushNotifications(messages);
|
let chunks = expo.chunkPushNotifications(messages);
|
||||||
let tickets = [];
|
let tickets = [];
|
||||||
|
|
||||||
for (let chunk of chunks) {
|
for (let chunk of chunks) {
|
||||||
try {
|
try {
|
||||||
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
||||||
tickets.push(...ticketChunk);
|
tickets.push(...ticketChunk);
|
||||||
|
|
||||||
for (let ticket of ticketChunk) {
|
for (let ticket of ticketChunk) {
|
||||||
if (ticket.status === 'ok') {
|
if (ticket.status === 'ok') {
|
||||||
console.log('Notification successfully sent:', ticket.id);
|
console.log('Notification successfully sent:', ticket.id);
|
||||||
} else if (ticket.status === 'error') {
|
} else if (ticket.status === 'error') {
|
||||||
console.error(`Notification error: ${ticket.message}`);
|
console.error(`Notification error: ${ticket.message}`);
|
||||||
if (ticket.details?.error === 'DeviceNotRegistered') {
|
if (ticket.details?.error === 'DeviceNotRegistered') {
|
||||||
await removeInvalidPushToken(ticket.to);
|
await removeInvalidPushToken(ticket.to);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending notification:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending notification:', error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
eventCount = 0;
|
// Reset state variables after notifications are sent
|
||||||
pushTokens = [];
|
eventCount = 0;
|
||||||
|
pushTokens = [];
|
||||||
|
notificationInProgress = false;
|
||||||
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
exports.createSubUser = onRequest(async (request, response) => {
|
exports.createSubUser = onRequest(async (request, response) => {
|
||||||
const authHeader = request.get('Authorization');
|
const authHeader = request.get('Authorization');
|
||||||
|
|
||||||
@ -200,7 +196,10 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn
|
|||||||
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
||||||
if (googleToken) {
|
if (googleToken) {
|
||||||
const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
|
const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
|
||||||
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}};
|
const updatedGoogleAccounts = {
|
||||||
|
...profileData.googleAccounts,
|
||||||
|
[googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}
|
||||||
|
};
|
||||||
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
||||||
console.log(`Google token updated for user ${profileDoc.id}`);
|
console.log(`Google token updated for user ${profileDoc.id}`);
|
||||||
}
|
}
|
||||||
@ -216,7 +215,10 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn
|
|||||||
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
||||||
if (microsoftToken) {
|
if (microsoftToken) {
|
||||||
const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken);
|
const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken);
|
||||||
const updatedMicrosoftAccounts = {...profileData.microsoftAccounts, [microsoftEmail]: refreshedMicrosoftToken};
|
const updatedMicrosoftAccounts = {
|
||||||
|
...profileData.microsoftAccounts,
|
||||||
|
[microsoftEmail]: refreshedMicrosoftToken
|
||||||
|
};
|
||||||
await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts});
|
await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts});
|
||||||
console.log(`Microsoft token updated for user ${profileDoc.id}`);
|
console.log(`Microsoft token updated for user ${profileDoc.id}`);
|
||||||
}
|
}
|
||||||
@ -486,7 +488,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
|
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch push tokens for the specific user
|
// Fetch user profile data for the specific user
|
||||||
const userDoc = await db.collection("Profiles").doc(userId).get();
|
const userDoc = await db.collection("Profiles").doc(userId).get();
|
||||||
const userData = userDoc.data();
|
const userData = userDoc.data();
|
||||||
|
|
||||||
@ -502,47 +504,197 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncMessage = "New events have been synced.";
|
// Call calendarSync with necessary parameters
|
||||||
|
const {googleAccounts} = userData;
|
||||||
|
const email = Object.keys(googleAccounts || {})[0]; // Assuming the first account is the primary
|
||||||
|
const accountData = googleAccounts[email] || {};
|
||||||
|
const token = accountData.accessToken;
|
||||||
|
const refreshToken = accountData.refreshToken;
|
||||||
|
const familyId = userData.familyId;
|
||||||
|
|
||||||
let messages = pushTokens.map(pushToken => {
|
console.log("Starting calendar sync...");
|
||||||
if (!Expo.isExpoPushToken(pushToken)) {
|
await calendarSync({userId, email, token, refreshToken, familyId});
|
||||||
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
console.log("Calendar sync completed.");
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
// Prepare and send push notifications after sync
|
||||||
to: pushToken,
|
// const syncMessage = "New events have been synced.";
|
||||||
sound: "default",
|
//
|
||||||
title: "Event Sync",
|
// let messages = pushTokens.map(pushToken => {
|
||||||
body: syncMessage,
|
// if (!Expo.isExpoPushToken(pushToken)) {
|
||||||
data: { userId, calendarId },
|
// console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
||||||
};
|
// return null;
|
||||||
}).filter(Boolean);
|
// }
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// to: pushToken,
|
||||||
|
// sound: "default",
|
||||||
|
// title: "Event Sync",
|
||||||
|
// body: syncMessage,
|
||||||
|
// data: { userId, calendarId },
|
||||||
|
// };
|
||||||
|
// }).filter(Boolean);
|
||||||
|
//
|
||||||
|
// let chunks = expo.chunkPushNotifications(messages);
|
||||||
|
// let tickets = [];
|
||||||
|
//
|
||||||
|
// for (let chunk of chunks) {
|
||||||
|
// try {
|
||||||
|
// let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
||||||
|
// tickets.push(...ticketChunk);
|
||||||
|
//
|
||||||
|
// for (let ticket of ticketChunk) {
|
||||||
|
// if (ticket.status === "ok") {
|
||||||
|
// console.log("Notification successfully sent:", ticket.id);
|
||||||
|
// } else if (ticket.status === "error") {
|
||||||
|
// console.error(`Notification error: ${ticket.message}`);
|
||||||
|
// if (ticket.details?.error === "DeviceNotRegistered") {
|
||||||
|
// await removeInvalidPushToken(ticket.to);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error sending notification:", error.message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// console.log(`Sync notification sent for user ${userId}`);
|
||||||
|
res.status(200).send("Sync notification sent.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||||
|
res.status(500).send("Failed to send sync notification.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let chunks = expo.chunkPushNotifications(messages);
|
async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId }) {
|
||||||
let tickets = [];
|
const baseDate = new Date();
|
||||||
|
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||||
|
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||||
|
|
||||||
for (let chunk of chunks) {
|
let events = [];
|
||||||
try {
|
let pageToken = null;
|
||||||
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
|
||||||
tickets.push(...ticketChunk);
|
|
||||||
|
|
||||||
for (let ticket of ticketChunk) {
|
try {
|
||||||
if (ticket.status === "ok") {
|
console.log(`Fetching events for user: ${email}`);
|
||||||
console.log("Notification successfully sent:", ticket.id);
|
|
||||||
} else if (ticket.status === "error") {
|
// Fetch all events from Google Calendar within the specified time range
|
||||||
console.error(`Notification error: ${ticket.message}`);
|
do {
|
||||||
if (ticket.details?.error === "DeviceNotRegistered") {
|
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||||
await removeInvalidPushToken(ticket.to);
|
url.searchParams.set("singleEvents", "true");
|
||||||
}
|
url.searchParams.set("timeMin", timeMin);
|
||||||
}
|
url.searchParams.set("timeMax", timeMax);
|
||||||
|
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log(`Token expired for user: ${email}, attempting to refresh`);
|
||||||
|
const refreshedToken = await refreshGoogleToken(refreshToken);
|
||||||
|
token = refreshedToken;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
return fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId });
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to refresh token for user: ${email}`);
|
||||||
|
await clearToken(email);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Error sending notification:", error.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing events for user: ${email}`);
|
||||||
|
data.items?.forEach((item) => {
|
||||||
|
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),
|
||||||
|
allDay: !item.start?.dateTime,
|
||||||
|
familyId,
|
||||||
|
email,
|
||||||
|
creatorId, // Add creatorId to each event
|
||||||
|
};
|
||||||
|
events.push(googleEvent);
|
||||||
|
console.log(`Processed event: ${JSON.stringify(googleEvent)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
pageToken = data.nextPageToken;
|
||||||
|
} while (pageToken);
|
||||||
|
|
||||||
|
console.log(`Saving events to Firestore for user: ${email}`);
|
||||||
|
await saveEventsToFirestore(events);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching Google Calendar events for ${email}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calendarSync({ userId, email, token, refreshToken, familyId }) {
|
||||||
|
console.log(`Starting calendar sync for user ${userId} with email ${email}`);
|
||||||
|
try {
|
||||||
|
await fetchAndSaveGoogleEvents({
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
email,
|
||||||
|
familyId,
|
||||||
|
creatorId: userId,
|
||||||
|
});
|
||||||
|
console.log("Calendar events synced successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error syncing calendar for user ${userId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.log(`Finished calendar sync for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||||
|
const userId = req.query.userId;
|
||||||
|
const calendarId = req.body.resourceId;
|
||||||
|
|
||||||
|
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userDoc = await db.collection("Profiles").doc(userId).get();
|
||||||
|
const userData = userDoc.data();
|
||||||
|
|
||||||
|
let pushTokens = [];
|
||||||
|
if (userData && userData.pushToken) {
|
||||||
|
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Sync notification sent for user ${userId}`);
|
if (pushTokens.length === 0) {
|
||||||
|
console.log(`No push tokens found for user ${userId}`);
|
||||||
|
res.status(200).send("No push tokens found for user.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { googleAccounts } = userData;
|
||||||
|
const email = Object.keys(googleAccounts || {})[0];
|
||||||
|
const accountData = googleAccounts[email] || {};
|
||||||
|
const token = accountData.accessToken;
|
||||||
|
const refreshToken = accountData.refreshToken;
|
||||||
|
const familyId = userData.familyId;
|
||||||
|
|
||||||
|
console.log("Starting calendar sync...");
|
||||||
|
await calendarSync({ userId, email, token, refreshToken, familyId });
|
||||||
|
console.log("Calendar sync completed.");
|
||||||
|
|
||||||
res.status(200).send("Sync notification sent.");
|
res.status(200).send("Sync notification sent.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||||
|
|||||||
@ -308,8 +308,8 @@ export const useCalSync = () => {
|
|||||||
const handleNotification = async (notification: Notifications.Notification) => {
|
const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
const eventId = notification?.request?.content?.data?.eventId;
|
const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
|
||||||
await resyncAllCalendars();
|
// await resyncAllCalendars();
|
||||||
// queryClient.invalidateQueries(["events"]);
|
queryClient.invalidateQueries(["events"]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
|||||||
@ -22,7 +22,8 @@
|
|||||||
"prebuild-build-submit-ios": "yarn run prebuild && yarn run build-ios && yarn run submit",
|
"prebuild-build-submit-ios": "yarn run prebuild && yarn run build-ios && yarn run submit",
|
||||||
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
|
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
|
||||||
"prebuild-build-submit-cicd": "yarn build-cicd",
|
"prebuild-build-submit-cicd": "yarn build-cicd",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package",
|
||||||
|
"functions-deploy": "cd firebase/functions && firebase deploy --only functions"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
|
|||||||
Reference in New Issue
Block a user