@abtnode/core 1.17.4-beta-20251201-225048-b1682a09 → 1.17.4-beta-20251202-122551-267b614d
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.
- package/lib/api/team.js +108 -4
- package/lib/blocklet/manager/disk.js +0 -9
- package/lib/blocklet/migration-dist/migration.cjs +1 -0
- package/lib/blocklet/webhook/event-bus.js +2 -2
- package/lib/blocklet/webhook/index.js +2 -2
- package/lib/blocklet/webhook/queues.js +49 -4
- package/lib/event/index.js +1 -1
- package/lib/index.js +10 -4
- package/lib/states/user.js +86 -13
- package/lib/states/webhook-endpoint.js +8 -0
- package/lib/states/webhook.js +65 -1
- package/lib/team/manager.js +160 -73
- package/lib/util/webhook.js +65 -0
- package/lib/webhook/index.js +67 -1
- package/lib/webhook/sender/api/index.js +2 -0
- package/lib/webhook/sender/slack/index.js +2 -0
- package/package.json +24 -24
package/lib/api/team.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
2
|
const axios = require('@abtnode/util/lib/axios');
|
|
3
3
|
const pick = require('lodash/pick');
|
|
4
|
+
const omit = require('lodash/omit');
|
|
4
5
|
const defaults = require('lodash/defaults');
|
|
5
6
|
const isEmpty = require('lodash/isEmpty');
|
|
6
7
|
const throttle = require('lodash/throttle');
|
|
@@ -21,6 +22,7 @@ const {
|
|
|
21
22
|
EVENTS,
|
|
22
23
|
NOTIFICATION_SEND_CHANNEL,
|
|
23
24
|
USER_SESSION_STATUS,
|
|
25
|
+
WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD,
|
|
24
26
|
} = require('@abtnode/constant');
|
|
25
27
|
const { isValid: isValidDid } = require('@arcblock/did');
|
|
26
28
|
const { BlockletEvents, TeamEvents, BlockletInternalEvents } = require('@blocklet/constant');
|
|
@@ -857,6 +859,112 @@ class TeamAPI extends EventEmitter {
|
|
|
857
859
|
}
|
|
858
860
|
}
|
|
859
861
|
|
|
862
|
+
async createWebhookDisabledNotification({ teamDid, userDid, webhook, action = '', locale = 'en' }) {
|
|
863
|
+
const notification = {
|
|
864
|
+
en: {
|
|
865
|
+
title: 'Your webhook has been temporarily disabled',
|
|
866
|
+
description: `Your Webhook has been temporarily disabled after ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} consecutive failed message delivery attempts. Please check its availability and you can click the button below to reactivate it if needed.`,
|
|
867
|
+
failureCount: 'Number of failures',
|
|
868
|
+
failureUrl: 'Failed Webhook URL',
|
|
869
|
+
},
|
|
870
|
+
zh: {
|
|
871
|
+
title: '您的 webhook 已被暂时禁用',
|
|
872
|
+
description: `您的 Webhook 在连续 ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} 次消息投递失败后已被暂时禁用。请检查其可用性,如果需要重新激活,请点击下方按钮。`,
|
|
873
|
+
failureCount: '失败次数',
|
|
874
|
+
failureUrl: '失败的 Webhook URL',
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
const nodeInfo = await this.node.read();
|
|
880
|
+
// server 的 webhook 页面地址
|
|
881
|
+
const actionPath = '/settings/integration';
|
|
882
|
+
let _action =
|
|
883
|
+
process.env.NODE_ENV === 'production' ? joinURL(nodeInfo.routing.adminPath, actionPath) : actionPath;
|
|
884
|
+
const channels = [NOTIFICATION_SEND_CHANNEL.WALLET, NOTIFICATION_SEND_CHANNEL.PUSH];
|
|
885
|
+
if (teamDid && !this.teamManager.isNodeTeam(teamDid)) {
|
|
886
|
+
channels.push(NOTIFICATION_SEND_CHANNEL.EMAIL);
|
|
887
|
+
_action = action || `${WELLKNOWN_SERVICE_PATH_PREFIX}/user/settings`; // 默认返回用户中心页面设置页面
|
|
888
|
+
}
|
|
889
|
+
await this.teamManager.createNotification({
|
|
890
|
+
title: notification[locale].title,
|
|
891
|
+
description: notification[locale].description,
|
|
892
|
+
attachments: [
|
|
893
|
+
{
|
|
894
|
+
type: 'section',
|
|
895
|
+
fields: [
|
|
896
|
+
{
|
|
897
|
+
type: 'text',
|
|
898
|
+
data: {
|
|
899
|
+
type: 'plain',
|
|
900
|
+
color: '#9397A1',
|
|
901
|
+
text: notification[locale].failureUrl,
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
type: 'text',
|
|
906
|
+
data: {
|
|
907
|
+
type: 'plain',
|
|
908
|
+
color: '#9397A1',
|
|
909
|
+
text: webhook.url,
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
type: 'text',
|
|
914
|
+
data: {
|
|
915
|
+
type: 'plain',
|
|
916
|
+
color: '#9397A1',
|
|
917
|
+
text: notification[locale].failureCount,
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
type: 'text',
|
|
922
|
+
data: {
|
|
923
|
+
type: 'plain',
|
|
924
|
+
color: '#9397A1',
|
|
925
|
+
text: `${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD}`,
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
source: 'system',
|
|
932
|
+
severity: 'warning',
|
|
933
|
+
action: _action,
|
|
934
|
+
channels,
|
|
935
|
+
...(teamDid ? { teamDid } : {}),
|
|
936
|
+
...(userDid ? { receiver: userDid } : {}),
|
|
937
|
+
});
|
|
938
|
+
} catch (error) {
|
|
939
|
+
logger.error('Failed to create webhook disabled notification', { teamDid, userDid, webhook, error });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async updateWebHookState({ teamDid, userDids, webhook, enabled, consecutiveFailures }) {
|
|
944
|
+
const state = await this.getUserState(teamDid);
|
|
945
|
+
return Promise.all(
|
|
946
|
+
userDids.map(async (userDid) => {
|
|
947
|
+
const webhookParams =
|
|
948
|
+
consecutiveFailures === undefined
|
|
949
|
+
? omit(
|
|
950
|
+
{
|
|
951
|
+
...webhook,
|
|
952
|
+
enabled: enabled ?? webhook.enabled,
|
|
953
|
+
},
|
|
954
|
+
['consecutiveFailures']
|
|
955
|
+
)
|
|
956
|
+
: {
|
|
957
|
+
...webhook,
|
|
958
|
+
enabled: enabled ?? webhook.enabled,
|
|
959
|
+
consecutiveFailures,
|
|
960
|
+
};
|
|
961
|
+
await state.updateWebhook(userDid, webhookParams, (updated) => {
|
|
962
|
+
this.createWebhookDisabledNotification({ teamDid, userDid, webhook: updated });
|
|
963
|
+
});
|
|
964
|
+
})
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
860
968
|
async updateUser({ teamDid, user }) {
|
|
861
969
|
const state = await this.getUserState(teamDid);
|
|
862
970
|
const exist = await state.getUserByDid(user.did);
|
|
@@ -2121,10 +2229,6 @@ class TeamAPI extends EventEmitter {
|
|
|
2121
2229
|
return this.teamManager.updateStoreList(teamDid, storeList);
|
|
2122
2230
|
}
|
|
2123
2231
|
|
|
2124
|
-
getNotificationReceivers(payload) {
|
|
2125
|
-
return this.teamManager.getNotificationReceivers(payload);
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
2232
|
createNotification = (payload) => {
|
|
2129
2233
|
return this.teamManager.createNotification(payload);
|
|
2130
2234
|
};
|
|
@@ -5148,15 +5148,6 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
5148
5148
|
await forEachBlocklet(blocklet, hookFn, { parallel: true, concurrencyLimit: 4 });
|
|
5149
5149
|
}
|
|
5150
5150
|
|
|
5151
|
-
async getReceiverDids(teamDid) {
|
|
5152
|
-
const receivers = await this.teamAPI.getNotificationReceivers({
|
|
5153
|
-
teamDid,
|
|
5154
|
-
roles: [ROLES.ADMIN, ROLES.OWNER],
|
|
5155
|
-
selection: { did: 1 },
|
|
5156
|
-
});
|
|
5157
|
-
return receivers.map((r) => r.did);
|
|
5158
|
-
}
|
|
5159
|
-
|
|
5160
5151
|
// 目前:Blocklet 的安装消息都会聚合在这里。
|
|
5161
5152
|
async _createNotification(did, notification, { skipGetBlocklet = false } = {}) {
|
|
5162
5153
|
if (isCLI()) {
|
|
@@ -19,8 +19,8 @@ const validateBlockletStatus = (blocklet, appDid) => {
|
|
|
19
19
|
return true;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const init = ({ states, teamManager }) => {
|
|
23
|
-
const { webhookEventQueue } = endpointQueueInit.init({ states, teamManager });
|
|
22
|
+
const init = ({ states, teamManager, teamAPI }) => {
|
|
23
|
+
const { webhookEventQueue } = endpointQueueInit.init({ states, teamManager, teamAPI });
|
|
24
24
|
|
|
25
25
|
const handleEventBusEvent = async (data) => {
|
|
26
26
|
logger.info('init webhook event bus');
|
|
@@ -20,7 +20,7 @@ const validatePaging = (paging = {}) => {
|
|
|
20
20
|
const getUserDid = (context) => get(context, 'user.did', '');
|
|
21
21
|
|
|
22
22
|
class WebhooksAPI extends EventEmitter {
|
|
23
|
-
constructor({ states, teamManager }) {
|
|
23
|
+
constructor({ states, teamManager, teamAPI }) {
|
|
24
24
|
super();
|
|
25
25
|
|
|
26
26
|
this.states = states;
|
|
@@ -28,7 +28,7 @@ class WebhooksAPI extends EventEmitter {
|
|
|
28
28
|
this.cache = new Map();
|
|
29
29
|
this.TTL = 5 * 60 * 1000;
|
|
30
30
|
|
|
31
|
-
const { webhookEndpointQueue } = endpointQueueInit.init({ states, teamManager });
|
|
31
|
+
const { webhookEndpointQueue } = endpointQueueInit.init({ states, teamManager, teamAPI });
|
|
32
32
|
this.webhookEndpointQueue = webhookEndpointQueue;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD, WELLKNOWN_BLOCKLET_ADMIN_PATH } = require('@abtnode/constant');
|
|
1
2
|
const logger = require('@abtnode/logger')('@abtnode/core:webhook:queues');
|
|
2
3
|
const axios = require('@abtnode/util/lib/axios');
|
|
3
4
|
const { Op } = require('sequelize');
|
|
@@ -8,7 +9,42 @@ const { getWebhookJobId, STATUS } = require('./util');
|
|
|
8
9
|
const MAX_RETRY_COUNT = 2; // 重试一次
|
|
9
10
|
const AXIOS_TIMEOUT = 1000 * 60 * 3;
|
|
10
11
|
|
|
11
|
-
const createWebhookEndpointQueue = ({ states, teamManager }) => {
|
|
12
|
+
const createWebhookEndpointQueue = ({ states, teamManager, teamAPI }) => {
|
|
13
|
+
const updateWebhookEndpointState = async ({ webhook, teamDid, consecutiveFailures, webhookEndpointState }) => {
|
|
14
|
+
try {
|
|
15
|
+
if (!webhook.url) {
|
|
16
|
+
logger.warn('Webhook not found', { webhook });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const updates = {
|
|
21
|
+
metadata: {
|
|
22
|
+
...(webhook.metadata || {}),
|
|
23
|
+
consecutiveFailures,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
if (consecutiveFailures >= WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD) {
|
|
27
|
+
updates.status = 'disabled';
|
|
28
|
+
teamAPI
|
|
29
|
+
.createWebhookDisabledNotification({
|
|
30
|
+
teamDid,
|
|
31
|
+
webhook: {
|
|
32
|
+
url: webhook.url,
|
|
33
|
+
enabled: false,
|
|
34
|
+
consecutiveFailures,
|
|
35
|
+
},
|
|
36
|
+
action: `${WELLKNOWN_BLOCKLET_ADMIN_PATH}/integrations/webhooks`,
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
logger.error('create webhook disabled notification error', err);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await webhookEndpointState.updateWebhook(webhook.id, updates);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger.error('update webhook endpoint state error', err, { consecutiveFailures, webhook });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
12
48
|
// https://stripe.com/docs/webhooks
|
|
13
49
|
/**
|
|
14
50
|
*
|
|
@@ -52,7 +88,16 @@ const createWebhookEndpointQueue = ({ states, teamManager }) => {
|
|
|
52
88
|
|
|
53
89
|
try {
|
|
54
90
|
// verify similar to component call, but supports external urls
|
|
55
|
-
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await axios.post(webhook.url, event.request, { timeout: AXIOS_TIMEOUT });
|
|
94
|
+
updateWebhookEndpointState({ webhook, teamDid: job.appDid, consecutiveFailures: 0, webhookEndpointState });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// 需要更新 webhook 的 consecutiveFailures
|
|
97
|
+
const consecutiveFailures = (webhook.metadata?.consecutiveFailures || 0) + 1;
|
|
98
|
+
updateWebhookEndpointState({ webhook, teamDid: job.appDid, consecutiveFailures, webhookEndpointState });
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
56
101
|
|
|
57
102
|
if (job.attemptId) {
|
|
58
103
|
await webhookAttemptState.update(
|
|
@@ -242,8 +287,8 @@ const createWebhookEventQueue = ({ states, teamManager }, webhookEndpointQueue)
|
|
|
242
287
|
};
|
|
243
288
|
|
|
244
289
|
module.exports = {
|
|
245
|
-
init: ({ states, teamManager }) => {
|
|
246
|
-
const webhookEndpointQueue = createWebhookEndpointQueue({ states, teamManager });
|
|
290
|
+
init: ({ states, teamManager, teamAPI }) => {
|
|
291
|
+
const webhookEndpointQueue = createWebhookEndpointQueue({ states, teamManager, teamAPI });
|
|
247
292
|
const webhookEventQueue = createWebhookEventQueue({ states, teamManager }, webhookEndpointQueue);
|
|
248
293
|
|
|
249
294
|
return { webhookEndpointQueue, webhookEventQueue };
|
package/lib/event/index.js
CHANGED
|
@@ -723,7 +723,7 @@ module.exports = ({
|
|
|
723
723
|
events.handleBlockletEvent = handleBlockletEvent;
|
|
724
724
|
|
|
725
725
|
if (daemon) {
|
|
726
|
-
const { handleEventBusEvent } = eventBusHandler({ states, teamManager });
|
|
726
|
+
const { handleEventBusEvent } = eventBusHandler({ states, teamManager, teamAPI });
|
|
727
727
|
eventHub.on(EVENT_BUS_EVENT, (data) => {
|
|
728
728
|
if (isInstanceWorker()) {
|
|
729
729
|
return null;
|
package/lib/index.js
CHANGED
|
@@ -307,7 +307,7 @@ function ABTNode(options) {
|
|
|
307
307
|
});
|
|
308
308
|
blockletManager.setMaxListeners(0);
|
|
309
309
|
const securityAPI = new SecurityAPI({ teamManager, blockletManager });
|
|
310
|
-
const webhookAPI = new WebhookAPI({ states, teamManager,
|
|
310
|
+
const webhookAPI = new WebhookAPI({ states, teamManager, teamAPI });
|
|
311
311
|
|
|
312
312
|
const {
|
|
313
313
|
handleBlockletWafChange,
|
|
@@ -653,8 +653,6 @@ function ABTNode(options) {
|
|
|
653
653
|
disconnectFromEndpoint: blockletManager.disconnectFromEndpoint.bind(blockletManager),
|
|
654
654
|
publishToEndpoint: blockletManager.publishToEndpoint.bind(blockletManager),
|
|
655
655
|
|
|
656
|
-
getNotificationReceivers: teamAPI.getNotificationReceivers.bind(teamAPI),
|
|
657
|
-
|
|
658
656
|
// ServerBlocklet Notifications
|
|
659
657
|
createNotification: teamAPI.createNotification.bind(teamAPI),
|
|
660
658
|
getNotifications: teamAPI.getNotification.bind(teamAPI),
|
|
@@ -858,6 +856,7 @@ function ABTNode(options) {
|
|
|
858
856
|
updateOAuthClient: teamAPI.updateOAuthClient.bind(teamAPI),
|
|
859
857
|
|
|
860
858
|
updateBlockletSettings: blockletManager.updateBlockletSettings.bind(blockletManager),
|
|
859
|
+
createWebhookDisabledNotification: teamAPI.createWebhookDisabledNotification.bind(teamAPI),
|
|
861
860
|
|
|
862
861
|
// migrate audit log
|
|
863
862
|
migrateAuditLog: (dataDir) => {
|
|
@@ -888,7 +887,7 @@ function ABTNode(options) {
|
|
|
888
887
|
webhookManager: webhookAPI,
|
|
889
888
|
});
|
|
890
889
|
|
|
891
|
-
const webhook = WebHook({ events, dataDirs, instance
|
|
890
|
+
const webhook = WebHook({ events, dataDirs, instance });
|
|
892
891
|
|
|
893
892
|
const initCron = async () => {
|
|
894
893
|
if (isInstanceWorker()) {
|
|
@@ -982,6 +981,13 @@ function ABTNode(options) {
|
|
|
982
981
|
getWebHooks: states.webhook.list.bind(states.webhook),
|
|
983
982
|
createWebHook: webhook.create.bind(webhook),
|
|
984
983
|
deleteWebHook: webhook.delete.bind(webhook),
|
|
984
|
+
updateWebHookState: (params) => {
|
|
985
|
+
const { isService, webhook: webhookParams, ...rest } = params;
|
|
986
|
+
if (isService) {
|
|
987
|
+
return teamAPI.updateWebHookState.call(teamAPI, { webhook: webhookParams, ...rest });
|
|
988
|
+
}
|
|
989
|
+
return webhook.updateWebHookState.call(webhook, { id: webhookParams?.id, url: webhookParams?.url, ...rest });
|
|
990
|
+
},
|
|
985
991
|
getWebhookSenders: webhook.listSenders.bind(webhook),
|
|
986
992
|
getMessageSender: webhook.getMessageSender.bind(webhook),
|
|
987
993
|
sendTestMessage: webhook.sendTestMessage.bind(webhook),
|
package/lib/states/user.js
CHANGED
|
@@ -3,6 +3,7 @@ const pickBy = require('lodash/pickBy');
|
|
|
3
3
|
const get = require('lodash/get');
|
|
4
4
|
const pick = require('lodash/pick');
|
|
5
5
|
const uniq = require('lodash/uniq');
|
|
6
|
+
const isEqual = require('lodash/isEqual');
|
|
6
7
|
const { isValid, toAddress } = require('@arcblock/did');
|
|
7
8
|
const { PASSPORT_STATUS, USER_MAX_INVITE_DEPTH, ROLES } = require('@abtnode/constant');
|
|
8
9
|
const { BaseState } = require('@abtnode/models');
|
|
@@ -17,6 +18,7 @@ const { validateOwner } = require('../util');
|
|
|
17
18
|
const { loginSchema, disconnectAccountSchema } = require('../validators/user');
|
|
18
19
|
const ExtendBase = require('./base');
|
|
19
20
|
const { isInDashboard, isUserPrivacyEnabled, isAdminUser } = require('../util/verify-user-private');
|
|
21
|
+
const { updateWebhookFailureState } = require('../util/webhook');
|
|
20
22
|
|
|
21
23
|
const isNullOrUndefined = (x) => x === undefined || x === null;
|
|
22
24
|
|
|
@@ -454,7 +456,7 @@ SELECT did,inviter,generation FROM UserTree`.trim();
|
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
// eslint-disable-next-line require-await
|
|
457
|
-
async getUsersByDids({ dids, query }, context = {}) {
|
|
459
|
+
async getUsersByDids({ dids, query, paging }, context = {}) {
|
|
458
460
|
const {
|
|
459
461
|
approved,
|
|
460
462
|
includeTags = false,
|
|
@@ -485,6 +487,20 @@ SELECT did,inviter,generation FROM UserTree`.trim();
|
|
|
485
487
|
include.push(this.getConnectedInclude());
|
|
486
488
|
}
|
|
487
489
|
|
|
490
|
+
// 如果提供了分页参数,使用分页查询
|
|
491
|
+
if (paging) {
|
|
492
|
+
const sorting = [['createdAt', 'DESC']];
|
|
493
|
+
selection.createdAt = 1;
|
|
494
|
+
const result = await this.paginate({ where: condition, include }, sorting, paging, selection);
|
|
495
|
+
|
|
496
|
+
if (includeFollowStatus && user?.did) {
|
|
497
|
+
result.list = await this._enrichWithFollowStatus(result.list, user.did, dids);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 否则返回所有结果
|
|
488
504
|
let result = await this.find({ where: condition, include }, selection);
|
|
489
505
|
|
|
490
506
|
if (includeFollowStatus && user?.did) {
|
|
@@ -540,27 +556,39 @@ SELECT did,inviter,generation FROM UserTree`.trim();
|
|
|
540
556
|
* get user by did
|
|
541
557
|
* @param {string} did user's did
|
|
542
558
|
*/
|
|
543
|
-
async getUser(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
559
|
+
async getUser(
|
|
560
|
+
did,
|
|
561
|
+
{
|
|
562
|
+
enableConnectedAccount = false,
|
|
563
|
+
includeTags = false,
|
|
564
|
+
includePassports = true,
|
|
565
|
+
includeConnectedAccounts = true,
|
|
566
|
+
selection = {},
|
|
567
|
+
} = {}
|
|
568
|
+
) {
|
|
569
|
+
let user = await this.findOne({ where: { did }, include: includeTags ? [this.getTagInclude()] : [] }, selection);
|
|
547
570
|
|
|
548
571
|
// search in connected accounts
|
|
549
572
|
if (!user && enableConnectedAccount) {
|
|
550
573
|
const connectedAccount = await this.connectedAccount.findOne({ did });
|
|
551
574
|
if (connectedAccount) {
|
|
552
|
-
user = await this.findOne({ did: connectedAccount.userDid });
|
|
575
|
+
user = await this.findOne({ did: connectedAccount.userDid }, selection);
|
|
553
576
|
}
|
|
554
577
|
}
|
|
555
578
|
|
|
556
579
|
if (user) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
if (includeConnectedAccounts && includePassports) {
|
|
581
|
+
const [connectedAccounts, passports] = await Promise.all([
|
|
582
|
+
this.connectedAccount.find({ userDid: user.did }),
|
|
583
|
+
this.passport.find({ userDid: user.did }),
|
|
584
|
+
]);
|
|
585
|
+
user.connectedAccounts = connectedAccounts;
|
|
586
|
+
user.passports = passports;
|
|
587
|
+
} else if (includeConnectedAccounts) {
|
|
588
|
+
user.connectedAccounts = await this.connectedAccount.find({ userDid: user.did });
|
|
589
|
+
} else if (includePassports) {
|
|
590
|
+
user.passports = await this.passport.find({ userDid: user.did });
|
|
591
|
+
}
|
|
564
592
|
}
|
|
565
593
|
|
|
566
594
|
return user;
|
|
@@ -1330,6 +1358,51 @@ SELECT did,inviter,generation FROM UserTree`.trim();
|
|
|
1330
1358
|
throw error;
|
|
1331
1359
|
}
|
|
1332
1360
|
}
|
|
1361
|
+
|
|
1362
|
+
async updateWebhook(userDid, webhook, createNotification = () => {}) {
|
|
1363
|
+
const user = await this.getUser(userDid);
|
|
1364
|
+
if (!user) {
|
|
1365
|
+
throw new CustomError(404, `User with did ${userDid} not found`);
|
|
1366
|
+
}
|
|
1367
|
+
if (!webhook || !webhook.url) {
|
|
1368
|
+
throw new CustomError(400, 'Invalid webhook');
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (
|
|
1372
|
+
webhook.consecutiveFailures !== undefined &&
|
|
1373
|
+
(!Number.isInteger(webhook.consecutiveFailures) || webhook.consecutiveFailures < 0)
|
|
1374
|
+
) {
|
|
1375
|
+
throw new CustomError(400, 'consecutiveFailures must be a non-negative integer.');
|
|
1376
|
+
}
|
|
1377
|
+
const existWebhooks = user.extra?.webhooks ?? [];
|
|
1378
|
+
|
|
1379
|
+
let hasChanged = false;
|
|
1380
|
+
const updatedWebhooks = existWebhooks.map((existing) => {
|
|
1381
|
+
if (existing.url === webhook.url) {
|
|
1382
|
+
const merged = { ...existing };
|
|
1383
|
+
// 使用抽离的核心逻辑更新失败状态
|
|
1384
|
+
updateWebhookFailureState(merged, webhook, createNotification);
|
|
1385
|
+
// Check if this specific webhook has changed
|
|
1386
|
+
if (!isEqual(existing, merged)) {
|
|
1387
|
+
hasChanged = true;
|
|
1388
|
+
return merged;
|
|
1389
|
+
}
|
|
1390
|
+
return existing;
|
|
1391
|
+
}
|
|
1392
|
+
return existing;
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// Skip update if webhook data hasn't changed
|
|
1396
|
+
if (!hasChanged) {
|
|
1397
|
+
return user;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const [updatedRows, [updatedUser]] = await this.update({ did: userDid }, { extra: { webhooks: updatedWebhooks } });
|
|
1401
|
+
if (updatedRows === 1) {
|
|
1402
|
+
return updatedUser;
|
|
1403
|
+
}
|
|
1404
|
+
return user;
|
|
1405
|
+
}
|
|
1333
1406
|
}
|
|
1334
1407
|
|
|
1335
1408
|
module.exports = User;
|
|
@@ -36,6 +36,7 @@ const updateWebhookEndpointSchema = Joi.object({
|
|
|
36
36
|
)
|
|
37
37
|
.optional(),
|
|
38
38
|
status: Joi.string().valid('enabled', 'disabled').optional(),
|
|
39
|
+
metadata: Joi.object().optional(),
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -64,6 +65,13 @@ class WebhookEndpointState extends BaseState {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
await updateWebhookEndpointSchema.validateAsync(updates, { stripUnknown: true });
|
|
68
|
+
if (updates.metadata && updates.status && updates.status !== 'enabled') {
|
|
69
|
+
updates.metadata = {
|
|
70
|
+
...(doc.metadata || {}),
|
|
71
|
+
...(updates.metadata || {}),
|
|
72
|
+
consecutiveFailures: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
67
75
|
|
|
68
76
|
await this.update({ id }, { $set: updates });
|
|
69
77
|
return doc;
|
package/lib/states/webhook.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const logger = require('@abtnode/logger')('@abtnode/core:states:webhook');
|
|
2
|
-
|
|
2
|
+
const { CustomError } = require('@blocklet/error');
|
|
3
3
|
const BaseState = require('./base');
|
|
4
4
|
const { validateWebhook } = require('../validators/webhook');
|
|
5
|
+
const { updateWebhookFailureState } = require('../util/webhook');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @extends BaseState<import('@abtnode/models').WebHookState>
|
|
@@ -65,6 +66,69 @@ class WebhookState extends BaseState {
|
|
|
65
66
|
throw new Error(`webhook with id ${id} not exist`);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 更新 webhook 状态(开启/关闭或连续失败次数)
|
|
72
|
+
* @param {string} id - webhook ID
|
|
73
|
+
* @param {object} options - 更新选项
|
|
74
|
+
* @param {string} [options.url] - 要更新的 URL,不传则更新所有 URL 类型的 param
|
|
75
|
+
* @param {boolean} [options.enabled] - 开启/关闭状态
|
|
76
|
+
* @param {number} [options.consecutiveFailures] - 连续失败次数,不传则默认+1,传入必须大于原有值或为0
|
|
77
|
+
* @throws {Error} 当同时提供 enabled 和 consecutiveFailures 时抛出错误
|
|
78
|
+
* @throws {Error} 当未提供任何更新参数时抛出错误
|
|
79
|
+
* @throws {Error} 当 consecutiveFailures 值无效时抛出错误
|
|
80
|
+
* @throws {Error} 当指定的 url 不存在时抛出错误
|
|
81
|
+
*/
|
|
82
|
+
async updateWebhook(id, { url, enabled, consecutiveFailures } = {}, createNotification = () => {}) {
|
|
83
|
+
// 验证不能同时设置开启/关闭和连续失败次数
|
|
84
|
+
if (enabled !== undefined && consecutiveFailures !== undefined) {
|
|
85
|
+
throw new CustomError(400, 'Cannot update enabled and consecutiveFailures at the same time.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 验证至少提供一个更新参数
|
|
89
|
+
if (enabled === undefined && consecutiveFailures === undefined) {
|
|
90
|
+
throw new CustomError(400, 'Must provide either enabled or consecutiveFailures to update.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 获取当前 webhook 状态
|
|
94
|
+
const currentWebhook = await this.findOne(id);
|
|
95
|
+
|
|
96
|
+
if (!currentWebhook) {
|
|
97
|
+
throw new CustomError(404, `webhook with id ${id} not exist`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 确定匹配函数
|
|
101
|
+
const matcher = url ? (item) => item.value === url : (item) => item.name === 'url' && item.value;
|
|
102
|
+
|
|
103
|
+
// 验证是否有匹配的项
|
|
104
|
+
const hasMatchingItems = currentWebhook.params.some(matcher);
|
|
105
|
+
if (!hasMatchingItems) {
|
|
106
|
+
if (url) {
|
|
107
|
+
throw new CustomError(404, `URL ${url} not found in webhook params.`);
|
|
108
|
+
}
|
|
109
|
+
throw new CustomError(400, 'No valid URL params found to update.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 遍历并更新匹配的项(只复制需要修改的项)
|
|
113
|
+
const updatedParams = currentWebhook.params.map((item) => {
|
|
114
|
+
if (matcher(item)) {
|
|
115
|
+
const cloned = { ...item };
|
|
116
|
+
// 使用抽离的核心逻辑更新失败状态(webhook.js 需要验证递增)
|
|
117
|
+
updateWebhookFailureState(cloned, { enabled, consecutiveFailures }, createNotification, {
|
|
118
|
+
validateIncremental: true,
|
|
119
|
+
});
|
|
120
|
+
return cloned;
|
|
121
|
+
}
|
|
122
|
+
return item;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 更新整个 params 数组
|
|
126
|
+
const [updatedRows, [updatedWebhook]] = await this.update({ id }, { params: updatedParams });
|
|
127
|
+
if (updatedRows === 1) {
|
|
128
|
+
return updatedWebhook;
|
|
129
|
+
}
|
|
130
|
+
return currentWebhook;
|
|
131
|
+
}
|
|
68
132
|
}
|
|
69
133
|
|
|
70
134
|
module.exports = WebhookState;
|
package/lib/team/manager.js
CHANGED
|
@@ -224,16 +224,38 @@ class TeamManager extends EventEmitter {
|
|
|
224
224
|
/**
|
|
225
225
|
* 判断 passport 是否已经过期
|
|
226
226
|
*/
|
|
227
|
-
isPassportExpired(passport) {
|
|
228
|
-
return passport.status !== 'valid' || (passport.expirationDate && dayjs(passport.expirationDate).isBefore(
|
|
227
|
+
isPassportExpired(passport, now) {
|
|
228
|
+
return passport.status !== 'valid' || (passport.expirationDate && dayjs(passport.expirationDate).isBefore(now));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 验证用户是否有效(是否存在、passport是否过期)
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
_filterValidUsers(users) {
|
|
236
|
+
const now = dayjs();
|
|
237
|
+
return users.filter((user) => {
|
|
238
|
+
if (!user) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const { passports = [] } = user;
|
|
242
|
+
// 如果用户没有 passport,或者 有一个 passport 没有过期,则返回 true
|
|
243
|
+
if (!passports?.length || passports.some((x) => !this.isPassportExpired(x, now))) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
logger.warn(`user's passports are all expired: ${user.did}`);
|
|
247
|
+
return false;
|
|
248
|
+
});
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
async getNotificationReceivers(payload) {
|
|
232
|
-
const { teamDid, userDids = [], roles = [], selection = {}, includeConnectedAccounts = false } = payload;
|
|
252
|
+
const { teamDid, userDids = [], roles = [], selection = {}, includeConnectedAccounts = false, paging } = payload;
|
|
233
253
|
// 会根据 teamDid 返回对应 state
|
|
234
254
|
const userState = await this.getUserState(teamDid);
|
|
235
255
|
if (!userDids.length && !roles.length) {
|
|
236
|
-
return
|
|
256
|
+
return paging
|
|
257
|
+
? { list: [], paging: { total: 0, pageSize: paging.size || 20, pageCount: 0, page: paging.page || 1 } }
|
|
258
|
+
: [];
|
|
237
259
|
}
|
|
238
260
|
try {
|
|
239
261
|
// 这些用户信息已经是 approved 的用户,不需要再次确认
|
|
@@ -245,7 +267,7 @@ class TeamManager extends EventEmitter {
|
|
|
245
267
|
if (userDids.length > 0) {
|
|
246
268
|
let queryUsers = [];
|
|
247
269
|
if (userDids.includes('*')) {
|
|
248
|
-
|
|
270
|
+
const result = await userState.getUsersByDids({
|
|
249
271
|
dids: userDids,
|
|
250
272
|
query: {
|
|
251
273
|
approved: true,
|
|
@@ -253,45 +275,57 @@ class TeamManager extends EventEmitter {
|
|
|
253
275
|
includeConnectedAccounts,
|
|
254
276
|
includePassports: true,
|
|
255
277
|
},
|
|
278
|
+
paging,
|
|
256
279
|
});
|
|
280
|
+
|
|
281
|
+
// 如果是分页查询,处理分页结果
|
|
282
|
+
if (paging && result.list) {
|
|
283
|
+
const validUsers = this._filterValidUsers(result.list);
|
|
284
|
+
return {
|
|
285
|
+
list: users.concat(validUsers),
|
|
286
|
+
paging: result.paging,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
queryUsers = result;
|
|
257
291
|
} else {
|
|
258
292
|
// 使用 getUser 查询的目的是为了避免传入的 receiver 不在user表中而存在于 connected_account 表中
|
|
259
293
|
queryUsers = await Promise.all(
|
|
260
|
-
userDids.map((did) =>
|
|
294
|
+
userDids.map((did) =>
|
|
295
|
+
userState.getUser(did, {
|
|
296
|
+
enableConnectedAccount: true,
|
|
297
|
+
includeConnectedAccounts: false,
|
|
298
|
+
selection,
|
|
299
|
+
})
|
|
300
|
+
)
|
|
261
301
|
);
|
|
262
302
|
}
|
|
263
303
|
|
|
264
|
-
const validUsers =
|
|
265
|
-
if (!user) {
|
|
266
|
-
logger.warn('receiver is not exist: ', { userDid: userDids[index] });
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
const { passports = [] } = user;
|
|
270
|
-
// 如果用户没有 passport,或者 有一个 passport 没有过期,则返回 true
|
|
271
|
-
if (!passports?.length || passports.some((x) => !this.isPassportExpired(x))) {
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
274
|
-
logger.warn(`user's passports are all expired: ${user.did}`);
|
|
275
|
-
return false;
|
|
276
|
-
});
|
|
277
|
-
|
|
304
|
+
const validUsers = this._filterValidUsers(queryUsers);
|
|
278
305
|
users = users.concat(validUsers);
|
|
279
306
|
}
|
|
280
307
|
|
|
281
308
|
return users;
|
|
282
309
|
} catch (error) {
|
|
283
310
|
logger.error('get receivers failed: ', error);
|
|
284
|
-
return
|
|
311
|
+
return paging
|
|
312
|
+
? { list: [], paging: { total: 0, pageSize: paging.size || 20, pageCount: 0, page: paging.page || 1 } }
|
|
313
|
+
: [];
|
|
285
314
|
}
|
|
286
315
|
}
|
|
287
316
|
|
|
288
317
|
async getAdminReceivers({ teamDid }) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
318
|
+
try {
|
|
319
|
+
const receivers = await this.getNotificationReceivers({
|
|
320
|
+
teamDid: teamDid ?? this.nodeDid,
|
|
321
|
+
roles: [ROLES.ADMIN, ROLES.OWNER],
|
|
322
|
+
selection: { did: 1 },
|
|
323
|
+
});
|
|
324
|
+
return receivers.map((r) => r.did);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
logger.error('get admin receivers failed', { error });
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
295
329
|
}
|
|
296
330
|
|
|
297
331
|
/**
|
|
@@ -351,30 +385,65 @@ class TeamManager extends EventEmitter {
|
|
|
351
385
|
}
|
|
352
386
|
|
|
353
387
|
/**
|
|
354
|
-
*
|
|
388
|
+
* 获取接收者列表(支持分页)
|
|
355
389
|
* @private
|
|
390
|
+
* @param {string} teamDid - 团队 DID
|
|
391
|
+
* @param {string|string[]} receiver - 接收者
|
|
392
|
+
* @param {boolean} isExist - 通知是否已存在
|
|
393
|
+
* @returns {AsyncGenerator<string[]>} 异步生成器,每次返回一页接收者的 DID 列表
|
|
356
394
|
*/
|
|
357
|
-
async _getReceiverList(teamDid, receiver, isExist) {
|
|
395
|
+
async *_getReceiverList(teamDid, receiver, isExist) {
|
|
396
|
+
// 1. 如果通知已存在,直接返回规范化后的接收者
|
|
397
|
+
// 这种情况 receiver 一定存在。
|
|
358
398
|
if (isExist) {
|
|
359
|
-
|
|
399
|
+
yield this._normalizeReceiver(receiver);
|
|
400
|
+
return;
|
|
360
401
|
}
|
|
361
402
|
|
|
362
403
|
const receivers = this._normalizeReceiver(receiver);
|
|
363
|
-
|
|
404
|
+
|
|
405
|
+
// 2. 如果传入的是具体的 DID 数组(不包含 *),不需要分页查询,直接拿来用
|
|
406
|
+
if (receivers && !receivers.includes('*')) {
|
|
407
|
+
yield receivers;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 3. 如果传入 receiver 包含 * 需要分页查询,返回一页的数据,开始执行一页
|
|
412
|
+
if (receivers && receivers.includes('*')) {
|
|
364
413
|
try {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
414
|
+
const pageSize = 100; // 每页查询 100 个用户
|
|
415
|
+
let currentPage = 1;
|
|
416
|
+
let hasMore = true;
|
|
417
|
+
|
|
418
|
+
// eslint-disable-next-line no-await-in-loop
|
|
419
|
+
while (hasMore) {
|
|
420
|
+
// eslint-disable-next-line no-await-in-loop
|
|
421
|
+
const result = await this.getNotificationReceivers({
|
|
422
|
+
teamDid,
|
|
423
|
+
userDids: receivers,
|
|
424
|
+
selection: { did: 1 },
|
|
425
|
+
paging: { page: currentPage, pageSize },
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const userDids = result.list.map((u) => u.did);
|
|
429
|
+
|
|
430
|
+
if (userDids.length > 0) {
|
|
431
|
+
yield userDids;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
hasMore = currentPage < result.paging.pageCount;
|
|
435
|
+
currentPage += 1;
|
|
436
|
+
}
|
|
371
437
|
} catch (error) {
|
|
372
|
-
logger.error('get receivers failed', { error });
|
|
373
|
-
|
|
438
|
+
logger.error('get receivers failed with pagination', { error });
|
|
439
|
+
yield [];
|
|
374
440
|
}
|
|
441
|
+
return;
|
|
375
442
|
}
|
|
376
443
|
|
|
377
|
-
|
|
444
|
+
// 4. 如果没有传入 receiver,需要查询站点的 admin 相关的用户
|
|
445
|
+
const adminReceivers = await this.getAdminReceivers({ teamDid });
|
|
446
|
+
yield adminReceivers;
|
|
378
447
|
}
|
|
379
448
|
|
|
380
449
|
/**
|
|
@@ -432,11 +501,10 @@ class TeamManager extends EventEmitter {
|
|
|
432
501
|
* 创建通知文档
|
|
433
502
|
* @private
|
|
434
503
|
*/
|
|
435
|
-
async _createNotificationDoc(notificationState, payload,
|
|
504
|
+
async _createNotificationDoc(notificationState, payload, source, isServices, teamDid) {
|
|
436
505
|
try {
|
|
437
506
|
return await notificationState.create({
|
|
438
507
|
...payload,
|
|
439
|
-
receiver: receivers,
|
|
440
508
|
source,
|
|
441
509
|
...(!isServices ? {} : { teamDid }),
|
|
442
510
|
});
|
|
@@ -539,18 +607,16 @@ class TeamManager extends EventEmitter {
|
|
|
539
607
|
|
|
540
608
|
// 检查通知是否已存在
|
|
541
609
|
const isExist = await this._checkExistingNotification(notificationState, id || notification?.id);
|
|
542
|
-
// 获取接收者列表
|
|
543
|
-
const receivers = await this._getReceiverList(teamDid, receiver, isExist);
|
|
544
610
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
});
|
|
551
|
-
return undefined;
|
|
611
|
+
let doc = null;
|
|
612
|
+
const source = this._determineSource(teamDid, payload);
|
|
613
|
+
const isServices = teamDid && !this.isNodeTeam(teamDid);
|
|
614
|
+
if (!isExist && !payload.pushOnly) {
|
|
615
|
+
doc = await this._createNotificationDoc(notificationState, payload, source, isServices, teamDid);
|
|
616
|
+
logger.info('notification created successfully', { teamDid, notificationId: doc.id });
|
|
552
617
|
}
|
|
553
618
|
|
|
619
|
+
// 获取 actor 信息
|
|
554
620
|
const notificationActor = notification?.activity?.actor || payload.activity?.actor;
|
|
555
621
|
let actorInfo = null;
|
|
556
622
|
if (notificationActor) {
|
|
@@ -559,32 +625,53 @@ class TeamManager extends EventEmitter {
|
|
|
559
625
|
actorInfo = pick(user, ['did', 'fullName', 'avatar']);
|
|
560
626
|
}
|
|
561
627
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
notification: {
|
|
566
|
-
...(notification || payload),
|
|
567
|
-
},
|
|
568
|
-
receivers: receivers ?? receiver,
|
|
569
|
-
teamDid: teamDid || this.nodeDid,
|
|
570
|
-
sender: payload.sender,
|
|
571
|
-
pushOnly: true,
|
|
572
|
-
isExist,
|
|
573
|
-
...(actorInfo ? { actorInfo } : {}),
|
|
574
|
-
options: typeof payload.options === 'object' && payload.options !== null ? payload.options : {},
|
|
575
|
-
});
|
|
576
|
-
return undefined;
|
|
577
|
-
}
|
|
628
|
+
// 获取接收者列表(使用异步生成器逐页处理)
|
|
629
|
+
const receiverGenerator = this._getReceiverList(teamDid, receiver, isExist);
|
|
630
|
+
let hasReceivers = false;
|
|
578
631
|
|
|
579
|
-
const
|
|
580
|
-
|
|
632
|
+
for await (const receiversPage of receiverGenerator) {
|
|
633
|
+
if (!receiversPage?.length) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
581
636
|
|
|
582
|
-
|
|
637
|
+
hasReceivers = true;
|
|
583
638
|
|
|
584
|
-
|
|
639
|
+
if (payload.pushOnly || isExist) {
|
|
640
|
+
this.emit(EVENTS.NOTIFICATION_CREATE_QUEUED, {
|
|
641
|
+
channels: payload.channels || [NOTIFICATION_SEND_CHANNEL.WALLET],
|
|
642
|
+
notification: {
|
|
643
|
+
...(notification || payload),
|
|
644
|
+
},
|
|
645
|
+
receivers: receiversPage,
|
|
646
|
+
teamDid: teamDid || this.nodeDid,
|
|
647
|
+
sender: payload.sender,
|
|
648
|
+
pushOnly: true,
|
|
649
|
+
isExist,
|
|
650
|
+
...(actorInfo ? { actorInfo } : {}),
|
|
651
|
+
options: typeof payload.options === 'object' && payload.options !== null ? payload.options : {},
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
const defaultChannel = this._getDefaultChannels(isServices, source);
|
|
655
|
+
await this._emitNotificationEvents(
|
|
656
|
+
doc,
|
|
657
|
+
payload,
|
|
658
|
+
receiversPage,
|
|
659
|
+
teamDid,
|
|
660
|
+
isServices,
|
|
661
|
+
defaultChannel,
|
|
662
|
+
actorInfo
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
585
666
|
|
|
586
|
-
|
|
587
|
-
|
|
667
|
+
if (!hasReceivers && process.env.NODE_ENV !== 'test') {
|
|
668
|
+
logger.warn('No valid receivers', {
|
|
669
|
+
teamDid,
|
|
670
|
+
receiver,
|
|
671
|
+
notification: JSON.stringify(notification || payload || {}),
|
|
672
|
+
});
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
588
675
|
|
|
589
676
|
return doc;
|
|
590
677
|
} catch (error) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { CustomError } = require('@blocklet/error');
|
|
2
|
+
const { WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD } = require('@abtnode/constant');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 更新 webhook 项的失败状态(直接修改传入的对象)
|
|
6
|
+
* @param {Object} item - webhook 对象(会被直接修改)
|
|
7
|
+
* @param {Object} updates - 更新参数
|
|
8
|
+
* @param {boolean} [updates.enabled] - 新的启用状态
|
|
9
|
+
* @param {number} [updates.consecutiveFailures] - 新的连续失败次数(如果不传则自动 +1)
|
|
10
|
+
* @param {Function} [createNotification] - 达到阈值时的通知回调
|
|
11
|
+
* @param {Object} [options] - 额外选项
|
|
12
|
+
* @param {boolean} [options.validateIncremental] - 是否验证 consecutiveFailures 必须递增(默认 false)
|
|
13
|
+
*/
|
|
14
|
+
function updateWebhookFailureState(item, updates, createNotification, options = {}) {
|
|
15
|
+
const { enabled, consecutiveFailures } = updates;
|
|
16
|
+
const { validateIncremental = false } = options;
|
|
17
|
+
|
|
18
|
+
// 处理 enabled 更新
|
|
19
|
+
if (enabled !== undefined) {
|
|
20
|
+
item.enabled = enabled;
|
|
21
|
+
// 如果关闭 webhook,自动将连续失败次数重置为 0
|
|
22
|
+
if (enabled === false) {
|
|
23
|
+
item.consecutiveFailures = 0;
|
|
24
|
+
}
|
|
25
|
+
return item;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 处理 consecutiveFailures 更新
|
|
29
|
+
const currentFailures = item.consecutiveFailures || 0;
|
|
30
|
+
|
|
31
|
+
if (consecutiveFailures === undefined) {
|
|
32
|
+
// 未传入值,默认 +1
|
|
33
|
+
// 这么做的原因是由于,在队列中拿到的值不是最新的值,在外面进行 + 1 操作是不准确的
|
|
34
|
+
item.consecutiveFailures = currentFailures + 1;
|
|
35
|
+
} else {
|
|
36
|
+
// 验证必须是非负整数
|
|
37
|
+
if (!Number.isInteger(consecutiveFailures) || consecutiveFailures < 0) {
|
|
38
|
+
throw new CustomError(400, 'consecutiveFailures must be a non-negative integer.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 如果需要验证递增
|
|
42
|
+
if (validateIncremental && consecutiveFailures !== 0 && consecutiveFailures <= currentFailures) {
|
|
43
|
+
throw new CustomError(
|
|
44
|
+
400,
|
|
45
|
+
`consecutiveFailures must be greater than the current value (${currentFailures}) or equal to 0.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
item.consecutiveFailures = consecutiveFailures;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 达到阈值则自动禁用并触发通知
|
|
53
|
+
if (item.consecutiveFailures >= WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD) {
|
|
54
|
+
item.enabled = false;
|
|
55
|
+
item.consecutiveFailures = 0;
|
|
56
|
+
if (createNotification) {
|
|
57
|
+
createNotification(item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return item;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
updateWebhookFailureState,
|
|
65
|
+
};
|
package/lib/webhook/index.js
CHANGED
|
@@ -77,10 +77,56 @@ const getSlackUrlInfo = async ({ blockletUrl, path: actionPath = '/notifications
|
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
// eslint-disable-next-line no-unused-vars
|
|
80
|
-
module.exports = ({ events, dataDirs, instance
|
|
80
|
+
module.exports = ({ events, dataDirs, instance }) => {
|
|
81
81
|
const nodeState = states.node;
|
|
82
82
|
const webhookState = states.webhook;
|
|
83
83
|
|
|
84
|
+
const updateWebHookState = async ({ id, url, enabled, consecutiveFailures }) => {
|
|
85
|
+
try {
|
|
86
|
+
const updateRes = await webhookState.updateWebhook(
|
|
87
|
+
id,
|
|
88
|
+
{
|
|
89
|
+
url,
|
|
90
|
+
enabled,
|
|
91
|
+
consecutiveFailures,
|
|
92
|
+
},
|
|
93
|
+
(updated) => {
|
|
94
|
+
instance.createWebhookDisabledNotification({ webhook: updated });
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
return updateRes;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.error('update webhook state error', { err });
|
|
100
|
+
throw new Error(err.message);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 安全地更新 webhook 状态,不影响主流程
|
|
106
|
+
* @param {object} item - webhook item
|
|
107
|
+
* @param {number} consecutiveFailures - 连续失败次数
|
|
108
|
+
*/
|
|
109
|
+
const safeUpdateWebhookState = (item, consecutiveFailures) => {
|
|
110
|
+
// 只处理需要跟踪状态的类型
|
|
111
|
+
if (item.type !== 'slack' && item.type !== 'api') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const urlParam = item.params.find((x) => x.name === 'url');
|
|
116
|
+
if (!urlParam || !urlParam.value) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 异步更新,不等待结果,不影响主流程
|
|
121
|
+
updateWebHookState({
|
|
122
|
+
id: item.id,
|
|
123
|
+
url: urlParam.value,
|
|
124
|
+
consecutiveFailures,
|
|
125
|
+
}).catch((err) => {
|
|
126
|
+
logger.debug('safe update webhook state failed', { err, itemId: item.id, url: urlParam.value });
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
84
130
|
const sendMessage = async ({ extra, isService, ...message } = {}) => {
|
|
85
131
|
try {
|
|
86
132
|
const webhookList = !isService ? await webhookState.list() : [];
|
|
@@ -97,6 +143,20 @@ module.exports = ({ events, dataDirs, instance, teamManager }) => {
|
|
|
97
143
|
if (webhookList.length) {
|
|
98
144
|
for (let i = 0; i < webhookList.length; i++) {
|
|
99
145
|
const item = webhookList[i];
|
|
146
|
+
// 对于 slack 和 api 类型,检查是否启用
|
|
147
|
+
if (item.type === 'slack' || item.type === 'api') {
|
|
148
|
+
const urlParam = item.params.find((x) => x.name === 'url');
|
|
149
|
+
if (!urlParam || !urlParam.value) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// 如果没有开启,则跳过发送
|
|
153
|
+
const isEnabled = urlParam.enabled ?? true;
|
|
154
|
+
if (!isEnabled) {
|
|
155
|
+
logger.debug('webhook is disabled, skip sending', { type: item.type, url: urlParam.value });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
100
160
|
if (typeof senderFns[item.type] !== 'function') {
|
|
101
161
|
const senderInstance = WebHookSender.getMessageSender(item.type);
|
|
102
162
|
senderFns[item.type] = senderInstance.send.bind(senderInstance);
|
|
@@ -134,8 +194,13 @@ module.exports = ({ events, dataDirs, instance, teamManager }) => {
|
|
|
134
194
|
try {
|
|
135
195
|
// eslint-disable-next-line
|
|
136
196
|
await senderFns[item.type](item.params, options);
|
|
197
|
+
// 发送成功,重置失败次数
|
|
198
|
+
safeUpdateWebhookState(item, 0);
|
|
137
199
|
} catch (err) {
|
|
138
200
|
logger.error('webhook sender error', { error: err, item });
|
|
201
|
+
// 发送失败,增加失败次数
|
|
202
|
+
const currentFailures = item.consecutiveFailures || 0;
|
|
203
|
+
safeUpdateWebhookState(item, currentFailures + 1);
|
|
139
204
|
}
|
|
140
205
|
}
|
|
141
206
|
}
|
|
@@ -269,6 +334,7 @@ module.exports = ({ events, dataDirs, instance, teamManager }) => {
|
|
|
269
334
|
getMessageSender: WebHookSender.getMessageSender,
|
|
270
335
|
create: createWebhook,
|
|
271
336
|
delete: deleteWebhook,
|
|
337
|
+
updateWebHookState,
|
|
272
338
|
sendTestMessage,
|
|
273
339
|
};
|
|
274
340
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.17.4-beta-
|
|
6
|
+
"version": "1.17.4-beta-20251202-122551-267b614d",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -17,21 +17,21 @@
|
|
|
17
17
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@abtnode/analytics": "1.17.4-beta-
|
|
21
|
-
"@abtnode/auth": "1.17.4-beta-
|
|
22
|
-
"@abtnode/certificate-manager": "1.17.4-beta-
|
|
23
|
-
"@abtnode/constant": "1.17.4-beta-
|
|
24
|
-
"@abtnode/cron": "1.17.4-beta-
|
|
25
|
-
"@abtnode/db-cache": "1.17.4-beta-
|
|
26
|
-
"@abtnode/docker-utils": "1.17.4-beta-
|
|
27
|
-
"@abtnode/logger": "1.17.4-beta-
|
|
28
|
-
"@abtnode/models": "1.17.4-beta-
|
|
29
|
-
"@abtnode/queue": "1.17.4-beta-
|
|
30
|
-
"@abtnode/rbac": "1.17.4-beta-
|
|
31
|
-
"@abtnode/router-provider": "1.17.4-beta-
|
|
32
|
-
"@abtnode/static-server": "1.17.4-beta-
|
|
33
|
-
"@abtnode/timemachine": "1.17.4-beta-
|
|
34
|
-
"@abtnode/util": "1.17.4-beta-
|
|
20
|
+
"@abtnode/analytics": "1.17.4-beta-20251202-122551-267b614d",
|
|
21
|
+
"@abtnode/auth": "1.17.4-beta-20251202-122551-267b614d",
|
|
22
|
+
"@abtnode/certificate-manager": "1.17.4-beta-20251202-122551-267b614d",
|
|
23
|
+
"@abtnode/constant": "1.17.4-beta-20251202-122551-267b614d",
|
|
24
|
+
"@abtnode/cron": "1.17.4-beta-20251202-122551-267b614d",
|
|
25
|
+
"@abtnode/db-cache": "1.17.4-beta-20251202-122551-267b614d",
|
|
26
|
+
"@abtnode/docker-utils": "1.17.4-beta-20251202-122551-267b614d",
|
|
27
|
+
"@abtnode/logger": "1.17.4-beta-20251202-122551-267b614d",
|
|
28
|
+
"@abtnode/models": "1.17.4-beta-20251202-122551-267b614d",
|
|
29
|
+
"@abtnode/queue": "1.17.4-beta-20251202-122551-267b614d",
|
|
30
|
+
"@abtnode/rbac": "1.17.4-beta-20251202-122551-267b614d",
|
|
31
|
+
"@abtnode/router-provider": "1.17.4-beta-20251202-122551-267b614d",
|
|
32
|
+
"@abtnode/static-server": "1.17.4-beta-20251202-122551-267b614d",
|
|
33
|
+
"@abtnode/timemachine": "1.17.4-beta-20251202-122551-267b614d",
|
|
34
|
+
"@abtnode/util": "1.17.4-beta-20251202-122551-267b614d",
|
|
35
35
|
"@aigne/aigne-hub": "^0.10.10",
|
|
36
36
|
"@arcblock/did": "^1.27.12",
|
|
37
37
|
"@arcblock/did-connect-js": "^1.27.12",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"@arcblock/pm2-events": "^0.0.5",
|
|
44
44
|
"@arcblock/validator": "^1.27.12",
|
|
45
45
|
"@arcblock/vc": "^1.27.12",
|
|
46
|
-
"@blocklet/constant": "1.17.4-beta-
|
|
46
|
+
"@blocklet/constant": "1.17.4-beta-20251202-122551-267b614d",
|
|
47
47
|
"@blocklet/did-space-js": "^1.2.6",
|
|
48
|
-
"@blocklet/env": "1.17.4-beta-
|
|
48
|
+
"@blocklet/env": "1.17.4-beta-20251202-122551-267b614d",
|
|
49
49
|
"@blocklet/error": "^0.3.3",
|
|
50
|
-
"@blocklet/meta": "1.17.4-beta-
|
|
51
|
-
"@blocklet/resolver": "1.17.4-beta-
|
|
52
|
-
"@blocklet/sdk": "1.17.4-beta-
|
|
53
|
-
"@blocklet/server-js": "1.17.4-beta-
|
|
54
|
-
"@blocklet/store": "1.17.4-beta-
|
|
50
|
+
"@blocklet/meta": "1.17.4-beta-20251202-122551-267b614d",
|
|
51
|
+
"@blocklet/resolver": "1.17.4-beta-20251202-122551-267b614d",
|
|
52
|
+
"@blocklet/sdk": "1.17.4-beta-20251202-122551-267b614d",
|
|
53
|
+
"@blocklet/server-js": "1.17.4-beta-20251202-122551-267b614d",
|
|
54
|
+
"@blocklet/store": "1.17.4-beta-20251202-122551-267b614d",
|
|
55
55
|
"@blocklet/theme": "^3.2.11",
|
|
56
56
|
"@fidm/x509": "^1.2.1",
|
|
57
57
|
"@ocap/mcrypto": "^1.27.12",
|
|
@@ -116,5 +116,5 @@
|
|
|
116
116
|
"express": "^4.18.2",
|
|
117
117
|
"unzipper": "^0.10.11"
|
|
118
118
|
},
|
|
119
|
-
"gitHead": "
|
|
119
|
+
"gitHead": "58a5f7d49ccce973fdb214337aee262435704ec5"
|
|
120
120
|
}
|