@codingfactory/socialkit-vue 0.1.0 → 0.3.0
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/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/services/circles.d.ts +470 -0
- package/dist/services/circles.d.ts.map +1 -0
- package/dist/services/circles.js +1924 -0
- package/dist/services/circles.js.map +1 -0
- package/dist/services/identity.d.ts +29 -0
- package/dist/services/identity.d.ts.map +1 -0
- package/dist/services/identity.js +150 -0
- package/dist/services/identity.js.map +1 -0
- package/dist/stores/auth.d.ts +3 -0
- package/dist/stores/auth.d.ts.map +1 -1
- package/dist/stores/circles.d.ts +4283 -0
- package/dist/stores/circles.d.ts.map +1 -0
- package/dist/stores/circles.js +1670 -0
- package/dist/stores/circles.js.map +1 -0
- package/dist/types/api.d.ts +19 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/auth.d.ts +34 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +8 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/identity.d.ts +34 -0
- package/dist/types/identity.d.ts.map +1 -0
- package/dist/types/identity.js +5 -0
- package/dist/types/identity.js.map +1 -0
- package/dist/types/user.d.ts +1 -0
- package/dist/types/user.d.ts.map +1 -1
- package/dist/types/user.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +105 -0
- package/src/services/circles.ts +2767 -0
- package/src/services/identity.ts +281 -0
- package/src/stores/circles.ts +2114 -0
- package/src/types/api.ts +20 -0
- package/src/types/auth.ts +39 -0
- package/src/types/identity.ts +41 -0
- package/src/types/user.ts +1 -0
|
@@ -0,0 +1,1924 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable circles service for SocialKit-powered frontends.
|
|
3
|
+
*/
|
|
4
|
+
export const CIRCLE_MANAGEMENT_SECTIONS = [
|
|
5
|
+
'overview',
|
|
6
|
+
'members',
|
|
7
|
+
'requests',
|
|
8
|
+
'content',
|
|
9
|
+
'discussions',
|
|
10
|
+
'events',
|
|
11
|
+
'roles',
|
|
12
|
+
'reports',
|
|
13
|
+
'bans',
|
|
14
|
+
'mutes',
|
|
15
|
+
'audit',
|
|
16
|
+
'automod',
|
|
17
|
+
'settings',
|
|
18
|
+
];
|
|
19
|
+
const DEFAULT_LIST_LIMIT = '10';
|
|
20
|
+
function isApiError(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null;
|
|
22
|
+
}
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function readRecord(value, key) {
|
|
27
|
+
if (!isRecord(value)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const entry = value[key];
|
|
31
|
+
return isRecord(entry) ? entry : null;
|
|
32
|
+
}
|
|
33
|
+
function readArray(value, key) {
|
|
34
|
+
if (!isRecord(value)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const entry = value[key];
|
|
38
|
+
return Array.isArray(entry) ? entry : null;
|
|
39
|
+
}
|
|
40
|
+
function readString(value) {
|
|
41
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
42
|
+
}
|
|
43
|
+
function readNullableString(value) {
|
|
44
|
+
if (value === null || value === undefined) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return readString(value);
|
|
48
|
+
}
|
|
49
|
+
function readBoolean(value) {
|
|
50
|
+
return typeof value === 'boolean' ? value : null;
|
|
51
|
+
}
|
|
52
|
+
function readNumber(value) {
|
|
53
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
54
|
+
}
|
|
55
|
+
function isCircleVisibility(value) {
|
|
56
|
+
return value === 'public' || value === 'closed' || value === 'secret';
|
|
57
|
+
}
|
|
58
|
+
function isCircleRole(value) {
|
|
59
|
+
return value === 'owner' || value === 'admin' || value === 'moderator' || value === 'member';
|
|
60
|
+
}
|
|
61
|
+
function isCircleMembershipRole(value) {
|
|
62
|
+
return value === 'owner' || value === 'member';
|
|
63
|
+
}
|
|
64
|
+
function isCircleModerationReportSource(value) {
|
|
65
|
+
return value === 'member' || value === 'automod' || typeof value === 'string';
|
|
66
|
+
}
|
|
67
|
+
function isCircleModerationReportSubjectType(value) {
|
|
68
|
+
return value === 'user' || value === 'post' || value === 'comment' || value === 'thread' || value === 'reply' || value === 'event' || typeof value === 'string';
|
|
69
|
+
}
|
|
70
|
+
function isManagementSection(value) {
|
|
71
|
+
return typeof value === 'string' && CIRCLE_MANAGEMENT_SECTIONS.includes(value);
|
|
72
|
+
}
|
|
73
|
+
function createEmptyPagination() {
|
|
74
|
+
return {
|
|
75
|
+
data: [],
|
|
76
|
+
meta: {
|
|
77
|
+
pagination: {
|
|
78
|
+
next_cursor: null,
|
|
79
|
+
has_more: false,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function createEmptyManagementCounts() {
|
|
85
|
+
return {
|
|
86
|
+
members: 0,
|
|
87
|
+
requests_pending: 0,
|
|
88
|
+
reports_pending: 0,
|
|
89
|
+
roles: 0,
|
|
90
|
+
bans_active: 0,
|
|
91
|
+
mutes_active: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizeManagementSections(value) {
|
|
95
|
+
if (!Array.isArray(value)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return value.filter((section) => isManagementSection(section));
|
|
99
|
+
}
|
|
100
|
+
function normalizePermissionKeys(value) {
|
|
101
|
+
if (!Array.isArray(value)) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
return value
|
|
105
|
+
.map((entry) => readString(entry))
|
|
106
|
+
.filter((entry) => entry !== null);
|
|
107
|
+
}
|
|
108
|
+
function normalizePermissionMap(value) {
|
|
109
|
+
if (!isRecord(value)) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
const normalized = {};
|
|
113
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
114
|
+
const flag = readBoolean(entry);
|
|
115
|
+
if (flag !== null) {
|
|
116
|
+
normalized[key] = flag;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
function normalizeManagementCounts(value) {
|
|
122
|
+
if (!isRecord(value)) {
|
|
123
|
+
return createEmptyManagementCounts();
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
members: readNumber(value.members) ?? 0,
|
|
127
|
+
requests_pending: readNumber(value.requests_pending) ?? 0,
|
|
128
|
+
reports_pending: readNumber(value.reports_pending) ?? 0,
|
|
129
|
+
roles: readNumber(value.roles) ?? 0,
|
|
130
|
+
bans_active: readNumber(value.bans_active) ?? 0,
|
|
131
|
+
mutes_active: readNumber(value.mutes_active) ?? 0,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function normalizeUserSummary(value) {
|
|
135
|
+
if (!isRecord(value)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const id = readString(value.id);
|
|
139
|
+
const name = readString(value.name);
|
|
140
|
+
const handle = readString(value.handle);
|
|
141
|
+
if (id === null || name === null || handle === null) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const avatar = readNullableString(value.avatar_url) ?? readNullableString(value.avatar);
|
|
145
|
+
const userSummary = {
|
|
146
|
+
id,
|
|
147
|
+
name,
|
|
148
|
+
handle,
|
|
149
|
+
avatar,
|
|
150
|
+
avatar_url: avatar,
|
|
151
|
+
bio: readNullableString(value.bio),
|
|
152
|
+
};
|
|
153
|
+
const isFollowing = readBoolean(value.is_following);
|
|
154
|
+
if (isFollowing !== null) {
|
|
155
|
+
userSummary.is_following = isFollowing;
|
|
156
|
+
}
|
|
157
|
+
const isFollowedBy = readBoolean(value.is_followed_by);
|
|
158
|
+
if (isFollowedBy !== null) {
|
|
159
|
+
userSummary.is_followed_by = isFollowedBy;
|
|
160
|
+
}
|
|
161
|
+
return userSummary;
|
|
162
|
+
}
|
|
163
|
+
function buildDefaultActorCapabilities(role, visibility, isMember) {
|
|
164
|
+
const canManage = role === 'owner' || role === 'admin';
|
|
165
|
+
const canManageRoles = role === 'owner';
|
|
166
|
+
const canReviewRequests = canManage && visibility !== 'public';
|
|
167
|
+
return {
|
|
168
|
+
is_owner: role === 'owner',
|
|
169
|
+
is_moderator: role === 'owner' || role === 'admin' || role === 'moderator',
|
|
170
|
+
moderation_level: role ?? 'member',
|
|
171
|
+
permissions: [],
|
|
172
|
+
can: {},
|
|
173
|
+
canJoin: !isMember && visibility === 'public',
|
|
174
|
+
canLeave: isMember && role !== 'owner',
|
|
175
|
+
canRequestToJoin: !isMember && visibility !== 'public',
|
|
176
|
+
canInvite: isMember,
|
|
177
|
+
canManage,
|
|
178
|
+
canManageMembers: canManage,
|
|
179
|
+
canManageRoles,
|
|
180
|
+
canReviewRequests,
|
|
181
|
+
canManageReports: canManage,
|
|
182
|
+
canManageBans: canManage,
|
|
183
|
+
canManageMutes: canManage,
|
|
184
|
+
canViewAuditLog: canManage,
|
|
185
|
+
canManageAutomod: canManage,
|
|
186
|
+
canManageSettings: canManage,
|
|
187
|
+
canTransferOwnership: role === 'owner',
|
|
188
|
+
canViewManagement: canManage,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function normalizeActorCapabilities(value, fallback) {
|
|
192
|
+
if (!isRecord(value)) {
|
|
193
|
+
return fallback;
|
|
194
|
+
}
|
|
195
|
+
const permissionMap = normalizePermissionMap(value.can);
|
|
196
|
+
const permissions = normalizePermissionKeys(value.permissions);
|
|
197
|
+
const hasPermission = (permissionKey) => {
|
|
198
|
+
if (typeof permissionMap[permissionKey] === 'boolean') {
|
|
199
|
+
return permissionMap[permissionKey];
|
|
200
|
+
}
|
|
201
|
+
return permissions.includes(permissionKey);
|
|
202
|
+
};
|
|
203
|
+
const hasAnyPermission = (permissionKeys) => {
|
|
204
|
+
return permissionKeys.some((permissionKey) => hasPermission(permissionKey));
|
|
205
|
+
};
|
|
206
|
+
const getFlag = (keys, defaultValue) => {
|
|
207
|
+
for (const key of keys) {
|
|
208
|
+
const flag = readBoolean(value[key]);
|
|
209
|
+
if (flag !== null) {
|
|
210
|
+
return flag;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return defaultValue;
|
|
214
|
+
};
|
|
215
|
+
const isOwner = getFlag(['isOwner', 'is_owner'], false);
|
|
216
|
+
const isModerator = getFlag(['isModerator', 'is_moderator'], false);
|
|
217
|
+
const moderationLevel = isCircleRole(value.moderation_level)
|
|
218
|
+
? value.moderation_level
|
|
219
|
+
: isOwner
|
|
220
|
+
? 'owner'
|
|
221
|
+
: isModerator
|
|
222
|
+
? fallback.moderation_level === 'admin'
|
|
223
|
+
? 'admin'
|
|
224
|
+
: 'moderator'
|
|
225
|
+
: fallback.moderation_level;
|
|
226
|
+
const canManageDerived = isOwner || isModerator || hasAnyPermission([
|
|
227
|
+
'group.details.edit',
|
|
228
|
+
'group.settings.manage',
|
|
229
|
+
'group.visibility.manage',
|
|
230
|
+
'group.rules.manage',
|
|
231
|
+
'group.roles.manage',
|
|
232
|
+
'group.roles.assign',
|
|
233
|
+
'group.members.invite',
|
|
234
|
+
'group.members.remove',
|
|
235
|
+
'group.members.mute',
|
|
236
|
+
'group.members.unmute',
|
|
237
|
+
'group.members.ban',
|
|
238
|
+
'group.members.unban',
|
|
239
|
+
'group.requests.review',
|
|
240
|
+
'group.reports.review',
|
|
241
|
+
'group.audit.view',
|
|
242
|
+
'group.automod.manage',
|
|
243
|
+
'group.owner.transfer',
|
|
244
|
+
]);
|
|
245
|
+
const canManageMembersDerived = hasAnyPermission([
|
|
246
|
+
'group.members.invite',
|
|
247
|
+
'group.members.remove',
|
|
248
|
+
'group.members.mute',
|
|
249
|
+
'group.members.unmute',
|
|
250
|
+
'group.members.ban',
|
|
251
|
+
'group.members.unban',
|
|
252
|
+
'group.roles.assign',
|
|
253
|
+
]);
|
|
254
|
+
const canManageRolesDerived = hasAnyPermission([
|
|
255
|
+
'group.roles.manage',
|
|
256
|
+
'group.roles.assign',
|
|
257
|
+
]);
|
|
258
|
+
const canReviewRequestsDerived = hasPermission('group.requests.review');
|
|
259
|
+
const canManageReportsDerived = hasPermission('group.reports.review');
|
|
260
|
+
const canManageBansDerived = hasAnyPermission(['group.members.ban', 'group.members.unban']);
|
|
261
|
+
const canManageMutesDerived = hasAnyPermission(['group.members.mute', 'group.members.unmute']);
|
|
262
|
+
const canViewAuditLogDerived = hasPermission('group.audit.view');
|
|
263
|
+
const canManageAutomodDerived = hasPermission('group.automod.manage');
|
|
264
|
+
const canManageSettingsDerived = hasAnyPermission([
|
|
265
|
+
'group.details.edit',
|
|
266
|
+
'group.settings.manage',
|
|
267
|
+
'group.visibility.manage',
|
|
268
|
+
'group.rules.manage',
|
|
269
|
+
]);
|
|
270
|
+
const canTransferOwnershipDerived = hasPermission('group.owner.transfer');
|
|
271
|
+
return {
|
|
272
|
+
is_owner: isOwner || fallback.is_owner,
|
|
273
|
+
is_moderator: isModerator || fallback.is_moderator,
|
|
274
|
+
moderation_level: moderationLevel,
|
|
275
|
+
permissions: permissions.length > 0 ? permissions : fallback.permissions,
|
|
276
|
+
can: Object.keys(permissionMap).length > 0 ? permissionMap : fallback.can,
|
|
277
|
+
canJoin: getFlag(['canJoin', 'can_join'], fallback.canJoin),
|
|
278
|
+
canLeave: getFlag(['canLeave', 'can_leave'], fallback.canLeave),
|
|
279
|
+
canRequestToJoin: getFlag(['canRequestToJoin', 'can_request_to_join'], fallback.canRequestToJoin),
|
|
280
|
+
canInvite: getFlag(['canInvite', 'can_invite'], hasPermission('group.members.invite') || fallback.canInvite),
|
|
281
|
+
canManage: getFlag(['canManage', 'can_manage'], canManageDerived || fallback.canManage),
|
|
282
|
+
canManageMembers: getFlag(['canManageMembers', 'can_manage_members'], canManageMembersDerived || fallback.canManageMembers),
|
|
283
|
+
canManageRoles: getFlag(['canManageRoles', 'can_manage_roles'], canManageRolesDerived || fallback.canManageRoles),
|
|
284
|
+
canReviewRequests: getFlag(['canReviewRequests', 'can_review_requests'], canReviewRequestsDerived || fallback.canReviewRequests),
|
|
285
|
+
canManageReports: getFlag(['canManageReports', 'can_manage_reports'], canManageReportsDerived || fallback.canManageReports),
|
|
286
|
+
canManageBans: getFlag(['canManageBans', 'can_manage_bans'], canManageBansDerived || fallback.canManageBans),
|
|
287
|
+
canManageMutes: getFlag(['canManageMutes', 'can_manage_mutes'], canManageMutesDerived || fallback.canManageMutes),
|
|
288
|
+
canViewAuditLog: getFlag(['canViewAuditLog', 'can_view_audit_log'], canViewAuditLogDerived || fallback.canViewAuditLog),
|
|
289
|
+
canManageAutomod: getFlag(['canManageAutomod', 'can_manage_automod'], canManageAutomodDerived || fallback.canManageAutomod),
|
|
290
|
+
canManageSettings: getFlag(['canManageSettings', 'can_manage_settings'], canManageSettingsDerived || fallback.canManageSettings),
|
|
291
|
+
canTransferOwnership: getFlag(['canTransferOwnership', 'can_transfer_ownership'], canTransferOwnershipDerived || fallback.canTransferOwnership),
|
|
292
|
+
canViewManagement: getFlag(['canViewManagement', 'can_view_management'], canManageDerived || fallback.canViewManagement),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function buildActorContext(circle) {
|
|
296
|
+
const role = isCircleRole(circle.user_role) ? circle.user_role : null;
|
|
297
|
+
const isMember = circle.is_member === true || role !== null;
|
|
298
|
+
const defaultCapabilities = buildDefaultActorCapabilities(role, circle.visibility, isMember);
|
|
299
|
+
const capabilities = normalizeActorCapabilities(circle.actor_capabilities ?? null, {
|
|
300
|
+
...defaultCapabilities,
|
|
301
|
+
canJoin: circle.can_join ?? defaultCapabilities.canJoin,
|
|
302
|
+
canLeave: circle.can_leave ?? defaultCapabilities.canLeave,
|
|
303
|
+
canRequestToJoin: circle.can_request_to_join ?? defaultCapabilities.canRequestToJoin,
|
|
304
|
+
canInvite: circle.can_invite ?? defaultCapabilities.canInvite,
|
|
305
|
+
canManage: circle.can_manage ?? defaultCapabilities.canManage,
|
|
306
|
+
canManageMembers: circle.can_manage_members ?? defaultCapabilities.canManageMembers,
|
|
307
|
+
});
|
|
308
|
+
const defaultSections = capabilities.canViewManagement
|
|
309
|
+
? [...CIRCLE_MANAGEMENT_SECTIONS]
|
|
310
|
+
: [];
|
|
311
|
+
const managementSections = circle.management_sections && circle.management_sections.length > 0
|
|
312
|
+
? [...circle.management_sections]
|
|
313
|
+
: defaultSections;
|
|
314
|
+
return {
|
|
315
|
+
isMember,
|
|
316
|
+
role,
|
|
317
|
+
hasPendingRequest: circle.has_pending_request === true,
|
|
318
|
+
capabilities,
|
|
319
|
+
managementSections,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function normalizeCollectionResponse(payload, normalizeItem) {
|
|
323
|
+
const data = readRecord(payload, 'data') ?? (isRecord(payload) ? payload : null);
|
|
324
|
+
const dataItems = readArray(payload, 'data');
|
|
325
|
+
const meta = readRecord(payload, 'meta');
|
|
326
|
+
const dataMeta = readRecord(data, 'meta');
|
|
327
|
+
const pagination = readRecord(meta, 'pagination') ?? readRecord(dataMeta, 'pagination');
|
|
328
|
+
const itemsValue = Array.isArray(data?.items)
|
|
329
|
+
? data.items
|
|
330
|
+
: dataItems ?? (Array.isArray(payload) ? payload : null);
|
|
331
|
+
const items = Array.isArray(itemsValue)
|
|
332
|
+
? itemsValue.map((item) => normalizeItem(item)).filter((item) => item !== null)
|
|
333
|
+
: [];
|
|
334
|
+
const nextCursor = readNullableString(pagination?.next_cursor)
|
|
335
|
+
?? readNullableString(data?.next);
|
|
336
|
+
const hasMore = readBoolean(pagination?.has_more) ?? (nextCursor !== null);
|
|
337
|
+
return {
|
|
338
|
+
data: items,
|
|
339
|
+
meta: {
|
|
340
|
+
pagination: {
|
|
341
|
+
next_cursor: nextCursor,
|
|
342
|
+
has_more: hasMore,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function normalizeInlineCollection(items, normalizeItem) {
|
|
348
|
+
if (!Array.isArray(items)) {
|
|
349
|
+
return createEmptyPagination();
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
data: items.map((item) => normalizeItem(item)).filter((item) => item !== null),
|
|
353
|
+
meta: createEmptyPagination().meta,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function normalizeCircle(value) {
|
|
357
|
+
if (!isRecord(value)) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const id = readString(value.id);
|
|
361
|
+
const name = readString(value.name);
|
|
362
|
+
const slug = readString(value.slug);
|
|
363
|
+
const visibility = value.visibility;
|
|
364
|
+
if (id === null || name === null || slug === null || !isCircleVisibility(visibility)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const managementSections = normalizeManagementSections(value.management_sections);
|
|
368
|
+
const circle = {
|
|
369
|
+
id,
|
|
370
|
+
name,
|
|
371
|
+
slug,
|
|
372
|
+
description: readNullableString(value.description),
|
|
373
|
+
visibility,
|
|
374
|
+
is_hidden: readBoolean(value.is_hidden) ?? false,
|
|
375
|
+
owner_id: readNullableString(value.owner_id),
|
|
376
|
+
meta: isRecord(value.meta) ? value.meta : {},
|
|
377
|
+
cover_photo_url: readNullableString(value.cover_photo_url),
|
|
378
|
+
cover_photo_media_id: readNullableString(value.cover_photo_media_id),
|
|
379
|
+
created_at: readString(value.created_at) ?? new Date(0).toISOString(),
|
|
380
|
+
updated_at: readString(value.updated_at) ?? new Date(0).toISOString(),
|
|
381
|
+
user_role: isCircleRole(value.user_role) ? value.user_role : null,
|
|
382
|
+
group_rules_summary: readNullableString(value.group_rules_summary),
|
|
383
|
+
welcome_message: readNullableString(value.welcome_message),
|
|
384
|
+
canonical_path: readNullableString(value.canonical_path),
|
|
385
|
+
};
|
|
386
|
+
const isMember = readBoolean(value.is_member);
|
|
387
|
+
if (isMember !== null) {
|
|
388
|
+
circle.is_member = isMember;
|
|
389
|
+
}
|
|
390
|
+
const memberCount = readNumber(value.member_count);
|
|
391
|
+
if (memberCount !== null) {
|
|
392
|
+
circle.member_count = memberCount;
|
|
393
|
+
}
|
|
394
|
+
const canJoin = readBoolean(value.can_join);
|
|
395
|
+
if (canJoin !== null) {
|
|
396
|
+
circle.can_join = canJoin;
|
|
397
|
+
}
|
|
398
|
+
const canManage = readBoolean(value.can_manage);
|
|
399
|
+
if (canManage !== null) {
|
|
400
|
+
circle.can_manage = canManage;
|
|
401
|
+
}
|
|
402
|
+
const canManageMembers = readBoolean(value.can_manage_members);
|
|
403
|
+
if (canManageMembers !== null) {
|
|
404
|
+
circle.can_manage_members = canManageMembers;
|
|
405
|
+
}
|
|
406
|
+
const canInvite = readBoolean(value.can_invite);
|
|
407
|
+
if (canInvite !== null) {
|
|
408
|
+
circle.can_invite = canInvite;
|
|
409
|
+
}
|
|
410
|
+
const canLeave = readBoolean(value.can_leave);
|
|
411
|
+
if (canLeave !== null) {
|
|
412
|
+
circle.can_leave = canLeave;
|
|
413
|
+
}
|
|
414
|
+
const canRequestToJoin = readBoolean(value.can_request_to_join);
|
|
415
|
+
if (canRequestToJoin !== null) {
|
|
416
|
+
circle.can_request_to_join = canRequestToJoin;
|
|
417
|
+
}
|
|
418
|
+
const hasPendingRequest = readBoolean(value.has_pending_request);
|
|
419
|
+
if (hasPendingRequest !== null) {
|
|
420
|
+
circle.has_pending_request = hasPendingRequest;
|
|
421
|
+
}
|
|
422
|
+
if (isRecord(value.actor_capabilities)) {
|
|
423
|
+
circle.actor_capabilities = normalizeActorCapabilities(value.actor_capabilities, buildDefaultActorCapabilities(null, visibility, false));
|
|
424
|
+
}
|
|
425
|
+
if (managementSections.length > 0) {
|
|
426
|
+
circle.management_sections = managementSections;
|
|
427
|
+
}
|
|
428
|
+
const creator = normalizeUserSummary(value.creator);
|
|
429
|
+
if (creator) {
|
|
430
|
+
circle.creator = creator;
|
|
431
|
+
}
|
|
432
|
+
circle.actor = buildActorContext(circle);
|
|
433
|
+
circle.actor_capabilities = circle.actor.capabilities;
|
|
434
|
+
circle.management_sections = circle.actor.managementSections;
|
|
435
|
+
circle.can_join = circle.actor.capabilities.canJoin;
|
|
436
|
+
circle.can_leave = circle.actor.capabilities.canLeave;
|
|
437
|
+
circle.can_request_to_join = circle.actor.capabilities.canRequestToJoin;
|
|
438
|
+
circle.can_invite = circle.actor.capabilities.canInvite;
|
|
439
|
+
circle.can_manage = circle.actor.capabilities.canManage;
|
|
440
|
+
circle.can_manage_members = circle.actor.capabilities.canManageMembers;
|
|
441
|
+
circle.has_pending_request = circle.actor.hasPendingRequest;
|
|
442
|
+
circle.is_member = circle.actor.isMember;
|
|
443
|
+
circle.user_role = circle.actor.role;
|
|
444
|
+
return circle;
|
|
445
|
+
}
|
|
446
|
+
function normalizeCircleMember(value) {
|
|
447
|
+
if (!isRecord(value)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const id = readString(value.id);
|
|
451
|
+
const userId = readString(value.user_id) ?? id;
|
|
452
|
+
const circleId = readString(value.circle_id) ?? '';
|
|
453
|
+
const joinedAt = readString(value.joined_at);
|
|
454
|
+
const role = value.role;
|
|
455
|
+
if (id === null || userId === null || joinedAt === null || !isCircleRole(role)) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const user = normalizeUserSummary(value.user);
|
|
459
|
+
const profileOverridesValue = isRecord(value.profile_overrides) ? value.profile_overrides : null;
|
|
460
|
+
let profileOverrides;
|
|
461
|
+
if (profileOverridesValue !== null) {
|
|
462
|
+
const nextProfileOverrides = {};
|
|
463
|
+
const overrideName = readString(profileOverridesValue.name);
|
|
464
|
+
if (overrideName !== null) {
|
|
465
|
+
nextProfileOverrides.name = overrideName;
|
|
466
|
+
}
|
|
467
|
+
const overrideBio = readString(profileOverridesValue.bio);
|
|
468
|
+
if (overrideBio !== null) {
|
|
469
|
+
nextProfileOverrides.bio = overrideBio;
|
|
470
|
+
}
|
|
471
|
+
const overrideAvatarUrl = readString(profileOverridesValue.avatar_url);
|
|
472
|
+
if (overrideAvatarUrl !== null) {
|
|
473
|
+
nextProfileOverrides.avatar_url = overrideAvatarUrl;
|
|
474
|
+
}
|
|
475
|
+
if (Object.keys(nextProfileOverrides).length > 0) {
|
|
476
|
+
profileOverrides = nextProfileOverrides;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const member = {
|
|
480
|
+
id,
|
|
481
|
+
user_id: userId,
|
|
482
|
+
circle_id: circleId,
|
|
483
|
+
role,
|
|
484
|
+
joined_at: joinedAt,
|
|
485
|
+
};
|
|
486
|
+
if (isCircleMembershipRole(value.membership_role)) {
|
|
487
|
+
member.membership_role = value.membership_role;
|
|
488
|
+
}
|
|
489
|
+
if (isCircleRole(value.display_role)) {
|
|
490
|
+
member.display_role = value.display_role;
|
|
491
|
+
}
|
|
492
|
+
const assignedRoles = Array.isArray(value.assigned_roles)
|
|
493
|
+
? value.assigned_roles
|
|
494
|
+
.map((roleDefinition) => normalizeRoleDefinition(roleDefinition))
|
|
495
|
+
.filter((roleDefinition) => roleDefinition !== null)
|
|
496
|
+
: [];
|
|
497
|
+
if (assignedRoles.length > 0) {
|
|
498
|
+
member.assigned_roles = assignedRoles;
|
|
499
|
+
}
|
|
500
|
+
const effectivePermissions = normalizePermissionKeys(value.effective_permissions);
|
|
501
|
+
if (effectivePermissions.length > 0) {
|
|
502
|
+
member.effective_permissions = effectivePermissions;
|
|
503
|
+
}
|
|
504
|
+
const isMuted = readBoolean(value.is_muted);
|
|
505
|
+
if (isMuted !== null) {
|
|
506
|
+
member.is_muted = isMuted;
|
|
507
|
+
}
|
|
508
|
+
const mutedUntil = readNullableString(value.muted_until);
|
|
509
|
+
if (mutedUntil !== null || value.muted_until === null) {
|
|
510
|
+
member.muted_until = mutedUntil;
|
|
511
|
+
}
|
|
512
|
+
const isBanned = readBoolean(value.is_banned);
|
|
513
|
+
if (isBanned !== null) {
|
|
514
|
+
member.is_banned = isBanned;
|
|
515
|
+
}
|
|
516
|
+
const canRemove = readBoolean(value.can_remove);
|
|
517
|
+
if (canRemove !== null) {
|
|
518
|
+
member.can_remove = canRemove;
|
|
519
|
+
}
|
|
520
|
+
const canChangeRole = readBoolean(value.can_change_role);
|
|
521
|
+
if (canChangeRole !== null) {
|
|
522
|
+
member.can_change_role = canChangeRole;
|
|
523
|
+
}
|
|
524
|
+
const canAssignRoles = readBoolean(value.can_assign_roles);
|
|
525
|
+
if (canAssignRoles !== null) {
|
|
526
|
+
member.can_assign_roles = canAssignRoles;
|
|
527
|
+
}
|
|
528
|
+
const canMute = readBoolean(value.can_mute);
|
|
529
|
+
if (canMute !== null) {
|
|
530
|
+
member.can_mute = canMute;
|
|
531
|
+
}
|
|
532
|
+
const canUnmute = readBoolean(value.can_unmute);
|
|
533
|
+
if (canUnmute !== null) {
|
|
534
|
+
member.can_unmute = canUnmute;
|
|
535
|
+
}
|
|
536
|
+
const canBan = readBoolean(value.can_ban);
|
|
537
|
+
if (canBan !== null) {
|
|
538
|
+
member.can_ban = canBan;
|
|
539
|
+
}
|
|
540
|
+
const canUnban = readBoolean(value.can_unban);
|
|
541
|
+
if (canUnban !== null) {
|
|
542
|
+
member.can_unban = canUnban;
|
|
543
|
+
}
|
|
544
|
+
const canViewAudit = readBoolean(value.can_view_audit);
|
|
545
|
+
if (canViewAudit !== null) {
|
|
546
|
+
member.can_view_audit = canViewAudit;
|
|
547
|
+
}
|
|
548
|
+
if (profileOverrides) {
|
|
549
|
+
member.profile_overrides = profileOverrides;
|
|
550
|
+
}
|
|
551
|
+
if (user) {
|
|
552
|
+
member.user = user;
|
|
553
|
+
}
|
|
554
|
+
return member;
|
|
555
|
+
}
|
|
556
|
+
function normalizeJoinRequest(value) {
|
|
557
|
+
if (!isRecord(value)) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
const id = readString(value.id);
|
|
561
|
+
const userId = readString(value.user_id);
|
|
562
|
+
const circleId = readString(value.circle_id);
|
|
563
|
+
const status = value.status;
|
|
564
|
+
const source = value.source;
|
|
565
|
+
const createdAt = readString(value.created_at);
|
|
566
|
+
const updatedAt = readString(value.updated_at);
|
|
567
|
+
if (id === null
|
|
568
|
+
|| userId === null
|
|
569
|
+
|| circleId === null
|
|
570
|
+
|| createdAt === null
|
|
571
|
+
|| updatedAt === null
|
|
572
|
+
|| (status !== 'pending' && status !== 'approved' && status !== 'rejected')
|
|
573
|
+
|| (source !== 'direct' && source !== 'invite')) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
const joinRequest = {
|
|
577
|
+
id,
|
|
578
|
+
user_id: userId,
|
|
579
|
+
circle_id: circleId,
|
|
580
|
+
status,
|
|
581
|
+
source,
|
|
582
|
+
message: readNullableString(value.message),
|
|
583
|
+
reviewed_by_id: readNullableString(value.reviewed_by_id),
|
|
584
|
+
reviewed_at: readNullableString(value.reviewed_at),
|
|
585
|
+
created_at: createdAt,
|
|
586
|
+
updated_at: updatedAt,
|
|
587
|
+
};
|
|
588
|
+
const user = normalizeUserSummary(value.user);
|
|
589
|
+
if (user) {
|
|
590
|
+
joinRequest.user = user;
|
|
591
|
+
}
|
|
592
|
+
return joinRequest;
|
|
593
|
+
}
|
|
594
|
+
function normalizeInviteLink(value) {
|
|
595
|
+
if (!isRecord(value)) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
const id = readString(value.id);
|
|
599
|
+
const token = readString(value.token);
|
|
600
|
+
const circleId = readString(value.circle_id);
|
|
601
|
+
const inviteUrl = readString(value.invite_url);
|
|
602
|
+
const qrSvg = readString(value.qr_svg);
|
|
603
|
+
const usesCount = readNumber(value.uses_count);
|
|
604
|
+
if (id === null || token === null || circleId === null || inviteUrl === null || qrSvg === null || usesCount === null) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
id,
|
|
609
|
+
token,
|
|
610
|
+
circle_id: circleId,
|
|
611
|
+
target_user_id: readNullableString(value.target_user_id),
|
|
612
|
+
max_uses: readNumber(value.max_uses),
|
|
613
|
+
uses_count: usesCount,
|
|
614
|
+
expires_at: readNullableString(value.expires_at),
|
|
615
|
+
invite_url: inviteUrl,
|
|
616
|
+
qr_svg: qrSvg,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function normalizeRoleDefinition(value) {
|
|
620
|
+
if (!isRecord(value)) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const id = readString(value.id);
|
|
624
|
+
const slug = readString(value.slug);
|
|
625
|
+
const name = readString(value.name);
|
|
626
|
+
if (id === null || slug === null || name === null) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
id,
|
|
631
|
+
slug,
|
|
632
|
+
name,
|
|
633
|
+
description: readNullableString(value.description),
|
|
634
|
+
color: readNullableString(value.color),
|
|
635
|
+
is_system: readBoolean(value.is_system) ?? false,
|
|
636
|
+
is_assignable: readBoolean(value.is_assignable) ?? true,
|
|
637
|
+
member_count: readNumber(value.member_count) ?? 0,
|
|
638
|
+
permissions: normalizePermissionKeys(value.permissions),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function normalizePermissionCatalogEntry(value) {
|
|
642
|
+
if (!isRecord(value)) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
const key = readString(value.key);
|
|
646
|
+
const label = readString(value.label);
|
|
647
|
+
const description = readString(value.description);
|
|
648
|
+
const moduleName = readString(value.module);
|
|
649
|
+
const section = value.section;
|
|
650
|
+
if (key === null || label === null || description === null || moduleName === null || !isManagementSection(section)) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
key,
|
|
655
|
+
label,
|
|
656
|
+
description,
|
|
657
|
+
module: moduleName,
|
|
658
|
+
owner_only: readBoolean(value.owner_only) ?? false,
|
|
659
|
+
surfaced: readBoolean(value.surfaced) ?? false,
|
|
660
|
+
section,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function normalizeModerationReport(value) {
|
|
664
|
+
if (!isRecord(value)) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const id = readString(value.id) ?? readString(value.report_id);
|
|
668
|
+
const reporterId = readNullableString(value.reporter_id);
|
|
669
|
+
const source = isCircleModerationReportSource(value.source)
|
|
670
|
+
? value.source
|
|
671
|
+
: 'member';
|
|
672
|
+
const subjectUserId = readNullableString(value.subject_user_id);
|
|
673
|
+
const subjectType = isCircleModerationReportSubjectType(value.subject_type)
|
|
674
|
+
? value.subject_type
|
|
675
|
+
: subjectUserId !== null
|
|
676
|
+
? 'user'
|
|
677
|
+
: null;
|
|
678
|
+
const subjectId = readNullableString(value.subject_id) ?? subjectUserId;
|
|
679
|
+
const subjectAuthorId = readNullableString(value.subject_author_id)
|
|
680
|
+
?? (subjectType === 'user' ? subjectUserId : null);
|
|
681
|
+
const category = readString(value.category) ?? 'general';
|
|
682
|
+
const status = value.status;
|
|
683
|
+
const priority = readString(value.priority) ?? 'normal';
|
|
684
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString();
|
|
685
|
+
if (id === null
|
|
686
|
+
|| (status !== 'pending' && status !== 'actioned' && status !== 'dismissed')) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
id,
|
|
691
|
+
reporter_id: reporterId,
|
|
692
|
+
source,
|
|
693
|
+
subject_user_id: subjectUserId,
|
|
694
|
+
subject_type: subjectType,
|
|
695
|
+
subject_id: subjectId,
|
|
696
|
+
subject_author_id: subjectAuthorId,
|
|
697
|
+
category,
|
|
698
|
+
notes: readNullableString(value.notes),
|
|
699
|
+
status,
|
|
700
|
+
priority,
|
|
701
|
+
resolution_action: readNullableString(value.resolution_action),
|
|
702
|
+
created_at: createdAt,
|
|
703
|
+
reviewed_at: readNullableString(value.reviewed_at),
|
|
704
|
+
resolution_notes: readNullableString(value.resolution_notes),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function normalizeBanRecord(value) {
|
|
708
|
+
if (!isRecord(value)) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const id = readString(value.id) ?? readString(value.user_id);
|
|
712
|
+
const userId = readString(value.user_id);
|
|
713
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString();
|
|
714
|
+
if (id === null || userId === null) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
const banRecord = {
|
|
718
|
+
id,
|
|
719
|
+
user_id: userId,
|
|
720
|
+
reason: readNullableString(value.reason),
|
|
721
|
+
created_at: createdAt,
|
|
722
|
+
updated_at: readNullableString(value.updated_at),
|
|
723
|
+
created_by_id: readNullableString(value.created_by_id),
|
|
724
|
+
};
|
|
725
|
+
const user = normalizeUserSummary(value.user);
|
|
726
|
+
if (user) {
|
|
727
|
+
banRecord.user = user;
|
|
728
|
+
}
|
|
729
|
+
return banRecord;
|
|
730
|
+
}
|
|
731
|
+
function normalizeMuteRecord(value) {
|
|
732
|
+
if (!isRecord(value)) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const id = readString(value.id) ?? readString(value.user_id);
|
|
736
|
+
const userId = readString(value.user_id);
|
|
737
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString();
|
|
738
|
+
if (id === null || userId === null) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const muteRecord = {
|
|
742
|
+
id,
|
|
743
|
+
user_id: userId,
|
|
744
|
+
reason: readNullableString(value.reason),
|
|
745
|
+
muted_until: readNullableString(value.muted_until),
|
|
746
|
+
created_at: createdAt,
|
|
747
|
+
updated_at: readNullableString(value.updated_at),
|
|
748
|
+
created_by_id: readNullableString(value.created_by_id),
|
|
749
|
+
};
|
|
750
|
+
const user = normalizeUserSummary(value.user);
|
|
751
|
+
if (user) {
|
|
752
|
+
muteRecord.user = user;
|
|
753
|
+
}
|
|
754
|
+
return muteRecord;
|
|
755
|
+
}
|
|
756
|
+
function normalizeAuditLogEntry(value) {
|
|
757
|
+
if (!isRecord(value)) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
const id = readString(value.id);
|
|
761
|
+
const action = readString(value.action);
|
|
762
|
+
const actorId = readString(value.actor_id);
|
|
763
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString();
|
|
764
|
+
if (id === null || action === null || actorId === null) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
id,
|
|
769
|
+
action,
|
|
770
|
+
actor_id: actorId,
|
|
771
|
+
target_user_id: readNullableString(value.target_user_id),
|
|
772
|
+
report_id: readNullableString(value.report_id),
|
|
773
|
+
meta: isRecord(value.meta) ? value.meta : {},
|
|
774
|
+
created_at: createdAt,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function isCircleModerationSubjectType(value) {
|
|
778
|
+
return value === 'post' || value === 'comment' || value === 'thread' || value === 'reply' || value === 'event';
|
|
779
|
+
}
|
|
780
|
+
function isCircleModerationSubjectState(value) {
|
|
781
|
+
return value === 'active' || value === 'removed' || value === 'locked';
|
|
782
|
+
}
|
|
783
|
+
function isCircleModerationSubjectAction(value) {
|
|
784
|
+
return value === 'remove' || value === 'restore' || value === 'lock' || value === 'unlock';
|
|
785
|
+
}
|
|
786
|
+
function normalizeModerationSubject(value) {
|
|
787
|
+
if (!isRecord(value)) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const subjectType = value.subject_type;
|
|
791
|
+
const subjectId = readString(value.subject_id);
|
|
792
|
+
const circleId = readString(value.circle_id);
|
|
793
|
+
const moderationState = value.moderation_state;
|
|
794
|
+
const createdAt = readString(value.created_at);
|
|
795
|
+
const updatedAt = readString(value.updated_at);
|
|
796
|
+
if (subjectId === null
|
|
797
|
+
|| circleId === null
|
|
798
|
+
|| createdAt === null
|
|
799
|
+
|| updatedAt === null
|
|
800
|
+
|| !isCircleModerationSubjectType(subjectType)
|
|
801
|
+
|| !isCircleModerationSubjectState(moderationState)) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const actions = Array.isArray(value.available_actions)
|
|
805
|
+
? value.available_actions
|
|
806
|
+
.filter((entry) => isCircleModerationSubjectAction(entry))
|
|
807
|
+
: [];
|
|
808
|
+
const latestReportStatusValue = value.latest_report_status;
|
|
809
|
+
const latestReportStatus = latestReportStatusValue === 'pending'
|
|
810
|
+
|| latestReportStatusValue === 'actioned'
|
|
811
|
+
|| latestReportStatusValue === 'dismissed'
|
|
812
|
+
? latestReportStatusValue
|
|
813
|
+
: null;
|
|
814
|
+
return {
|
|
815
|
+
subject_type: subjectType,
|
|
816
|
+
subject_id: subjectId,
|
|
817
|
+
circle_id: circleId,
|
|
818
|
+
author_id: readNullableString(value.author_id),
|
|
819
|
+
report_count: readNumber(value.report_count) ?? 0,
|
|
820
|
+
latest_report_status: latestReportStatus,
|
|
821
|
+
moderation_state: moderationState,
|
|
822
|
+
created_at: createdAt,
|
|
823
|
+
updated_at: updatedAt,
|
|
824
|
+
preview: isRecord(value.preview) ? value.preview : {},
|
|
825
|
+
available_actions: actions,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function normalizeAutomodCondition(value) {
|
|
829
|
+
if (!isRecord(value)) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
const type = readString(value.type);
|
|
833
|
+
if (type === null) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
type,
|
|
838
|
+
config: isRecord(value.config) ? value.config : {},
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function normalizeAutomodAction(value) {
|
|
842
|
+
if (!isRecord(value)) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
const type = readString(value.type);
|
|
846
|
+
if (type === null) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
type,
|
|
851
|
+
config: isRecord(value.config) ? value.config : {},
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function normalizeAutomodRule(value) {
|
|
855
|
+
if (!isRecord(value)) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
const id = readString(value.id);
|
|
859
|
+
const name = readString(value.name);
|
|
860
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString();
|
|
861
|
+
if (id === null || name === null) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
const conditions = Array.isArray(value.conditions)
|
|
865
|
+
? value.conditions.map((condition) => normalizeAutomodCondition(condition)).filter((condition) => condition !== null)
|
|
866
|
+
: [];
|
|
867
|
+
const actions = Array.isArray(value.actions)
|
|
868
|
+
? value.actions.map((action) => normalizeAutomodAction(action)).filter((action) => action !== null)
|
|
869
|
+
: [];
|
|
870
|
+
return {
|
|
871
|
+
id,
|
|
872
|
+
name,
|
|
873
|
+
description: readNullableString(value.description),
|
|
874
|
+
is_enabled: readBoolean(value.is_enabled) ?? true,
|
|
875
|
+
conditions,
|
|
876
|
+
actions,
|
|
877
|
+
priority: readNumber(value.priority) ?? 0,
|
|
878
|
+
created_at: createdAt,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function isCircleWithSlug(value) {
|
|
882
|
+
if (!isRecord(value)) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
return typeof value.id === 'string' && typeof value.slug === 'string';
|
|
886
|
+
}
|
|
887
|
+
function defaultSanitizeString(value) {
|
|
888
|
+
return value.trim();
|
|
889
|
+
}
|
|
890
|
+
function defaultGetCircleTerm(options) {
|
|
891
|
+
return options?.case === 'title' ? 'Circle' : 'circle';
|
|
892
|
+
}
|
|
893
|
+
class CirclesService {
|
|
894
|
+
client;
|
|
895
|
+
sanitizeString;
|
|
896
|
+
onNotify;
|
|
897
|
+
onUnauthorized;
|
|
898
|
+
getCircleTerm;
|
|
899
|
+
onError;
|
|
900
|
+
baseURL = '/v1/circles';
|
|
901
|
+
constructor(client, sanitizeString, onNotify, onUnauthorized, getCircleTerm = defaultGetCircleTerm, onError) {
|
|
902
|
+
this.client = client;
|
|
903
|
+
this.sanitizeString = sanitizeString;
|
|
904
|
+
this.onNotify = onNotify;
|
|
905
|
+
this.onUnauthorized = onUnauthorized;
|
|
906
|
+
this.getCircleTerm = getCircleTerm;
|
|
907
|
+
this.onError = onError;
|
|
908
|
+
}
|
|
909
|
+
isUuid(value) {
|
|
910
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
911
|
+
}
|
|
912
|
+
sanitizeInput(data) {
|
|
913
|
+
if (typeof data === 'string') {
|
|
914
|
+
return this.sanitizeString(data.trim());
|
|
915
|
+
}
|
|
916
|
+
if (Array.isArray(data)) {
|
|
917
|
+
return data.map((item) => this.sanitizeInput(item));
|
|
918
|
+
}
|
|
919
|
+
if (isRecord(data)) {
|
|
920
|
+
const sanitized = {};
|
|
921
|
+
Object.keys(data).forEach((key) => {
|
|
922
|
+
sanitized[key] = this.sanitizeInput(data[key]);
|
|
923
|
+
});
|
|
924
|
+
return sanitized;
|
|
925
|
+
}
|
|
926
|
+
return data;
|
|
927
|
+
}
|
|
928
|
+
emitNotification(notification) {
|
|
929
|
+
this.onNotify?.(notification);
|
|
930
|
+
}
|
|
931
|
+
reportError(context) {
|
|
932
|
+
const consoleMethod = context.logLevel === 'warn' ? console.warn : console.error;
|
|
933
|
+
const label = context.logLevel === 'warn' ? 'Circles API Not Found:' : 'Circles API Error:';
|
|
934
|
+
consoleMethod(label, {
|
|
935
|
+
url: context.url,
|
|
936
|
+
status: context.status,
|
|
937
|
+
timestamp: context.timestamp,
|
|
938
|
+
});
|
|
939
|
+
this.onError?.(context);
|
|
940
|
+
}
|
|
941
|
+
async handleError(error) {
|
|
942
|
+
const apiError = isApiError(error) ? error : null;
|
|
943
|
+
const status = apiError?.response?.status;
|
|
944
|
+
const context = {
|
|
945
|
+
error,
|
|
946
|
+
url: apiError?.config?.url,
|
|
947
|
+
status,
|
|
948
|
+
timestamp: new Date().toISOString(),
|
|
949
|
+
logLevel: status === 404 ? 'warn' : 'error',
|
|
950
|
+
};
|
|
951
|
+
this.reportError(context);
|
|
952
|
+
let userMessage = 'Something went wrong. Please try again.';
|
|
953
|
+
switch (status) {
|
|
954
|
+
case 401:
|
|
955
|
+
userMessage = 'Please sign in to continue.';
|
|
956
|
+
this.onUnauthorized?.();
|
|
957
|
+
break;
|
|
958
|
+
case 403:
|
|
959
|
+
userMessage = `You do not have permission to access this ${this.getCircleTerm()}.`;
|
|
960
|
+
break;
|
|
961
|
+
case 404:
|
|
962
|
+
userMessage = `${this.getCircleTerm({ case: 'title' })} not found.`;
|
|
963
|
+
break;
|
|
964
|
+
case 422:
|
|
965
|
+
userMessage = readString(apiError?.response?.data?.message) ?? 'Please check your input and try again.';
|
|
966
|
+
break;
|
|
967
|
+
case 429:
|
|
968
|
+
userMessage = 'Too many requests. Please wait a moment.';
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
this.emitNotification({
|
|
972
|
+
description: userMessage,
|
|
973
|
+
color: 'error',
|
|
974
|
+
duration: 4000,
|
|
975
|
+
});
|
|
976
|
+
throw new Error(userMessage);
|
|
977
|
+
}
|
|
978
|
+
isHttpStatus(error, status) {
|
|
979
|
+
return isApiError(error) && error.response?.status === status;
|
|
980
|
+
}
|
|
981
|
+
async findBySlug(slug) {
|
|
982
|
+
const response = await this.client.get(this.baseURL, {
|
|
983
|
+
params: {
|
|
984
|
+
search: this.sanitizeInput(slug),
|
|
985
|
+
limit: DEFAULT_LIST_LIMIT,
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
const items = readRecord(response.data, 'data')?.items;
|
|
989
|
+
if (!Array.isArray(items)) {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
const normalizedSlug = slug.trim().toLowerCase();
|
|
993
|
+
const matchedCircle = items.find((item) => isCircleWithSlug(item) && item.slug.toLowerCase() === normalizedSlug);
|
|
994
|
+
return matchedCircle ? normalizeCircle(matchedCircle) : null;
|
|
995
|
+
}
|
|
996
|
+
async list(cursor, filters) {
|
|
997
|
+
try {
|
|
998
|
+
const params = {
|
|
999
|
+
limit: DEFAULT_LIST_LIMIT,
|
|
1000
|
+
};
|
|
1001
|
+
if (cursor) {
|
|
1002
|
+
params.cursor = cursor;
|
|
1003
|
+
}
|
|
1004
|
+
if (filters?.visibility) {
|
|
1005
|
+
params.visibility = filters.visibility;
|
|
1006
|
+
}
|
|
1007
|
+
if (filters?.membership) {
|
|
1008
|
+
params.membership = filters.membership;
|
|
1009
|
+
}
|
|
1010
|
+
if (filters?.search) {
|
|
1011
|
+
params.search = this.sanitizeInput(filters.search);
|
|
1012
|
+
}
|
|
1013
|
+
const response = await this.client.get(this.baseURL, { params });
|
|
1014
|
+
return normalizeCollectionResponse(response.data, normalizeCircle);
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
return await this.handleError(error);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async get(id) {
|
|
1021
|
+
const identifier = this.sanitizeInput(id);
|
|
1022
|
+
try {
|
|
1023
|
+
const response = await this.client.get(`${this.baseURL}/${identifier}`);
|
|
1024
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1025
|
+
const normalized = normalizeCircle(data?.circle ?? data);
|
|
1026
|
+
if (normalized !== null) {
|
|
1027
|
+
return normalized;
|
|
1028
|
+
}
|
|
1029
|
+
throw new Error('Invalid circle payload');
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
if (this.isHttpStatus(error, 404) && !this.isUuid(identifier)) {
|
|
1033
|
+
try {
|
|
1034
|
+
const matchedCircle = await this.findBySlug(identifier);
|
|
1035
|
+
if (matchedCircle !== null) {
|
|
1036
|
+
return matchedCircle;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
// Fall through to canonical error handling.
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return await this.handleError(error);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async create(data) {
|
|
1047
|
+
try {
|
|
1048
|
+
const response = await this.client.post(this.baseURL, this.sanitizeInput(data));
|
|
1049
|
+
const payload = readRecord(response.data, 'data') ?? {};
|
|
1050
|
+
const id = readString(payload.id);
|
|
1051
|
+
if (id === null) {
|
|
1052
|
+
throw new Error('Invalid circle creation payload');
|
|
1053
|
+
}
|
|
1054
|
+
return { id };
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
return await this.handleError(error);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async update(id, data) {
|
|
1061
|
+
try {
|
|
1062
|
+
await this.client.patch(`${this.baseURL}/${id}`, this.sanitizeInput(data));
|
|
1063
|
+
return await this.get(id);
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
return await this.handleError(error);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
async delete(id) {
|
|
1070
|
+
try {
|
|
1071
|
+
await this.client.delete(`${this.baseURL}/${id}`);
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
return await this.handleError(error);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
async join(id) {
|
|
1078
|
+
try {
|
|
1079
|
+
await this.client.post(`${this.baseURL}/${id}/join`);
|
|
1080
|
+
this.emitNotification({
|
|
1081
|
+
description: `Successfully joined ${this.getCircleTerm()}!`,
|
|
1082
|
+
color: 'success',
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
return await this.handleError(error);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async leave(id) {
|
|
1090
|
+
try {
|
|
1091
|
+
await this.client.post(`${this.baseURL}/${id}/leave`);
|
|
1092
|
+
this.emitNotification({
|
|
1093
|
+
description: `Left ${this.getCircleTerm()} successfully`,
|
|
1094
|
+
color: 'success',
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
return await this.handleError(error);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async getMembers(id, cursor) {
|
|
1102
|
+
try {
|
|
1103
|
+
const params = {};
|
|
1104
|
+
if (cursor) {
|
|
1105
|
+
params.cursor = cursor;
|
|
1106
|
+
}
|
|
1107
|
+
const response = await this.client.get(`${this.baseURL}/${id}/members`, { params });
|
|
1108
|
+
return normalizeCollectionResponse(response.data, normalizeCircleMember);
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
return await this.handleError(error);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async requestToJoin(id, message) {
|
|
1115
|
+
try {
|
|
1116
|
+
const body = {};
|
|
1117
|
+
if (message) {
|
|
1118
|
+
body.message = this.sanitizeInput(message);
|
|
1119
|
+
}
|
|
1120
|
+
const response = await this.client.post(`${this.baseURL}/${id}/join-requests`, body);
|
|
1121
|
+
const payload = readRecord(response.data, 'data') ?? {};
|
|
1122
|
+
const requestId = readString(payload.request_id);
|
|
1123
|
+
const status = readString(payload.status);
|
|
1124
|
+
if (requestId === null || status === null) {
|
|
1125
|
+
throw new Error('Invalid join request payload');
|
|
1126
|
+
}
|
|
1127
|
+
this.emitNotification({
|
|
1128
|
+
description: `Join request sent! The ${this.getCircleTerm()} admins will review it.`,
|
|
1129
|
+
color: 'success',
|
|
1130
|
+
duration: 4000,
|
|
1131
|
+
});
|
|
1132
|
+
return {
|
|
1133
|
+
request_id: requestId,
|
|
1134
|
+
status,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
return await this.handleError(error);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async listJoinRequests(circleId, cursor, status) {
|
|
1142
|
+
try {
|
|
1143
|
+
const params = {};
|
|
1144
|
+
if (cursor) {
|
|
1145
|
+
params.cursor = cursor;
|
|
1146
|
+
}
|
|
1147
|
+
if (status) {
|
|
1148
|
+
params.status = status;
|
|
1149
|
+
}
|
|
1150
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/join-requests`, { params });
|
|
1151
|
+
return normalizeCollectionResponse(response.data, normalizeJoinRequest);
|
|
1152
|
+
}
|
|
1153
|
+
catch (error) {
|
|
1154
|
+
return await this.handleError(error);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async moderateJoinRequest(circleId, requestId, decision) {
|
|
1158
|
+
try {
|
|
1159
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/join-requests/${requestId}`, { decision });
|
|
1160
|
+
const payload = readRecord(response.data, 'data') ?? {};
|
|
1161
|
+
const status = readString(payload.status);
|
|
1162
|
+
const resolvedRequestId = readString(payload.request_id) ?? requestId;
|
|
1163
|
+
if (status === null) {
|
|
1164
|
+
throw new Error('Invalid join request moderation payload');
|
|
1165
|
+
}
|
|
1166
|
+
this.emitNotification({
|
|
1167
|
+
description: `Join request ${decision === 'approve' ? 'approved' : 'rejected'}.`,
|
|
1168
|
+
color: decision === 'approve' ? 'success' : 'warning',
|
|
1169
|
+
});
|
|
1170
|
+
return {
|
|
1171
|
+
request_id: resolvedRequestId,
|
|
1172
|
+
status,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
return await this.handleError(error);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
async fetchRoleDefinitions(circleId, cursor) {
|
|
1180
|
+
return await this.listRoleDefinitions(circleId, cursor);
|
|
1181
|
+
}
|
|
1182
|
+
async listRoleDefinitions(circleId, cursor) {
|
|
1183
|
+
try {
|
|
1184
|
+
const params = {};
|
|
1185
|
+
if (cursor) {
|
|
1186
|
+
params.cursor = cursor;
|
|
1187
|
+
}
|
|
1188
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/roles`, { params });
|
|
1189
|
+
const directCollection = normalizeCollectionResponse(response.data, normalizeRoleDefinition);
|
|
1190
|
+
if (directCollection.data.length > 0 || directCollection.meta.pagination.has_more) {
|
|
1191
|
+
return directCollection;
|
|
1192
|
+
}
|
|
1193
|
+
const data = readRecord(response.data, 'data');
|
|
1194
|
+
const roles = readArray(response.data, 'data') ?? data?.roles ?? data?.items;
|
|
1195
|
+
if (Array.isArray(roles)) {
|
|
1196
|
+
return normalizeInlineCollection(roles, normalizeRoleDefinition);
|
|
1197
|
+
}
|
|
1198
|
+
return createEmptyPagination();
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
if (this.isHttpStatus(error, 404)) {
|
|
1202
|
+
return createEmptyPagination();
|
|
1203
|
+
}
|
|
1204
|
+
return await this.handleError(error);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async listRoles(circleId, cursor) {
|
|
1208
|
+
return await this.listRoleDefinitions(circleId, cursor);
|
|
1209
|
+
}
|
|
1210
|
+
async createRoleDefinition(circleId, input) {
|
|
1211
|
+
try {
|
|
1212
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/roles`, this.sanitizeInput(input));
|
|
1213
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1214
|
+
const normalized = normalizeRoleDefinition(data);
|
|
1215
|
+
if (normalized !== null) {
|
|
1216
|
+
this.emitNotification({
|
|
1217
|
+
description: 'Group role created.',
|
|
1218
|
+
color: 'success',
|
|
1219
|
+
});
|
|
1220
|
+
return normalized;
|
|
1221
|
+
}
|
|
1222
|
+
throw new Error('Invalid group role payload');
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
return await this.handleError(error);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async createRole(circleId, input) {
|
|
1229
|
+
return await this.createRoleDefinition(circleId, input);
|
|
1230
|
+
}
|
|
1231
|
+
async updateRoleDefinition(circleId, roleId, input) {
|
|
1232
|
+
try {
|
|
1233
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/roles/${roleId}`, this.sanitizeInput(input));
|
|
1234
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1235
|
+
const normalized = normalizeRoleDefinition(data);
|
|
1236
|
+
if (normalized !== null) {
|
|
1237
|
+
this.emitNotification({
|
|
1238
|
+
description: 'Group role updated.',
|
|
1239
|
+
color: 'success',
|
|
1240
|
+
});
|
|
1241
|
+
return normalized;
|
|
1242
|
+
}
|
|
1243
|
+
throw new Error('Invalid group role payload');
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
return await this.handleError(error);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async updateRole(circleId, roleId, input) {
|
|
1250
|
+
return await this.updateRoleDefinition(circleId, roleId, input);
|
|
1251
|
+
}
|
|
1252
|
+
async replaceRoleDefinitionPermissions(circleId, roleId, permissions) {
|
|
1253
|
+
try {
|
|
1254
|
+
const response = await this.client.put(`${this.baseURL}/${circleId}/roles/${roleId}/permissions`, {
|
|
1255
|
+
permissions: this.sanitizeInput(permissions),
|
|
1256
|
+
});
|
|
1257
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1258
|
+
const normalized = normalizeRoleDefinition(data);
|
|
1259
|
+
if (normalized !== null) {
|
|
1260
|
+
this.emitNotification({
|
|
1261
|
+
description: 'Group role permissions updated.',
|
|
1262
|
+
color: 'success',
|
|
1263
|
+
});
|
|
1264
|
+
return normalized;
|
|
1265
|
+
}
|
|
1266
|
+
throw new Error('Invalid group role payload');
|
|
1267
|
+
}
|
|
1268
|
+
catch (error) {
|
|
1269
|
+
return await this.handleError(error);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
async replaceRolePermissions(circleId, roleId, permissions) {
|
|
1273
|
+
return await this.replaceRoleDefinitionPermissions(circleId, roleId, permissions);
|
|
1274
|
+
}
|
|
1275
|
+
async archiveRoleDefinition(circleId, roleId) {
|
|
1276
|
+
try {
|
|
1277
|
+
await this.client.delete(`${this.baseURL}/${circleId}/roles/${roleId}`);
|
|
1278
|
+
this.emitNotification({
|
|
1279
|
+
description: 'Group role archived.',
|
|
1280
|
+
color: 'success',
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
return await this.handleError(error);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async archiveRole(circleId, roleId) {
|
|
1288
|
+
return await this.archiveRoleDefinition(circleId, roleId);
|
|
1289
|
+
}
|
|
1290
|
+
async assignMemberRoles(circleId, userId, roleIds) {
|
|
1291
|
+
try {
|
|
1292
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/members/${userId}/roles`, {
|
|
1293
|
+
role_ids: this.sanitizeInput(roleIds),
|
|
1294
|
+
});
|
|
1295
|
+
const data = readRecord(response.data, 'data');
|
|
1296
|
+
return {
|
|
1297
|
+
assigned_roles: Array.isArray(data?.assigned_roles)
|
|
1298
|
+
? data.assigned_roles
|
|
1299
|
+
.map((roleDefinition) => normalizeRoleDefinition(roleDefinition))
|
|
1300
|
+
.filter((roleDefinition) => roleDefinition !== null)
|
|
1301
|
+
: [],
|
|
1302
|
+
effective_permissions: normalizePermissionKeys(data?.effective_permissions),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
return await this.handleError(error);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async removeMemberRole(circleId, userId, roleId) {
|
|
1310
|
+
try {
|
|
1311
|
+
await this.client.delete(`${this.baseURL}/${circleId}/members/${userId}/roles/${roleId}`);
|
|
1312
|
+
this.emitNotification({
|
|
1313
|
+
description: 'Member role removed.',
|
|
1314
|
+
color: 'success',
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
return await this.handleError(error);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async changeMemberRole(circleId, userId, role) {
|
|
1322
|
+
try {
|
|
1323
|
+
try {
|
|
1324
|
+
await this.client.patch(`${this.baseURL}/${circleId}/roles/${userId}`, { role });
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
if (!this.isHttpStatus(error, 404) && !this.isHttpStatus(error, 405)) {
|
|
1328
|
+
throw error;
|
|
1329
|
+
}
|
|
1330
|
+
await this.client.patch(`${this.baseURL}/${circleId}/members/role`, { user_id: userId, role });
|
|
1331
|
+
}
|
|
1332
|
+
const roleLabel = role === 'admin' ? 'an admin' : 'a member';
|
|
1333
|
+
this.emitNotification({
|
|
1334
|
+
description: `Member role changed to ${roleLabel}.`,
|
|
1335
|
+
color: 'success',
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
catch (error) {
|
|
1339
|
+
return await this.handleError(error);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async transferOwnership(circleId, userId, options) {
|
|
1343
|
+
try {
|
|
1344
|
+
const payload = {
|
|
1345
|
+
user_id: userId,
|
|
1346
|
+
};
|
|
1347
|
+
if (typeof options?.note === 'string' && options.note.trim().length > 0) {
|
|
1348
|
+
payload.note = this.sanitizeInput(options.note);
|
|
1349
|
+
}
|
|
1350
|
+
if (typeof options?.assign_previous_owner_admin === 'boolean') {
|
|
1351
|
+
payload.assign_previous_owner_admin = options.assign_previous_owner_admin;
|
|
1352
|
+
}
|
|
1353
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/owner/transfer`, payload);
|
|
1354
|
+
const data = readRecord(response.data, 'data') ?? {};
|
|
1355
|
+
const resolvedCircleId = readString(data.circle_id);
|
|
1356
|
+
const previousOwnerId = readString(data.previous_owner_id);
|
|
1357
|
+
const newOwnerId = readString(data.new_owner_id);
|
|
1358
|
+
if (resolvedCircleId === null || previousOwnerId === null || newOwnerId === null) {
|
|
1359
|
+
throw new Error('Invalid ownership transfer payload');
|
|
1360
|
+
}
|
|
1361
|
+
this.emitNotification({
|
|
1362
|
+
description: `${this.getCircleTerm({ case: 'title' })} ownership transferred.`,
|
|
1363
|
+
color: 'success',
|
|
1364
|
+
});
|
|
1365
|
+
return {
|
|
1366
|
+
circle_id: resolvedCircleId,
|
|
1367
|
+
previous_owner_id: previousOwnerId,
|
|
1368
|
+
new_owner_id: newOwnerId,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
return await this.handleError(error);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async removeMember(circleId, userId) {
|
|
1376
|
+
try {
|
|
1377
|
+
await this.client.delete(`${this.baseURL}/${circleId}/members`, { data: { user_id: userId } });
|
|
1378
|
+
this.emitNotification({
|
|
1379
|
+
description: `Member removed from ${this.getCircleTerm()}.`,
|
|
1380
|
+
color: 'success',
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
return await this.handleError(error);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async banMember(circleId, userId, options) {
|
|
1388
|
+
try {
|
|
1389
|
+
const payload = { user_id: userId };
|
|
1390
|
+
if (typeof options?.reason === 'string' && options.reason.trim().length > 0) {
|
|
1391
|
+
payload.reason = this.sanitizeInput(options.reason);
|
|
1392
|
+
}
|
|
1393
|
+
await this.client.post(`${this.baseURL}/${circleId}/moderation/bans`, payload);
|
|
1394
|
+
this.emitNotification({
|
|
1395
|
+
description: `Member banned from ${this.getCircleTerm()}.`,
|
|
1396
|
+
color: 'success',
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
catch (error) {
|
|
1400
|
+
return await this.handleError(error);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async listBans(circleId, cursor) {
|
|
1404
|
+
try {
|
|
1405
|
+
const params = {};
|
|
1406
|
+
if (cursor) {
|
|
1407
|
+
params.cursor = cursor;
|
|
1408
|
+
}
|
|
1409
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/bans`, { params });
|
|
1410
|
+
return normalizeCollectionResponse(response.data, normalizeBanRecord);
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
if (this.isHttpStatus(error, 404)) {
|
|
1414
|
+
return createEmptyPagination();
|
|
1415
|
+
}
|
|
1416
|
+
return await this.handleError(error);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async unbanMember(circleId, userId) {
|
|
1420
|
+
try {
|
|
1421
|
+
await this.client.delete(`${this.baseURL}/${circleId}/moderation/bans/${userId}`);
|
|
1422
|
+
this.emitNotification({
|
|
1423
|
+
description: `Member unbanned in ${this.getCircleTerm()}.`,
|
|
1424
|
+
color: 'success',
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
catch (error) {
|
|
1428
|
+
return await this.handleError(error);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async muteMember(circleId, userId, options) {
|
|
1432
|
+
try {
|
|
1433
|
+
const payload = { user_id: userId };
|
|
1434
|
+
if (typeof options?.reason === 'string' && options.reason.trim().length > 0) {
|
|
1435
|
+
payload.reason = this.sanitizeInput(options.reason);
|
|
1436
|
+
}
|
|
1437
|
+
if (typeof options?.muted_until === 'string' && options.muted_until.trim().length > 0) {
|
|
1438
|
+
payload.muted_until = options.muted_until.trim();
|
|
1439
|
+
}
|
|
1440
|
+
await this.client.post(`${this.baseURL}/${circleId}/moderation/mutes`, payload);
|
|
1441
|
+
this.emitNotification({
|
|
1442
|
+
description: `Member muted in ${this.getCircleTerm()}.`,
|
|
1443
|
+
color: 'success',
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
return await this.handleError(error);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async listMutes(circleId, cursor) {
|
|
1451
|
+
try {
|
|
1452
|
+
const params = {};
|
|
1453
|
+
if (cursor) {
|
|
1454
|
+
params.cursor = cursor;
|
|
1455
|
+
}
|
|
1456
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/mutes`, { params });
|
|
1457
|
+
return normalizeCollectionResponse(response.data, normalizeMuteRecord);
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
if (this.isHttpStatus(error, 404)) {
|
|
1461
|
+
return createEmptyPagination();
|
|
1462
|
+
}
|
|
1463
|
+
return await this.handleError(error);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async unmuteMember(circleId, userId) {
|
|
1467
|
+
try {
|
|
1468
|
+
await this.client.delete(`${this.baseURL}/${circleId}/moderation/mutes/${userId}`);
|
|
1469
|
+
this.emitNotification({
|
|
1470
|
+
description: `Member unmuted in ${this.getCircleTerm()}.`,
|
|
1471
|
+
color: 'success',
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
return await this.handleError(error);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async reportMember(circleId, subjectUserId, category, notes) {
|
|
1479
|
+
try {
|
|
1480
|
+
const payload = {
|
|
1481
|
+
subject_user_id: subjectUserId,
|
|
1482
|
+
category: this.sanitizeInput(category),
|
|
1483
|
+
};
|
|
1484
|
+
if (typeof notes === 'string' && notes.trim().length > 0) {
|
|
1485
|
+
payload.notes = this.sanitizeInput(notes);
|
|
1486
|
+
}
|
|
1487
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/moderation/reports`, payload);
|
|
1488
|
+
const data = readRecord(response.data, 'data') ?? {};
|
|
1489
|
+
const reportId = readString(data.report_id);
|
|
1490
|
+
const status = readString(data.status);
|
|
1491
|
+
if (reportId === null || status === null) {
|
|
1492
|
+
throw new Error('Invalid moderation report payload');
|
|
1493
|
+
}
|
|
1494
|
+
this.emitNotification({
|
|
1495
|
+
description: 'Report submitted for review.',
|
|
1496
|
+
color: 'success',
|
|
1497
|
+
});
|
|
1498
|
+
return {
|
|
1499
|
+
report_id: reportId,
|
|
1500
|
+
status,
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
catch (error) {
|
|
1504
|
+
return await this.handleError(error);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
async listModerationReports(circleId, cursor, status) {
|
|
1508
|
+
try {
|
|
1509
|
+
const params = {};
|
|
1510
|
+
if (cursor) {
|
|
1511
|
+
params.cursor = cursor;
|
|
1512
|
+
}
|
|
1513
|
+
if (status) {
|
|
1514
|
+
params.status = status;
|
|
1515
|
+
}
|
|
1516
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/reports`, { params });
|
|
1517
|
+
return normalizeCollectionResponse(response.data, normalizeModerationReport);
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
return await this.handleError(error);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async resolveModerationReport(circleId, reportId, decision, resolutionNotes) {
|
|
1524
|
+
try {
|
|
1525
|
+
const payload = { decision };
|
|
1526
|
+
if (typeof resolutionNotes === 'string' && resolutionNotes.trim().length > 0) {
|
|
1527
|
+
payload.resolution_notes = this.sanitizeInput(resolutionNotes);
|
|
1528
|
+
}
|
|
1529
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/moderation/reports/${reportId}`, payload);
|
|
1530
|
+
const data = readRecord(response.data, 'data') ?? {};
|
|
1531
|
+
const resolvedReportId = readString(data.report_id) ?? reportId;
|
|
1532
|
+
const status = readString(data.status);
|
|
1533
|
+
if (status === null) {
|
|
1534
|
+
throw new Error('Invalid moderation resolution payload');
|
|
1535
|
+
}
|
|
1536
|
+
this.emitNotification({
|
|
1537
|
+
description: `Report ${decision === 'action' ? 'actioned' : 'dismissed'}.`,
|
|
1538
|
+
color: 'success',
|
|
1539
|
+
});
|
|
1540
|
+
return {
|
|
1541
|
+
report_id: resolvedReportId,
|
|
1542
|
+
status,
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
catch (error) {
|
|
1546
|
+
return await this.handleError(error);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async getModerationAuditLog(circleId, cursor) {
|
|
1550
|
+
try {
|
|
1551
|
+
const params = {};
|
|
1552
|
+
if (cursor) {
|
|
1553
|
+
params.cursor = cursor;
|
|
1554
|
+
}
|
|
1555
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/audit-log`, { params });
|
|
1556
|
+
return normalizeCollectionResponse(response.data, normalizeAuditLogEntry);
|
|
1557
|
+
}
|
|
1558
|
+
catch (error) {
|
|
1559
|
+
return await this.handleError(error);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
async listModerationSubjects(circleId, subjectType, cursor, state) {
|
|
1563
|
+
try {
|
|
1564
|
+
const params = {
|
|
1565
|
+
subject_type: subjectType,
|
|
1566
|
+
};
|
|
1567
|
+
if (cursor) {
|
|
1568
|
+
params.cursor = cursor;
|
|
1569
|
+
}
|
|
1570
|
+
if (state) {
|
|
1571
|
+
params.state = state;
|
|
1572
|
+
}
|
|
1573
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/subjects`, { params });
|
|
1574
|
+
return normalizeCollectionResponse(response.data, normalizeModerationSubject);
|
|
1575
|
+
}
|
|
1576
|
+
catch (error) {
|
|
1577
|
+
return await this.handleError(error);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async actOnModerationSubject(circleId, subjectType, subjectId, action, reason) {
|
|
1581
|
+
try {
|
|
1582
|
+
const payload = { action };
|
|
1583
|
+
if (typeof reason === 'string' && reason.trim().length > 0) {
|
|
1584
|
+
payload.reason = this.sanitizeInput(reason);
|
|
1585
|
+
}
|
|
1586
|
+
await this.client.post(`${this.baseURL}/${circleId}/moderation/subjects/${subjectType}/${subjectId}/actions`, payload);
|
|
1587
|
+
this.emitNotification({
|
|
1588
|
+
description: `Moderation action "${action}" applied.`,
|
|
1589
|
+
color: 'success',
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
return await this.handleError(error);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async listAutomodRules(circleId, cursor) {
|
|
1597
|
+
try {
|
|
1598
|
+
const params = {};
|
|
1599
|
+
if (cursor) {
|
|
1600
|
+
params.cursor = cursor;
|
|
1601
|
+
}
|
|
1602
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/automod-rules`, { params });
|
|
1603
|
+
return normalizeCollectionResponse(response.data, normalizeAutomodRule);
|
|
1604
|
+
}
|
|
1605
|
+
catch (error) {
|
|
1606
|
+
return await this.handleError(error);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async getAutomodRule(circleId, ruleId) {
|
|
1610
|
+
try {
|
|
1611
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/automod-rules/${ruleId}`);
|
|
1612
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1613
|
+
const normalized = normalizeAutomodRule(data);
|
|
1614
|
+
if (normalized !== null) {
|
|
1615
|
+
return normalized;
|
|
1616
|
+
}
|
|
1617
|
+
throw new Error('Invalid automod rule payload');
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
return await this.handleError(error);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
async createAutomodRule(circleId, input) {
|
|
1624
|
+
try {
|
|
1625
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/automod-rules`, this.sanitizeInput(input));
|
|
1626
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1627
|
+
const normalized = normalizeAutomodRule(data);
|
|
1628
|
+
if (normalized !== null) {
|
|
1629
|
+
this.emitNotification({
|
|
1630
|
+
description: 'Automoderation rule created.',
|
|
1631
|
+
color: 'success',
|
|
1632
|
+
});
|
|
1633
|
+
return normalized;
|
|
1634
|
+
}
|
|
1635
|
+
throw new Error('Invalid automod rule payload');
|
|
1636
|
+
}
|
|
1637
|
+
catch (error) {
|
|
1638
|
+
return await this.handleError(error);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
async updateAutomodRule(circleId, ruleId, input) {
|
|
1642
|
+
try {
|
|
1643
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/automod-rules/${ruleId}`, this.sanitizeInput(input));
|
|
1644
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1645
|
+
const normalized = normalizeAutomodRule(data);
|
|
1646
|
+
if (normalized !== null) {
|
|
1647
|
+
this.emitNotification({
|
|
1648
|
+
description: 'Automoderation rule updated.',
|
|
1649
|
+
color: 'success',
|
|
1650
|
+
});
|
|
1651
|
+
return normalized;
|
|
1652
|
+
}
|
|
1653
|
+
throw new Error('Invalid automod rule payload');
|
|
1654
|
+
}
|
|
1655
|
+
catch (error) {
|
|
1656
|
+
return await this.handleError(error);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async deleteAutomodRule(circleId, ruleId) {
|
|
1660
|
+
try {
|
|
1661
|
+
await this.client.delete(`${this.baseURL}/${circleId}/automod-rules/${ruleId}`);
|
|
1662
|
+
this.emitNotification({
|
|
1663
|
+
description: 'Automoderation rule deleted.',
|
|
1664
|
+
color: 'success',
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
return await this.handleError(error);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async getManagementBootstrap(circleId) {
|
|
1672
|
+
const identifier = this.sanitizeInput(circleId);
|
|
1673
|
+
let resolvedCircle = null;
|
|
1674
|
+
const bootstrapIdentifier = this.isUuid(identifier)
|
|
1675
|
+
? identifier
|
|
1676
|
+
: await (async () => {
|
|
1677
|
+
resolvedCircle = await this.get(identifier);
|
|
1678
|
+
return resolvedCircle.id;
|
|
1679
|
+
})();
|
|
1680
|
+
try {
|
|
1681
|
+
const response = await this.client.get(`${this.baseURL}/${bootstrapIdentifier}/management/bootstrap`);
|
|
1682
|
+
const data = readRecord(response.data, 'data');
|
|
1683
|
+
const bootstrapCircle = data?.circle;
|
|
1684
|
+
const bootstrapManagementSections = normalizeManagementSections(data?.management_sections);
|
|
1685
|
+
const directCircle = normalizeCircle(bootstrapCircle ?? data);
|
|
1686
|
+
const mergedCircleSource = directCircle === null
|
|
1687
|
+
? {
|
|
1688
|
+
...(resolvedCircle ?? await this.get(bootstrapIdentifier)),
|
|
1689
|
+
...(bootstrapCircle ?? {}),
|
|
1690
|
+
actor_capabilities: data?.actor_capabilities,
|
|
1691
|
+
management_sections: bootstrapManagementSections.length > 0
|
|
1692
|
+
? bootstrapManagementSections
|
|
1693
|
+
: undefined,
|
|
1694
|
+
}
|
|
1695
|
+
: {
|
|
1696
|
+
...directCircle,
|
|
1697
|
+
actor_capabilities: data?.actor_capabilities ?? directCircle.actor_capabilities,
|
|
1698
|
+
management_sections: bootstrapManagementSections.length > 0
|
|
1699
|
+
? bootstrapManagementSections
|
|
1700
|
+
: directCircle.management_sections,
|
|
1701
|
+
};
|
|
1702
|
+
const circle = normalizeCircle(mergedCircleSource);
|
|
1703
|
+
if (circle !== null) {
|
|
1704
|
+
const actor = circle.actor ?? buildActorContext(circle);
|
|
1705
|
+
return {
|
|
1706
|
+
circle,
|
|
1707
|
+
actor,
|
|
1708
|
+
management_sections: bootstrapManagementSections.length > 0
|
|
1709
|
+
? bootstrapManagementSections
|
|
1710
|
+
: actor.managementSections,
|
|
1711
|
+
members: normalizeCollectionResponse(data?.members ?? {}, normalizeCircleMember),
|
|
1712
|
+
requests: normalizeCollectionResponse(data?.requests ?? {}, normalizeJoinRequest),
|
|
1713
|
+
roles: Array.isArray(data?.roles)
|
|
1714
|
+
? normalizeInlineCollection(data.roles, normalizeRoleDefinition)
|
|
1715
|
+
: normalizeCollectionResponse(data?.roles ?? {}, normalizeRoleDefinition),
|
|
1716
|
+
counts: normalizeManagementCounts(data?.counts),
|
|
1717
|
+
permission_catalog: Array.isArray(data?.permission_catalog)
|
|
1718
|
+
? data.permission_catalog
|
|
1719
|
+
.map((entry) => normalizePermissionCatalogEntry(entry))
|
|
1720
|
+
.filter((entry) => entry !== null)
|
|
1721
|
+
: [],
|
|
1722
|
+
reports: normalizeCollectionResponse(data?.reports ?? {}, normalizeModerationReport),
|
|
1723
|
+
bans: normalizeCollectionResponse(data?.bans ?? {}, normalizeBanRecord),
|
|
1724
|
+
mutes: normalizeCollectionResponse(data?.mutes ?? {}, normalizeMuteRecord),
|
|
1725
|
+
audit: normalizeCollectionResponse(data?.audit ?? {}, normalizeAuditLogEntry),
|
|
1726
|
+
automod: normalizeCollectionResponse(data?.automod ?? {}, normalizeAutomodRule),
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
catch (error) {
|
|
1731
|
+
if (!this.isHttpStatus(error, 404)) {
|
|
1732
|
+
return await this.handleError(error);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const circle = resolvedCircle ?? await this.get(identifier);
|
|
1736
|
+
const actor = circle.actor ?? buildActorContext(circle);
|
|
1737
|
+
if (!actor.capabilities.canViewManagement) {
|
|
1738
|
+
return {
|
|
1739
|
+
circle,
|
|
1740
|
+
actor,
|
|
1741
|
+
management_sections: actor.managementSections,
|
|
1742
|
+
members: createEmptyPagination(),
|
|
1743
|
+
requests: createEmptyPagination(),
|
|
1744
|
+
roles: createEmptyPagination(),
|
|
1745
|
+
counts: createEmptyManagementCounts(),
|
|
1746
|
+
permission_catalog: [],
|
|
1747
|
+
reports: createEmptyPagination(),
|
|
1748
|
+
bans: createEmptyPagination(),
|
|
1749
|
+
mutes: createEmptyPagination(),
|
|
1750
|
+
audit: createEmptyPagination(),
|
|
1751
|
+
automod: createEmptyPagination(),
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
const requestsPromise = actor.capabilities.canReviewRequests
|
|
1755
|
+
? this.listJoinRequests(circle.id, null, 'pending')
|
|
1756
|
+
: Promise.resolve(createEmptyPagination());
|
|
1757
|
+
const [members, requests, roles, reports, bans, mutes, audit, automod,] = await Promise.all([
|
|
1758
|
+
this.getMembers(circle.id),
|
|
1759
|
+
requestsPromise,
|
|
1760
|
+
this.listRoles(circle.id),
|
|
1761
|
+
this.listModerationReports(circle.id, null, 'pending'),
|
|
1762
|
+
this.listBans(circle.id),
|
|
1763
|
+
this.listMutes(circle.id),
|
|
1764
|
+
this.getModerationAuditLog(circle.id),
|
|
1765
|
+
this.listAutomodRules(circle.id),
|
|
1766
|
+
]);
|
|
1767
|
+
return {
|
|
1768
|
+
circle,
|
|
1769
|
+
actor,
|
|
1770
|
+
management_sections: actor.managementSections,
|
|
1771
|
+
members,
|
|
1772
|
+
requests,
|
|
1773
|
+
roles,
|
|
1774
|
+
counts: {
|
|
1775
|
+
members: circle.member_count ?? members.data.length,
|
|
1776
|
+
requests_pending: requests.data.length,
|
|
1777
|
+
reports_pending: reports.data.length,
|
|
1778
|
+
roles: roles.data.length,
|
|
1779
|
+
bans_active: bans.data.length,
|
|
1780
|
+
mutes_active: mutes.data.length,
|
|
1781
|
+
},
|
|
1782
|
+
permission_catalog: [],
|
|
1783
|
+
reports,
|
|
1784
|
+
bans,
|
|
1785
|
+
mutes,
|
|
1786
|
+
audit,
|
|
1787
|
+
automod,
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
async searchInviteCandidates(circleId, query) {
|
|
1791
|
+
void circleId;
|
|
1792
|
+
try {
|
|
1793
|
+
const sanitizedQuery = query.trim();
|
|
1794
|
+
if (sanitizedQuery.length < 2) {
|
|
1795
|
+
return [];
|
|
1796
|
+
}
|
|
1797
|
+
const response = await this.client.get('/v1/search/aggregate', {
|
|
1798
|
+
params: {
|
|
1799
|
+
q: sanitizedQuery,
|
|
1800
|
+
'types[]': 'user',
|
|
1801
|
+
limit: 10,
|
|
1802
|
+
},
|
|
1803
|
+
});
|
|
1804
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1805
|
+
const results = readRecord(data, 'results');
|
|
1806
|
+
const byType = readRecord(results, 'by_type');
|
|
1807
|
+
const users = Array.isArray(byType?.user) ? byType.user : [];
|
|
1808
|
+
return users
|
|
1809
|
+
.map((entry) => {
|
|
1810
|
+
if (!isRecord(entry)) {
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
const id = readString(entry.id);
|
|
1814
|
+
const name = readString(entry.title);
|
|
1815
|
+
const meta = readRecord(entry, 'meta');
|
|
1816
|
+
const handle = readString(meta?.handle);
|
|
1817
|
+
if (id === null || name === null || handle === null) {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
id,
|
|
1822
|
+
name,
|
|
1823
|
+
handle,
|
|
1824
|
+
avatar_url: readNullableString(meta?.avatar_url),
|
|
1825
|
+
};
|
|
1826
|
+
})
|
|
1827
|
+
.filter((entry) => entry !== null);
|
|
1828
|
+
}
|
|
1829
|
+
catch (error) {
|
|
1830
|
+
return await this.handleError(error);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
async createInviteLink(circleId, options, requestOptions) {
|
|
1834
|
+
try {
|
|
1835
|
+
const body = {};
|
|
1836
|
+
if (typeof options?.max_uses === 'number') {
|
|
1837
|
+
body.max_uses = options.max_uses;
|
|
1838
|
+
}
|
|
1839
|
+
if (typeof options?.expires_in_minutes === 'number') {
|
|
1840
|
+
body.expires_in_minutes = options.expires_in_minutes;
|
|
1841
|
+
}
|
|
1842
|
+
if (typeof options?.target_user_id === 'string' && options.target_user_id.trim() !== '') {
|
|
1843
|
+
body.target_user_id = options.target_user_id;
|
|
1844
|
+
}
|
|
1845
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/invite-links`, body);
|
|
1846
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null);
|
|
1847
|
+
const normalized = normalizeInviteLink(data);
|
|
1848
|
+
if (normalized !== null) {
|
|
1849
|
+
if (requestOptions?.showSuccessToast !== false) {
|
|
1850
|
+
this.emitNotification({
|
|
1851
|
+
description: `Invite link created for ${this.getCircleTerm()}!`,
|
|
1852
|
+
color: 'success',
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return normalized;
|
|
1856
|
+
}
|
|
1857
|
+
throw new Error('Invalid invite link payload');
|
|
1858
|
+
}
|
|
1859
|
+
catch (error) {
|
|
1860
|
+
return await this.handleError(error);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async joinViaInvite(token) {
|
|
1864
|
+
try {
|
|
1865
|
+
const response = await this.client.post(`/v1/circle-invites/${encodeURIComponent(token)}/join`);
|
|
1866
|
+
const data = readRecord(response.data, 'data') ?? {};
|
|
1867
|
+
const circleId = readString(data.circle_id);
|
|
1868
|
+
const requested = readBoolean(data.requested);
|
|
1869
|
+
if (circleId === null || requested === null) {
|
|
1870
|
+
throw new Error('Invalid invite accept payload');
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
circle_id: circleId,
|
|
1874
|
+
requested,
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
catch (error) {
|
|
1878
|
+
return await this.handleError(error);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
async declineInvite(token) {
|
|
1882
|
+
try {
|
|
1883
|
+
await this.client.post(`/v1/circle-invites/${encodeURIComponent(token)}/decline`);
|
|
1884
|
+
}
|
|
1885
|
+
catch (error) {
|
|
1886
|
+
return await this.handleError(error);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
async checkSlugAvailability(slug) {
|
|
1890
|
+
try {
|
|
1891
|
+
const sanitizedSlug = this.sanitizeInput(slug);
|
|
1892
|
+
const response = await this.client.get(`${this.baseURL}/check-slug`, {
|
|
1893
|
+
params: { slug: sanitizedSlug },
|
|
1894
|
+
});
|
|
1895
|
+
const data = readRecord(response.data, 'data');
|
|
1896
|
+
const payloadAvailability = readBoolean(data?.available);
|
|
1897
|
+
const rootAvailability = readBoolean(isRecord(response.data) ? response.data.available : undefined);
|
|
1898
|
+
return payloadAvailability ?? rootAvailability ?? true;
|
|
1899
|
+
}
|
|
1900
|
+
catch (error) {
|
|
1901
|
+
if (this.isHttpStatus(error, 404)) {
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
return await this.handleError(error);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
generateSlug(name) {
|
|
1908
|
+
if (name.trim().length === 0) {
|
|
1909
|
+
return '';
|
|
1910
|
+
}
|
|
1911
|
+
return this.sanitizeInput(name)
|
|
1912
|
+
.toLowerCase()
|
|
1913
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
1914
|
+
.replace(/\s+/g, '-')
|
|
1915
|
+
.replace(/-+/g, '-')
|
|
1916
|
+
.replace(/^-|-$/g, '')
|
|
1917
|
+
.substring(0, 140);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
export function createCirclesService(config) {
|
|
1921
|
+
const { client, sanitizeString = defaultSanitizeString, onNotify, onUnauthorized, getCircleTerm = defaultGetCircleTerm, onError, } = config;
|
|
1922
|
+
return new CirclesService(client, sanitizeString, onNotify, onUnauthorized, getCircleTerm, onError);
|
|
1923
|
+
}
|
|
1924
|
+
//# sourceMappingURL=circles.js.map
|