Add Firebase Functions for automated prayer reminders
This commit is contained in:
1 parent
5ea16bba66
commit
a2ff235dcc
7 files changed
+10412
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"google",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
project: ["tsconfig.json", "tsconfig.dev.json"],
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
"/lib/**/*", // Ignore built files.
|
||||||
|
"/generated/**/*", // Ignore generated files.
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"import",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"import/no-unresolved": 0,
|
||||||
|
"indent": ["error", 2],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Compiled JavaScript files
|
||||||
|
lib/**/*.js
|
||||||
|
lib/**/*.js.map
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Node.js dependency directory
|
||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
Generated
+9929
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "functions",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"build": "tsc",
|
||||||
|
"build:watch": "tsc --watch",
|
||||||
|
"serve": "npm run build && firebase emulators:start --only functions",
|
||||||
|
"shell": "npm run build && firebase functions:shell",
|
||||||
|
"start": "npm run shell",
|
||||||
|
"deploy": "firebase deploy --only functions",
|
||||||
|
"logs": "firebase functions:log"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "22"
|
||||||
|
},
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"firebase-admin": "^12.6.0",
|
||||||
|
"firebase-functions": "^6.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||||
|
"@typescript-eslint/parser": "^5.12.0",
|
||||||
|
"eslint": "^8.9.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
"firebase-functions-test": "^3.1.0",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
/**
|
||||||
|
* Import function triggers from their respective submodules:
|
||||||
|
*
|
||||||
|
* import {onCall} from "firebase-functions/v2/https";
|
||||||
|
* import {onDocumentWritten} from "firebase-functions/v2/firestore";
|
||||||
|
*
|
||||||
|
* See a full list of supported triggers at https://firebase.google.com/docs/functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {setGlobalOptions} from "firebase-functions";
|
||||||
|
import {onRequest} from "firebase-functions/https";
|
||||||
|
import {onSchedule} from "firebase-functions/v2/scheduler";
|
||||||
|
import * as logger from "firebase-functions/logger";
|
||||||
|
import * as admin from "firebase-admin";
|
||||||
|
|
||||||
|
// Initialize Firebase Admin
|
||||||
|
admin.initializeApp();
|
||||||
|
|
||||||
|
// For cost control, you can set the maximum number of containers that can be
|
||||||
|
// running at the same time. This helps mitigate the impact of unexpected
|
||||||
|
// traffic spikes by instead downgrading performance. This limit is a
|
||||||
|
// per-function limit. You can override the limit for each function using the
|
||||||
|
// `maxInstances` option in the function's options, e.g.
|
||||||
|
// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`.
|
||||||
|
// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1
|
||||||
|
// functions should each use functions.runWith({ maxInstances: 10 }) instead.
|
||||||
|
// In the v1 API, each function can only serve one request per container, so
|
||||||
|
// this will be the maximum concurrent request count.
|
||||||
|
setGlobalOptions({ maxInstances: 10 });
|
||||||
|
|
||||||
|
// Daily prayer reminders function
|
||||||
|
export const sendDailyPrayerReminders = onSchedule(
|
||||||
|
{
|
||||||
|
schedule: "0 9 * * *", // Run daily at 9 AM UTC
|
||||||
|
timeZone: "America/New_York", // Adjust timezone as needed
|
||||||
|
retryCount: 3,
|
||||||
|
maxInstances: 1,
|
||||||
|
},
|
||||||
|
async (event) => {
|
||||||
|
logger.info("Starting daily prayer reminders", {structuredData: true});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all users who have notifications enabled
|
||||||
|
const usersRef = admin.firestore().collection('users');
|
||||||
|
const usersSnapshot = await usersRef
|
||||||
|
.where('notificationsEnabled', '==', true)
|
||||||
|
.where('fcmToken', '!=', null)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
logger.info(`Found ${usersSnapshot.size} users with notifications enabled`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
|
|
||||||
|
// Send notifications to each user
|
||||||
|
for (const userDoc of usersSnapshot.docs) {
|
||||||
|
const userData = userDoc.data();
|
||||||
|
const userId = userDoc.id;
|
||||||
|
const fcmToken = userData.fcmToken;
|
||||||
|
const reminderFrequency = userData.reminderFrequency || 'never';
|
||||||
|
|
||||||
|
// Skip if user doesn't want daily reminders
|
||||||
|
if (reminderFrequency !== 'daily') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get user's active prayers for summary
|
||||||
|
const prayersRef = admin.firestore().collection('prayers');
|
||||||
|
const activePrayersSnapshot = await prayersRef
|
||||||
|
.where('userId', '==', userId)
|
||||||
|
.where('archived', '==', false)
|
||||||
|
.where('answered', '==', false)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const activePrayersCount = activePrayersSnapshot.size;
|
||||||
|
const includeActiveSummary = userData.includeActiveSummary || false;
|
||||||
|
|
||||||
|
// Prepare notification message
|
||||||
|
let notificationBody = "Time for your daily prayer reflection!";
|
||||||
|
|
||||||
|
if (includeActiveSummary && activePrayersCount > 0) {
|
||||||
|
notificationBody += ` You have ${activePrayersCount} active prayer${activePrayersCount !== 1 ? 's' : ''} waiting for your attention.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send FCM message
|
||||||
|
const message = {
|
||||||
|
token: fcmToken,
|
||||||
|
notification: {
|
||||||
|
title: "Daily Prayer Reminder",
|
||||||
|
body: notificationBody,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'daily_reminder',
|
||||||
|
userId: userId,
|
||||||
|
activePrayersCount: activePrayersCount.toString(),
|
||||||
|
},
|
||||||
|
webpush: {
|
||||||
|
fcmOptions: {
|
||||||
|
link: "/", // Link to your app
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await admin.messaging().send(message);
|
||||||
|
logger.info(`Successfully sent daily reminder to user ${userId}`, {
|
||||||
|
messageId: response,
|
||||||
|
activePrayersCount
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
// Update last email sent timestamp
|
||||||
|
await usersRef.doc(userId).update({
|
||||||
|
lastReminderSent: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to send daily reminder to user ${userId}`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
fcmToken: fcmToken.substring(0, 20) + '...' // Log partial token for debugging
|
||||||
|
});
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Daily prayer reminders completed", {
|
||||||
|
totalUsers: usersSnapshot.size,
|
||||||
|
successCount,
|
||||||
|
failureCount,
|
||||||
|
structuredData: true
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in daily prayer reminders function", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
structuredData: true
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Weekly prayer reminders function
|
||||||
|
export const sendWeeklyPrayerReminders = onSchedule(
|
||||||
|
{
|
||||||
|
schedule: "0 9 * * 1", // Run weekly on Monday at 9 AM UTC
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
retryCount: 3,
|
||||||
|
maxInstances: 1,
|
||||||
|
},
|
||||||
|
async (event) => {
|
||||||
|
logger.info("Starting weekly prayer reminders", {structuredData: true});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usersRef = admin.firestore().collection('users');
|
||||||
|
const usersSnapshot = await usersRef
|
||||||
|
.where('notificationsEnabled', '==', true)
|
||||||
|
.where('fcmToken', '!=', null)
|
||||||
|
.where('reminderFrequency', '==', 'weekly')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
logger.info(`Found ${usersSnapshot.size} users with weekly notifications enabled`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
|
|
||||||
|
for (const userDoc of usersSnapshot.docs) {
|
||||||
|
const userData = userDoc.data();
|
||||||
|
const userId = userDoc.id;
|
||||||
|
const fcmToken = userData.fcmToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prayersRef = admin.firestore().collection('prayers');
|
||||||
|
const activePrayersSnapshot = await prayersRef
|
||||||
|
.where('userId', '==', userId)
|
||||||
|
.where('archived', '==', false)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const activePrayersCount = activePrayersSnapshot.size;
|
||||||
|
const answeredPrayersCount = activePrayersSnapshot.docs.filter(doc => doc.data().answered).length;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
token: fcmToken,
|
||||||
|
notification: {
|
||||||
|
title: "Weekly Prayer Reflection",
|
||||||
|
body: `This week: ${answeredPrayersCount} prayers answered, ${activePrayersCount - answeredPrayersCount} still active. Take time to reflect and pray!`,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'weekly_reminder',
|
||||||
|
userId: userId,
|
||||||
|
activePrayersCount: (activePrayersCount - answeredPrayersCount).toString(),
|
||||||
|
answeredPrayersCount: answeredPrayersCount.toString(),
|
||||||
|
},
|
||||||
|
webpush: {
|
||||||
|
fcmOptions: {
|
||||||
|
link: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await admin.messaging().send(message);
|
||||||
|
logger.info(`Successfully sent weekly reminder to user ${userId}`, {
|
||||||
|
messageId: response,
|
||||||
|
activePrayersCount: activePrayersCount - answeredPrayersCount,
|
||||||
|
answeredPrayersCount
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
await usersRef.doc(userId).update({
|
||||||
|
lastReminderSent: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to send weekly reminder to user ${userId}`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Weekly prayer reminders completed", {
|
||||||
|
totalUsers: usersSnapshot.size,
|
||||||
|
successCount,
|
||||||
|
failureCount,
|
||||||
|
structuredData: true
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in weekly prayer reminders function", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
structuredData: true
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Monthly prayer reminders function
|
||||||
|
export const sendMonthlyPrayerReminders = onSchedule(
|
||||||
|
{
|
||||||
|
schedule: "0 9 1 * *", // Run monthly on the 1st at 9 AM UTC
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
retryCount: 3,
|
||||||
|
maxInstances: 1,
|
||||||
|
},
|
||||||
|
async (event) => {
|
||||||
|
logger.info("Starting monthly prayer reminders", {structuredData: true});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usersRef = admin.firestore().collection('users');
|
||||||
|
const usersSnapshot = await usersRef
|
||||||
|
.where('notificationsEnabled', '==', true)
|
||||||
|
.where('fcmToken', '!=', null)
|
||||||
|
.where('reminderFrequency', '==', 'monthly')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
logger.info(`Found ${usersSnapshot.size} users with monthly notifications enabled`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
|
|
||||||
|
for (const userDoc of usersSnapshot.docs) {
|
||||||
|
const userData = userDoc.data();
|
||||||
|
const userId = userDoc.id;
|
||||||
|
const fcmToken = userData.fcmToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prayersRef = admin.firestore().collection('prayers');
|
||||||
|
const allPrayersSnapshot = await prayersRef
|
||||||
|
.where('userId', '==', userId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const totalPrayers = allPrayersSnapshot.size;
|
||||||
|
const answeredPrayers = allPrayersSnapshot.docs.filter(doc => doc.data().answered).length;
|
||||||
|
const activePrayers = totalPrayers - answeredPrayers;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
token: fcmToken,
|
||||||
|
notification: {
|
||||||
|
title: "Monthly Prayer Review",
|
||||||
|
body: `This month: ${totalPrayers} total prayers, ${answeredPrayers} answered, ${activePrayers} still active. Reflect on God's faithfulness!`,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'monthly_reminder',
|
||||||
|
userId: userId,
|
||||||
|
totalPrayers: totalPrayers.toString(),
|
||||||
|
answeredPrayers: answeredPrayers.toString(),
|
||||||
|
activePrayers: activePrayers.toString(),
|
||||||
|
},
|
||||||
|
webpush: {
|
||||||
|
fcmOptions: {
|
||||||
|
link: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await admin.messaging().send(message);
|
||||||
|
logger.info(`Successfully sent monthly reminder to user ${userId}`, {
|
||||||
|
messageId: response,
|
||||||
|
totalPrayers,
|
||||||
|
answeredPrayers,
|
||||||
|
activePrayers
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
await usersRef.doc(userId).update({
|
||||||
|
lastReminderSent: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to send monthly reminder to user ${userId}`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Monthly prayer reminders completed", {
|
||||||
|
totalUsers: usersSnapshot.size,
|
||||||
|
successCount,
|
||||||
|
failureCount,
|
||||||
|
structuredData: true
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in monthly prayer reminders function", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
structuredData: true
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test function for manual triggering
|
||||||
|
export const testPrayerReminders = onRequest(async (req, res) => {
|
||||||
|
logger.info("Manual test of prayer reminders triggered");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usersRef = admin.firestore().collection('users');
|
||||||
|
const usersSnapshot = await usersRef
|
||||||
|
.where('notificationsEnabled', '==', true)
|
||||||
|
.where('fcmToken', '!=', null)
|
||||||
|
.limit(1) // Only test with first user
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (usersSnapshot.empty) {
|
||||||
|
res.json({ success: false, message: "No users with notifications enabled found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDoc = usersSnapshot.docs[0];
|
||||||
|
const userData = userDoc.data();
|
||||||
|
const fcmToken = userData.fcmToken;
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
token: fcmToken,
|
||||||
|
notification: {
|
||||||
|
title: "Test Prayer Reminder",
|
||||||
|
body: "This is a test notification from your Prayer App!",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'test_reminder',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await admin.messaging().send(message);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Test notification sent successfully",
|
||||||
|
messageId: response,
|
||||||
|
userId: userDoc.id
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in test prayer reminders", { error: error instanceof Error ? error.message : String(error) });
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to send test notification",
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
".eslintrc.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2017"
|
||||||
|
},
|
||||||
|
"compileOnSave": true,
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user