forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpush-notifications.processor.ts
More file actions
123 lines (108 loc) · 4.17 KB
/
Copy pathpush-notifications.processor.ts
File metadata and controls
123 lines (108 loc) · 4.17 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
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { Job } from 'bull';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import * as firebaseAdmin from 'firebase-admin';
import { DeviceRegistration } from './entities/device-registration.entity';
import { NotificationEventType } from './entities/notification-preference.entity';
@Processor('push_queue')
export class PushNotificationProcessor {
private readonly logger = new Logger(PushNotificationProcessor.name);
private firebaseApp: firebaseAdmin.app.App | undefined;
constructor(
@InjectRepository(DeviceRegistration)
private deviceRepo: Repository<DeviceRegistration>,
private configService: ConfigService,
) {
this.initializeFirebase();
}
private initializeFirebase() {
const serviceAccount = this.configService.get('FIREBASE_SERVICE_ACCOUNT');
if (serviceAccount) {
try {
// Check if already initialized to avoid error
if (!firebaseAdmin.apps.length) {
this.firebaseApp = firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(JSON.parse(serviceAccount)),
});
} else {
this.firebaseApp = firebaseAdmin.app();
}
} catch (error) {
this.logger.error('Failed to initialize Firebase Admin', error);
}
} else {
this.logger.warn('Firebase service account not provided. Push notifications will not work.');
}
}
@Process('sendPush')
async handleSendPush(job: Job<{ userId: string; eventType: NotificationEventType; title: string; body: string; data?: Record<string, string> }>) {
const { userId, eventType, title, body, data } = job.data;
this.logger.debug(`Processing push notification for user ${userId} event ${eventType}`);
// Get devices
const devices = await this.deviceRepo.find({
where: { userId, isActive: true },
});
if (devices.length === 0) {
this.logger.debug(`No active devices for user ${userId}`);
return;
}
const tokens = devices.map((d) => d.deviceToken);
// Send via FCM
if (!this.firebaseApp) {
this.logger.warn('Firebase not initialized. Skipping notification send.');
return;
}
const message: firebaseAdmin.messaging.MulticastMessage = {
tokens,
notification: {
title,
body,
},
data,
android: {
priority: 'high',
},
apns: {
payload: {
aps: {
sound: 'default',
},
},
},
};
try {
const response = await this.firebaseApp.messaging().sendEachForMulticast(message);
// Handle failed tokens
if (response.failureCount > 0) {
const failedTokens: string[] = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
const error = resp.error;
if (error && (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered')) {
failedTokens.push(tokens[idx]);
}
// If error is internal or retryable, we could throw to let Bull retry the job.
// But since sendEachForMulticast is partial success, retrying the whole job might resend to successful tokens.
// Ideally we should create new job for failed tokens, but that's complex.
// For now, we accept partial success.
}
});
if (failedTokens.length > 0) {
await this.handleFailedTokens(failedTokens);
}
}
this.logger.log(`Sent push notification to ${response.successCount}/${tokens.length} devices for user ${userId}`);
} catch (error) {
this.logger.error('Error sending multicast message', error);
throw error; // Let Bull retry
}
}
private async handleFailedTokens(tokens: string[]) {
this.logger.log(`Removing ${tokens.length} invalid tokens`);
await this.deviceRepo.delete({ deviceToken: In(tokens) });
}
}