-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathworker.js
More file actions
250 lines (232 loc) · 8.58 KB
/
worker.js
File metadata and controls
250 lines (232 loc) · 8.58 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
245
246
247
248
249
250
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
import { Resend } from 'resend';
import { createClerkClient } from "@clerk/backend";
// Event template configuration
const EVENT_TEMPLATES = {
button_pressed: {
subject: 'Doorbell Alert',
intro: 'Someone is at your doorbell',
details: (location) => `Your doorbell was activated${location ? ` at ${location}` : ''}`,
icon: '🚪',
priority: 'high',
},
motion_detected: {
subject: 'Motion Alert',
intro: 'Motion was detected',
details: (location) => `Motion was detected${location ? ` at ${location}` : ''}`,
icon: '👥',
priority: 'medium',
},
package_detected: {
subject: 'Package Alert',
intro: 'A package was detected',
details: (location) => `A package was detected${location ? ` at ${location}` : ''}`,
icon: '📦',
priority: 'medium',
},
person_detected: {
subject: 'Person Alert',
intro: 'A person was detected',
details: (location) => `A person was detected${location ? ` at ${location}` : ''}`,
icon: '🧍',
priority: 'medium',
},
doorbell_offline: {
subject: 'Device Offline',
intro: 'A device is offline',
details: (location) => `Your device lost connection${location ? ` at ${location}` : ''}`,
icon: '⚠️',
priority: 'low',
},
sound_detected: {
subject: 'Sound Alert',
intro: 'A sound was detected',
details: (location) => `A sound was detected${location ? ` at ${location}` : ''}`,
icon: '🔊',
priority: 'medium',
},
battery_low: {
subject: 'Battery Low',
intro: 'Device battery is low',
details: (location) => `Your device needs charging${location ? ` at ${location}` : ''}`,
icon: '🔋',
priority: 'medium',
},
};
// Default template for unknown event types
const DEFAULT_TEMPLATE = {
subject: 'Smart Home Alert',
intro: 'New alert from your device',
details: (location) => `An event was detected${location ? ` at ${location}` : ''}`,
icon: '🏠',
priority: 'medium',
};
/**
* Fetch user details from Clerk
*/
async function getUserDetailsFromClerk(userId, clerkClient) {
if (!userId) {
console.warn('getUserDetailsFromClerk called with no userId');
return { firstName: null, email: null };
}
try {
const user = await clerkClient.users.getUser(userId);
const primaryEmail = user.emailAddresses.find(
(e) => e.id === user.primaryEmailAddressId
);
return {
firstName: user.firstName,
email: primaryEmail?.emailAddress || null,
};
} catch (error) {
console.error(`Error fetching user ${userId} from Clerk:`, error.message);
if (error.status === 404) {
console.warn(`User ${userId} not found in Clerk.`);
}
return { firstName: null, email: null };
}
}
/**
* Generate the HTML email content for an event
*/
function generateEmailContent(event, userInfo) {
const {
event_type,
device_name,
device_location,
payload,
occurred_at,
} = event;
const safePayload = payload || {};
const media_url = safePayload.media_url;
const media_transcript = safePayload.media_transcript;
const additionalInfo = safePayload.message || '';
const template = EVENT_TEMPLATES[event_type] || DEFAULT_TEMPLATE;
const date = new Date(occurred_at).toLocaleString();
const subject = `${template.icon} ${template.subject}: ${device_name}`;
const intro = `${template.intro}!`;
const details = template.details(device_location);
const html = `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; color: #333; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; background: #ffffff; }
.header { background-color: ${template.priority === 'high' ? '#ff4444' : template.priority === 'medium' ? '#4a90e2' : '#666666'}; padding: 20px; color: white; border-radius: 8px 8px 0 0; }
.content { padding: 30px; background: #f9f9f9; border: 1px solid #eee; border-top: none; border-radius: 0 0 8px 8px; }
.event-icon { font-size: 2em; margin-bottom: 10px; }
.event-time { color: #666; font-size: 0.9em; margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
.media-section { margin-top: 20px; padding: 15px; background: #fff; border: 1px solid #eee; border-radius: 4px; }
.transcript { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; font-style: italic; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; margin-top: 20px; }
@media only screen and (max-width: 480px) { .container { padding: 10px; } .header, .content { padding: 15px; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="event-icon">${template.icon}</div>
<h1>${subject}</h1>
</div>
<div class="content">
<p>Hi ${userInfo.firstName || 'there'},</p>
<p><strong>${intro}</strong></p>
<p>${details}</p>
${additionalInfo ? `<p>${additionalInfo}</p>` : ''}
${media_url ? `
<div class="media-section">
<p><strong>Media Available:</strong> <a href="${media_url}">View Recording</a></p>
${media_transcript ? `
<div class="transcript">
<p><strong>Transcript:</strong></p>
<p>${media_transcript}</p>
</div>
` : ''}
</div>
` : ''}
<div class="event-time">
<p>Event Time: ${date}</p>
<p>Device: ${device_name}${device_location ? ` (${device_location})` : ''}</p>
</div>
</div>
<div class="footer">
<p>This is an automated message from your Smart Doorbell system.</p>
<p>Please do not reply to this email.</p>
</div>
</div>
</body>
</html>`;
return { subject, html };
}
/**
* Process pending events via Supabase
*/
async function processEvents(supabase, resend, clerkClient) {
let processed = 0, successful = 0, failed = 0;
console.log('Fetching unsent events from Supabase...');
const { data: events, error: fetchError } = await supabase
.from('events')
.select('event_id, device_id, event_type, payload, occurred_at')
.eq('send_email', false)
.order('occurred_at', { ascending: true })
.limit(5);
if (fetchError) throw fetchError;
if (!events?.length) {
return { processed: 0, successful: 0, failed: 0, message: 'No events to process.' };
}
processed = events.length;
for (const event of events) {
try {
const { data: device, error: deviceError } = await supabase
.from('devices')
.select('name, location, owner_id')
.eq('device_id', event.device_id)
.single();
if (deviceError) throw deviceError;
const userDetails = await getUserDetailsFromClerk(device.owner_id, clerkClient);
if (!userDetails.email) throw new Error('No recipient email');
console.log(`Sending email for event ${event.event_id} to ${userDetails.email}`);
const { subject, html } = generateEmailContent(
{ ...event, device_name: device.name, device_location: device.location },
userDetails
);
console.log(`Sending email to ${userDetails.email} with subject ${subject}`);
await resend.emails.send({
from: `Smart Doorbell <alerts@arthurlian.com>`,
to: userDetails.email,
subject,
html,
});
console.log(`Email sent for event ${event.event_id}`);
const { error: updateError } = await supabase
.from('events')
.update({ send_email: true })
.eq('event_id', event.event_id);
if (updateError) throw updateError;
successful++;
} catch (error) {
console.error(`Failed to process event ${event.event_id}:`, error);
failed++;
}
}
return { processed, successful, failed, message: `Done: ${successful}/${processed} sent, ${failed} failed.` };
}
export default {
async fetch(request, env) {
if (request.method === 'GET') {
return new Response('Smart Doorbell Notification Worker Running', { status: 200 });
}
if (request.method === 'POST') {
const supabase = createSupabaseClient(env.SUPABASE_URL, env.SUPABASE_KEY);
const clerkClient = createClerkClient({ secretKey: env.CLERK_SECRET_KEY });
const resend = new Resend(env.RESEND_API_KEY);
const result = await processEvents(supabase, resend, clerkClient);
return new Response(JSON.stringify({ success: true, ...result }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Method Not Allowed', { status: 405 });
}
};