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