-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirebase-functions-example.js
More file actions
244 lines (194 loc) Β· 7.73 KB
/
Copy pathfirebase-functions-example.js
File metadata and controls
244 lines (194 loc) Β· 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Firebase Cloud Function for Scheduled Notifications
// Deploy this to Firebase Functions to handle background notifications
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
// Helper function to send ntfy notification
async function sendNtfyNotification(topic, title, message, options = {}) {
if (!topic) return false;
try {
const url = `https://ntfy.sh/${encodeURIComponent(topic)}`;
const headers = {
'Content-Type': 'text/plain',
'Title': title
};
if (options.priority) headers['Priority'] = String(options.priority);
if (options.tags) headers['Tags'] = options.tags.join(',');
const response = await fetch(url, {
method: 'POST',
headers,
body: message
});
console.log(`ntfy notification sent to ${topic}: ${response.ok}`);
return response.ok;
} catch (error) {
console.error('ntfy notification error:', error);
return false;
}
}
// Check for due notifications every hour
exports.checkScheduledNotifications = functions.pubsub
.schedule('every 1 hours')
.timeZone('UTC')
.onRun(async (context) => {
console.log('Starting scheduled notification check...');
const now = new Date();
try {
// Get all users with notification settings
const usersSnapshot = await db.collection('users').get();
for (const userDoc of usersSnapshot.docs) {
const userData = userDoc.data();
const userId = userDoc.id;
const ntfyTopic = userData.notifications?.ntfyTopic;
if (!ntfyTopic) continue; // Skip users without ntfy setup
// Check morning motivation
await checkMorningMotivation(userId, userData, now);
// Check evening reflection
await checkEveningReflection(userId, userData, now);
// Check goal deadlines
await checkGoalDeadlines(userId, ntfyTopic, now);
// Check streak reminders
await checkStreakReminders(userId, ntfyTopic, now);
}
console.log('Scheduled notification check completed');
} catch (error) {
console.error('Error in scheduled notification check:', error);
}
return null;
});
// Check morning motivation notifications
async function checkMorningMotivation(userId, userData, now) {
const settings = userData.notifications?.morningMotivation;
if (!settings?.enabled) return;
const hour = now.getHours();
const targetHour = parseInt(settings.time?.split(':')[0] || '8');
// Send if it's the right hour and we haven't sent today
if (hour === targetHour) {
const today = now.toISOString().split('T')[0];
const lastSent = userData.notifications?.lastMorningMotivation;
if (lastSent !== today) {
const messages = [
"Good morning! Ready to crush your goals today? π
",
"Rise and shine! Your goals are waiting for you! β",
"New day, new opportunities! Let's make progress! π",
"Morning motivation: You're capable of amazing things! πͺ"
];
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
await sendNtfyNotification(
userData.notifications.ntfyTopic,
"JustGoals - Morning Motivation",
randomMessage,
{ priority: 3, tags: ['sunrise', 'muscle'] }
);
// Update last sent timestamp
await db.collection('users').doc(userId).update({
'notifications.lastMorningMotivation': today
});
}
}
}
// Check evening reflection notifications
async function checkEveningReflection(userId, userData, now) {
const settings = userData.notifications?.eveningReflection;
if (!settings?.enabled) return;
const hour = now.getHours();
const targetHour = parseInt(settings.time?.split(':')[0] || '20');
if (hour === targetHour) {
const today = now.toISOString().split('T')[0];
const lastSent = userData.notifications?.lastEveningReflection;
if (lastSent !== today) {
const message = "Time to reflect on your day! How did you do with your goals? π";
await sendNtfyNotification(
userData.notifications.ntfyTopic,
"JustGoals - Evening Reflection",
message,
{ priority: 2, tags: ['moon', 'thought_balloon'] }
);
await db.collection('users').doc(userId).update({
'notifications.lastEveningReflection': today
});
}
}
}
// Check goal deadline notifications
async function checkGoalDeadlines(userId, ntfyTopic, now) {
const goalsSnapshot = await db.collection('goals')
.where('userId', '==', userId)
.where('completed', '==', false)
.get();
for (const goalDoc of goalsSnapshot.docs) {
const goal = goalDoc.data();
if (!goal.deadline || !goal.notifications?.deadlineAlerts) continue;
const deadlineDate = new Date(goal.deadline);
const timeDiff = deadlineDate.getTime() - now.getTime();
const daysUntilDeadline = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
// Check if we should send a deadline alert
const alertDays = [7, 3, 1]; // Alert 7, 3, and 1 days before
const lastAlert = goal.notifications?.lastDeadlineAlert;
for (const alertDay of alertDays) {
if (daysUntilDeadline === alertDay && lastAlert !== alertDay) {
let title, message;
if (alertDay === 1) {
title = "π¨ Deadline Tomorrow!";
message = `"${goal.title}" is due tomorrow! Time to push hard!`;
} else {
title = "π
Deadline Approaching";
message = `"${goal.title}" is due in ${alertDay} days. Keep pushing!`;
}
await sendNtfyNotification(ntfyTopic, title, message, {
priority: alertDay === 1 ? 5 : 4,
tags: ['calendar', 'warning']
});
// Update last alert sent
await db.collection('goals').doc(goalDoc.id).update({
'notifications.lastDeadlineAlert': alertDay
});
break; // Only send one alert per check
}
}
}
}
// Check streak reminder notifications
async function checkStreakReminders(userId, ntfyTopic, now) {
// This would check for habit streaks or goal streaks that need attention
// Implementation depends on your streak tracking system
console.log(`Checking streak reminders for user ${userId}`);
}
// Process test notifications queue
exports.processTestNotifications = functions.firestore
.document('notificationQueue/{docId}')
.onCreate(async (snap, context) => {
const data = snap.data();
if (data.type === 'test' && !data.processed) {
// Get user's ntfy topic
const userDoc = await db.collection('users').doc(data.userId).get();
const ntfyTopic = userDoc.data()?.notifications?.ntfyTopic;
if (ntfyTopic) {
await sendNtfyNotification(
ntfyTopic,
"JustGoals Backend Test",
data.message,
{ priority: 3, tags: ['test_tube', 'checkmark'] }
);
}
// Mark as processed
await snap.ref.update({ processed: true });
}
return null;
});
// Clean up old processed notifications
exports.cleanupNotificationQueue = functions.pubsub
.schedule('every 24 hours')
.onRun(async (context) => {
const cutoffTime = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
const oldDocs = await db.collection('notificationQueue')
.where('processed', '==', true)
.where('timestamp', '<', cutoffTime.toISOString())
.get();
const batch = db.batch();
oldDocs.docs.forEach(doc => batch.delete(doc.ref));
await batch.commit();
console.log(`Cleaned up ${oldDocs.docs.length} old notification queue items`);
return null;
});