sync update

This commit is contained in:
Milan Paunovic
2024-11-02 22:27:17 +01:00
parent 8a0370933d
commit f1b0bcd32d
6 changed files with 221 additions and 118 deletions

View File

@ -15,7 +15,7 @@ let eventCount = 0;
let pushTokens = [];
const GOOGLE_CALENDAR_ID = "primary";
const CHANNEL_ID = "unique-channel-id";
const CHANNEL_ID = "cally-family-calendar";
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
@ -25,10 +25,10 @@ exports.sendNotificationOnEventCreation = functions.firestore
const eventData = snapshot.data();
const { familyId, creatorId, email } = eventData;
if (email) {
console.log('Event has an email field. Skipping notification.');
return;
}
// if (email) {
// console.log('Event has an email field. Skipping notification.');
// return;
// }
if (!familyId || !creatorId) {
console.error('Missing familyId or creatorId in event data');
@ -199,8 +199,8 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}};
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
console.log(`Google token updated for user ${profileDoc.id}`);
}
@ -244,21 +244,6 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn
return null;
});
async function refreshGoogleToken(refreshToken) {
try {
const response = await axios.post('https://oauth2.googleapis.com/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
});
return response.data.access_token; // Return the new access token
} catch (error) {
console.error("Error refreshing Google token:", error);
throw error;
}
}
async function refreshMicrosoftToken(refreshToken) {
try {
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
@ -311,148 +296,256 @@ async function removeInvalidPushToken(pushToken) {
// TODO
}
const fetch = require("node-fetch");
// Function to refresh Google Token with additional logging
async function refreshGoogleToken(refreshToken) {
try {
const response = await axios.post("https://oauth2.googleapis.com/token", {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
console.log("Refreshing Google token...");
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
}),
});
return response.data.access_token;
if (!response.ok) {
const errorData = await response.json();
console.error("Error refreshing Google token:", errorData);
throw new Error(`Failed to refresh Google token: ${errorData.error || response.statusText}`);
}
const data = await response.json();
console.log("Google token refreshed successfully");
// Return both the access token and refresh token (if a new one is provided)
return {
refreshedGoogleToken: data.access_token,
refreshedRefreshToken: data.refresh_token || refreshToken, // Return the existing refresh token if a new one is not provided
};
} catch (error) {
console.error("Error refreshing Google token:", error.message);
throw error;
}
}
// Helper function to get Google access tokens for all users and refresh them if needed
// Helper function to get Google access tokens for all users and refresh them if needed with logging
async function getGoogleAccessTokens() {
console.log("Fetching Google access tokens for all users...");
const tokens = {};
const profilesSnapshot = await db.collection("Profiles").get();
await Promise.all(
profilesSnapshot.docs.map(async (doc) => {
const profileData = doc.data();
const googleAccounts = profileData.googleAccounts || {};
const googleAccounts = profileData?.googleAccounts || {};
for (const googleEmail of Object.keys(googleAccounts)) {
const { refreshToken } = googleAccounts[googleEmail];
// Check if the googleAccount entry exists and has a refreshToken
const accountInfo = googleAccounts[googleEmail];
const refreshToken = accountInfo?.refreshToken;
if (refreshToken) {
try {
const accessToken = await refreshGoogleToken(refreshToken);
console.log(`Refreshing token for user ${doc.id} (email: ${googleEmail})`);
const {refreshedGoogleToken} = await refreshGoogleToken(refreshToken);
tokens[doc.id] = accessToken;
console.log(`Token refreshed successfully for user ${doc.id}`);
} catch (error) {
tokens[doc.id] = accountInfo?.accessToken;
console.error(`Failed to refresh token for user ${doc.id}:`, error.message);
}
} else {
console.log(`No refresh token available for user ${doc.id} (email: ${googleEmail})`);
}
}
})
);
console.log("Access tokens fetched and refreshed as needed");
return tokens;
}
// Function to watch Google Calendar events
// Function to watch Google Calendar events with additional logging
const watchCalendarEvents = async (userId, token) => {
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: `${CHANNEL_ID}-${userId}`, // Unique ID per user
type: "web_hook",
address: `${WEBHOOK_URL}?userId=${userId}`, // Pass userId to identify notifications
params: {
ttl: "80000", // Set to 20 hours in seconds
// Verify the token is valid
console.log(`Attempting to watch calendar for user ${userId}`);
console.log(`Token being used: ${token ? 'present' : 'missing'}`);
console.log(`Calendar ID: ${GOOGLE_CALENDAR_ID}`);
console.log(`Webhook URL: ${WEBHOOK_URL}?userId=${userId}`);
try {
// Test the token first
const testResponse = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events?maxResults=1`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (!testResponse.ok) {
console.error(`Token validation failed for user ${userId}:`, await testResponse.text());
throw new Error('Token validation failed');
}
console.log(`Token validated successfully for user ${userId}`);
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}),
});
body: JSON.stringify({
id: `${CHANNEL_ID}-${userId}`,
type: "web_hook",
address: `${WEBHOOK_URL}?userId=${userId}`,
params: {
ttl: "80000",
},
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to watch calendar: ${errorData.error?.message || response.statusText}`);
const responseText = await response.text();
console.log(`Watch response for user ${userId}:`, responseText);
if (!response.ok) {
console.error(`Failed to watch calendar for user ${userId}:`, responseText);
throw new Error(`Failed to watch calendar: ${responseText}`);
}
const result = JSON.parse(responseText);
console.log(`Successfully set up Google Calendar watch for user ${userId}`, result);
// Store the watch details in Firestore for monitoring
await db.collection('CalendarWatches').doc(userId).set({
watchId: result.id,
resourceId: result.resourceId,
expiration: result.expiration,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
return result;
} catch (error) {
console.error(`Error in watchCalendarEvents for user ${userId}:`, error);
// Store the error in Firestore for monitoring
await db.collection('CalendarWatchErrors').add({
userId,
error: error.message,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
});
throw error;
}
return response.json();
};
// Schedule function to renew Google Calendar watch every 20 hours for each user
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 20 hours").onRun(async (context) => {
// Add this to test webhook connectivity
exports.testWebhook = functions.https.onRequest(async (req, res) => {
console.log('Test webhook received');
console.log('Headers:', req.headers);
console.log('Body:', req.body);
console.log('Query:', req.query);
res.status(200).send('Test webhook received successfully');
});
// Schedule function to renew Google Calendar watch every 20 hours for each user with logging
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 10 minutes").onRun(async (context) => {
console.log("Starting Google Calendar watch renewal process...");
try {
const tokens = await getGoogleAccessTokens();
console.log("Tokens: ", tokens);
for (const [userId, token] of Object.entries(tokens)) {
try {
const result = await watchCalendarEvents(userId, token);
console.log(`Successfully renewed Google Calendar watch for user ${userId}:`, result);
await watchCalendarEvents(userId, token);
} catch (error) {
console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message);
}
}
console.log("Google Calendar watch renewal process completed");
} catch (error) {
console.error("Error in renewGoogleCalendarWatch function:", error.message);
}
});
// Function to handle notifications from Google Calendar for a specific user
// Function to handle notifications from Google Calendar with additional logging
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
const userId = req.query.userId; // Extract userId from query params
const calendarId = req.body.resourceId;
// Fetch push tokens for the specific user
const userDoc = await db.collection("Profiles").doc(userId).get();
const userData = userDoc.data();
const pushTokens = userData ? userData.pushToken : [];
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
if (pushTokens.length === 0) {
console.log(`No push tokens found for user ${userId}`);
res.status(200).send("No push tokens found for user.");
return;
}
try {
// Fetch push tokens for the specific user
const userDoc = await db.collection("Profiles").doc(userId).get();
const userData = userDoc.data();
const syncMessage = "New events have been synced.";
let messages = pushTokens.map(pushToken => {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
return null;
// Ensure pushTokens is an array
let pushTokens = [];
if (userData && userData.pushToken) {
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
}
return {
to: pushToken,
sound: "default",
title: "Event Sync",
body: syncMessage,
data: { userId, calendarId },
};
}).filter(Boolean);
if (pushTokens.length === 0) {
console.log(`No push tokens found for user ${userId}`);
res.status(200).send("No push tokens found for user.");
return;
}
let chunks = expo.chunkPushNotifications(messages);
let tickets = [];
const syncMessage = "New events have been synced.";
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
let messages = pushTokens.map(pushToken => {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
return null;
}
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);
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);
}
} catch (error) {
console.error("Error sending notification:", error.message);
}
}
res.status(200).send("Sync notification sent.");
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.");
}
});