@abtnode/core 1.17.8-beta-20260109-075740-5f484e08 → 1.17.8-beta-20260113-015027-32a1cec4

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 (65) hide show
  1. package/lib/api/team/access-key-manager.js +104 -0
  2. package/lib/api/team/invitation-manager.js +461 -0
  3. package/lib/api/team/notification-manager.js +189 -0
  4. package/lib/api/team/oauth-manager.js +60 -0
  5. package/lib/api/team/org-crud-manager.js +202 -0
  6. package/lib/api/team/org-manager.js +56 -0
  7. package/lib/api/team/org-member-manager.js +403 -0
  8. package/lib/api/team/org-query-manager.js +126 -0
  9. package/lib/api/team/org-resource-manager.js +186 -0
  10. package/lib/api/team/passport-manager.js +670 -0
  11. package/lib/api/team/rbac-manager.js +335 -0
  12. package/lib/api/team/session-manager.js +540 -0
  13. package/lib/api/team/store-manager.js +198 -0
  14. package/lib/api/team/tag-manager.js +230 -0
  15. package/lib/api/team/user-auth-manager.js +132 -0
  16. package/lib/api/team/user-manager.js +78 -0
  17. package/lib/api/team/user-query-manager.js +299 -0
  18. package/lib/api/team/user-social-manager.js +354 -0
  19. package/lib/api/team/user-update-manager.js +224 -0
  20. package/lib/api/team/verify-code-manager.js +161 -0
  21. package/lib/api/team.js +439 -3287
  22. package/lib/blocklet/manager/disk/auth-manager.js +68 -0
  23. package/lib/blocklet/manager/disk/backup-manager.js +288 -0
  24. package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
  25. package/lib/blocklet/manager/disk/component-manager.js +83 -0
  26. package/lib/blocklet/manager/disk/config-manager.js +191 -0
  27. package/lib/blocklet/manager/disk/controller-manager.js +64 -0
  28. package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
  29. package/lib/blocklet/manager/disk/download-manager.js +96 -0
  30. package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
  31. package/lib/blocklet/manager/disk/federated-manager.js +651 -0
  32. package/lib/blocklet/manager/disk/hook-manager.js +124 -0
  33. package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
  34. package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
  35. package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
  36. package/lib/blocklet/manager/disk/install-manager.js +36 -0
  37. package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
  38. package/lib/blocklet/manager/disk/job-manager.js +467 -0
  39. package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
  40. package/lib/blocklet/manager/disk/notification-manager.js +343 -0
  41. package/lib/blocklet/manager/disk/query-manager.js +562 -0
  42. package/lib/blocklet/manager/disk/settings-manager.js +507 -0
  43. package/lib/blocklet/manager/disk/start-manager.js +611 -0
  44. package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
  45. package/lib/blocklet/manager/disk/update-manager.js +153 -0
  46. package/lib/blocklet/manager/disk.js +669 -5796
  47. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
  48. package/lib/blocklet/manager/lock.js +18 -0
  49. package/lib/event/index.js +28 -24
  50. package/lib/util/blocklet/app-utils.js +192 -0
  51. package/lib/util/blocklet/blocklet-loader.js +258 -0
  52. package/lib/util/blocklet/config-manager.js +232 -0
  53. package/lib/util/blocklet/did-document.js +240 -0
  54. package/lib/util/blocklet/environment.js +555 -0
  55. package/lib/util/blocklet/health-check.js +449 -0
  56. package/lib/util/blocklet/install-utils.js +365 -0
  57. package/lib/util/blocklet/logo.js +57 -0
  58. package/lib/util/blocklet/meta-utils.js +269 -0
  59. package/lib/util/blocklet/port-manager.js +141 -0
  60. package/lib/util/blocklet/process-manager.js +504 -0
  61. package/lib/util/blocklet/runtime-info.js +105 -0
  62. package/lib/util/blocklet/validation.js +418 -0
  63. package/lib/util/blocklet.js +98 -3066
  64. package/lib/util/wallet-app-notification.js +40 -0
  65. package/package.json +22 -22
@@ -0,0 +1,299 @@
1
+ const pick = require('lodash/pick');
2
+ const pMap = require('p-map');
3
+ const { MAX_USER_PAGE_SIZE, PASSPORT_STATUS } = require('@abtnode/constant');
4
+
5
+ /**
6
+ * Get users
7
+ * @param {Object} api - TeamAPI instance
8
+ * @param {Object} params
9
+ * @param {string} params.teamDid - Team DID
10
+ * @param {Object} params.query - Query
11
+ * @param {Object} params.paging - Paging
12
+ * @param {Object} params.sort - Sort
13
+ * @param {Array} params.dids - DIDs
14
+ * @param {Object} context
15
+ * @returns {Promise<Object>}
16
+ */
17
+ async function getUsers(api, { teamDid, query, paging: inputPaging, sort, dids }, context = {}) {
18
+ const state = await api.getUserState(teamDid);
19
+ const nodeInfo = await api.node.read();
20
+
21
+ // HACK: 如果查询的是 server 的数据库,则不应该查询 userSession 表
22
+ if (teamDid === nodeInfo.did) {
23
+ if (query) {
24
+ delete query.includeUserSessions;
25
+ }
26
+ }
27
+
28
+ if (inputPaging?.pageSize > MAX_USER_PAGE_SIZE) {
29
+ throw new Error(`Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
30
+ }
31
+
32
+ if (dids && dids.length > MAX_USER_PAGE_SIZE) {
33
+ throw new Error(`Length of users should not exceed ${MAX_USER_PAGE_SIZE}`);
34
+ }
35
+
36
+ let list;
37
+ let paging;
38
+
39
+ if (dids) {
40
+ list = await state.getUsersByDids({ query, dids }, context);
41
+ paging = {
42
+ total: list.length,
43
+ pageSize: dids.length,
44
+ pageCount: 1,
45
+ page: 1,
46
+ };
47
+ } else {
48
+ const doc = await state.getUsers({ query, sort, paging: { pageSize: 20, ...inputPaging } }, context);
49
+ list = doc.list;
50
+ paging = doc.paging;
51
+ }
52
+
53
+ let userSessionsCountList = [];
54
+ if (teamDid !== nodeInfo.did && query?.includeUserSessions) {
55
+ userSessionsCountList = await pMap(
56
+ list,
57
+ async (x) => {
58
+ const { count: userSessionsCount } = await api.getUserSessionsCount({
59
+ teamDid,
60
+ query: {
61
+ userDid: x.did,
62
+ appPid: teamDid,
63
+ },
64
+ });
65
+ return userSessionsCount;
66
+ },
67
+ { concurrency: 10 }
68
+ );
69
+ }
70
+ const users = list.map(
71
+ (d, index) => {
72
+ const pickData = pick(d, [
73
+ 'did',
74
+ 'pk',
75
+ 'role',
76
+ 'email',
77
+ 'phone',
78
+ 'fullName',
79
+ 'approved',
80
+ 'createdAt',
81
+ 'updatedAt',
82
+ 'passports',
83
+ 'firstLoginAt',
84
+ 'lastLoginAt',
85
+ 'lastLoginIp',
86
+ 'remark',
87
+ 'avatar',
88
+ 'locale',
89
+ 'tags',
90
+ 'url',
91
+ 'inviter',
92
+ 'generation',
93
+ 'emailVerified',
94
+ 'phoneVerified',
95
+ // oauth relate fields
96
+ 'sourceProvider',
97
+ 'sourceAppPid',
98
+ 'connectedAccounts',
99
+
100
+ 'metadata',
101
+ 'isFollowing',
102
+ ]);
103
+
104
+ return {
105
+ ...pickData,
106
+ userSessions: [],
107
+ userSessionsCount: userSessionsCountList[index] || 0,
108
+ };
109
+ }
110
+
111
+ // eslint-disable-next-line function-paren-newline
112
+ );
113
+ return {
114
+ // FIXME: @zhanghan 这里做字段过滤的目的是?gql 本身已经对字段做了过滤了
115
+ users,
116
+ paging,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Get users count
122
+ * @param {Object} api - TeamAPI instance
123
+ * @param {Object} params
124
+ * @param {string} params.teamDid - Team DID
125
+ * @returns {Promise<number>}
126
+ */
127
+ async function getUsersCount(api, { teamDid }) {
128
+ const state = await api.getUserState(teamDid);
129
+ return state.count();
130
+ }
131
+
132
+ /**
133
+ * Get users count per role
134
+ * @param {Object} api - TeamAPI instance
135
+ * @param {Object} params
136
+ * @param {string} params.teamDid - Team DID
137
+ * @returns {Promise<Array>}
138
+ */
139
+ async function getUsersCountPerRole(api, { teamDid }) {
140
+ const roles = await api.getRoles({ teamDid });
141
+ const state = await api.getUserState(teamDid);
142
+ const names = ['$all', ...roles.filter((x) => !x.orgId).map((x) => x.name), '$none', '$blocked'];
143
+ return Promise.all(
144
+ names.map(async (name) => {
145
+ const count = await state.countByPassport({ name, status: PASSPORT_STATUS.VALID });
146
+ return { key: name, value: count };
147
+ })
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Get user
153
+ * @param {Object} api - TeamAPI instance
154
+ * @param {Object} params
155
+ * @param {string} params.teamDid - Team DID
156
+ * @param {Object} params.user - User
157
+ * @param {Object} params.options - Options
158
+ * @returns {Promise<Object>}
159
+ */
160
+ async function getUser(api, { teamDid, user, options = {} }) {
161
+ const state = await api.getUserState(teamDid);
162
+ return state.getUser(user.did, options);
163
+ }
164
+
165
+ /**
166
+ * Get connected account
167
+ * @param {Object} api - TeamAPI instance
168
+ * @param {Object} params
169
+ * @param {string} params.teamDid - Team DID
170
+ * @param {string} params.id - Account ID
171
+ * @returns {Promise<Object>}
172
+ */
173
+ async function getConnectedAccount(api, { teamDid, id }) {
174
+ const state = await api.getUserState(teamDid);
175
+ return state.getConnectedAccount(id);
176
+ }
177
+
178
+ /**
179
+ * Check if email is used
180
+ * @param {Object} api - TeamAPI instance
181
+ * @param {Object} params
182
+ * @param {string} params.teamDid - Team DID
183
+ * @param {string} params.email - Email
184
+ * @param {boolean} params.verified - Verified
185
+ * @param {string} params.sourceProvider - Source provider
186
+ * @returns {Promise<boolean>}
187
+ */
188
+ async function isEmailUsed(api, { teamDid, email, verified = false, sourceProvider = '' }) {
189
+ const state = await api.getUserState(teamDid);
190
+ return state.isEmailUsed(email, verified, sourceProvider);
191
+ }
192
+
193
+ /**
194
+ * Check if phone is used
195
+ * @param {Object} api - TeamAPI instance
196
+ * @param {Object} params
197
+ * @param {string} params.teamDid - Team DID
198
+ * @param {string} params.phone - Phone
199
+ * @param {boolean} params.verified - Verified
200
+ * @returns {Promise<boolean>}
201
+ */
202
+ async function isPhoneUsed(api, { teamDid, phone, verified = false }) {
203
+ const state = await api.getUserState(teamDid);
204
+ return state.isPhoneUsed(phone, verified);
205
+ }
206
+
207
+ /**
208
+ * Get user by DID
209
+ * @param {Object} api - TeamAPI instance
210
+ * @param {Object} params
211
+ * @param {string} params.teamDid - Team DID
212
+ * @param {string} params.userDid - User DID
213
+ * @param {Array} params.attributes - Attributes
214
+ * @returns {Promise<Object>}
215
+ */
216
+ async function getUserByDid(api, { teamDid, userDid, attributes = [] }) {
217
+ const state = await api.getUserState(teamDid);
218
+ return state.getUserByDid(userDid, attributes);
219
+ }
220
+
221
+ /**
222
+ * Check if user is valid
223
+ * @param {Object} api - TeamAPI instance
224
+ * @param {Object} params
225
+ * @param {string} params.teamDid - Team DID
226
+ * @param {string} params.userDid - User DID
227
+ * @returns {Promise<boolean>}
228
+ */
229
+ async function isUserValid(api, { teamDid, userDid }) {
230
+ const state = await api.getUserState(teamDid);
231
+ return state.isUserValid(userDid);
232
+ }
233
+
234
+ /**
235
+ * Check if passport is valid
236
+ * @param {Object} api - TeamAPI instance
237
+ * @param {Object} params
238
+ * @param {string} params.teamDid - Team DID
239
+ * @param {string} params.passportId - Passport ID
240
+ * @returns {Promise<boolean>}
241
+ */
242
+ async function isPassportValid(api, { teamDid, passportId }) {
243
+ const state = await api.getUserState(teamDid);
244
+ return state.isPassportValid(passportId);
245
+ }
246
+
247
+ /**
248
+ * Check if is connected account
249
+ * @param {Object} api - TeamAPI instance
250
+ * @param {Object} params
251
+ * @param {string} params.teamDid - Team DID
252
+ * @param {string} params.did - DID
253
+ * @returns {Promise<boolean>}
254
+ */
255
+ async function isConnectedAccount(api, { teamDid, did }) {
256
+ const state = await api.getUserState(teamDid);
257
+ return state.isConnectedAccount(did);
258
+ }
259
+
260
+ /**
261
+ * Get owner
262
+ * @param {Object} api - TeamAPI instance
263
+ * @param {Object} params
264
+ * @param {string} params.teamDid - Team DID
265
+ * @returns {Promise<Object>}
266
+ */
267
+ async function getOwner(api, { teamDid }) {
268
+ let owner = await api.teamManager.getOwner(teamDid);
269
+ const state = await api.getUserState(teamDid);
270
+ if (!owner) {
271
+ const ownerDids = await state.getOwnerDids();
272
+ if (ownerDids.length > 0) {
273
+ owner = { did: ownerDids[0] };
274
+ }
275
+ }
276
+
277
+ if (!owner) {
278
+ return null;
279
+ }
280
+
281
+ // NOTICE: 目前 owner 只能为 did-wallet,无需查询 connectedAccount
282
+ const full = await state.getUser(owner.did);
283
+ return full || owner;
284
+ }
285
+
286
+ module.exports = {
287
+ getUsers,
288
+ getUsersCount,
289
+ getUsersCountPerRole,
290
+ getUser,
291
+ getConnectedAccount,
292
+ isEmailUsed,
293
+ isPhoneUsed,
294
+ getUserByDid,
295
+ isUserValid,
296
+ isPassportValid,
297
+ isConnectedAccount,
298
+ getOwner,
299
+ };
@@ -0,0 +1,354 @@
1
+ const omit = require('lodash/omit');
2
+ const { joinURL } = require('ufo');
3
+ const logger = require('@abtnode/logger')('@abtnode/core:api:team:user-social');
4
+ const {
5
+ MAX_USER_PAGE_SIZE,
6
+ USER_AVATAR_URL_PREFIX,
7
+ WELLKNOWN_SERVICE_PATH_PREFIX,
8
+ NOTIFICATION_SEND_CHANNEL,
9
+ WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD,
10
+ } = require('@abtnode/constant');
11
+ const { CustomError } = require('@blocklet/error');
12
+ const { getUserAvatarUrl: getFederatedUserAvatarUrl } = require('@abtnode/auth/lib/util/federated');
13
+
14
+ const { getBlocklet } = require('../../util/blocklet');
15
+
16
+ // 关注取消关注相关接口
17
+ const USER_RELATION_ACTIONS = {
18
+ follow: (state, followerDid, userDid, options) => state.followUser(followerDid, userDid, options),
19
+ unfollow: (state, followerDid, userDid, options) => state.unfollowUser(followerDid, userDid, options),
20
+ };
21
+
22
+ // 获取用户相关接口
23
+ const USER_RELATION_QUERIES = {
24
+ followers: (state, userDid, options, context) => state.getFollowers(userDid, options, context),
25
+ following: (state, userDid, options, context) => state.getFollowing(userDid, options, context),
26
+ };
27
+
28
+ /**
29
+ * User follow action
30
+ * @param {Object} api - TeamAPI instance
31
+ * @param {Object} params
32
+ * @param {string} params.teamDid - Team DID
33
+ * @param {string} params.userDid - User DID
34
+ * @param {string} params.followerDid - Follower DID
35
+ * @param {string} params.action - Action
36
+ * @param {Object} params.options - Options
37
+ * @param {Object} context
38
+ * @returns {Promise<Object>}
39
+ */
40
+ async function userFollowAction(api, { teamDid, userDid, followerDid, action = 'follow', options = {} }, context = {}) {
41
+ const targetDid = followerDid || context.userDid;
42
+
43
+ if (!USER_RELATION_ACTIONS[action]) {
44
+ throw new CustomError(
45
+ 400,
46
+ `Invalid action: ${action}. Supported: ${Object.keys(USER_RELATION_ACTIONS).join(', ')}`
47
+ );
48
+ }
49
+ let result;
50
+ try {
51
+ const state = await api.getUserState(teamDid);
52
+ result = await USER_RELATION_ACTIONS[action](state, targetDid, userDid, options);
53
+ } catch (error) {
54
+ logger.error(`Failed to ${action} user`, {
55
+ followerDid: targetDid,
56
+ userDid,
57
+ error,
58
+ });
59
+ throw error;
60
+ }
61
+
62
+ try {
63
+ if (action === 'follow' && !options.skipNotification) {
64
+ const state = await api.getUserState(teamDid);
65
+ const currentUser = await state.getUser(targetDid);
66
+ await api.createNotification({
67
+ teamDid,
68
+ receiver: userDid,
69
+ title: 'Followed',
70
+ description: `<${currentUser.fullName}(did:abt:${currentUser.did})> followed you`,
71
+ activity: {
72
+ type: 'follow',
73
+ actor: followerDid,
74
+ target: {
75
+ type: 'user',
76
+ id: followerDid,
77
+ },
78
+ },
79
+ entityId: teamDid,
80
+ source: 'system',
81
+ severity: 'success',
82
+ });
83
+ }
84
+ } catch (error) {
85
+ // 消息创建失败,不影响 follow 操作
86
+ logger.error('Failed to create notification', { error });
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * Get user follows
94
+ * @param {Object} api - TeamAPI instance
95
+ * @param {Object} params
96
+ * @param {string} params.teamDid - Team DID
97
+ * @param {string} params.userDid - User DID
98
+ * @param {string} params.type - Type
99
+ * @param {Object} params.paging - Paging
100
+ * @param {Object} params.sort - Sort
101
+ * @param {Object} params.options - Options
102
+ * @param {Object} context
103
+ * @returns {Promise<Object>}
104
+ */
105
+ async function getUserFollows(api, { teamDid, userDid, type = 'following', paging, sort, options = {} }, context = {}) {
106
+ const state = await api.getUserState(teamDid);
107
+
108
+ const blocklet = await getBlocklet({ did: teamDid, states: api.states, dataDirs: api.dataDirs });
109
+
110
+ if (paging?.pageSize > MAX_USER_PAGE_SIZE) {
111
+ throw new Error(`Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
112
+ }
113
+
114
+ if (!USER_RELATION_QUERIES[type]) {
115
+ throw new CustomError(400, `Invalid type: ${type}. Supported: ${Object.keys(USER_RELATION_QUERIES).join(', ')}`);
116
+ }
117
+
118
+ try {
119
+ const { list, paging: resultPaging } = await USER_RELATION_QUERIES[type](
120
+ state,
121
+ userDid,
122
+ {
123
+ paging,
124
+ sort,
125
+ ...options,
126
+ },
127
+ context
128
+ );
129
+
130
+ list.forEach((item) => {
131
+ const { user } = item;
132
+ if (user && user?.avatar && user?.avatar?.startsWith(USER_AVATAR_URL_PREFIX)) {
133
+ user.avatar = `${getFederatedUserAvatarUrl(user.avatar, blocklet)}?imageFilter=resize&w=48&h=48`;
134
+ }
135
+ });
136
+
137
+ return {
138
+ data: list,
139
+ paging: resultPaging,
140
+ };
141
+ } catch (error) {
142
+ logger.error('Failed to get user follows', { error });
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Get user follow stats
149
+ * @param {Object} api - TeamAPI instance
150
+ * @param {Object} params
151
+ * @param {string} params.teamDid - Team DID
152
+ * @param {Array} params.userDids - User DIDs
153
+ * @param {Object} params.options - Options
154
+ * @param {Object} context
155
+ * @returns {Promise<Object>}
156
+ */
157
+ async function getUserFollowStats(api, { teamDid, userDids, options = {} }, context = {}) {
158
+ const state = await api.getUserState(teamDid);
159
+ const info = await api.node.read();
160
+ const prefix = process.env.NODE_ENV === 'production' ? info.routing.adminPath : '';
161
+ return state.getFollowStats({ userDids, teamDid, prefix, options }, context);
162
+ }
163
+
164
+ /**
165
+ * Check following
166
+ * @param {Object} api - TeamAPI instance
167
+ * @param {Object} params
168
+ * @param {string} params.teamDid - Team DID
169
+ * @param {Array} params.userDids - User DIDs
170
+ * @param {string} params.followerDid - Follower DID
171
+ * @returns {Promise<Object>}
172
+ */
173
+ async function checkFollowing(api, { teamDid, userDids = [], followerDid }) {
174
+ if (!followerDid) {
175
+ throw new CustomError(400, 'Follower DID is required');
176
+ }
177
+ const state = await api.getUserState(teamDid);
178
+ return state.isFollowing(followerDid, userDids);
179
+ }
180
+
181
+ /**
182
+ * Get user invites
183
+ * @param {Object} api - TeamAPI instance
184
+ * @param {Object} params
185
+ * @param {string} params.teamDid - Team DID
186
+ * @param {string} params.userDid - User DID
187
+ * @param {Object} params.paging - Paging
188
+ * @param {Object} params.sort - Sort
189
+ * @param {Object} params.options - Options
190
+ * @param {Object} context
191
+ * @returns {Promise<Object>}
192
+ */
193
+ async function getUserInvites(api, { teamDid, userDid, paging, sort, options = {} }, context = {}) {
194
+ try {
195
+ if (paging?.pageSize > MAX_USER_PAGE_SIZE) {
196
+ throw new CustomError(400, `Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
197
+ }
198
+ const state = await api.getUserState(teamDid);
199
+ const { users, paging: resultPaging } = await state.getInvitees(userDid, { paging, sort, ...options }, context);
200
+ // 处理头像
201
+ const blocklet = await getBlocklet({ did: teamDid, states: api.states, dataDirs: api.dataDirs });
202
+ users.forEach((user) => {
203
+ if (user && user?.avatar && user?.avatar?.startsWith(USER_AVATAR_URL_PREFIX)) {
204
+ user.avatar = `${getFederatedUserAvatarUrl(user.avatar, blocklet)}?imageFilter=resize&w=48&h=48`;
205
+ }
206
+ });
207
+ return {
208
+ users,
209
+ paging: resultPaging,
210
+ };
211
+ } catch (error) {
212
+ logger.error('Failed to get user invites', { teamDid, userDid, error });
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Create webhook disabled notification
219
+ * @param {Object} api - TeamAPI instance
220
+ * @param {Object} params
221
+ * @param {string} params.teamDid - Team DID
222
+ * @param {string} params.userDid - User DID
223
+ * @param {Object} params.webhook - Webhook
224
+ * @param {string} params.action - Action
225
+ * @param {string} params.locale - Locale
226
+ * @returns {Promise<void>}
227
+ */
228
+ async function createWebhookDisabledNotification(api, { teamDid, userDid, webhook, action = '', locale = 'en' }) {
229
+ const notification = {
230
+ en: {
231
+ title: 'Your webhook has been temporarily disabled',
232
+ 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.`,
233
+ failureCount: 'Number of failures',
234
+ failureUrl: 'Failed Webhook URL',
235
+ },
236
+ zh: {
237
+ title: '您的 webhook 已被暂时禁用',
238
+ description: `您的 Webhook 在连续 ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} 次消息投递失败后已被暂时禁用。请检查其可用性,如果需要重新激活,请点击下方按钮。`,
239
+ failureCount: '失败次数',
240
+ failureUrl: '失败的 Webhook URL',
241
+ },
242
+ };
243
+
244
+ try {
245
+ const nodeInfo = await api.node.read();
246
+ // server 的 webhook 页面地址
247
+ const actionPath = '/settings/integration';
248
+ let _action = process.env.NODE_ENV === 'production' ? joinURL(nodeInfo.routing.adminPath, actionPath) : actionPath;
249
+ const channels = [NOTIFICATION_SEND_CHANNEL.WALLET, NOTIFICATION_SEND_CHANNEL.PUSH];
250
+ if (teamDid && !api.teamManager.isNodeTeam(teamDid)) {
251
+ channels.push(NOTIFICATION_SEND_CHANNEL.EMAIL);
252
+ _action = action || `${WELLKNOWN_SERVICE_PATH_PREFIX}/user/settings`; // 默认返回用户中心页面设置页面
253
+ }
254
+ await api.teamManager.createNotification({
255
+ title: notification[locale].title,
256
+ description: notification[locale].description,
257
+ attachments: [
258
+ {
259
+ type: 'section',
260
+ fields: [
261
+ {
262
+ type: 'text',
263
+ data: {
264
+ type: 'plain',
265
+ color: '#9397A1',
266
+ text: notification[locale].failureUrl,
267
+ },
268
+ },
269
+ {
270
+ type: 'text',
271
+ data: {
272
+ type: 'plain',
273
+ color: '#9397A1',
274
+ text: webhook.url,
275
+ },
276
+ },
277
+ {
278
+ type: 'text',
279
+ data: {
280
+ type: 'plain',
281
+ color: '#9397A1',
282
+ text: notification[locale].failureCount,
283
+ },
284
+ },
285
+ {
286
+ type: 'text',
287
+ data: {
288
+ type: 'plain',
289
+ color: '#9397A1',
290
+ text: `${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD}`,
291
+ },
292
+ },
293
+ ],
294
+ },
295
+ ],
296
+ source: 'system',
297
+ severity: 'warning',
298
+ action: _action,
299
+ channels,
300
+ ...(teamDid ? { teamDid } : {}),
301
+ ...(userDid ? { receiver: userDid } : {}),
302
+ });
303
+ } catch (error) {
304
+ logger.error('Failed to create webhook disabled notification', { teamDid, userDid, webhook, error });
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Update webhook state
310
+ * @param {Object} api - TeamAPI instance
311
+ * @param {Object} params
312
+ * @param {string} params.teamDid - Team DID
313
+ * @param {Array} params.userDids - User DIDs
314
+ * @param {Object} params.webhook - Webhook
315
+ * @param {boolean} params.enabled - Enabled
316
+ * @param {number} params.consecutiveFailures - Consecutive failures
317
+ * @returns {Promise<Array>}
318
+ */
319
+ async function updateWebHookState(api, { teamDid, userDids, webhook, enabled, consecutiveFailures }) {
320
+ const state = await api.getUserState(teamDid);
321
+ return Promise.all(
322
+ userDids.map(async (userDid) => {
323
+ const webhookParams =
324
+ consecutiveFailures === undefined
325
+ ? omit(
326
+ {
327
+ ...webhook,
328
+ enabled: enabled ?? webhook.enabled,
329
+ },
330
+ ['consecutiveFailures']
331
+ )
332
+ : {
333
+ ...webhook,
334
+ enabled: enabled ?? webhook.enabled,
335
+ consecutiveFailures,
336
+ };
337
+ await state.updateWebhook(userDid, webhookParams, (updated) => {
338
+ createWebhookDisabledNotification(api, { teamDid, userDid, webhook: updated });
339
+ });
340
+ })
341
+ );
342
+ }
343
+
344
+ module.exports = {
345
+ USER_RELATION_ACTIONS,
346
+ USER_RELATION_QUERIES,
347
+ userFollowAction,
348
+ getUserFollows,
349
+ getUserFollowStats,
350
+ checkFollowing,
351
+ getUserInvites,
352
+ createWebhookDisabledNotification,
353
+ updateWebHookState,
354
+ };