Add Firebase Functions for automated prayer reminders

This commit is contained in:
nmemmert committed 2025-11-03 08:30:53 -05:00
1 parent 5ea16bba66
commit a2ff235dcc
7 files changed
+10412

No files matched your search

+33
View File
@@ -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],
},
};
+10
View File
@@ -0,0 +1,10 @@
# Compiled JavaScript files
lib/**/*.js
lib/**/*.js.map
# TypeScript v1 declaration files
typings/
# Node.js dependency directory
node_modules/
*.local
+9929
View File
File diff suppressed because it is too large. Load diff
+31
View File
@@ -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
}
+387
View File
@@ -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)
});
}
});
+5
View File
@@ -0,0 +1,5 @@
{
"include": [
".eslintrc.js"
]
}
+17
View File
@@ -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"
]
}