@abtnode/blocklet-services 1.16.39-beta-20250213-123635-cabff5af → 1.16.39-beta-20250218-110004-a308c501

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/api/index.js +6 -2
  2. package/api/libs/email.js +2 -2
  3. package/api/services/notification/index.js +4 -2
  4. package/api/services/notification/notification-queue.js +671 -0
  5. package/api/socket/channel/did.js +146 -473
  6. package/dist/assets/{Add-TglZpKpw.js → Add-eTw9dWVa.js} +1 -1
  7. package/dist/assets/{ArrowDropDown-DCM27ed6.js → ArrowDropDown-D-RD-2MB.js} +1 -1
  8. package/dist/assets/{CheckCircle-CH8SYy4d.js → CheckCircle-fnIa_Tjy.js} +1 -1
  9. package/dist/assets/{ChevronLeft-B7m8AFzR.js → ChevronLeft-B6BBtJvf.js} +1 -1
  10. package/dist/assets/{ChevronRight-DeRklga5.js → ChevronRight-BO6iBxGF.js} +1 -1
  11. package/dist/assets/{DeleteOutline-DZA1YKE4.js → DeleteOutline-6B6Fe5fo.js} +1 -1
  12. package/dist/assets/{Done-CVdTMvGH.js → Done-BpK9makc.js} +1 -1
  13. package/dist/assets/{Download-DECMRaRC.js → Download-CHVAgVy8.js} +1 -1
  14. package/dist/assets/{Error-B61ZB0Rr.js → Error-kGbi4Zxj.js} +1 -1
  15. package/dist/assets/{Google-D_UbrpMW.js → Google-T-9fe6qI.js} +1 -1
  16. package/dist/assets/{InfoOutlined-QBw5AmWP.js → InfoOutlined-BgLmQpjA.js} +1 -1
  17. package/dist/assets/{Launch-Cpp3LVnV.js → Launch-C9zEVLvk.js} +1 -1
  18. package/dist/assets/{LaunchOutlined-WdAKKV2H.js → LaunchOutlined-Cql1NmZZ.js} +1 -1
  19. package/dist/assets/{Loop-CbPhaPlF.js → Loop-CawJEzLG.js} +1 -1
  20. package/dist/assets/{MoreHoriz-BsTY94ce.js → MoreHoriz-M9rI-Gh1.js} +1 -1
  21. package/dist/assets/{PlayArrow-BgVzxA9Q.js → PlayArrow-BAowUCpY.js} +1 -1
  22. package/dist/assets/{ViewList-DBUe2NSG.js → ViewList-BPtSTNst.js} +1 -1
  23. package/dist/assets/{access-control-Bi0VTaOk.js → access-control-DEUU0fDz.js} +1 -1
  24. package/dist/assets/{actions-DP_Rm54e.js → actions-wYAFmtYW.js} +1 -1
  25. package/dist/assets/{add-component-core-BEey6wbp.js → add-component-core-C36Oh0S_.js} +1 -1
  26. package/dist/assets/{add-resource-FReSj-BV.js → add-resource-Xm48tGBR.js} +1 -1
  27. package/dist/assets/{addon-DH34oI6i.js → addon-DJRfOF39.js} +1 -1
  28. package/dist/assets/{analytics-Ca_QfsU9.js → analytics-BxMLe4sZ.js} +1 -1
  29. package/dist/assets/{ar-BJD4HTM3.js → ar-BFUPmTkR.js} +1 -1
  30. package/dist/assets/audit-logs-QI7Dc290.js +35 -0
  31. package/dist/assets/{base32-CwwDjweV.js → base32-BMSr-Oor.js} +1 -1
  32. package/dist/assets/{branding-CPKpYWoE.js → branding-BiBArxE7.js} +2 -2
  33. package/dist/assets/{bundle-avatar-fBOGFPuw.js → bundle-avatar-CcKbW8ZA.js} +1 -1
  34. package/dist/assets/{button-4qfNeG4u.js → button-hN0nkg5h.js} +1 -1
  35. package/dist/assets/{click-to-copy-886R1sA9.js → click-to-copy-DlRwVMBE.js} +1 -1
  36. package/dist/assets/{complete-DV_B2Akx.js → complete-BCHhjgui.js} +2 -2
  37. package/dist/assets/{component-BFuSJ7_N.js → component---BohhtR.js} +1 -1
  38. package/dist/assets/{config-BBRmjpUL.js → config-BT7DA9eX.js} +1 -1
  39. package/dist/assets/{config-DjP6h0XJ.js → config-Chtg5c7S.js} +1 -1
  40. package/dist/assets/{config-navigation-ObpJPhkA.js → config-navigation-DGQXESo8.js} +3 -3
  41. package/dist/assets/config-space-D3_ddjoK.js +1 -0
  42. package/dist/assets/{confirm-BGxdRdSL.js → confirm-BUf2FX6y.js} +1 -1
  43. package/dist/assets/{connect-Clh-D5g-.js → connect-9ktVrhs9.js} +1 -1
  44. package/dist/assets/{connect-CVdbQs_b.js → connect-Bg1fiX7Q.js} +1 -1
  45. package/dist/assets/{connect-to-QEsnDZMk.js → connect-to-6-yxL1od.js} +1 -1
  46. package/dist/assets/{dashboard-7MqA_6AS.js → dashboard-CRO3eTt1.js} +4 -4
  47. package/dist/assets/{de-CaPBInxb.js → de-hV0yD3gq.js} +1 -1
  48. package/dist/assets/{did-address-CgDvtQbT.js → did-address-Rncailh0.js} +1 -1
  49. package/dist/assets/{domain-DwtXLEdV.js → domain-CiQ58Nbb.js} +1 -1
  50. package/dist/assets/{domain-list-D-ZZFmEQ.js → domain-list-DlMnYtcT.js} +2 -2
  51. package/dist/assets/{email-BmLIq6Hu.js → email-C7B2iQRL.js} +1 -1
  52. package/dist/assets/{es-hhXhgxGX.js → es-BcSAobJW.js} +1 -1
  53. package/dist/assets/{exchange-passport-C8_WcBsP.js → exchange-passport-DvgnrFhH.js} +1 -1
  54. package/dist/assets/{form-text-input-DM0E2BGK.js → form-text-input-B3XB3R1O.js} +1 -1
  55. package/dist/assets/{fr-BwQvAhhM.js → fr-8qti32p1.js} +1 -1
  56. package/dist/assets/{fuel-B78lL2Fn.js → fuel-De_bc9rw.js} +1 -1
  57. package/dist/assets/{fullpage-DLk_Ap1u.js → fullpage-ClwEgpoB.js} +1 -1
  58. package/dist/assets/{get-safe-url-BzE2b8Zw.js → get-safe-url-3FtlbQXR.js} +1 -1
  59. package/dist/assets/{get-safe-url-C2hTfmjV.js → get-safe-url-DgnUmHPg.js} +1 -1
  60. package/dist/assets/{hi-CRWmR5oy.js → hi-DDEUhQcX.js} +1 -1
  61. package/dist/assets/{home-B30MMEWW.js → home-s4XxyP6G.js} +1 -1
  62. package/dist/assets/{id-Dlr6Q1sk.js → id-DkUYINLx.js} +1 -1
  63. package/dist/assets/{iframe-DvysnY8O.js → iframe-DkAOGH82.js} +1 -1
  64. package/dist/assets/index-AKILhCB1.js +104 -0
  65. package/dist/assets/{index-DZevAS4q.js → index-BM6MW3P4.js} +1 -1
  66. package/dist/assets/{index-CYDwuSY3.js → index-BQD1ktrC.js} +1 -1
  67. package/dist/assets/{index-V7v6XM-s.js → index-BuAHCrPZ.js} +1 -1
  68. package/dist/assets/{index-BaTH8sfn.js → index-C3Z6Hby2.js} +1 -1
  69. package/dist/assets/{index-xzyguKIp.js → index-C3cP3eho.js} +1 -1
  70. package/dist/assets/{index-B4_ve2gt.js → index-CKmE0Sqg.js} +1 -1
  71. package/dist/assets/index-CctdI8X8.js +125 -0
  72. package/dist/assets/{index-BnU6Au83.js → index-DEkiPG8Y.js} +1 -1
  73. package/dist/assets/{index-p_TKo4z5.js → index-DFzh5oL0.js} +1 -1
  74. package/dist/assets/index-DiLNptzT.js +5 -0
  75. package/dist/assets/{index-DnUMPb7W.js → index-IC1Rf7BW.js} +1 -1
  76. package/dist/assets/{index-DaNBEDBM.js → index-Pdt8JY75.js} +2 -2
  77. package/dist/assets/{index-BXJDCni0.js → index-XjSUrEf9.js} +1 -1
  78. package/dist/assets/{index-DbZgctRe.js → index-vSb5bw1I.js} +4 -4
  79. package/dist/assets/{invitation-CLUC3FWC.js → invitation-398ugOww.js} +1 -1
  80. package/dist/assets/{invite-upfjzZhU.js → invite-CxctjCks.js} +1 -1
  81. package/dist/assets/{issue-passport-a41Odi7S.js → issue-passport-BzEXCFP0.js} +1 -1
  82. package/dist/assets/{item-DFHyV0HY.js → item-BzigISWG.js} +1 -1
  83. package/dist/assets/{ja-CmKylH6K.js → ja-C2LP4WQG.js} +1 -1
  84. package/dist/assets/{ko-Blp8936a.js → ko-C-DMeONa.js} +1 -1
  85. package/dist/assets/{layout-Ctv3Y3ew.js → layout-N5xbN7Ti.js} +1 -1
  86. package/dist/assets/{list-eFeJihdG.js → list-QwzSO7Cs.js} +1 -1
  87. package/dist/assets/{list-header-BBHpD8dC.js → list-header-bKaZ4zsu.js} +1 -1
  88. package/dist/assets/{localization-Ciwo-y3M.js → localization-CNvFSQc0.js} +1 -1
  89. package/dist/assets/{log-BsBzWP3w.js → log-Bz9TkxcV.js} +1 -1
  90. package/dist/assets/{login-m03_Nf1G.js → login-DbJD9Sgu.js} +1 -1
  91. package/dist/assets/{login-oauth-callback-D_bP2yYx.js → login-oauth-callback-BagCPt1j.js} +1 -1
  92. package/dist/assets/{logo-uploader-CMRcZtFG.js → logo-uploader-QnBnkpOO.js} +3 -3
  93. package/dist/assets/{lost-passport-PcFxL_2W.js → lost-passport-se4N22vE.js} +1 -1
  94. package/dist/assets/{open-window-Q0PVNWdu.js → open-window-BWPlSJZK.js} +1 -1
  95. package/dist/assets/{overview-O8Z47dUY.js → overview-u84fbCZU.js} +1 -1
  96. package/dist/assets/{page-header-BDwGXuEW.js → page-header-CVCtfFXY.js} +1 -1
  97. package/dist/assets/{permission-Cqed8iP-.js → permission-CoRqvd18.js} +1 -1
  98. package/dist/assets/{preferences-CBm0eML7.js → preferences-CCm3JuB9.js} +1 -1
  99. package/dist/assets/{pt-BbwAvpfn.js → pt-CbxvF__Z.js} +1 -1
  100. package/dist/assets/publish-resource-Cmlpn7H3.js +1 -0
  101. package/dist/assets/{react-beautiful-dnd.esm-BTWrOgJp.js → react-beautiful-dnd.esm-CrvlS-3X.js} +1 -1
  102. package/dist/assets/{relative-time-BcL7VB-n.js → relative-time-IDiRzYUd.js} +1 -1
  103. package/dist/assets/{ru-DgfLeWul.js → ru-DLHrAeE7.js} +1 -1
  104. package/dist/assets/sdk-DRntvOGy.js +1 -0
  105. package/dist/assets/{session-BKdZFtFw.js → session-BzY_i5lZ.js} +1 -1
  106. package/dist/assets/{setup-DcoLUEuM.js → setup-CFL-k7o4.js} +3 -3
  107. package/dist/assets/{start-iucPKmSu.js → start-Bnol7r-4.js} +1 -1
  108. package/dist/assets/{step-actions-DXTmms_Q.js → step-actions-vFuOlgmX.js} +1 -1
  109. package/dist/assets/{studio-BzK2t7MU.js → studio-D8dNrjXX.js} +1 -1
  110. package/dist/assets/{switch-control-BSKk9hbF.js → switch-control-DTOAJIEb.js} +1 -1
  111. package/dist/assets/{th-DXRi_ppC.js → th-Dil9kwjH.js} +1 -1
  112. package/dist/assets/{traffic-Dg0xCbfH.js → traffic-C-Or__6J.js} +1 -1
  113. package/dist/assets/{transfer-DW5JevxW.js → transfer-tWhnvLV8.js} +1 -1
  114. package/dist/assets/{unsubscribe-BxmY3IWn.js → unsubscribe-8PpNeEGq.js} +1 -1
  115. package/dist/assets/{useLocalStorage-Bx8dJwZw.js → useLocalStorage-DDsWbihG.js} +1 -1
  116. package/dist/assets/{user-center-BSOPivqw.js → user-center-FVVJclAc.js} +1 -1
  117. package/dist/assets/util-CqwLA_zk.js +1 -0
  118. package/dist/assets/{util-7Hs7DKO_.js → util-DfAqJble.js} +1 -1
  119. package/dist/assets/{vendor-arcblock-HzAExiTd.js → vendor-arcblock-naCuSDgJ.js} +5 -5
  120. package/dist/assets/{vi-nO7JrO24.js → vi-B46hyfUM.js} +1 -1
  121. package/dist/assets/wrap-locale-JORvcC3E.js +1 -0
  122. package/dist/assets/{zh-CEvYioAt.js → zh-DWWuPzr1.js} +1 -1
  123. package/dist/assets/{zh-tw-BjfRYnXu.js → zh-tw-Ci21sS2Y.js} +1 -1
  124. package/dist/index.html +2 -2
  125. package/dist/service-worker.js +1 -1
  126. package/package.json +24 -24
  127. package/dist/assets/audit-logs-97b1Xxh-.js +0 -18
  128. package/dist/assets/config-space-BbJdWcbl.js +0 -1
  129. package/dist/assets/index-BGAIEQ8H.js +0 -104
  130. package/dist/assets/index-Bg0fGJqy.js +0 -5
  131. package/dist/assets/index-JiEuwDJt.js +0 -123
  132. package/dist/assets/publish-resource-HnBr77fP.js +0 -1
  133. package/dist/assets/sdk-B4wNFenI.js +0 -1
  134. package/dist/assets/util-BtA4vTl6.js +0 -1
  135. package/dist/assets/wrap-locale--wnFaGbc.js +0 -1
@@ -0,0 +1,671 @@
1
+ const createQueue = require('@abtnode/core/lib/util/queue');
2
+ const { validateNotification } = require('@blocklet/sdk/lib/validators/notification');
3
+ const states = require('@abtnode/core/lib/states');
4
+ const JWT = require('@arcblock/jwt');
5
+ const md5 = require('@abtnode/util/lib/md5');
6
+ const {
7
+ NODE_MODES,
8
+ EVENTS,
9
+ NOTIFICATION_SEND_CHANNEL,
10
+ NOTIFICATION_SEND_STATUS,
11
+ NOTIFICATION_SEND_FAILED_REASON,
12
+ } = require('@abtnode/constant');
13
+ const get = require('lodash/get');
14
+ const uniqBy = require('lodash/uniqBy');
15
+ const { getWalletDid } = require('@blocklet/meta/lib/did-utils');
16
+ const { getBlockletInfo } = require('../../cache');
17
+ const { updateNotificationSendStatus } = require('../../socket/channel/did');
18
+ const eventHub =
19
+ process.env.NODE_ENV === 'test' ? require('@arcblock/event-hub/single') : require('@arcblock/event-hub');
20
+
21
+ const logger = require('../../libs/logger')('notification');
22
+
23
+ const createNotificationQueue = (name, options, handler) => {
24
+ if (!handler || typeof handler !== 'function') {
25
+ throw new Error('Handler is required');
26
+ }
27
+ return createQueue({
28
+ name,
29
+ model: states.job,
30
+ daemon: true,
31
+ options: {
32
+ maxRetries: 3,
33
+ retryDelay: 10 * 1000,
34
+ maxTimeout: 60 * 1000, // throw timeout error after 1 minutes
35
+ ...(options ?? {}),
36
+ },
37
+ onJob: async (job) => {
38
+ await handler(job);
39
+ },
40
+ });
41
+ };
42
+
43
+ const init = ({ node, notificationService }) => {
44
+ const webhookState = states.webhook;
45
+
46
+ const getServerWebhooks = async () => {
47
+ const webhookList = (await webhookState.list()) ?? [];
48
+ return webhookList.flatMap((item) =>
49
+ item.params.filter((param) => param.name === 'url').map((param) => ({ type: item.type, url: param.value }))
50
+ );
51
+ };
52
+
53
+ /**
54
+ * wallet 推送队列
55
+ */
56
+ const walletPushQueue = createNotificationQueue(
57
+ 'send-notification-wallet',
58
+ {
59
+ maxRetries: 1,
60
+ retryDelay: 0,
61
+ },
62
+ async (job) => {
63
+ const { receiver, notification, sender, options } = job;
64
+
65
+ if (!receiver) {
66
+ logger.error('Invalid receiver', { did: receiver });
67
+ throw new Error('Invalid receiver');
68
+ }
69
+
70
+ const nodeInfo = await node.getNodeInfo({ useCache: true });
71
+
72
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
73
+ try {
74
+ await validateNotification(notification);
75
+ } catch (error) {
76
+ logger.error('Failed to validate notification', { error });
77
+ // 抛出错误,阻止后续执行
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ if (sender.type === 'server' && !sender.token) {
83
+ sender.token = JWT.sign(sender.appDid, sender.appSk);
84
+ }
85
+
86
+ // 发送钱包通知
87
+ await notificationService.sendToApp.exec({
88
+ sender,
89
+ receiver,
90
+ notification,
91
+ options,
92
+ });
93
+ }
94
+ );
95
+
96
+ /**
97
+ * Push Kit 推送队列
98
+ */
99
+ const pushKitPushQueue = createNotificationQueue('send-notification-push', {}, async (job) => {
100
+ const { notification, receiver, sender } = job;
101
+
102
+ if (!receiver) {
103
+ logger.error('Invalid receiver', { did: receiver });
104
+ throw new Error('Invalid receiver');
105
+ }
106
+
107
+ const nodeInfo = await node.getNodeInfo({ useCache: true });
108
+
109
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
110
+ try {
111
+ await validateNotification(notification);
112
+ } catch (error) {
113
+ logger.error('Failed to validate notification', { error });
114
+ // 抛出错误,阻止后续执行
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ if (sender.type === 'server' && !sender.token) {
120
+ sender.token = JWT.sign(sender.appDid, sender.appSk);
121
+ }
122
+
123
+ // 发送 push kit 通知
124
+ await notificationService.sendToPush.exec({
125
+ sender,
126
+ receiver,
127
+ notification,
128
+ });
129
+ });
130
+
131
+ /**
132
+ * email 推送队列
133
+ */
134
+ const emailPushQueue = createNotificationQueue(
135
+ 'send-notification-email',
136
+ {
137
+ maxRetries: 1,
138
+ retryDelay: 0,
139
+ maxTimeout: 60 * 1000,
140
+ id: (job) => (job ? md5(`${job.email}_${job.notificationId}`) : ''),
141
+ enableScheduledJob: true,
142
+ },
143
+ async (job) => {
144
+ const { input, email } = job;
145
+ if (!input) {
146
+ logger.error('Job input is missing or invalid', { notificationId: job.notificationId });
147
+ throw new Error('Job input is missing or invalid');
148
+ }
149
+
150
+ if (!email) {
151
+ logger.error('Email address is missing. Unable to send email notification', {
152
+ notificationId: job.notificationId,
153
+ });
154
+ throw new Error('Email address is missing. Unable to send email notification');
155
+ }
156
+
157
+ const { notification, sender, teamDid, receivers = [], userInfo } = input;
158
+
159
+ if (!userInfo) {
160
+ logger.error('Invalid receiver', { dids: userInfo.did });
161
+ throw new Error('Invalid receiver');
162
+ }
163
+
164
+ const nodeInfo = await node.getNodeInfo({ useCache: true });
165
+
166
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
167
+ try {
168
+ await validateNotification(notification);
169
+ } catch (error) {
170
+ logger.error('Failed to validate notification', { error });
171
+ // 抛出错误,阻止后续执行
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ if (sender.type === 'server' && !sender.token) {
177
+ sender.token = JWT.sign(sender.appDid, sender.appSk);
178
+ }
179
+
180
+ let wallet;
181
+
182
+ if (sender.type !== 'server') {
183
+ const blockletInfo = await getBlockletInfo({ did: teamDid, node });
184
+ wallet = blockletInfo.wallet;
185
+ }
186
+ const now = Math.floor(Date.now() / 1000);
187
+ const unsubscribeToken = JWT.signV2(wallet.address, wallet.secretKey, {
188
+ userDid: userInfo.did,
189
+ channel: 'email',
190
+ exp: String(now + 30 * 24 * 60 * 60), // 30 days
191
+ });
192
+ notificationService.sendToMail.exec({
193
+ sender,
194
+ receiver: email,
195
+ notification: {
196
+ ...notification,
197
+ appInfo: {
198
+ ...(notification.appInfo || {}),
199
+ receivers: receivers.length > 0 ? receivers : [userInfo.did],
200
+ unsubscribeToken,
201
+ userInfo: {
202
+ did: userInfo.did,
203
+ fullName: userInfo.fullName,
204
+ },
205
+ },
206
+ },
207
+ });
208
+ }
209
+ );
210
+
211
+ /**
212
+ * webhook 推送队列
213
+ */
214
+ const webhookQueue = createNotificationQueue(
215
+ 'send-notification-webhook',
216
+ {
217
+ maxRetries: 3,
218
+ retryDelay: 10 * 1000,
219
+ maxTimeout: 5 * 60 * 1000, // throw timeout error after 5 minutes
220
+ id: (job) => (job ? md5(`${job.url}_${job.notificationId}`) : ''),
221
+ enableScheduledJob: true,
222
+ },
223
+ async (job) => {
224
+ const { input } = job;
225
+ if (!input) {
226
+ logger.error('Invalid job', { url: job.url });
227
+ throw new Error('Invalid job');
228
+ }
229
+
230
+ const { notification, webhook, sender, receivers = [] } = input;
231
+
232
+ if (!receivers.length) {
233
+ logger.error('Invalid receiver', { dids: receivers });
234
+ throw new Error('Invalid receiver');
235
+ }
236
+
237
+ const nodeInfo = await node.getNodeInfo({ useCache: true });
238
+
239
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
240
+ try {
241
+ await validateNotification(notification);
242
+ } catch (error) {
243
+ logger.error('Failed to validate notification', { error });
244
+ // 抛出错误,阻止后续执行
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ if (!webhook || !webhook.url) {
250
+ logger.error('Invalid webhook', { webhook });
251
+ throw new Error('Invalid webhook');
252
+ }
253
+
254
+ if (sender.type === 'server' && !sender.token) {
255
+ sender.token = JWT.sign(sender.appDid, sender.appSk);
256
+ }
257
+
258
+ notificationService.sendToWebhook.exec({
259
+ sender,
260
+ receiver: receivers,
261
+ notification: {
262
+ ...notification,
263
+ appInfo: {
264
+ ...(notification.appInfo || {}),
265
+ webhooks: [webhook],
266
+ },
267
+ },
268
+ });
269
+ }
270
+ );
271
+
272
+ const insertToEmailPushQueue = (props, nodeInfo, emailMap) => {
273
+ const { channels, notification, sender, userInfo, teamDid } = props;
274
+
275
+ const commonParams = {
276
+ node,
277
+ teamDid: teamDid ?? nodeInfo.did,
278
+ notificationId: notification.id,
279
+ };
280
+
281
+ const emailEnabled = get(userInfo, 'extra.notifications.email', true);
282
+
283
+ const email = userInfo?.email;
284
+
285
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
286
+
287
+ const channelEnabled = emailEnabled && channels.includes(NOTIFICATION_SEND_CHANNEL.EMAIL);
288
+
289
+ const { receivers, pushUser } = emailMap.get(email) || { receivers: [], pushUser: {} };
290
+
291
+ const pushUserDid = getWalletDid(pushUser) || pushUser.did;
292
+
293
+ // 如果 receivers 中已经存在 receiverDid,并且 pushUserDid 和 receiverDid 不一致,则不发送
294
+ if (receivers.includes(receiverDid) && pushUserDid !== receiverDid) {
295
+ return;
296
+ }
297
+
298
+ if (channelEnabled && email) {
299
+ const _userInfo = {
300
+ did: receiverDid,
301
+ fullName: userInfo.fullName,
302
+ };
303
+ // 这里可以根据 email 获取到需要发送的 userInfo 和 receivers
304
+ emailPushQueue.push({
305
+ job: {
306
+ email,
307
+ notificationId: notification.id,
308
+ input: {
309
+ notification,
310
+ sender,
311
+ teamDid,
312
+ receivers,
313
+ userInfo: _userInfo,
314
+ },
315
+ },
316
+ delay: 5,
317
+ });
318
+ } else {
319
+ updateNotificationSendStatus({
320
+ ...commonParams,
321
+ receivers: receivers.length > 0 ? receivers : [receiverDid],
322
+ channel: NOTIFICATION_SEND_CHANNEL.EMAIL,
323
+ status: NOTIFICATION_SEND_STATUS.FAILED,
324
+ failedReason:
325
+ !emailEnabled || !email
326
+ ? NOTIFICATION_SEND_FAILED_REASON.USER_DISABLED
327
+ : NOTIFICATION_SEND_FAILED_REASON.CHANNEL_DISABLED,
328
+ });
329
+ }
330
+ };
331
+
332
+ const insertToWebhookPushQueue = async (props, nodeInfo, webhookMap) => {
333
+ const { channels, notification, sender, userInfo, teamDid } = props;
334
+
335
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
336
+
337
+ const commonParams = {
338
+ node,
339
+ teamDid: teamDid ?? nodeInfo.did,
340
+ notificationId: notification.id,
341
+ };
342
+
343
+ const isServer = teamDid === nodeInfo.did;
344
+
345
+ let webhooks = get(userInfo, 'extra.webhooks', []);
346
+ if (isServer) {
347
+ webhooks = await getServerWebhooks();
348
+ }
349
+
350
+ const webhookList = uniqBy(webhooks, 'url')
351
+ .map((webhook) => {
352
+ const { url, type } = webhook;
353
+ const { receivers, pushUser } = webhookMap.get(url) || {
354
+ type: type ?? 'api',
355
+ receivers: [],
356
+ pushUser: {},
357
+ };
358
+ const pushUserDid = getWalletDid(pushUser) || pushUser.did;
359
+ if (receivers.includes(receiverDid) && pushUserDid !== receiverDid) {
360
+ return false;
361
+ }
362
+ return {
363
+ url,
364
+ type,
365
+ receivers,
366
+ pushUser,
367
+ };
368
+ })
369
+ .filter(Boolean);
370
+
371
+ if (webhookList.length > 0 && channels.includes(NOTIFICATION_SEND_CHANNEL.WEBHOOK)) {
372
+ for (const webhook of webhookList) {
373
+ const { url, type, receivers } = webhook;
374
+ webhookQueue.push({
375
+ job: {
376
+ url,
377
+ notificationId: notification.id,
378
+ input: {
379
+ notification,
380
+ sender,
381
+ receivers: receivers.length > 0 ? receivers : [receiverDid],
382
+ webhook: {
383
+ url,
384
+ type,
385
+ },
386
+ },
387
+ },
388
+ delay: 5,
389
+ });
390
+ }
391
+ } else {
392
+ // eslint-disable-next-line no-lonely-if
393
+ if (webhookList.length > 0) {
394
+ const webhookParams = {};
395
+
396
+ for (const { url, type, receivers } of webhookList) {
397
+ webhookParams[url] = {
398
+ type,
399
+ sendAt: new Date(),
400
+ status: NOTIFICATION_SEND_STATUS.FAILED,
401
+ failedReason: NOTIFICATION_SEND_FAILED_REASON.CHANNEL_DISABLED,
402
+ };
403
+
404
+ // eslint-disable-next-line no-await-in-loop
405
+ await updateNotificationSendStatus({
406
+ ...commonParams,
407
+ receivers: receivers.length > 0 ? receivers : [receiverDid],
408
+ channel: NOTIFICATION_SEND_CHANNEL.WEBHOOK,
409
+ status: NOTIFICATION_SEND_STATUS.FAILED,
410
+ webhookParams: { [url]: webhookParams[url] },
411
+ });
412
+ }
413
+ }
414
+ }
415
+ };
416
+
417
+ const insertToPushKitPushQueue = (props, nodeInfo) => {
418
+ const { channels, notification, sender, userInfo, teamDid, options = {} } = props;
419
+
420
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
421
+
422
+ const commonParams = {
423
+ node,
424
+ teamDid: teamDid ?? nodeInfo.did,
425
+ notificationId: notification.id,
426
+ };
427
+ const pushEnabled = get(userInfo, 'extra.notifications.push', true);
428
+ if (pushEnabled && channels.includes(NOTIFICATION_SEND_CHANNEL.PUSH)) {
429
+ pushKitPushQueue.push({
430
+ notification,
431
+ sender,
432
+ options,
433
+ receiver: receiverDid,
434
+ });
435
+ } else {
436
+ updateNotificationSendStatus({
437
+ ...commonParams,
438
+ receivers: [receiverDid],
439
+ channel: NOTIFICATION_SEND_CHANNEL.PUSH,
440
+ status: NOTIFICATION_SEND_STATUS.FAILED,
441
+ failedReason: !pushEnabled
442
+ ? NOTIFICATION_SEND_FAILED_REASON.CHANNEL_DISABLED
443
+ : NOTIFICATION_SEND_FAILED_REASON.USER_DISABLED,
444
+ });
445
+ }
446
+ };
447
+
448
+ const insertToWalletPushQueue = (props, nodeInfo) => {
449
+ const { channels, notification, sender, userInfo, teamDid, options = {} } = props;
450
+
451
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
452
+
453
+ const commonParams = {
454
+ node,
455
+ teamDid: teamDid ?? nodeInfo.did,
456
+ notificationId: notification.id,
457
+ };
458
+
459
+ const walletEnabled = get(userInfo, 'extra.notifications.wallet', true);
460
+ if (walletEnabled && channels.includes(NOTIFICATION_SEND_CHANNEL.WALLET)) {
461
+ walletPushQueue.push({
462
+ notification,
463
+ sender,
464
+ options,
465
+ receiver: receiverDid,
466
+ });
467
+ } else {
468
+ updateNotificationSendStatus({
469
+ ...commonParams,
470
+ receivers: [receiverDid],
471
+ channel: NOTIFICATION_SEND_CHANNEL.WALLET,
472
+ status: NOTIFICATION_SEND_STATUS.FAILED,
473
+ failedReason: !walletEnabled
474
+ ? NOTIFICATION_SEND_FAILED_REASON.CHANNEL_DISABLED
475
+ : NOTIFICATION_SEND_FAILED_REASON.USER_DISABLED,
476
+ });
477
+ }
478
+ };
479
+
480
+ const insertToNotificationReceiver = async (nodeInfo, userInfo, teamDid, notificationId, channels) => {
481
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
482
+
483
+ const receiverInstance = {
484
+ receiver: receiverDid,
485
+ notificationId,
486
+ };
487
+
488
+ const emailEnabled = get(userInfo, 'extra.notifications.email', true);
489
+ const email = userInfo?.email;
490
+
491
+ if (emailEnabled && email && channels.includes(NOTIFICATION_SEND_CHANNEL.EMAIL)) {
492
+ receiverInstance.email = email;
493
+ }
494
+ const isServer = teamDid === nodeInfo.did;
495
+
496
+ let webhooks = get(userInfo, 'extra.webhooks', []);
497
+ if (isServer) {
498
+ webhooks = await getServerWebhooks();
499
+ }
500
+ const webhookMap = new Map();
501
+ webhooks.forEach((webhook) => {
502
+ if (!webhookMap.has(webhook.url)) {
503
+ webhookMap.set(webhook.url, webhook.type ?? 'api');
504
+ }
505
+ });
506
+
507
+ if (webhookMap.size > 0 && channels.includes(NOTIFICATION_SEND_CHANNEL.WEBHOOK)) {
508
+ receiverInstance.webhookUrls = Array.from(webhookMap.keys()).join('#');
509
+ }
510
+
511
+ await node.createNotificationReceiver({
512
+ teamDid,
513
+ receiverInstance,
514
+ });
515
+ };
516
+
517
+ /**
518
+ * job 格式
519
+ * {
520
+ * channels: 需要发送到的 channels,
521
+ * notification: {},
522
+ * receivers: 接收的 receiver 列表
523
+ * }
524
+ */
525
+ const queue = createNotificationQueue('notification-receivers', {}, async (job) => {
526
+ const { teamDid, channels, receiver, sender, notification, nodeInfo, userInfo, emailMap, webhookMap, ...rest } =
527
+ job;
528
+
529
+ try {
530
+ if (!userInfo) {
531
+ logger.error('Invalid receiver user', { receiver });
532
+ throw new Error('Invalid receiver user');
533
+ }
534
+
535
+ if (!job.pushOnly) {
536
+ // 如果添加失败,那么需要终止执行推送
537
+ await insertToNotificationReceiver(nodeInfo, userInfo, teamDid, notification.id, channels);
538
+ }
539
+
540
+ const baseParams = {
541
+ ...rest,
542
+ teamDid,
543
+ channels,
544
+ receiver,
545
+ sender,
546
+ notification,
547
+ userInfo,
548
+ nodeInfo,
549
+ };
550
+ // 每个渠道之间的推送不会影响
551
+ // 添加到 wallet 推送队列
552
+ try {
553
+ insertToWalletPushQueue(baseParams, nodeInfo);
554
+ } catch (error) {
555
+ logger.error('Failed to insert to wallet push queue', { error });
556
+ }
557
+ // 添加到 push 推送队列
558
+ try {
559
+ insertToPushKitPushQueue(baseParams, nodeInfo);
560
+ } catch (error) {
561
+ logger.error('Failed to insert to push kit push queue', { error });
562
+ }
563
+
564
+ // 添加到 email 推送队列
565
+ try {
566
+ insertToEmailPushQueue(baseParams, nodeInfo, emailMap);
567
+ } catch (error) {
568
+ logger.error('Failed to insert to email push queue', { error });
569
+ }
570
+
571
+ //
572
+ try {
573
+ insertToWebhookPushQueue(baseParams, nodeInfo, webhookMap);
574
+ } catch (error) {
575
+ logger.error('Failed to insert to webhook push queue', { error });
576
+ }
577
+ } catch (error) {
578
+ logger.error('Failed to create notification receiver', { error });
579
+ }
580
+ });
581
+
582
+ // 监听 notification_create 事件
583
+ eventHub.on(EVENTS.NOTIFICATION_CREATE_QUEUED, async (data) => {
584
+ // Only first worker process handle blocklet event
585
+ try {
586
+ if (process.env.NODE_ENV !== 'test' && process.env.NODE_APP_INSTANCE !== '0') {
587
+ return;
588
+ }
589
+ if (!data.notification) {
590
+ logger.error('Invalid notification', { data });
591
+ return;
592
+ }
593
+ const nodeInfo = await node.getNodeInfo({ useCache: true });
594
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
595
+ try {
596
+ await validateNotification(data.notification);
597
+ } catch (error) {
598
+ logger.error('Failed to validate notification', { error });
599
+ // 抛出错误,阻止后续执行
600
+ throw error;
601
+ }
602
+ }
603
+
604
+ // 按照 receiver 添加到推送队列
605
+ const { receivers, teamDid, channels, ...rest } = data;
606
+ const sender = data.sender ?? {
607
+ appDid: nodeInfo.did,
608
+ appSk: nodeInfo.sk,
609
+ type: 'server',
610
+ };
611
+
612
+ const isServer = teamDid === nodeInfo.did;
613
+
614
+ const users = await node.getNotificationReceivers({
615
+ teamDid,
616
+ userDids: receivers,
617
+ enableConnectedAccounts: true,
618
+ });
619
+
620
+ const webhookMap = new Map();
621
+ const emailMap = new Map();
622
+ const serverWebhooks = await getServerWebhooks();
623
+
624
+ users.forEach((userInfo) => {
625
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
626
+
627
+ // 获取 email 列表
628
+ const { email } = userInfo;
629
+ const enabledEmail = get(userInfo, 'extra.notifications.email', true);
630
+ if (enabledEmail && email) {
631
+ const emailEntry = emailMap.get(email) || { receivers: [], pushUser: userInfo };
632
+ emailEntry.receivers = [...emailEntry.receivers, receiverDid];
633
+ emailEntry.pushUser = userInfo;
634
+ emailMap.set(email, emailEntry);
635
+ }
636
+
637
+ // 获取 webhook 列表
638
+ const webhooks = isServer ? serverWebhooks : get(userInfo, 'extra.webhooks', []);
639
+ webhooks.forEach((webhook) => {
640
+ const webhookEntry = webhookMap.get(webhook.url) || {
641
+ type: webhook.type ?? 'api',
642
+ receivers: [],
643
+ pushUser: userInfo,
644
+ };
645
+ webhookEntry.receivers = [...webhookEntry.receivers, receiverDid];
646
+ webhookEntry.pushUser = userInfo;
647
+ webhookMap.set(webhook.url, webhookEntry);
648
+ });
649
+ });
650
+
651
+ users.forEach((userInfo) => {
652
+ const receiverDid = getWalletDid(userInfo) || userInfo.did;
653
+ queue.push({
654
+ ...rest,
655
+ teamDid,
656
+ channels,
657
+ receiver: receiverDid,
658
+ sender,
659
+ nodeInfo,
660
+ userInfo,
661
+ emailMap,
662
+ webhookMap,
663
+ });
664
+ });
665
+ } catch (error) {
666
+ logger.error('Failed to create notification receiver', { error });
667
+ }
668
+ });
669
+ };
670
+
671
+ module.exports = { init };