1+ const OneSignal = require ( '@onesignal/node-onesignal' )
2+ const z = require ( 'zod' )
3+
4+ const { generateUUIDv4 } = require ( '@open-condo/miniapp-utils/helpers/uuid' )
5+
6+ const { PUSH_TYPE_SILENT_DATA } = require ( '@condo/domains/notification/constants/constants' )
7+
8+ const HIGHEST_PRIORITY = 10
9+ const APNS_PUSH_TYPE_OVERRIDE_VOIP = 'voip'
10+ const TARGET_CHANNEL = 'push'
11+
12+
13+
14+ const CONFIG_SCHEMA = z . object ( {
15+ providerAppId : z . string ( ) ,
16+ apiKey : z . string ( ) ,
17+ } )
18+
19+ class OneSignalNotificationSender {
20+ /** @type {import('@onesignal/node-onesignal').DefaultApi } */
21+ _app
22+ providerAppId
23+
24+ constructor ( config ) {
25+ const { success, data : parsedConfig , error } = CONFIG_SCHEMA . safeParse ( config )
26+ if ( ! success ) {
27+ throw new AggregateError ( [
28+ Error ( 'OneSignalNotificationSender config is invalid' ) ,
29+ error ,
30+ ] )
31+ }
32+ const oneSignalConfiguration = OneSignal . createConfiguration ( { restApiKey : parsedConfig . apiKey } )
33+ this . _app = new OneSignal . DefaultApi ( oneSignalConfiguration )
34+ this . providerAppId = parsedConfig . providerAppId
35+ }
36+
37+ /**
38+ * @param notification {import('@onesignal/node-onesignal').Notification }
39+ * @returns {Promise<{
40+ * notification: import('@onesignal /node-onesignal').Notification,
41+ * response?: import('@onesignal/node-onesignal').CreateNotificationSuccessResponse,
42+ * error?: Error
43+ * }> }
44+ */
45+ async sendPush ( notification ) {
46+ try {
47+ notification . idempotency_key ??= generateUUIDv4 ( )
48+ return { notification, response : await this . _app . createNotification ( notification ) }
49+ } catch ( err ) {
50+ return { notification, error : err }
51+ }
52+ }
53+
54+ /**
55+ * @param notifications
56+ * @param isVoIP
57+ * @returns {Promise<{responses: {token: string, success: boolean, response: import('@onesignal/node-onesignal').CreateNotificationSuccessResponse | null, error?: Error}[], successCount: number, failureCount: number}> }
58+ */
59+ async sendAll ( notifications , isVoIP = false ) {
60+ const responses = [ ]
61+ let successCount = 0 , failureCount = 0
62+
63+ const providerNotificationsObjects = this . _buildPayloadsForProvider ( notifications , isVoIP )
64+
65+ const promises = await Promise . allSettled ( providerNotificationsObjects . map ( async ( providerNotificationsObject ) => {
66+ return await this . sendPush ( providerNotificationsObject )
67+ } ) )
68+
69+ for ( const p of promises ) {
70+ if ( p . status !== 'fulfilled' ) {
71+ responses . push ( { error : p . reason } )
72+ failureCount += 1
73+ continue
74+ }
75+ /** @type {import('@onesignal/node-onesignal').CreateNotificationSuccessResponse } */
76+ const { notification, response, error } = p . value
77+ if ( error ) {
78+ responses . push ( ...notification . include_subscription_ids . map ( token => ( {
79+ token,
80+ success : false ,
81+ response,
82+ error,
83+ } ) ) )
84+ failureCount += notification . include_subscription_ids . length
85+ } else {
86+ const didSendMessageToSomebody = ! ! response . id
87+
88+ // TODO(YEgorLu): DOMA-12941 add token invalidation for provider using that:
89+ // if (response.errors) {
90+ // if (response.errors.includes('All included players are not subscribed')) {
91+ // invalidSubscriptionIds = notification.include_subscription_ids
92+ // }
93+ // else if (response.errors.invalid_player_ids) {
94+ // invalidSubscriptionIds = response.errors.invalid_player_ids
95+ // }
96+ // }
97+
98+ if ( didSendMessageToSomebody ) {
99+ const surelyNonSuccessTokens = ( response . errors ?. invalid_player_ids ?? [ ] )
100+
101+ responses . push ( ...notification . include_subscription_ids . map ( token => {
102+ const success = ! surelyNonSuccessTokens . includes ( token )
103+ return {
104+ token,
105+ success,
106+ response,
107+ }
108+ } ) )
109+ successCount += notification . include_subscription_ids . length - surelyNonSuccessTokens . length
110+ failureCount += surelyNonSuccessTokens . length
111+ } else {
112+ responses . push ( ...notification . include_subscription_ids . map ( token => ( {
113+ token,
114+ success : false ,
115+ response,
116+ } ) ) )
117+ failureCount += notification . include_subscription_ids . length
118+ }
119+ }
120+ }
121+
122+ return { responses, successCount, failureCount }
123+ }
124+
125+ /**
126+ * Dedupes notifications by their content and builds payloads for provider
127+ * @param notifications
128+ * @param isVoIP
129+ * @returns {Notification[] }
130+ * @private
131+ */
132+ _buildPayloadsForProvider ( notifications , isVoIP ) {
133+ const sameNotificationForMultipleTokensByBody = { }
134+ notifications . forEach ( notification => {
135+ const body = { data : notification . data , notification : notification . notification , type : notification . type }
136+ const bodyStringified = JSON . stringify ( body )
137+ if ( ! sameNotificationForMultipleTokensByBody [ bodyStringified ] ) sameNotificationForMultipleTokensByBody [ bodyStringified ] = {
138+ data : notification . data ,
139+ notification : notification . notification ,
140+ type : notification . type ,
141+ tokens : [ ] ,
142+ }
143+ sameNotificationForMultipleTokensByBody [ bodyStringified ] . tokens . push ( notification . token )
144+ } )
145+
146+ return Object . values ( sameNotificationForMultipleTokensByBody ) . map ( ( { tokens, type, data, notification } ) => {
147+ const providerNotification = new OneSignal . Notification ( )
148+
149+ providerNotification . app_id = this . providerAppId
150+ providerNotification . include_subscription_ids = tokens
151+ providerNotification . data = data
152+ providerNotification . target_channel = TARGET_CHANNEL
153+ providerNotification . mutable_content = true
154+
155+ if ( type === PUSH_TYPE_SILENT_DATA ) {
156+ providerNotification . content_available = true
157+ } else {
158+ providerNotification . headings = { en : notification . title } // NOTE(YEgorLu): provide only one language, it will be used for everyone
159+ providerNotification . contents = { en : notification . body } // NOTE(YEgorLu): provide only one language, it will be used for everyone
160+ }
161+
162+ if ( isVoIP ) {
163+ providerNotification . apns_push_type_override = APNS_PUSH_TYPE_OVERRIDE_VOIP
164+ providerNotification . priority = HIGHEST_PRIORITY
165+ }
166+
167+ return providerNotification
168+ } )
169+ }
170+
171+ }
172+
173+ module . exports = { OneSignalNotificationSender }
0 commit comments