@codingfactory/socialkit-vue 0.2.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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/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/package.json +1 -1
- package/src/index.ts +74 -0
- package/src/services/circles.ts +2767 -0
- package/src/stores/circles.ts +2114 -0
|
@@ -0,0 +1,2767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable circles service for SocialKit-powered frontends.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AxiosInstance } from 'axios'
|
|
6
|
+
|
|
7
|
+
export const CIRCLE_MANAGEMENT_SECTIONS = [
|
|
8
|
+
'overview',
|
|
9
|
+
'members',
|
|
10
|
+
'requests',
|
|
11
|
+
'content',
|
|
12
|
+
'discussions',
|
|
13
|
+
'events',
|
|
14
|
+
'roles',
|
|
15
|
+
'reports',
|
|
16
|
+
'bans',
|
|
17
|
+
'mutes',
|
|
18
|
+
'audit',
|
|
19
|
+
'automod',
|
|
20
|
+
'settings',
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
export type CircleManagementSection = typeof CIRCLE_MANAGEMENT_SECTIONS[number]
|
|
24
|
+
export type CircleVisibility = 'public' | 'closed' | 'secret'
|
|
25
|
+
export type CircleRole = 'owner' | 'admin' | 'moderator' | 'member'
|
|
26
|
+
export type CircleActorRole = CircleRole | null
|
|
27
|
+
export type CircleMembershipRole = 'owner' | 'member'
|
|
28
|
+
export type CircleJoinRequestStatus = 'pending' | 'approved' | 'rejected'
|
|
29
|
+
export type CircleReportStatus = 'pending' | 'actioned' | 'dismissed'
|
|
30
|
+
export type CircleReportDecision = 'action' | 'dismiss'
|
|
31
|
+
export type CircleModerationReportSource = 'member' | 'automod' | (string & {})
|
|
32
|
+
export type CircleModerationReportSubjectType = 'user' | CircleModerationSubjectType | (string & {})
|
|
33
|
+
export type CircleModerationSubjectType = 'post' | 'comment' | 'thread' | 'reply' | 'event'
|
|
34
|
+
export type CircleModerationSubjectState = 'active' | 'removed' | 'locked'
|
|
35
|
+
export type CircleModerationSubjectAction = 'remove' | 'restore' | 'lock' | 'unlock'
|
|
36
|
+
export type CirclePermissionKey =
|
|
37
|
+
| 'group.details.edit'
|
|
38
|
+
| 'group.settings.manage'
|
|
39
|
+
| 'group.visibility.manage'
|
|
40
|
+
| 'group.rules.manage'
|
|
41
|
+
| 'group.roles.manage'
|
|
42
|
+
| 'group.roles.assign'
|
|
43
|
+
| 'group.members.invite'
|
|
44
|
+
| 'group.members.remove'
|
|
45
|
+
| 'group.members.mute'
|
|
46
|
+
| 'group.members.unmute'
|
|
47
|
+
| 'group.members.ban'
|
|
48
|
+
| 'group.members.unban'
|
|
49
|
+
| 'group.requests.review'
|
|
50
|
+
| 'group.posts.review'
|
|
51
|
+
| 'group.posts.remove'
|
|
52
|
+
| 'group.posts.restore'
|
|
53
|
+
| 'group.comments.review'
|
|
54
|
+
| 'group.comments.remove'
|
|
55
|
+
| 'group.comments.restore'
|
|
56
|
+
| 'group.discussions.review'
|
|
57
|
+
| 'group.discussions.lock'
|
|
58
|
+
| 'group.discussions.unlock'
|
|
59
|
+
| 'group.discussions.remove'
|
|
60
|
+
| 'group.discussions.restore'
|
|
61
|
+
| 'group.events.manage'
|
|
62
|
+
| 'group.events.remove'
|
|
63
|
+
| 'group.events.restore'
|
|
64
|
+
| 'group.reports.review'
|
|
65
|
+
| 'group.audit.view'
|
|
66
|
+
| 'group.automod.manage'
|
|
67
|
+
| 'group.owner.transfer'
|
|
68
|
+
| (string & {})
|
|
69
|
+
export type CircleAutomodConditionType =
|
|
70
|
+
| 'keyword_match'
|
|
71
|
+
| 'regex'
|
|
72
|
+
| 'account_age'
|
|
73
|
+
| 'community_karma'
|
|
74
|
+
| 'link_domains'
|
|
75
|
+
| string
|
|
76
|
+
export type CircleAutomodActionType = 'remove' | 'flag' | 'approve' | 'add_flair' | string
|
|
77
|
+
|
|
78
|
+
type UnknownRecord = Record<string, unknown>
|
|
79
|
+
|
|
80
|
+
export interface CircleUserSummary {
|
|
81
|
+
id: string
|
|
82
|
+
name: string
|
|
83
|
+
handle: string
|
|
84
|
+
avatar: string | null
|
|
85
|
+
avatar_url: string | null
|
|
86
|
+
bio?: string | null
|
|
87
|
+
is_following?: boolean
|
|
88
|
+
is_followed_by?: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CircleActorCapabilities {
|
|
92
|
+
is_owner: boolean
|
|
93
|
+
is_moderator: boolean
|
|
94
|
+
moderation_level: CircleRole
|
|
95
|
+
permissions: CirclePermissionKey[]
|
|
96
|
+
can: Record<string, boolean>
|
|
97
|
+
canJoin: boolean
|
|
98
|
+
canLeave: boolean
|
|
99
|
+
canRequestToJoin: boolean
|
|
100
|
+
canInvite: boolean
|
|
101
|
+
canManage: boolean
|
|
102
|
+
canManageMembers: boolean
|
|
103
|
+
canManageRoles: boolean
|
|
104
|
+
canReviewRequests: boolean
|
|
105
|
+
canManageReports: boolean
|
|
106
|
+
canManageBans: boolean
|
|
107
|
+
canManageMutes: boolean
|
|
108
|
+
canViewAuditLog: boolean
|
|
109
|
+
canManageAutomod: boolean
|
|
110
|
+
canManageSettings: boolean
|
|
111
|
+
canTransferOwnership: boolean
|
|
112
|
+
canViewManagement: boolean
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CircleActorContext {
|
|
116
|
+
isMember: boolean
|
|
117
|
+
role: CircleActorRole
|
|
118
|
+
hasPendingRequest: boolean
|
|
119
|
+
capabilities: CircleActorCapabilities
|
|
120
|
+
managementSections: CircleManagementSection[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface Circle {
|
|
124
|
+
id: string
|
|
125
|
+
name: string
|
|
126
|
+
slug: string
|
|
127
|
+
description?: string | null
|
|
128
|
+
visibility: CircleVisibility
|
|
129
|
+
is_hidden: boolean
|
|
130
|
+
owner_id?: string | null
|
|
131
|
+
meta: Record<string, unknown>
|
|
132
|
+
cover_photo_url?: string | null
|
|
133
|
+
cover_photo_media_id?: string | null
|
|
134
|
+
created_at: string
|
|
135
|
+
updated_at: string
|
|
136
|
+
is_member?: boolean
|
|
137
|
+
user_role?: CircleActorRole
|
|
138
|
+
member_count?: number
|
|
139
|
+
can_join?: boolean
|
|
140
|
+
can_manage?: boolean
|
|
141
|
+
can_manage_members?: boolean
|
|
142
|
+
can_invite?: boolean
|
|
143
|
+
can_leave?: boolean
|
|
144
|
+
can_request_to_join?: boolean
|
|
145
|
+
has_pending_request?: boolean
|
|
146
|
+
actor_capabilities?: Partial<CircleActorCapabilities>
|
|
147
|
+
management_sections?: CircleManagementSection[]
|
|
148
|
+
group_rules_summary?: string | null
|
|
149
|
+
welcome_message?: string | null
|
|
150
|
+
canonical_path?: string | null
|
|
151
|
+
actor?: CircleActorContext
|
|
152
|
+
creator?: CircleUserSummary
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface CircleProfileOverrides {
|
|
156
|
+
name?: string
|
|
157
|
+
bio?: string
|
|
158
|
+
avatar_url?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface CircleMember {
|
|
162
|
+
id: string
|
|
163
|
+
user_id: string
|
|
164
|
+
circle_id: string
|
|
165
|
+
role: CircleRole
|
|
166
|
+
joined_at: string
|
|
167
|
+
membership_role?: CircleMembershipRole
|
|
168
|
+
display_role?: CircleRole
|
|
169
|
+
assigned_roles?: CircleRoleDefinition[]
|
|
170
|
+
effective_permissions?: CirclePermissionKey[]
|
|
171
|
+
is_muted?: boolean
|
|
172
|
+
muted_until?: string | null
|
|
173
|
+
is_banned?: boolean
|
|
174
|
+
can_remove?: boolean
|
|
175
|
+
can_change_role?: boolean
|
|
176
|
+
can_assign_roles?: boolean
|
|
177
|
+
can_mute?: boolean
|
|
178
|
+
can_unmute?: boolean
|
|
179
|
+
can_ban?: boolean
|
|
180
|
+
can_unban?: boolean
|
|
181
|
+
can_view_audit?: boolean
|
|
182
|
+
profile_overrides?: CircleProfileOverrides
|
|
183
|
+
user?: CircleUserSummary
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface JoinRequest {
|
|
187
|
+
id: string
|
|
188
|
+
user_id: string
|
|
189
|
+
circle_id: string
|
|
190
|
+
status: CircleJoinRequestStatus
|
|
191
|
+
source: 'direct' | 'invite'
|
|
192
|
+
message?: string | null
|
|
193
|
+
reviewed_by_id?: string | null
|
|
194
|
+
reviewed_at?: string | null
|
|
195
|
+
created_at: string
|
|
196
|
+
updated_at: string
|
|
197
|
+
user?: CircleUserSummary
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface InviteLink {
|
|
201
|
+
id: string
|
|
202
|
+
token: string
|
|
203
|
+
circle_id: string
|
|
204
|
+
target_user_id: string | null
|
|
205
|
+
max_uses: number | null
|
|
206
|
+
uses_count: number
|
|
207
|
+
expires_at: string | null
|
|
208
|
+
invite_url: string
|
|
209
|
+
qr_svg: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface CircleInviteSearchUser {
|
|
213
|
+
id: string
|
|
214
|
+
name: string
|
|
215
|
+
handle: string
|
|
216
|
+
avatar_url: string | null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface CircleRoleDefinition {
|
|
220
|
+
id: string
|
|
221
|
+
slug: string
|
|
222
|
+
name: string
|
|
223
|
+
description: string | null
|
|
224
|
+
color: string | null
|
|
225
|
+
is_system: boolean
|
|
226
|
+
is_assignable: boolean
|
|
227
|
+
member_count: number
|
|
228
|
+
permissions: CirclePermissionKey[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @deprecated Use CircleRoleDefinition. Kept only as a temporary compatibility export.
|
|
233
|
+
*/
|
|
234
|
+
export interface CircleRoleAssignment extends CircleRoleDefinition {
|
|
235
|
+
id: string
|
|
236
|
+
user_id?: string
|
|
237
|
+
role?: CircleRole
|
|
238
|
+
assigned_at?: string | null
|
|
239
|
+
user?: CircleUserSummary
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface CirclePermissionCatalogEntry {
|
|
243
|
+
key: CirclePermissionKey
|
|
244
|
+
label: string
|
|
245
|
+
description: string
|
|
246
|
+
module: string
|
|
247
|
+
owner_only: boolean
|
|
248
|
+
surfaced: boolean
|
|
249
|
+
section: CircleManagementSection
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface CircleManagementCounts {
|
|
253
|
+
members: number
|
|
254
|
+
requests_pending: number
|
|
255
|
+
reports_pending: number
|
|
256
|
+
roles: number
|
|
257
|
+
bans_active: number
|
|
258
|
+
mutes_active: number
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface CircleMemberRoleAssignmentResult {
|
|
262
|
+
assigned_roles: CircleRoleDefinition[]
|
|
263
|
+
effective_permissions: CirclePermissionKey[]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface CircleOwnershipTransferResult {
|
|
267
|
+
circle_id: string
|
|
268
|
+
previous_owner_id: string
|
|
269
|
+
new_owner_id: string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface CircleModerationReport {
|
|
273
|
+
id: string
|
|
274
|
+
reporter_id: string | null
|
|
275
|
+
source: CircleModerationReportSource
|
|
276
|
+
subject_user_id: string | null
|
|
277
|
+
subject_type: CircleModerationReportSubjectType | null
|
|
278
|
+
subject_id: string | null
|
|
279
|
+
subject_author_id: string | null
|
|
280
|
+
category: string
|
|
281
|
+
notes?: string | null
|
|
282
|
+
status: CircleReportStatus
|
|
283
|
+
priority: string
|
|
284
|
+
resolution_action?: string | null
|
|
285
|
+
created_at: string
|
|
286
|
+
reviewed_at?: string | null
|
|
287
|
+
resolution_notes?: string | null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface CircleBanRecord {
|
|
291
|
+
id: string
|
|
292
|
+
user_id: string
|
|
293
|
+
reason?: string | null
|
|
294
|
+
created_at: string
|
|
295
|
+
updated_at?: string | null
|
|
296
|
+
created_by_id?: string | null
|
|
297
|
+
user?: CircleUserSummary
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface CircleMuteRecord {
|
|
301
|
+
id: string
|
|
302
|
+
user_id: string
|
|
303
|
+
reason?: string | null
|
|
304
|
+
muted_until?: string | null
|
|
305
|
+
created_at: string
|
|
306
|
+
updated_at?: string | null
|
|
307
|
+
created_by_id?: string | null
|
|
308
|
+
user?: CircleUserSummary
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface CircleModerationAuditLogEntry {
|
|
312
|
+
id: string
|
|
313
|
+
action: string
|
|
314
|
+
actor_id: string
|
|
315
|
+
target_user_id: string | null
|
|
316
|
+
report_id: string | null
|
|
317
|
+
meta: Record<string, unknown>
|
|
318
|
+
created_at: string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface CircleModerationSubject {
|
|
322
|
+
subject_type: CircleModerationSubjectType
|
|
323
|
+
subject_id: string
|
|
324
|
+
circle_id: string
|
|
325
|
+
author_id: string | null
|
|
326
|
+
report_count: number
|
|
327
|
+
latest_report_status: CircleReportStatus | null
|
|
328
|
+
moderation_state: CircleModerationSubjectState
|
|
329
|
+
created_at: string
|
|
330
|
+
updated_at: string
|
|
331
|
+
preview: Record<string, unknown>
|
|
332
|
+
available_actions: CircleModerationSubjectAction[]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface CircleAutomodCondition {
|
|
336
|
+
type: CircleAutomodConditionType
|
|
337
|
+
config: Record<string, unknown>
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface CircleAutomodAction {
|
|
341
|
+
type: CircleAutomodActionType
|
|
342
|
+
config: Record<string, unknown>
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface CircleAutomodRule {
|
|
346
|
+
id: string
|
|
347
|
+
name: string
|
|
348
|
+
description?: string | null
|
|
349
|
+
is_enabled: boolean
|
|
350
|
+
conditions: CircleAutomodCondition[]
|
|
351
|
+
actions: CircleAutomodAction[]
|
|
352
|
+
priority: number
|
|
353
|
+
created_at: string
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface CircleManagementBootstrap {
|
|
357
|
+
circle: Circle
|
|
358
|
+
actor: CircleActorContext
|
|
359
|
+
management_sections: CircleManagementSection[]
|
|
360
|
+
members: PaginationResponse<CircleMember>
|
|
361
|
+
requests: PaginationResponse<JoinRequest>
|
|
362
|
+
roles: PaginationResponse<CircleRoleDefinition>
|
|
363
|
+
counts: CircleManagementCounts
|
|
364
|
+
permission_catalog: CirclePermissionCatalogEntry[]
|
|
365
|
+
reports: PaginationResponse<CircleModerationReport>
|
|
366
|
+
bans: PaginationResponse<CircleBanRecord>
|
|
367
|
+
mutes: PaginationResponse<CircleMuteRecord>
|
|
368
|
+
audit: PaginationResponse<CircleModerationAuditLogEntry>
|
|
369
|
+
automod: PaginationResponse<CircleAutomodRule>
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface CircleFilters {
|
|
373
|
+
visibility?: CircleVisibility
|
|
374
|
+
membership?: 'joined' | 'managed' | 'available'
|
|
375
|
+
search?: string
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface PaginationResponse<T> {
|
|
379
|
+
data: T[]
|
|
380
|
+
meta: {
|
|
381
|
+
pagination: {
|
|
382
|
+
next_cursor: string | null
|
|
383
|
+
has_more: boolean
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface CircleAutomodRuleInput {
|
|
389
|
+
name: string
|
|
390
|
+
description?: string | null
|
|
391
|
+
is_enabled?: boolean
|
|
392
|
+
conditions: CircleAutomodCondition[]
|
|
393
|
+
actions: CircleAutomodAction[]
|
|
394
|
+
priority?: number
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface CircleCreateInput {
|
|
398
|
+
name: string
|
|
399
|
+
slug?: string
|
|
400
|
+
description?: string
|
|
401
|
+
visibility: CircleVisibility
|
|
402
|
+
is_hidden?: boolean
|
|
403
|
+
meta?: Record<string, unknown>
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export interface CircleUpdateInput {
|
|
407
|
+
name?: string
|
|
408
|
+
slug?: string
|
|
409
|
+
description?: string | null
|
|
410
|
+
visibility?: CircleVisibility
|
|
411
|
+
is_hidden?: boolean
|
|
412
|
+
meta?: Record<string, unknown>
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface CircleRoleDefinitionInput {
|
|
416
|
+
name: string
|
|
417
|
+
description?: string | null
|
|
418
|
+
color?: string | null
|
|
419
|
+
clone_from_role_id?: string | null
|
|
420
|
+
permissions?: CirclePermissionKey[]
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface CircleRoleDefinitionUpdateInput {
|
|
424
|
+
name?: string
|
|
425
|
+
description?: string | null
|
|
426
|
+
color?: string | null
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export interface CircleOwnershipTransferOptions {
|
|
430
|
+
note?: string | null
|
|
431
|
+
assign_previous_owner_admin?: boolean
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export interface CircleBanMemberOptions {
|
|
435
|
+
reason?: string
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface CircleMuteMemberOptions {
|
|
439
|
+
reason?: string
|
|
440
|
+
muted_until?: string
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export interface CircleInviteLinkOptions {
|
|
444
|
+
max_uses?: number
|
|
445
|
+
expires_in_minutes?: number
|
|
446
|
+
target_user_id?: string
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export interface CircleInviteLinkRequestOptions {
|
|
450
|
+
showSuccessToast?: boolean
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export interface CirclesNotification {
|
|
454
|
+
description: string
|
|
455
|
+
color: 'success' | 'error' | 'warning'
|
|
456
|
+
duration?: number
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface CircleTermOptions {
|
|
460
|
+
case?: 'title'
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export interface CirclesServiceErrorContext {
|
|
464
|
+
error: unknown
|
|
465
|
+
url?: string
|
|
466
|
+
status?: number
|
|
467
|
+
timestamp: string
|
|
468
|
+
logLevel: 'warn' | 'error'
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export interface CirclesServiceConfig {
|
|
472
|
+
client: AxiosInstance
|
|
473
|
+
sanitizeString?: (value: string) => string
|
|
474
|
+
onNotify?: (notification: CirclesNotification) => void
|
|
475
|
+
onUnauthorized?: () => void
|
|
476
|
+
getCircleTerm?: (options?: CircleTermOptions) => string
|
|
477
|
+
onError?: (context: CirclesServiceErrorContext) => void
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
type SanitizedInput<T> = T extends string
|
|
481
|
+
? string
|
|
482
|
+
: T extends Array<infer U>
|
|
483
|
+
? SanitizedInput<U>[]
|
|
484
|
+
: T extends Record<string, unknown>
|
|
485
|
+
? { [K in keyof T]: SanitizedInput<T[K]> }
|
|
486
|
+
: T
|
|
487
|
+
|
|
488
|
+
interface ApiError {
|
|
489
|
+
config?: { url?: string }
|
|
490
|
+
response?: { status?: number; data?: { message?: string } }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const DEFAULT_LIST_LIMIT = '10'
|
|
494
|
+
|
|
495
|
+
function isApiError(value: unknown): value is ApiError {
|
|
496
|
+
return typeof value === 'object' && value !== null
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
500
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function readRecord(value: unknown, key: string): UnknownRecord | null {
|
|
504
|
+
if (!isRecord(value)) {
|
|
505
|
+
return null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const entry = value[key]
|
|
509
|
+
return isRecord(entry) ? entry : null
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readArray(value: unknown, key: string): unknown[] | null {
|
|
513
|
+
if (!isRecord(value)) {
|
|
514
|
+
return null
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const entry = value[key]
|
|
518
|
+
return Array.isArray(entry) ? entry : null
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function readString(value: unknown): string | null {
|
|
522
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function readNullableString(value: unknown): string | null {
|
|
526
|
+
if (value === null || value === undefined) {
|
|
527
|
+
return null
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return readString(value)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function readBoolean(value: unknown): boolean | null {
|
|
534
|
+
return typeof value === 'boolean' ? value : null
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function readNumber(value: unknown): number | null {
|
|
538
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isCircleVisibility(value: unknown): value is CircleVisibility {
|
|
542
|
+
return value === 'public' || value === 'closed' || value === 'secret'
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isCircleRole(value: unknown): value is CircleRole {
|
|
546
|
+
return value === 'owner' || value === 'admin' || value === 'moderator' || value === 'member'
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isCircleMembershipRole(value: unknown): value is CircleMembershipRole {
|
|
550
|
+
return value === 'owner' || value === 'member'
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isCircleModerationReportSource(value: unknown): value is CircleModerationReportSource {
|
|
554
|
+
return value === 'member' || value === 'automod' || typeof value === 'string'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isCircleModerationReportSubjectType(value: unknown): value is CircleModerationReportSubjectType {
|
|
558
|
+
return value === 'user' || value === 'post' || value === 'comment' || value === 'thread' || value === 'reply' || value === 'event' || typeof value === 'string'
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isManagementSection(value: unknown): value is CircleManagementSection {
|
|
562
|
+
return typeof value === 'string' && CIRCLE_MANAGEMENT_SECTIONS.includes(value as CircleManagementSection)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function createEmptyPagination<T>(): PaginationResponse<T> {
|
|
566
|
+
return {
|
|
567
|
+
data: [],
|
|
568
|
+
meta: {
|
|
569
|
+
pagination: {
|
|
570
|
+
next_cursor: null,
|
|
571
|
+
has_more: false,
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function createEmptyManagementCounts(): CircleManagementCounts {
|
|
578
|
+
return {
|
|
579
|
+
members: 0,
|
|
580
|
+
requests_pending: 0,
|
|
581
|
+
reports_pending: 0,
|
|
582
|
+
roles: 0,
|
|
583
|
+
bans_active: 0,
|
|
584
|
+
mutes_active: 0,
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function normalizeManagementSections(value: unknown): CircleManagementSection[] {
|
|
589
|
+
if (!Array.isArray(value)) {
|
|
590
|
+
return []
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return value.filter((section): section is CircleManagementSection => isManagementSection(section))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function normalizePermissionKeys(value: unknown): CirclePermissionKey[] {
|
|
597
|
+
if (!Array.isArray(value)) {
|
|
598
|
+
return []
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return value
|
|
602
|
+
.map((entry) => readString(entry))
|
|
603
|
+
.filter((entry): entry is CirclePermissionKey => entry !== null)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function normalizePermissionMap(value: unknown): Record<string, boolean> {
|
|
607
|
+
if (!isRecord(value)) {
|
|
608
|
+
return {}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const normalized: Record<string, boolean> = {}
|
|
612
|
+
|
|
613
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
614
|
+
const flag = readBoolean(entry)
|
|
615
|
+
if (flag !== null) {
|
|
616
|
+
normalized[key] = flag
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return normalized
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function normalizeManagementCounts(value: unknown): CircleManagementCounts {
|
|
624
|
+
if (!isRecord(value)) {
|
|
625
|
+
return createEmptyManagementCounts()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
members: readNumber(value.members) ?? 0,
|
|
630
|
+
requests_pending: readNumber(value.requests_pending) ?? 0,
|
|
631
|
+
reports_pending: readNumber(value.reports_pending) ?? 0,
|
|
632
|
+
roles: readNumber(value.roles) ?? 0,
|
|
633
|
+
bans_active: readNumber(value.bans_active) ?? 0,
|
|
634
|
+
mutes_active: readNumber(value.mutes_active) ?? 0,
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function normalizeUserSummary(value: unknown): CircleUserSummary | undefined {
|
|
639
|
+
if (!isRecord(value)) {
|
|
640
|
+
return undefined
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const id = readString(value.id)
|
|
644
|
+
const name = readString(value.name)
|
|
645
|
+
const handle = readString(value.handle)
|
|
646
|
+
|
|
647
|
+
if (id === null || name === null || handle === null) {
|
|
648
|
+
return undefined
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const avatar = readNullableString(value.avatar_url) ?? readNullableString(value.avatar)
|
|
652
|
+
const userSummary: CircleUserSummary = {
|
|
653
|
+
id,
|
|
654
|
+
name,
|
|
655
|
+
handle,
|
|
656
|
+
avatar,
|
|
657
|
+
avatar_url: avatar,
|
|
658
|
+
bio: readNullableString(value.bio),
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const isFollowing = readBoolean(value.is_following)
|
|
662
|
+
if (isFollowing !== null) {
|
|
663
|
+
userSummary.is_following = isFollowing
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const isFollowedBy = readBoolean(value.is_followed_by)
|
|
667
|
+
if (isFollowedBy !== null) {
|
|
668
|
+
userSummary.is_followed_by = isFollowedBy
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return userSummary
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function buildDefaultActorCapabilities(
|
|
675
|
+
role: CircleActorRole,
|
|
676
|
+
visibility: CircleVisibility,
|
|
677
|
+
isMember: boolean,
|
|
678
|
+
): CircleActorCapabilities {
|
|
679
|
+
const canManage = role === 'owner' || role === 'admin'
|
|
680
|
+
const canManageRoles = role === 'owner'
|
|
681
|
+
const canReviewRequests = canManage && visibility !== 'public'
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
is_owner: role === 'owner',
|
|
685
|
+
is_moderator: role === 'owner' || role === 'admin' || role === 'moderator',
|
|
686
|
+
moderation_level: role ?? 'member',
|
|
687
|
+
permissions: [],
|
|
688
|
+
can: {},
|
|
689
|
+
canJoin: !isMember && visibility === 'public',
|
|
690
|
+
canLeave: isMember && role !== 'owner',
|
|
691
|
+
canRequestToJoin: !isMember && visibility !== 'public',
|
|
692
|
+
canInvite: isMember,
|
|
693
|
+
canManage,
|
|
694
|
+
canManageMembers: canManage,
|
|
695
|
+
canManageRoles,
|
|
696
|
+
canReviewRequests,
|
|
697
|
+
canManageReports: canManage,
|
|
698
|
+
canManageBans: canManage,
|
|
699
|
+
canManageMutes: canManage,
|
|
700
|
+
canViewAuditLog: canManage,
|
|
701
|
+
canManageAutomod: canManage,
|
|
702
|
+
canManageSettings: canManage,
|
|
703
|
+
canTransferOwnership: role === 'owner',
|
|
704
|
+
canViewManagement: canManage,
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function normalizeActorCapabilities(
|
|
709
|
+
value: unknown,
|
|
710
|
+
fallback: CircleActorCapabilities,
|
|
711
|
+
): CircleActorCapabilities {
|
|
712
|
+
if (!isRecord(value)) {
|
|
713
|
+
return fallback
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const permissionMap = normalizePermissionMap(value.can)
|
|
717
|
+
const permissions = normalizePermissionKeys(value.permissions)
|
|
718
|
+
|
|
719
|
+
const hasPermission = (permissionKey: string): boolean => {
|
|
720
|
+
if (typeof permissionMap[permissionKey] === 'boolean') {
|
|
721
|
+
return permissionMap[permissionKey]
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return permissions.includes(permissionKey)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const hasAnyPermission = (permissionKeys: readonly string[]): boolean => {
|
|
728
|
+
return permissionKeys.some((permissionKey) => hasPermission(permissionKey))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const getFlag = (keys: readonly string[], defaultValue: boolean): boolean => {
|
|
732
|
+
for (const key of keys) {
|
|
733
|
+
const flag = readBoolean(value[key])
|
|
734
|
+
if (flag !== null) {
|
|
735
|
+
return flag
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return defaultValue
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const isOwner = getFlag(['isOwner', 'is_owner'], false)
|
|
743
|
+
const isModerator = getFlag(['isModerator', 'is_moderator'], false)
|
|
744
|
+
const moderationLevel = isCircleRole(value.moderation_level)
|
|
745
|
+
? value.moderation_level
|
|
746
|
+
: isOwner
|
|
747
|
+
? 'owner'
|
|
748
|
+
: isModerator
|
|
749
|
+
? fallback.moderation_level === 'admin'
|
|
750
|
+
? 'admin'
|
|
751
|
+
: 'moderator'
|
|
752
|
+
: fallback.moderation_level
|
|
753
|
+
const canManageDerived = isOwner || isModerator || hasAnyPermission([
|
|
754
|
+
'group.details.edit',
|
|
755
|
+
'group.settings.manage',
|
|
756
|
+
'group.visibility.manage',
|
|
757
|
+
'group.rules.manage',
|
|
758
|
+
'group.roles.manage',
|
|
759
|
+
'group.roles.assign',
|
|
760
|
+
'group.members.invite',
|
|
761
|
+
'group.members.remove',
|
|
762
|
+
'group.members.mute',
|
|
763
|
+
'group.members.unmute',
|
|
764
|
+
'group.members.ban',
|
|
765
|
+
'group.members.unban',
|
|
766
|
+
'group.requests.review',
|
|
767
|
+
'group.reports.review',
|
|
768
|
+
'group.audit.view',
|
|
769
|
+
'group.automod.manage',
|
|
770
|
+
'group.owner.transfer',
|
|
771
|
+
])
|
|
772
|
+
const canManageMembersDerived = hasAnyPermission([
|
|
773
|
+
'group.members.invite',
|
|
774
|
+
'group.members.remove',
|
|
775
|
+
'group.members.mute',
|
|
776
|
+
'group.members.unmute',
|
|
777
|
+
'group.members.ban',
|
|
778
|
+
'group.members.unban',
|
|
779
|
+
'group.roles.assign',
|
|
780
|
+
])
|
|
781
|
+
const canManageRolesDerived = hasAnyPermission([
|
|
782
|
+
'group.roles.manage',
|
|
783
|
+
'group.roles.assign',
|
|
784
|
+
])
|
|
785
|
+
const canReviewRequestsDerived = hasPermission('group.requests.review')
|
|
786
|
+
const canManageReportsDerived = hasPermission('group.reports.review')
|
|
787
|
+
const canManageBansDerived = hasAnyPermission(['group.members.ban', 'group.members.unban'])
|
|
788
|
+
const canManageMutesDerived = hasAnyPermission(['group.members.mute', 'group.members.unmute'])
|
|
789
|
+
const canViewAuditLogDerived = hasPermission('group.audit.view')
|
|
790
|
+
const canManageAutomodDerived = hasPermission('group.automod.manage')
|
|
791
|
+
const canManageSettingsDerived = hasAnyPermission([
|
|
792
|
+
'group.details.edit',
|
|
793
|
+
'group.settings.manage',
|
|
794
|
+
'group.visibility.manage',
|
|
795
|
+
'group.rules.manage',
|
|
796
|
+
])
|
|
797
|
+
const canTransferOwnershipDerived = hasPermission('group.owner.transfer')
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
is_owner: isOwner || fallback.is_owner,
|
|
801
|
+
is_moderator: isModerator || fallback.is_moderator,
|
|
802
|
+
moderation_level: moderationLevel,
|
|
803
|
+
permissions: permissions.length > 0 ? permissions : fallback.permissions,
|
|
804
|
+
can: Object.keys(permissionMap).length > 0 ? permissionMap : fallback.can,
|
|
805
|
+
canJoin: getFlag(['canJoin', 'can_join'], fallback.canJoin),
|
|
806
|
+
canLeave: getFlag(['canLeave', 'can_leave'], fallback.canLeave),
|
|
807
|
+
canRequestToJoin: getFlag(['canRequestToJoin', 'can_request_to_join'], fallback.canRequestToJoin),
|
|
808
|
+
canInvite: getFlag(['canInvite', 'can_invite'], hasPermission('group.members.invite') || fallback.canInvite),
|
|
809
|
+
canManage: getFlag(['canManage', 'can_manage'], canManageDerived || fallback.canManage),
|
|
810
|
+
canManageMembers: getFlag(['canManageMembers', 'can_manage_members'], canManageMembersDerived || fallback.canManageMembers),
|
|
811
|
+
canManageRoles: getFlag(['canManageRoles', 'can_manage_roles'], canManageRolesDerived || fallback.canManageRoles),
|
|
812
|
+
canReviewRequests: getFlag(['canReviewRequests', 'can_review_requests'], canReviewRequestsDerived || fallback.canReviewRequests),
|
|
813
|
+
canManageReports: getFlag(['canManageReports', 'can_manage_reports'], canManageReportsDerived || fallback.canManageReports),
|
|
814
|
+
canManageBans: getFlag(['canManageBans', 'can_manage_bans'], canManageBansDerived || fallback.canManageBans),
|
|
815
|
+
canManageMutes: getFlag(['canManageMutes', 'can_manage_mutes'], canManageMutesDerived || fallback.canManageMutes),
|
|
816
|
+
canViewAuditLog: getFlag(['canViewAuditLog', 'can_view_audit_log'], canViewAuditLogDerived || fallback.canViewAuditLog),
|
|
817
|
+
canManageAutomod: getFlag(['canManageAutomod', 'can_manage_automod'], canManageAutomodDerived || fallback.canManageAutomod),
|
|
818
|
+
canManageSettings: getFlag(['canManageSettings', 'can_manage_settings'], canManageSettingsDerived || fallback.canManageSettings),
|
|
819
|
+
canTransferOwnership: getFlag(['canTransferOwnership', 'can_transfer_ownership'], canTransferOwnershipDerived || fallback.canTransferOwnership),
|
|
820
|
+
canViewManagement: getFlag(['canViewManagement', 'can_view_management'], canManageDerived || fallback.canViewManagement),
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function buildActorContext(circle: Circle): CircleActorContext {
|
|
825
|
+
const role = isCircleRole(circle.user_role) ? circle.user_role : null
|
|
826
|
+
const isMember = circle.is_member === true || role !== null
|
|
827
|
+
const defaultCapabilities = buildDefaultActorCapabilities(role, circle.visibility, isMember)
|
|
828
|
+
const capabilities = normalizeActorCapabilities(circle.actor_capabilities ?? null, {
|
|
829
|
+
...defaultCapabilities,
|
|
830
|
+
canJoin: circle.can_join ?? defaultCapabilities.canJoin,
|
|
831
|
+
canLeave: circle.can_leave ?? defaultCapabilities.canLeave,
|
|
832
|
+
canRequestToJoin: circle.can_request_to_join ?? defaultCapabilities.canRequestToJoin,
|
|
833
|
+
canInvite: circle.can_invite ?? defaultCapabilities.canInvite,
|
|
834
|
+
canManage: circle.can_manage ?? defaultCapabilities.canManage,
|
|
835
|
+
canManageMembers: circle.can_manage_members ?? defaultCapabilities.canManageMembers,
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
const defaultSections = capabilities.canViewManagement
|
|
839
|
+
? [...CIRCLE_MANAGEMENT_SECTIONS]
|
|
840
|
+
: []
|
|
841
|
+
const managementSections = circle.management_sections && circle.management_sections.length > 0
|
|
842
|
+
? [...circle.management_sections]
|
|
843
|
+
: defaultSections
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
isMember,
|
|
847
|
+
role,
|
|
848
|
+
hasPendingRequest: circle.has_pending_request === true,
|
|
849
|
+
capabilities,
|
|
850
|
+
managementSections,
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function normalizeCollectionResponse<T>(
|
|
855
|
+
payload: unknown,
|
|
856
|
+
normalizeItem: (value: unknown) => T | null,
|
|
857
|
+
): PaginationResponse<T> {
|
|
858
|
+
const data = readRecord(payload, 'data') ?? (isRecord(payload) ? payload : null)
|
|
859
|
+
const dataItems = readArray(payload, 'data')
|
|
860
|
+
const meta = readRecord(payload, 'meta')
|
|
861
|
+
const dataMeta = readRecord(data, 'meta')
|
|
862
|
+
const pagination = readRecord(meta, 'pagination') ?? readRecord(dataMeta, 'pagination')
|
|
863
|
+
const itemsValue = Array.isArray(data?.items)
|
|
864
|
+
? data.items
|
|
865
|
+
: dataItems ?? (Array.isArray(payload) ? payload : null)
|
|
866
|
+
const items = Array.isArray(itemsValue)
|
|
867
|
+
? itemsValue.map((item) => normalizeItem(item)).filter((item): item is T => item !== null)
|
|
868
|
+
: []
|
|
869
|
+
|
|
870
|
+
const nextCursor = readNullableString(pagination?.next_cursor)
|
|
871
|
+
?? readNullableString(data?.next)
|
|
872
|
+
const hasMore = readBoolean(pagination?.has_more) ?? (nextCursor !== null)
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
data: items,
|
|
876
|
+
meta: {
|
|
877
|
+
pagination: {
|
|
878
|
+
next_cursor: nextCursor,
|
|
879
|
+
has_more: hasMore,
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function normalizeInlineCollection<T>(
|
|
886
|
+
items: unknown,
|
|
887
|
+
normalizeItem: (value: unknown) => T | null,
|
|
888
|
+
): PaginationResponse<T> {
|
|
889
|
+
if (!Array.isArray(items)) {
|
|
890
|
+
return createEmptyPagination<T>()
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
data: items.map((item) => normalizeItem(item)).filter((item): item is T => item !== null),
|
|
895
|
+
meta: createEmptyPagination<T>().meta,
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function normalizeCircle(value: unknown): Circle | null {
|
|
900
|
+
if (!isRecord(value)) {
|
|
901
|
+
return null
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const id = readString(value.id)
|
|
905
|
+
const name = readString(value.name)
|
|
906
|
+
const slug = readString(value.slug)
|
|
907
|
+
const visibility = value.visibility
|
|
908
|
+
|
|
909
|
+
if (id === null || name === null || slug === null || !isCircleVisibility(visibility)) {
|
|
910
|
+
return null
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const managementSections = normalizeManagementSections(value.management_sections)
|
|
914
|
+
const circle: Circle = {
|
|
915
|
+
id,
|
|
916
|
+
name,
|
|
917
|
+
slug,
|
|
918
|
+
description: readNullableString(value.description),
|
|
919
|
+
visibility,
|
|
920
|
+
is_hidden: readBoolean(value.is_hidden) ?? false,
|
|
921
|
+
owner_id: readNullableString(value.owner_id),
|
|
922
|
+
meta: isRecord(value.meta) ? value.meta : {},
|
|
923
|
+
cover_photo_url: readNullableString(value.cover_photo_url),
|
|
924
|
+
cover_photo_media_id: readNullableString(value.cover_photo_media_id),
|
|
925
|
+
created_at: readString(value.created_at) ?? new Date(0).toISOString(),
|
|
926
|
+
updated_at: readString(value.updated_at) ?? new Date(0).toISOString(),
|
|
927
|
+
user_role: isCircleRole(value.user_role) ? value.user_role : null,
|
|
928
|
+
group_rules_summary: readNullableString(value.group_rules_summary),
|
|
929
|
+
welcome_message: readNullableString(value.welcome_message),
|
|
930
|
+
canonical_path: readNullableString(value.canonical_path),
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const isMember = readBoolean(value.is_member)
|
|
934
|
+
if (isMember !== null) {
|
|
935
|
+
circle.is_member = isMember
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const memberCount = readNumber(value.member_count)
|
|
939
|
+
if (memberCount !== null) {
|
|
940
|
+
circle.member_count = memberCount
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const canJoin = readBoolean(value.can_join)
|
|
944
|
+
if (canJoin !== null) {
|
|
945
|
+
circle.can_join = canJoin
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const canManage = readBoolean(value.can_manage)
|
|
949
|
+
if (canManage !== null) {
|
|
950
|
+
circle.can_manage = canManage
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const canManageMembers = readBoolean(value.can_manage_members)
|
|
954
|
+
if (canManageMembers !== null) {
|
|
955
|
+
circle.can_manage_members = canManageMembers
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const canInvite = readBoolean(value.can_invite)
|
|
959
|
+
if (canInvite !== null) {
|
|
960
|
+
circle.can_invite = canInvite
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const canLeave = readBoolean(value.can_leave)
|
|
964
|
+
if (canLeave !== null) {
|
|
965
|
+
circle.can_leave = canLeave
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const canRequestToJoin = readBoolean(value.can_request_to_join)
|
|
969
|
+
if (canRequestToJoin !== null) {
|
|
970
|
+
circle.can_request_to_join = canRequestToJoin
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const hasPendingRequest = readBoolean(value.has_pending_request)
|
|
974
|
+
if (hasPendingRequest !== null) {
|
|
975
|
+
circle.has_pending_request = hasPendingRequest
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (isRecord(value.actor_capabilities)) {
|
|
979
|
+
circle.actor_capabilities = normalizeActorCapabilities(
|
|
980
|
+
value.actor_capabilities,
|
|
981
|
+
buildDefaultActorCapabilities(null, visibility, false),
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (managementSections.length > 0) {
|
|
986
|
+
circle.management_sections = managementSections
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const creator = normalizeUserSummary(value.creator)
|
|
990
|
+
if (creator) {
|
|
991
|
+
circle.creator = creator
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
circle.actor = buildActorContext(circle)
|
|
995
|
+
circle.actor_capabilities = circle.actor.capabilities
|
|
996
|
+
circle.management_sections = circle.actor.managementSections
|
|
997
|
+
circle.can_join = circle.actor.capabilities.canJoin
|
|
998
|
+
circle.can_leave = circle.actor.capabilities.canLeave
|
|
999
|
+
circle.can_request_to_join = circle.actor.capabilities.canRequestToJoin
|
|
1000
|
+
circle.can_invite = circle.actor.capabilities.canInvite
|
|
1001
|
+
circle.can_manage = circle.actor.capabilities.canManage
|
|
1002
|
+
circle.can_manage_members = circle.actor.capabilities.canManageMembers
|
|
1003
|
+
circle.has_pending_request = circle.actor.hasPendingRequest
|
|
1004
|
+
circle.is_member = circle.actor.isMember
|
|
1005
|
+
circle.user_role = circle.actor.role
|
|
1006
|
+
|
|
1007
|
+
return circle
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function normalizeCircleMember(value: unknown): CircleMember | null {
|
|
1011
|
+
if (!isRecord(value)) {
|
|
1012
|
+
return null
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const id = readString(value.id)
|
|
1016
|
+
const userId = readString(value.user_id) ?? id
|
|
1017
|
+
const circleId = readString(value.circle_id) ?? ''
|
|
1018
|
+
const joinedAt = readString(value.joined_at)
|
|
1019
|
+
const role = value.role
|
|
1020
|
+
|
|
1021
|
+
if (id === null || userId === null || joinedAt === null || !isCircleRole(role)) {
|
|
1022
|
+
return null
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const user = normalizeUserSummary(value.user)
|
|
1026
|
+
const profileOverridesValue = isRecord(value.profile_overrides) ? value.profile_overrides : null
|
|
1027
|
+
let profileOverrides: CircleProfileOverrides | undefined
|
|
1028
|
+
if (profileOverridesValue !== null) {
|
|
1029
|
+
const nextProfileOverrides: CircleProfileOverrides = {}
|
|
1030
|
+
const overrideName = readString(profileOverridesValue.name)
|
|
1031
|
+
if (overrideName !== null) {
|
|
1032
|
+
nextProfileOverrides.name = overrideName
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const overrideBio = readString(profileOverridesValue.bio)
|
|
1036
|
+
if (overrideBio !== null) {
|
|
1037
|
+
nextProfileOverrides.bio = overrideBio
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const overrideAvatarUrl = readString(profileOverridesValue.avatar_url)
|
|
1041
|
+
if (overrideAvatarUrl !== null) {
|
|
1042
|
+
nextProfileOverrides.avatar_url = overrideAvatarUrl
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (Object.keys(nextProfileOverrides).length > 0) {
|
|
1046
|
+
profileOverrides = nextProfileOverrides
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const member: CircleMember = {
|
|
1051
|
+
id,
|
|
1052
|
+
user_id: userId,
|
|
1053
|
+
circle_id: circleId,
|
|
1054
|
+
role,
|
|
1055
|
+
joined_at: joinedAt,
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (isCircleMembershipRole(value.membership_role)) {
|
|
1059
|
+
member.membership_role = value.membership_role
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (isCircleRole(value.display_role)) {
|
|
1063
|
+
member.display_role = value.display_role
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const assignedRoles = Array.isArray(value.assigned_roles)
|
|
1067
|
+
? value.assigned_roles
|
|
1068
|
+
.map((roleDefinition) => normalizeRoleDefinition(roleDefinition))
|
|
1069
|
+
.filter((roleDefinition): roleDefinition is CircleRoleDefinition => roleDefinition !== null)
|
|
1070
|
+
: []
|
|
1071
|
+
if (assignedRoles.length > 0) {
|
|
1072
|
+
member.assigned_roles = assignedRoles
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const effectivePermissions = normalizePermissionKeys(value.effective_permissions)
|
|
1076
|
+
if (effectivePermissions.length > 0) {
|
|
1077
|
+
member.effective_permissions = effectivePermissions
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const isMuted = readBoolean(value.is_muted)
|
|
1081
|
+
if (isMuted !== null) {
|
|
1082
|
+
member.is_muted = isMuted
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const mutedUntil = readNullableString(value.muted_until)
|
|
1086
|
+
if (mutedUntil !== null || value.muted_until === null) {
|
|
1087
|
+
member.muted_until = mutedUntil
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const isBanned = readBoolean(value.is_banned)
|
|
1091
|
+
if (isBanned !== null) {
|
|
1092
|
+
member.is_banned = isBanned
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const canRemove = readBoolean(value.can_remove)
|
|
1096
|
+
if (canRemove !== null) {
|
|
1097
|
+
member.can_remove = canRemove
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const canChangeRole = readBoolean(value.can_change_role)
|
|
1101
|
+
if (canChangeRole !== null) {
|
|
1102
|
+
member.can_change_role = canChangeRole
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const canAssignRoles = readBoolean(value.can_assign_roles)
|
|
1106
|
+
if (canAssignRoles !== null) {
|
|
1107
|
+
member.can_assign_roles = canAssignRoles
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const canMute = readBoolean(value.can_mute)
|
|
1111
|
+
if (canMute !== null) {
|
|
1112
|
+
member.can_mute = canMute
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const canUnmute = readBoolean(value.can_unmute)
|
|
1116
|
+
if (canUnmute !== null) {
|
|
1117
|
+
member.can_unmute = canUnmute
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const canBan = readBoolean(value.can_ban)
|
|
1121
|
+
if (canBan !== null) {
|
|
1122
|
+
member.can_ban = canBan
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const canUnban = readBoolean(value.can_unban)
|
|
1126
|
+
if (canUnban !== null) {
|
|
1127
|
+
member.can_unban = canUnban
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const canViewAudit = readBoolean(value.can_view_audit)
|
|
1131
|
+
if (canViewAudit !== null) {
|
|
1132
|
+
member.can_view_audit = canViewAudit
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (profileOverrides) {
|
|
1136
|
+
member.profile_overrides = profileOverrides
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (user) {
|
|
1140
|
+
member.user = user
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return member
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function normalizeJoinRequest(value: unknown): JoinRequest | null {
|
|
1147
|
+
if (!isRecord(value)) {
|
|
1148
|
+
return null
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const id = readString(value.id)
|
|
1152
|
+
const userId = readString(value.user_id)
|
|
1153
|
+
const circleId = readString(value.circle_id)
|
|
1154
|
+
const status = value.status
|
|
1155
|
+
const source = value.source
|
|
1156
|
+
const createdAt = readString(value.created_at)
|
|
1157
|
+
const updatedAt = readString(value.updated_at)
|
|
1158
|
+
|
|
1159
|
+
if (
|
|
1160
|
+
id === null
|
|
1161
|
+
|| userId === null
|
|
1162
|
+
|| circleId === null
|
|
1163
|
+
|| createdAt === null
|
|
1164
|
+
|| updatedAt === null
|
|
1165
|
+
|| (status !== 'pending' && status !== 'approved' && status !== 'rejected')
|
|
1166
|
+
|| (source !== 'direct' && source !== 'invite')
|
|
1167
|
+
) {
|
|
1168
|
+
return null
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const joinRequest: JoinRequest = {
|
|
1172
|
+
id,
|
|
1173
|
+
user_id: userId,
|
|
1174
|
+
circle_id: circleId,
|
|
1175
|
+
status,
|
|
1176
|
+
source,
|
|
1177
|
+
message: readNullableString(value.message),
|
|
1178
|
+
reviewed_by_id: readNullableString(value.reviewed_by_id),
|
|
1179
|
+
reviewed_at: readNullableString(value.reviewed_at),
|
|
1180
|
+
created_at: createdAt,
|
|
1181
|
+
updated_at: updatedAt,
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const user = normalizeUserSummary(value.user)
|
|
1185
|
+
if (user) {
|
|
1186
|
+
joinRequest.user = user
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return joinRequest
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function normalizeInviteLink(value: unknown): InviteLink | null {
|
|
1193
|
+
if (!isRecord(value)) {
|
|
1194
|
+
return null
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const id = readString(value.id)
|
|
1198
|
+
const token = readString(value.token)
|
|
1199
|
+
const circleId = readString(value.circle_id)
|
|
1200
|
+
const inviteUrl = readString(value.invite_url)
|
|
1201
|
+
const qrSvg = readString(value.qr_svg)
|
|
1202
|
+
const usesCount = readNumber(value.uses_count)
|
|
1203
|
+
|
|
1204
|
+
if (id === null || token === null || circleId === null || inviteUrl === null || qrSvg === null || usesCount === null) {
|
|
1205
|
+
return null
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return {
|
|
1209
|
+
id,
|
|
1210
|
+
token,
|
|
1211
|
+
circle_id: circleId,
|
|
1212
|
+
target_user_id: readNullableString(value.target_user_id),
|
|
1213
|
+
max_uses: readNumber(value.max_uses),
|
|
1214
|
+
uses_count: usesCount,
|
|
1215
|
+
expires_at: readNullableString(value.expires_at),
|
|
1216
|
+
invite_url: inviteUrl,
|
|
1217
|
+
qr_svg: qrSvg,
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function normalizeRoleDefinition(value: unknown): CircleRoleDefinition | null {
|
|
1222
|
+
if (!isRecord(value)) {
|
|
1223
|
+
return null
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const id = readString(value.id)
|
|
1227
|
+
const slug = readString(value.slug)
|
|
1228
|
+
const name = readString(value.name)
|
|
1229
|
+
|
|
1230
|
+
if (id === null || slug === null || name === null) {
|
|
1231
|
+
return null
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
id,
|
|
1236
|
+
slug,
|
|
1237
|
+
name,
|
|
1238
|
+
description: readNullableString(value.description),
|
|
1239
|
+
color: readNullableString(value.color),
|
|
1240
|
+
is_system: readBoolean(value.is_system) ?? false,
|
|
1241
|
+
is_assignable: readBoolean(value.is_assignable) ?? true,
|
|
1242
|
+
member_count: readNumber(value.member_count) ?? 0,
|
|
1243
|
+
permissions: normalizePermissionKeys(value.permissions),
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function normalizePermissionCatalogEntry(value: unknown): CirclePermissionCatalogEntry | null {
|
|
1248
|
+
if (!isRecord(value)) {
|
|
1249
|
+
return null
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const key = readString(value.key)
|
|
1253
|
+
const label = readString(value.label)
|
|
1254
|
+
const description = readString(value.description)
|
|
1255
|
+
const moduleName = readString(value.module)
|
|
1256
|
+
const section = value.section
|
|
1257
|
+
|
|
1258
|
+
if (key === null || label === null || description === null || moduleName === null || !isManagementSection(section)) {
|
|
1259
|
+
return null
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return {
|
|
1263
|
+
key,
|
|
1264
|
+
label,
|
|
1265
|
+
description,
|
|
1266
|
+
module: moduleName,
|
|
1267
|
+
owner_only: readBoolean(value.owner_only) ?? false,
|
|
1268
|
+
surfaced: readBoolean(value.surfaced) ?? false,
|
|
1269
|
+
section,
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function normalizeModerationReport(value: unknown): CircleModerationReport | null {
|
|
1274
|
+
if (!isRecord(value)) {
|
|
1275
|
+
return null
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const id = readString(value.id) ?? readString(value.report_id)
|
|
1279
|
+
const reporterId = readNullableString(value.reporter_id)
|
|
1280
|
+
const source = isCircleModerationReportSource(value.source)
|
|
1281
|
+
? value.source
|
|
1282
|
+
: 'member'
|
|
1283
|
+
const subjectUserId = readNullableString(value.subject_user_id)
|
|
1284
|
+
const subjectType = isCircleModerationReportSubjectType(value.subject_type)
|
|
1285
|
+
? value.subject_type
|
|
1286
|
+
: subjectUserId !== null
|
|
1287
|
+
? 'user'
|
|
1288
|
+
: null
|
|
1289
|
+
const subjectId = readNullableString(value.subject_id) ?? subjectUserId
|
|
1290
|
+
const subjectAuthorId = readNullableString(value.subject_author_id)
|
|
1291
|
+
?? (subjectType === 'user' ? subjectUserId : null)
|
|
1292
|
+
const category = readString(value.category) ?? 'general'
|
|
1293
|
+
const status = value.status
|
|
1294
|
+
const priority = readString(value.priority) ?? 'normal'
|
|
1295
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString()
|
|
1296
|
+
|
|
1297
|
+
if (
|
|
1298
|
+
id === null
|
|
1299
|
+
|| (status !== 'pending' && status !== 'actioned' && status !== 'dismissed')
|
|
1300
|
+
) {
|
|
1301
|
+
return null
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return {
|
|
1305
|
+
id,
|
|
1306
|
+
reporter_id: reporterId,
|
|
1307
|
+
source,
|
|
1308
|
+
subject_user_id: subjectUserId,
|
|
1309
|
+
subject_type: subjectType,
|
|
1310
|
+
subject_id: subjectId,
|
|
1311
|
+
subject_author_id: subjectAuthorId,
|
|
1312
|
+
category,
|
|
1313
|
+
notes: readNullableString(value.notes),
|
|
1314
|
+
status,
|
|
1315
|
+
priority,
|
|
1316
|
+
resolution_action: readNullableString(value.resolution_action),
|
|
1317
|
+
created_at: createdAt,
|
|
1318
|
+
reviewed_at: readNullableString(value.reviewed_at),
|
|
1319
|
+
resolution_notes: readNullableString(value.resolution_notes),
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function normalizeBanRecord(value: unknown): CircleBanRecord | null {
|
|
1324
|
+
if (!isRecord(value)) {
|
|
1325
|
+
return null
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const id = readString(value.id) ?? readString(value.user_id)
|
|
1329
|
+
const userId = readString(value.user_id)
|
|
1330
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString()
|
|
1331
|
+
|
|
1332
|
+
if (id === null || userId === null) {
|
|
1333
|
+
return null
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const banRecord: CircleBanRecord = {
|
|
1337
|
+
id,
|
|
1338
|
+
user_id: userId,
|
|
1339
|
+
reason: readNullableString(value.reason),
|
|
1340
|
+
created_at: createdAt,
|
|
1341
|
+
updated_at: readNullableString(value.updated_at),
|
|
1342
|
+
created_by_id: readNullableString(value.created_by_id),
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const user = normalizeUserSummary(value.user)
|
|
1346
|
+
if (user) {
|
|
1347
|
+
banRecord.user = user
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return banRecord
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function normalizeMuteRecord(value: unknown): CircleMuteRecord | null {
|
|
1354
|
+
if (!isRecord(value)) {
|
|
1355
|
+
return null
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const id = readString(value.id) ?? readString(value.user_id)
|
|
1359
|
+
const userId = readString(value.user_id)
|
|
1360
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString()
|
|
1361
|
+
|
|
1362
|
+
if (id === null || userId === null) {
|
|
1363
|
+
return null
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const muteRecord: CircleMuteRecord = {
|
|
1367
|
+
id,
|
|
1368
|
+
user_id: userId,
|
|
1369
|
+
reason: readNullableString(value.reason),
|
|
1370
|
+
muted_until: readNullableString(value.muted_until),
|
|
1371
|
+
created_at: createdAt,
|
|
1372
|
+
updated_at: readNullableString(value.updated_at),
|
|
1373
|
+
created_by_id: readNullableString(value.created_by_id),
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const user = normalizeUserSummary(value.user)
|
|
1377
|
+
if (user) {
|
|
1378
|
+
muteRecord.user = user
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return muteRecord
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function normalizeAuditLogEntry(value: unknown): CircleModerationAuditLogEntry | null {
|
|
1385
|
+
if (!isRecord(value)) {
|
|
1386
|
+
return null
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const id = readString(value.id)
|
|
1390
|
+
const action = readString(value.action)
|
|
1391
|
+
const actorId = readString(value.actor_id)
|
|
1392
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString()
|
|
1393
|
+
|
|
1394
|
+
if (id === null || action === null || actorId === null) {
|
|
1395
|
+
return null
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return {
|
|
1399
|
+
id,
|
|
1400
|
+
action,
|
|
1401
|
+
actor_id: actorId,
|
|
1402
|
+
target_user_id: readNullableString(value.target_user_id),
|
|
1403
|
+
report_id: readNullableString(value.report_id),
|
|
1404
|
+
meta: isRecord(value.meta) ? value.meta : {},
|
|
1405
|
+
created_at: createdAt,
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function isCircleModerationSubjectType(value: unknown): value is CircleModerationSubjectType {
|
|
1410
|
+
return value === 'post' || value === 'comment' || value === 'thread' || value === 'reply' || value === 'event'
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function isCircleModerationSubjectState(value: unknown): value is CircleModerationSubjectState {
|
|
1414
|
+
return value === 'active' || value === 'removed' || value === 'locked'
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function isCircleModerationSubjectAction(value: unknown): value is CircleModerationSubjectAction {
|
|
1418
|
+
return value === 'remove' || value === 'restore' || value === 'lock' || value === 'unlock'
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function normalizeModerationSubject(value: unknown): CircleModerationSubject | null {
|
|
1422
|
+
if (!isRecord(value)) {
|
|
1423
|
+
return null
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const subjectType = value.subject_type
|
|
1427
|
+
const subjectId = readString(value.subject_id)
|
|
1428
|
+
const circleId = readString(value.circle_id)
|
|
1429
|
+
const moderationState = value.moderation_state
|
|
1430
|
+
const createdAt = readString(value.created_at)
|
|
1431
|
+
const updatedAt = readString(value.updated_at)
|
|
1432
|
+
|
|
1433
|
+
if (
|
|
1434
|
+
subjectId === null
|
|
1435
|
+
|| circleId === null
|
|
1436
|
+
|| createdAt === null
|
|
1437
|
+
|| updatedAt === null
|
|
1438
|
+
|| !isCircleModerationSubjectType(subjectType)
|
|
1439
|
+
|| !isCircleModerationSubjectState(moderationState)
|
|
1440
|
+
) {
|
|
1441
|
+
return null
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const actions = Array.isArray(value.available_actions)
|
|
1445
|
+
? value.available_actions
|
|
1446
|
+
.filter((entry): entry is CircleModerationSubjectAction => isCircleModerationSubjectAction(entry))
|
|
1447
|
+
: []
|
|
1448
|
+
|
|
1449
|
+
const latestReportStatusValue = value.latest_report_status
|
|
1450
|
+
const latestReportStatus: CircleReportStatus | null = latestReportStatusValue === 'pending'
|
|
1451
|
+
|| latestReportStatusValue === 'actioned'
|
|
1452
|
+
|| latestReportStatusValue === 'dismissed'
|
|
1453
|
+
? latestReportStatusValue
|
|
1454
|
+
: null
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
subject_type: subjectType,
|
|
1458
|
+
subject_id: subjectId,
|
|
1459
|
+
circle_id: circleId,
|
|
1460
|
+
author_id: readNullableString(value.author_id),
|
|
1461
|
+
report_count: readNumber(value.report_count) ?? 0,
|
|
1462
|
+
latest_report_status: latestReportStatus,
|
|
1463
|
+
moderation_state: moderationState,
|
|
1464
|
+
created_at: createdAt,
|
|
1465
|
+
updated_at: updatedAt,
|
|
1466
|
+
preview: isRecord(value.preview) ? value.preview : {},
|
|
1467
|
+
available_actions: actions,
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function normalizeAutomodCondition(value: unknown): CircleAutomodCondition | null {
|
|
1472
|
+
if (!isRecord(value)) {
|
|
1473
|
+
return null
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const type = readString(value.type)
|
|
1477
|
+
if (type === null) {
|
|
1478
|
+
return null
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return {
|
|
1482
|
+
type,
|
|
1483
|
+
config: isRecord(value.config) ? value.config : {},
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function normalizeAutomodAction(value: unknown): CircleAutomodAction | null {
|
|
1488
|
+
if (!isRecord(value)) {
|
|
1489
|
+
return null
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const type = readString(value.type)
|
|
1493
|
+
if (type === null) {
|
|
1494
|
+
return null
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
return {
|
|
1498
|
+
type,
|
|
1499
|
+
config: isRecord(value.config) ? value.config : {},
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function normalizeAutomodRule(value: unknown): CircleAutomodRule | null {
|
|
1504
|
+
if (!isRecord(value)) {
|
|
1505
|
+
return null
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const id = readString(value.id)
|
|
1509
|
+
const name = readString(value.name)
|
|
1510
|
+
const createdAt = readString(value.created_at) ?? new Date(0).toISOString()
|
|
1511
|
+
|
|
1512
|
+
if (id === null || name === null) {
|
|
1513
|
+
return null
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const conditions = Array.isArray(value.conditions)
|
|
1517
|
+
? value.conditions.map((condition) => normalizeAutomodCondition(condition)).filter((condition): condition is CircleAutomodCondition => condition !== null)
|
|
1518
|
+
: []
|
|
1519
|
+
const actions = Array.isArray(value.actions)
|
|
1520
|
+
? value.actions.map((action) => normalizeAutomodAction(action)).filter((action): action is CircleAutomodAction => action !== null)
|
|
1521
|
+
: []
|
|
1522
|
+
|
|
1523
|
+
return {
|
|
1524
|
+
id,
|
|
1525
|
+
name,
|
|
1526
|
+
description: readNullableString(value.description),
|
|
1527
|
+
is_enabled: readBoolean(value.is_enabled) ?? true,
|
|
1528
|
+
conditions,
|
|
1529
|
+
actions,
|
|
1530
|
+
priority: readNumber(value.priority) ?? 0,
|
|
1531
|
+
created_at: createdAt,
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function isCircleWithSlug(value: unknown): value is { id: string; slug: string } {
|
|
1536
|
+
if (!isRecord(value)) {
|
|
1537
|
+
return false
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
return typeof value.id === 'string' && typeof value.slug === 'string'
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function defaultSanitizeString(value: string): string {
|
|
1544
|
+
return value.trim()
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function defaultGetCircleTerm(options?: CircleTermOptions): string {
|
|
1548
|
+
return options?.case === 'title' ? 'Circle' : 'circle'
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
class CirclesService {
|
|
1552
|
+
private readonly baseURL = '/v1/circles'
|
|
1553
|
+
|
|
1554
|
+
public constructor(
|
|
1555
|
+
private readonly client: AxiosInstance,
|
|
1556
|
+
private readonly sanitizeString: (value: string) => string,
|
|
1557
|
+
private readonly onNotify?: (notification: CirclesNotification) => void,
|
|
1558
|
+
private readonly onUnauthorized?: () => void,
|
|
1559
|
+
private readonly getCircleTerm: (options?: CircleTermOptions) => string = defaultGetCircleTerm,
|
|
1560
|
+
private readonly onError?: (context: CirclesServiceErrorContext) => void,
|
|
1561
|
+
) {}
|
|
1562
|
+
|
|
1563
|
+
private isUuid(value: string): boolean {
|
|
1564
|
+
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)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
private sanitizeInput<T>(data: T): SanitizedInput<T> {
|
|
1568
|
+
if (typeof data === 'string') {
|
|
1569
|
+
return this.sanitizeString(data.trim()) as SanitizedInput<T>
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (Array.isArray(data)) {
|
|
1573
|
+
return data.map((item) => this.sanitizeInput(item)) as SanitizedInput<T>
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (isRecord(data)) {
|
|
1577
|
+
const sanitized: Record<string, unknown> = {}
|
|
1578
|
+
Object.keys(data).forEach((key) => {
|
|
1579
|
+
sanitized[key] = this.sanitizeInput(data[key])
|
|
1580
|
+
})
|
|
1581
|
+
return sanitized as SanitizedInput<T>
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
return data as SanitizedInput<T>
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
private emitNotification(notification: CirclesNotification): void {
|
|
1588
|
+
this.onNotify?.(notification)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
private reportError(context: CirclesServiceErrorContext): void {
|
|
1592
|
+
const consoleMethod = context.logLevel === 'warn' ? console.warn : console.error
|
|
1593
|
+
const label = context.logLevel === 'warn' ? 'Circles API Not Found:' : 'Circles API Error:'
|
|
1594
|
+
|
|
1595
|
+
consoleMethod(label, {
|
|
1596
|
+
url: context.url,
|
|
1597
|
+
status: context.status,
|
|
1598
|
+
timestamp: context.timestamp,
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
this.onError?.(context)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
private async handleError(error: unknown): Promise<never> {
|
|
1605
|
+
const apiError = isApiError(error) ? error : null
|
|
1606
|
+
const status = apiError?.response?.status
|
|
1607
|
+
const context: CirclesServiceErrorContext = {
|
|
1608
|
+
error,
|
|
1609
|
+
url: apiError?.config?.url,
|
|
1610
|
+
status,
|
|
1611
|
+
timestamp: new Date().toISOString(),
|
|
1612
|
+
logLevel: status === 404 ? 'warn' : 'error',
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
this.reportError(context)
|
|
1616
|
+
|
|
1617
|
+
let userMessage = 'Something went wrong. Please try again.'
|
|
1618
|
+
|
|
1619
|
+
switch (status) {
|
|
1620
|
+
case 401:
|
|
1621
|
+
userMessage = 'Please sign in to continue.'
|
|
1622
|
+
this.onUnauthorized?.()
|
|
1623
|
+
break
|
|
1624
|
+
case 403:
|
|
1625
|
+
userMessage = `You do not have permission to access this ${this.getCircleTerm()}.`
|
|
1626
|
+
break
|
|
1627
|
+
case 404:
|
|
1628
|
+
userMessage = `${this.getCircleTerm({ case: 'title' })} not found.`
|
|
1629
|
+
break
|
|
1630
|
+
case 422:
|
|
1631
|
+
userMessage = readString(apiError?.response?.data?.message) ?? 'Please check your input and try again.'
|
|
1632
|
+
break
|
|
1633
|
+
case 429:
|
|
1634
|
+
userMessage = 'Too many requests. Please wait a moment.'
|
|
1635
|
+
break
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
this.emitNotification({
|
|
1639
|
+
description: userMessage,
|
|
1640
|
+
color: 'error',
|
|
1641
|
+
duration: 4000,
|
|
1642
|
+
})
|
|
1643
|
+
|
|
1644
|
+
throw new Error(userMessage)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
private isHttpStatus(error: unknown, status: number): boolean {
|
|
1648
|
+
return isApiError(error) && error.response?.status === status
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
private async findBySlug(slug: string): Promise<Circle | null> {
|
|
1652
|
+
const response = await this.client.get(this.baseURL, {
|
|
1653
|
+
params: {
|
|
1654
|
+
search: this.sanitizeInput(slug),
|
|
1655
|
+
limit: DEFAULT_LIST_LIMIT,
|
|
1656
|
+
},
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
const items = readRecord(response.data, 'data')?.items
|
|
1660
|
+
if (!Array.isArray(items)) {
|
|
1661
|
+
return null
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const normalizedSlug = slug.trim().toLowerCase()
|
|
1665
|
+
const matchedCircle = items.find((item: unknown) => isCircleWithSlug(item) && item.slug.toLowerCase() === normalizedSlug)
|
|
1666
|
+
|
|
1667
|
+
return matchedCircle ? normalizeCircle(matchedCircle) : null
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
public async list(cursor?: string | null, filters?: CircleFilters): Promise<PaginationResponse<Circle>> {
|
|
1671
|
+
try {
|
|
1672
|
+
const params: Record<string, string> = {
|
|
1673
|
+
limit: DEFAULT_LIST_LIMIT,
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (cursor) {
|
|
1677
|
+
params.cursor = cursor
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (filters?.visibility) {
|
|
1681
|
+
params.visibility = filters.visibility
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (filters?.membership) {
|
|
1685
|
+
params.membership = filters.membership
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (filters?.search) {
|
|
1689
|
+
params.search = this.sanitizeInput(filters.search)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const response = await this.client.get(this.baseURL, { params })
|
|
1693
|
+
return normalizeCollectionResponse(response.data, normalizeCircle)
|
|
1694
|
+
} catch (error: unknown) {
|
|
1695
|
+
return await this.handleError(error)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
public async get(id: string): Promise<Circle> {
|
|
1700
|
+
const identifier = this.sanitizeInput(id)
|
|
1701
|
+
|
|
1702
|
+
try {
|
|
1703
|
+
const response = await this.client.get(`${this.baseURL}/${identifier}`)
|
|
1704
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
1705
|
+
const normalized = normalizeCircle(data?.circle ?? data)
|
|
1706
|
+
|
|
1707
|
+
if (normalized !== null) {
|
|
1708
|
+
return normalized
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
throw new Error('Invalid circle payload')
|
|
1712
|
+
} catch (error: unknown) {
|
|
1713
|
+
if (this.isHttpStatus(error, 404) && !this.isUuid(identifier)) {
|
|
1714
|
+
try {
|
|
1715
|
+
const matchedCircle = await this.findBySlug(identifier)
|
|
1716
|
+
if (matchedCircle !== null) {
|
|
1717
|
+
return matchedCircle
|
|
1718
|
+
}
|
|
1719
|
+
} catch {
|
|
1720
|
+
// Fall through to canonical error handling.
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
return await this.handleError(error)
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
public async create(data: CircleCreateInput): Promise<{ id: string }> {
|
|
1729
|
+
try {
|
|
1730
|
+
const response = await this.client.post(this.baseURL, this.sanitizeInput(data))
|
|
1731
|
+
const payload = readRecord(response.data, 'data') ?? {}
|
|
1732
|
+
const id = readString(payload.id)
|
|
1733
|
+
|
|
1734
|
+
if (id === null) {
|
|
1735
|
+
throw new Error('Invalid circle creation payload')
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
return { id }
|
|
1739
|
+
} catch (error: unknown) {
|
|
1740
|
+
return await this.handleError(error)
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
public async update(id: string, data: CircleUpdateInput): Promise<Circle> {
|
|
1745
|
+
try {
|
|
1746
|
+
await this.client.patch(`${this.baseURL}/${id}`, this.sanitizeInput(data))
|
|
1747
|
+
return await this.get(id)
|
|
1748
|
+
} catch (error: unknown) {
|
|
1749
|
+
return await this.handleError(error)
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
public async delete(id: string): Promise<void> {
|
|
1754
|
+
try {
|
|
1755
|
+
await this.client.delete(`${this.baseURL}/${id}`)
|
|
1756
|
+
} catch (error: unknown) {
|
|
1757
|
+
return await this.handleError(error)
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
public async join(id: string): Promise<void> {
|
|
1762
|
+
try {
|
|
1763
|
+
await this.client.post(`${this.baseURL}/${id}/join`)
|
|
1764
|
+
this.emitNotification({
|
|
1765
|
+
description: `Successfully joined ${this.getCircleTerm()}!`,
|
|
1766
|
+
color: 'success',
|
|
1767
|
+
})
|
|
1768
|
+
} catch (error: unknown) {
|
|
1769
|
+
return await this.handleError(error)
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
public async leave(id: string): Promise<void> {
|
|
1774
|
+
try {
|
|
1775
|
+
await this.client.post(`${this.baseURL}/${id}/leave`)
|
|
1776
|
+
this.emitNotification({
|
|
1777
|
+
description: `Left ${this.getCircleTerm()} successfully`,
|
|
1778
|
+
color: 'success',
|
|
1779
|
+
})
|
|
1780
|
+
} catch (error: unknown) {
|
|
1781
|
+
return await this.handleError(error)
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
public async getMembers(id: string, cursor?: string | null): Promise<PaginationResponse<CircleMember>> {
|
|
1786
|
+
try {
|
|
1787
|
+
const params: Record<string, string> = {}
|
|
1788
|
+
if (cursor) {
|
|
1789
|
+
params.cursor = cursor
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
const response = await this.client.get(`${this.baseURL}/${id}/members`, { params })
|
|
1793
|
+
return normalizeCollectionResponse(response.data, normalizeCircleMember)
|
|
1794
|
+
} catch (error: unknown) {
|
|
1795
|
+
return await this.handleError(error)
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
public async requestToJoin(id: string, message?: string): Promise<{ request_id: string; status: string }> {
|
|
1800
|
+
try {
|
|
1801
|
+
const body: Record<string, string> = {}
|
|
1802
|
+
if (message) {
|
|
1803
|
+
body.message = this.sanitizeInput(message)
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const response = await this.client.post(`${this.baseURL}/${id}/join-requests`, body)
|
|
1807
|
+
const payload = readRecord(response.data, 'data') ?? {}
|
|
1808
|
+
const requestId = readString(payload.request_id)
|
|
1809
|
+
const status = readString(payload.status)
|
|
1810
|
+
|
|
1811
|
+
if (requestId === null || status === null) {
|
|
1812
|
+
throw new Error('Invalid join request payload')
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
this.emitNotification({
|
|
1816
|
+
description: `Join request sent! The ${this.getCircleTerm()} admins will review it.`,
|
|
1817
|
+
color: 'success',
|
|
1818
|
+
duration: 4000,
|
|
1819
|
+
})
|
|
1820
|
+
|
|
1821
|
+
return {
|
|
1822
|
+
request_id: requestId,
|
|
1823
|
+
status,
|
|
1824
|
+
}
|
|
1825
|
+
} catch (error: unknown) {
|
|
1826
|
+
return await this.handleError(error)
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
public async listJoinRequests(circleId: string, cursor?: string | null, status?: string): Promise<PaginationResponse<JoinRequest>> {
|
|
1831
|
+
try {
|
|
1832
|
+
const params: Record<string, string> = {}
|
|
1833
|
+
if (cursor) {
|
|
1834
|
+
params.cursor = cursor
|
|
1835
|
+
}
|
|
1836
|
+
if (status) {
|
|
1837
|
+
params.status = status
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/join-requests`, { params })
|
|
1841
|
+
return normalizeCollectionResponse(response.data, normalizeJoinRequest)
|
|
1842
|
+
} catch (error: unknown) {
|
|
1843
|
+
return await this.handleError(error)
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
public async moderateJoinRequest(circleId: string, requestId: string, decision: 'approve' | 'reject'): Promise<{ request_id: string; status: string }> {
|
|
1848
|
+
try {
|
|
1849
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/join-requests/${requestId}`, { decision })
|
|
1850
|
+
const payload = readRecord(response.data, 'data') ?? {}
|
|
1851
|
+
const status = readString(payload.status)
|
|
1852
|
+
const resolvedRequestId = readString(payload.request_id) ?? requestId
|
|
1853
|
+
|
|
1854
|
+
if (status === null) {
|
|
1855
|
+
throw new Error('Invalid join request moderation payload')
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
this.emitNotification({
|
|
1859
|
+
description: `Join request ${decision === 'approve' ? 'approved' : 'rejected'}.`,
|
|
1860
|
+
color: decision === 'approve' ? 'success' : 'warning',
|
|
1861
|
+
})
|
|
1862
|
+
|
|
1863
|
+
return {
|
|
1864
|
+
request_id: resolvedRequestId,
|
|
1865
|
+
status,
|
|
1866
|
+
}
|
|
1867
|
+
} catch (error: unknown) {
|
|
1868
|
+
return await this.handleError(error)
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
public async fetchRoleDefinitions(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleRoleDefinition>> {
|
|
1873
|
+
return await this.listRoleDefinitions(circleId, cursor)
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
public async listRoleDefinitions(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleRoleDefinition>> {
|
|
1877
|
+
try {
|
|
1878
|
+
const params: Record<string, string> = {}
|
|
1879
|
+
if (cursor) {
|
|
1880
|
+
params.cursor = cursor
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/roles`, { params })
|
|
1884
|
+
const directCollection = normalizeCollectionResponse(response.data, normalizeRoleDefinition)
|
|
1885
|
+
if (directCollection.data.length > 0 || directCollection.meta.pagination.has_more) {
|
|
1886
|
+
return directCollection
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const data = readRecord(response.data, 'data')
|
|
1890
|
+
const roles = readArray(response.data, 'data') ?? data?.roles ?? data?.items
|
|
1891
|
+
if (Array.isArray(roles)) {
|
|
1892
|
+
return normalizeInlineCollection(roles, normalizeRoleDefinition)
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
return createEmptyPagination<CircleRoleDefinition>()
|
|
1896
|
+
} catch (error: unknown) {
|
|
1897
|
+
if (this.isHttpStatus(error, 404)) {
|
|
1898
|
+
return createEmptyPagination<CircleRoleDefinition>()
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return await this.handleError(error)
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
public async listRoles(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleRoleDefinition>> {
|
|
1906
|
+
return await this.listRoleDefinitions(circleId, cursor)
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
public async createRoleDefinition(circleId: string, input: CircleRoleDefinitionInput): Promise<CircleRoleDefinition> {
|
|
1910
|
+
try {
|
|
1911
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/roles`, this.sanitizeInput(input))
|
|
1912
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
1913
|
+
const normalized = normalizeRoleDefinition(data)
|
|
1914
|
+
|
|
1915
|
+
if (normalized !== null) {
|
|
1916
|
+
this.emitNotification({
|
|
1917
|
+
description: 'Group role created.',
|
|
1918
|
+
color: 'success',
|
|
1919
|
+
})
|
|
1920
|
+
return normalized
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
throw new Error('Invalid group role payload')
|
|
1924
|
+
} catch (error: unknown) {
|
|
1925
|
+
return await this.handleError(error)
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
public async createRole(circleId: string, input: CircleRoleDefinitionInput): Promise<CircleRoleDefinition> {
|
|
1930
|
+
return await this.createRoleDefinition(circleId, input)
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
public async updateRoleDefinition(circleId: string, roleId: string, input: CircleRoleDefinitionUpdateInput): Promise<CircleRoleDefinition> {
|
|
1934
|
+
try {
|
|
1935
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/roles/${roleId}`, this.sanitizeInput(input))
|
|
1936
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
1937
|
+
const normalized = normalizeRoleDefinition(data)
|
|
1938
|
+
|
|
1939
|
+
if (normalized !== null) {
|
|
1940
|
+
this.emitNotification({
|
|
1941
|
+
description: 'Group role updated.',
|
|
1942
|
+
color: 'success',
|
|
1943
|
+
})
|
|
1944
|
+
return normalized
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
throw new Error('Invalid group role payload')
|
|
1948
|
+
} catch (error: unknown) {
|
|
1949
|
+
return await this.handleError(error)
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
public async updateRole(circleId: string, roleId: string, input: CircleRoleDefinitionUpdateInput): Promise<CircleRoleDefinition> {
|
|
1954
|
+
return await this.updateRoleDefinition(circleId, roleId, input)
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
public async replaceRoleDefinitionPermissions(
|
|
1958
|
+
circleId: string,
|
|
1959
|
+
roleId: string,
|
|
1960
|
+
permissions: CirclePermissionKey[],
|
|
1961
|
+
): Promise<CircleRoleDefinition> {
|
|
1962
|
+
try {
|
|
1963
|
+
const response = await this.client.put(`${this.baseURL}/${circleId}/roles/${roleId}/permissions`, {
|
|
1964
|
+
permissions: this.sanitizeInput(permissions),
|
|
1965
|
+
})
|
|
1966
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
1967
|
+
const normalized = normalizeRoleDefinition(data)
|
|
1968
|
+
|
|
1969
|
+
if (normalized !== null) {
|
|
1970
|
+
this.emitNotification({
|
|
1971
|
+
description: 'Group role permissions updated.',
|
|
1972
|
+
color: 'success',
|
|
1973
|
+
})
|
|
1974
|
+
return normalized
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
throw new Error('Invalid group role payload')
|
|
1978
|
+
} catch (error: unknown) {
|
|
1979
|
+
return await this.handleError(error)
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
public async replaceRolePermissions(
|
|
1984
|
+
circleId: string,
|
|
1985
|
+
roleId: string,
|
|
1986
|
+
permissions: CirclePermissionKey[],
|
|
1987
|
+
): Promise<CircleRoleDefinition> {
|
|
1988
|
+
return await this.replaceRoleDefinitionPermissions(circleId, roleId, permissions)
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
public async archiveRoleDefinition(circleId: string, roleId: string): Promise<void> {
|
|
1992
|
+
try {
|
|
1993
|
+
await this.client.delete(`${this.baseURL}/${circleId}/roles/${roleId}`)
|
|
1994
|
+
this.emitNotification({
|
|
1995
|
+
description: 'Group role archived.',
|
|
1996
|
+
color: 'success',
|
|
1997
|
+
})
|
|
1998
|
+
} catch (error: unknown) {
|
|
1999
|
+
return await this.handleError(error)
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
public async archiveRole(circleId: string, roleId: string): Promise<void> {
|
|
2004
|
+
return await this.archiveRoleDefinition(circleId, roleId)
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
public async assignMemberRoles(
|
|
2008
|
+
circleId: string,
|
|
2009
|
+
userId: string,
|
|
2010
|
+
roleIds: string[],
|
|
2011
|
+
): Promise<CircleMemberRoleAssignmentResult> {
|
|
2012
|
+
try {
|
|
2013
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/members/${userId}/roles`, {
|
|
2014
|
+
role_ids: this.sanitizeInput(roleIds),
|
|
2015
|
+
})
|
|
2016
|
+
const data = readRecord(response.data, 'data')
|
|
2017
|
+
|
|
2018
|
+
return {
|
|
2019
|
+
assigned_roles: Array.isArray(data?.assigned_roles)
|
|
2020
|
+
? data.assigned_roles
|
|
2021
|
+
.map((roleDefinition) => normalizeRoleDefinition(roleDefinition))
|
|
2022
|
+
.filter((roleDefinition): roleDefinition is CircleRoleDefinition => roleDefinition !== null)
|
|
2023
|
+
: [],
|
|
2024
|
+
effective_permissions: normalizePermissionKeys(data?.effective_permissions),
|
|
2025
|
+
}
|
|
2026
|
+
} catch (error: unknown) {
|
|
2027
|
+
return await this.handleError(error)
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
public async removeMemberRole(circleId: string, userId: string, roleId: string): Promise<void> {
|
|
2032
|
+
try {
|
|
2033
|
+
await this.client.delete(`${this.baseURL}/${circleId}/members/${userId}/roles/${roleId}`)
|
|
2034
|
+
this.emitNotification({
|
|
2035
|
+
description: 'Member role removed.',
|
|
2036
|
+
color: 'success',
|
|
2037
|
+
})
|
|
2038
|
+
} catch (error: unknown) {
|
|
2039
|
+
return await this.handleError(error)
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
public async changeMemberRole(circleId: string, userId: string, role: 'admin' | 'member'): Promise<void> {
|
|
2044
|
+
try {
|
|
2045
|
+
try {
|
|
2046
|
+
await this.client.patch(`${this.baseURL}/${circleId}/roles/${userId}`, { role })
|
|
2047
|
+
} catch (error: unknown) {
|
|
2048
|
+
if (!this.isHttpStatus(error, 404) && !this.isHttpStatus(error, 405)) {
|
|
2049
|
+
throw error
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
await this.client.patch(`${this.baseURL}/${circleId}/members/role`, { user_id: userId, role })
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
const roleLabel = role === 'admin' ? 'an admin' : 'a member'
|
|
2056
|
+
this.emitNotification({
|
|
2057
|
+
description: `Member role changed to ${roleLabel}.`,
|
|
2058
|
+
color: 'success',
|
|
2059
|
+
})
|
|
2060
|
+
} catch (error: unknown) {
|
|
2061
|
+
return await this.handleError(error)
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
public async transferOwnership(
|
|
2066
|
+
circleId: string,
|
|
2067
|
+
userId: string,
|
|
2068
|
+
options?: CircleOwnershipTransferOptions,
|
|
2069
|
+
): Promise<CircleOwnershipTransferResult> {
|
|
2070
|
+
try {
|
|
2071
|
+
const payload: Record<string, string | boolean> = {
|
|
2072
|
+
user_id: userId,
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (typeof options?.note === 'string' && options.note.trim().length > 0) {
|
|
2076
|
+
payload.note = this.sanitizeInput(options.note)
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
if (typeof options?.assign_previous_owner_admin === 'boolean') {
|
|
2080
|
+
payload.assign_previous_owner_admin = options.assign_previous_owner_admin
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/owner/transfer`, payload)
|
|
2084
|
+
const data = readRecord(response.data, 'data') ?? {}
|
|
2085
|
+
const resolvedCircleId = readString(data.circle_id)
|
|
2086
|
+
const previousOwnerId = readString(data.previous_owner_id)
|
|
2087
|
+
const newOwnerId = readString(data.new_owner_id)
|
|
2088
|
+
|
|
2089
|
+
if (resolvedCircleId === null || previousOwnerId === null || newOwnerId === null) {
|
|
2090
|
+
throw new Error('Invalid ownership transfer payload')
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
this.emitNotification({
|
|
2094
|
+
description: `${this.getCircleTerm({ case: 'title' })} ownership transferred.`,
|
|
2095
|
+
color: 'success',
|
|
2096
|
+
})
|
|
2097
|
+
|
|
2098
|
+
return {
|
|
2099
|
+
circle_id: resolvedCircleId,
|
|
2100
|
+
previous_owner_id: previousOwnerId,
|
|
2101
|
+
new_owner_id: newOwnerId,
|
|
2102
|
+
}
|
|
2103
|
+
} catch (error: unknown) {
|
|
2104
|
+
return await this.handleError(error)
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
public async removeMember(circleId: string, userId: string): Promise<void> {
|
|
2109
|
+
try {
|
|
2110
|
+
await this.client.delete(`${this.baseURL}/${circleId}/members`, { data: { user_id: userId } })
|
|
2111
|
+
this.emitNotification({
|
|
2112
|
+
description: `Member removed from ${this.getCircleTerm()}.`,
|
|
2113
|
+
color: 'success',
|
|
2114
|
+
})
|
|
2115
|
+
} catch (error: unknown) {
|
|
2116
|
+
return await this.handleError(error)
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
public async banMember(circleId: string, userId: string, options?: CircleBanMemberOptions): Promise<void> {
|
|
2121
|
+
try {
|
|
2122
|
+
const payload: Record<string, string> = { user_id: userId }
|
|
2123
|
+
if (typeof options?.reason === 'string' && options.reason.trim().length > 0) {
|
|
2124
|
+
payload.reason = this.sanitizeInput(options.reason)
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
await this.client.post(`${this.baseURL}/${circleId}/moderation/bans`, payload)
|
|
2128
|
+
this.emitNotification({
|
|
2129
|
+
description: `Member banned from ${this.getCircleTerm()}.`,
|
|
2130
|
+
color: 'success',
|
|
2131
|
+
})
|
|
2132
|
+
} catch (error: unknown) {
|
|
2133
|
+
return await this.handleError(error)
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
public async listBans(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleBanRecord>> {
|
|
2138
|
+
try {
|
|
2139
|
+
const params: Record<string, string> = {}
|
|
2140
|
+
if (cursor) {
|
|
2141
|
+
params.cursor = cursor
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/bans`, { params })
|
|
2145
|
+
return normalizeCollectionResponse(response.data, normalizeBanRecord)
|
|
2146
|
+
} catch (error: unknown) {
|
|
2147
|
+
if (this.isHttpStatus(error, 404)) {
|
|
2148
|
+
return createEmptyPagination<CircleBanRecord>()
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
return await this.handleError(error)
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
public async unbanMember(circleId: string, userId: string): Promise<void> {
|
|
2156
|
+
try {
|
|
2157
|
+
await this.client.delete(`${this.baseURL}/${circleId}/moderation/bans/${userId}`)
|
|
2158
|
+
this.emitNotification({
|
|
2159
|
+
description: `Member unbanned in ${this.getCircleTerm()}.`,
|
|
2160
|
+
color: 'success',
|
|
2161
|
+
})
|
|
2162
|
+
} catch (error: unknown) {
|
|
2163
|
+
return await this.handleError(error)
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
public async muteMember(
|
|
2168
|
+
circleId: string,
|
|
2169
|
+
userId: string,
|
|
2170
|
+
options?: CircleMuteMemberOptions,
|
|
2171
|
+
): Promise<void> {
|
|
2172
|
+
try {
|
|
2173
|
+
const payload: Record<string, string> = { user_id: userId }
|
|
2174
|
+
if (typeof options?.reason === 'string' && options.reason.trim().length > 0) {
|
|
2175
|
+
payload.reason = this.sanitizeInput(options.reason)
|
|
2176
|
+
}
|
|
2177
|
+
if (typeof options?.muted_until === 'string' && options.muted_until.trim().length > 0) {
|
|
2178
|
+
payload.muted_until = options.muted_until.trim()
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
await this.client.post(`${this.baseURL}/${circleId}/moderation/mutes`, payload)
|
|
2182
|
+
this.emitNotification({
|
|
2183
|
+
description: `Member muted in ${this.getCircleTerm()}.`,
|
|
2184
|
+
color: 'success',
|
|
2185
|
+
})
|
|
2186
|
+
} catch (error: unknown) {
|
|
2187
|
+
return await this.handleError(error)
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
public async listMutes(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleMuteRecord>> {
|
|
2192
|
+
try {
|
|
2193
|
+
const params: Record<string, string> = {}
|
|
2194
|
+
if (cursor) {
|
|
2195
|
+
params.cursor = cursor
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/mutes`, { params })
|
|
2199
|
+
return normalizeCollectionResponse(response.data, normalizeMuteRecord)
|
|
2200
|
+
} catch (error: unknown) {
|
|
2201
|
+
if (this.isHttpStatus(error, 404)) {
|
|
2202
|
+
return createEmptyPagination<CircleMuteRecord>()
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return await this.handleError(error)
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
public async unmuteMember(circleId: string, userId: string): Promise<void> {
|
|
2210
|
+
try {
|
|
2211
|
+
await this.client.delete(`${this.baseURL}/${circleId}/moderation/mutes/${userId}`)
|
|
2212
|
+
this.emitNotification({
|
|
2213
|
+
description: `Member unmuted in ${this.getCircleTerm()}.`,
|
|
2214
|
+
color: 'success',
|
|
2215
|
+
})
|
|
2216
|
+
} catch (error: unknown) {
|
|
2217
|
+
return await this.handleError(error)
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
public async reportMember(
|
|
2222
|
+
circleId: string,
|
|
2223
|
+
subjectUserId: string,
|
|
2224
|
+
category: string,
|
|
2225
|
+
notes?: string,
|
|
2226
|
+
): Promise<{ report_id: string; status: string }> {
|
|
2227
|
+
try {
|
|
2228
|
+
const payload: Record<string, string> = {
|
|
2229
|
+
subject_user_id: subjectUserId,
|
|
2230
|
+
category: this.sanitizeInput(category),
|
|
2231
|
+
}
|
|
2232
|
+
if (typeof notes === 'string' && notes.trim().length > 0) {
|
|
2233
|
+
payload.notes = this.sanitizeInput(notes)
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/moderation/reports`, payload)
|
|
2237
|
+
const data = readRecord(response.data, 'data') ?? {}
|
|
2238
|
+
const reportId = readString(data.report_id)
|
|
2239
|
+
const status = readString(data.status)
|
|
2240
|
+
|
|
2241
|
+
if (reportId === null || status === null) {
|
|
2242
|
+
throw new Error('Invalid moderation report payload')
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
this.emitNotification({
|
|
2246
|
+
description: 'Report submitted for review.',
|
|
2247
|
+
color: 'success',
|
|
2248
|
+
})
|
|
2249
|
+
|
|
2250
|
+
return {
|
|
2251
|
+
report_id: reportId,
|
|
2252
|
+
status,
|
|
2253
|
+
}
|
|
2254
|
+
} catch (error: unknown) {
|
|
2255
|
+
return await this.handleError(error)
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
public async listModerationReports(
|
|
2260
|
+
circleId: string,
|
|
2261
|
+
cursor?: string | null,
|
|
2262
|
+
status?: CircleReportStatus,
|
|
2263
|
+
): Promise<PaginationResponse<CircleModerationReport>> {
|
|
2264
|
+
try {
|
|
2265
|
+
const params: Record<string, string> = {}
|
|
2266
|
+
if (cursor) {
|
|
2267
|
+
params.cursor = cursor
|
|
2268
|
+
}
|
|
2269
|
+
if (status) {
|
|
2270
|
+
params.status = status
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/reports`, { params })
|
|
2274
|
+
return normalizeCollectionResponse(response.data, normalizeModerationReport)
|
|
2275
|
+
} catch (error: unknown) {
|
|
2276
|
+
return await this.handleError(error)
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
public async resolveModerationReport(
|
|
2281
|
+
circleId: string,
|
|
2282
|
+
reportId: string,
|
|
2283
|
+
decision: CircleReportDecision,
|
|
2284
|
+
resolutionNotes?: string,
|
|
2285
|
+
): Promise<{ report_id: string; status: string }> {
|
|
2286
|
+
try {
|
|
2287
|
+
const payload: Record<string, string> = { decision }
|
|
2288
|
+
if (typeof resolutionNotes === 'string' && resolutionNotes.trim().length > 0) {
|
|
2289
|
+
payload.resolution_notes = this.sanitizeInput(resolutionNotes)
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/moderation/reports/${reportId}`, payload)
|
|
2293
|
+
const data = readRecord(response.data, 'data') ?? {}
|
|
2294
|
+
const resolvedReportId = readString(data.report_id) ?? reportId
|
|
2295
|
+
const status = readString(data.status)
|
|
2296
|
+
|
|
2297
|
+
if (status === null) {
|
|
2298
|
+
throw new Error('Invalid moderation resolution payload')
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
this.emitNotification({
|
|
2302
|
+
description: `Report ${decision === 'action' ? 'actioned' : 'dismissed'}.`,
|
|
2303
|
+
color: 'success',
|
|
2304
|
+
})
|
|
2305
|
+
|
|
2306
|
+
return {
|
|
2307
|
+
report_id: resolvedReportId,
|
|
2308
|
+
status,
|
|
2309
|
+
}
|
|
2310
|
+
} catch (error: unknown) {
|
|
2311
|
+
return await this.handleError(error)
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
public async getModerationAuditLog(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleModerationAuditLogEntry>> {
|
|
2316
|
+
try {
|
|
2317
|
+
const params: Record<string, string> = {}
|
|
2318
|
+
if (cursor) {
|
|
2319
|
+
params.cursor = cursor
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/audit-log`, { params })
|
|
2323
|
+
return normalizeCollectionResponse(response.data, normalizeAuditLogEntry)
|
|
2324
|
+
} catch (error: unknown) {
|
|
2325
|
+
return await this.handleError(error)
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
public async listModerationSubjects(
|
|
2330
|
+
circleId: string,
|
|
2331
|
+
subjectType: CircleModerationSubjectType,
|
|
2332
|
+
cursor?: string | null,
|
|
2333
|
+
state?: CircleModerationSubjectState | 'all',
|
|
2334
|
+
): Promise<PaginationResponse<CircleModerationSubject>> {
|
|
2335
|
+
try {
|
|
2336
|
+
const params: Record<string, string> = {
|
|
2337
|
+
subject_type: subjectType,
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
if (cursor) {
|
|
2341
|
+
params.cursor = cursor
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
if (state) {
|
|
2345
|
+
params.state = state
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/moderation/subjects`, { params })
|
|
2349
|
+
return normalizeCollectionResponse(response.data, normalizeModerationSubject)
|
|
2350
|
+
} catch (error: unknown) {
|
|
2351
|
+
return await this.handleError(error)
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
public async actOnModerationSubject(
|
|
2356
|
+
circleId: string,
|
|
2357
|
+
subjectType: CircleModerationSubjectType,
|
|
2358
|
+
subjectId: string,
|
|
2359
|
+
action: CircleModerationSubjectAction,
|
|
2360
|
+
reason?: string,
|
|
2361
|
+
): Promise<void> {
|
|
2362
|
+
try {
|
|
2363
|
+
const payload: Record<string, string> = { action }
|
|
2364
|
+
if (typeof reason === 'string' && reason.trim().length > 0) {
|
|
2365
|
+
payload.reason = this.sanitizeInput(reason)
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
await this.client.post(`${this.baseURL}/${circleId}/moderation/subjects/${subjectType}/${subjectId}/actions`, payload)
|
|
2369
|
+
|
|
2370
|
+
this.emitNotification({
|
|
2371
|
+
description: `Moderation action "${action}" applied.`,
|
|
2372
|
+
color: 'success',
|
|
2373
|
+
})
|
|
2374
|
+
} catch (error: unknown) {
|
|
2375
|
+
return await this.handleError(error)
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
public async listAutomodRules(circleId: string, cursor?: string | null): Promise<PaginationResponse<CircleAutomodRule>> {
|
|
2380
|
+
try {
|
|
2381
|
+
const params: Record<string, string> = {}
|
|
2382
|
+
if (cursor) {
|
|
2383
|
+
params.cursor = cursor
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/automod-rules`, { params })
|
|
2387
|
+
return normalizeCollectionResponse(response.data, normalizeAutomodRule)
|
|
2388
|
+
} catch (error: unknown) {
|
|
2389
|
+
return await this.handleError(error)
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
public async getAutomodRule(circleId: string, ruleId: string): Promise<CircleAutomodRule> {
|
|
2394
|
+
try {
|
|
2395
|
+
const response = await this.client.get(`${this.baseURL}/${circleId}/automod-rules/${ruleId}`)
|
|
2396
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
2397
|
+
const normalized = normalizeAutomodRule(data)
|
|
2398
|
+
|
|
2399
|
+
if (normalized !== null) {
|
|
2400
|
+
return normalized
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
throw new Error('Invalid automod rule payload')
|
|
2404
|
+
} catch (error: unknown) {
|
|
2405
|
+
return await this.handleError(error)
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
public async createAutomodRule(circleId: string, input: CircleAutomodRuleInput): Promise<CircleAutomodRule> {
|
|
2410
|
+
try {
|
|
2411
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/automod-rules`, this.sanitizeInput(input))
|
|
2412
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
2413
|
+
const normalized = normalizeAutomodRule(data)
|
|
2414
|
+
|
|
2415
|
+
if (normalized !== null) {
|
|
2416
|
+
this.emitNotification({
|
|
2417
|
+
description: 'Automoderation rule created.',
|
|
2418
|
+
color: 'success',
|
|
2419
|
+
})
|
|
2420
|
+
return normalized
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
throw new Error('Invalid automod rule payload')
|
|
2424
|
+
} catch (error: unknown) {
|
|
2425
|
+
return await this.handleError(error)
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
public async updateAutomodRule(circleId: string, ruleId: string, input: Partial<CircleAutomodRuleInput>): Promise<CircleAutomodRule> {
|
|
2430
|
+
try {
|
|
2431
|
+
const response = await this.client.patch(`${this.baseURL}/${circleId}/automod-rules/${ruleId}`, this.sanitizeInput(input))
|
|
2432
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
2433
|
+
const normalized = normalizeAutomodRule(data)
|
|
2434
|
+
|
|
2435
|
+
if (normalized !== null) {
|
|
2436
|
+
this.emitNotification({
|
|
2437
|
+
description: 'Automoderation rule updated.',
|
|
2438
|
+
color: 'success',
|
|
2439
|
+
})
|
|
2440
|
+
return normalized
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
throw new Error('Invalid automod rule payload')
|
|
2444
|
+
} catch (error: unknown) {
|
|
2445
|
+
return await this.handleError(error)
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
public async deleteAutomodRule(circleId: string, ruleId: string): Promise<void> {
|
|
2450
|
+
try {
|
|
2451
|
+
await this.client.delete(`${this.baseURL}/${circleId}/automod-rules/${ruleId}`)
|
|
2452
|
+
this.emitNotification({
|
|
2453
|
+
description: 'Automoderation rule deleted.',
|
|
2454
|
+
color: 'success',
|
|
2455
|
+
})
|
|
2456
|
+
} catch (error: unknown) {
|
|
2457
|
+
return await this.handleError(error)
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
public async getManagementBootstrap(circleId: string): Promise<CircleManagementBootstrap> {
|
|
2462
|
+
const identifier = this.sanitizeInput(circleId)
|
|
2463
|
+
let resolvedCircle: Circle | null = null
|
|
2464
|
+
const bootstrapIdentifier = this.isUuid(identifier)
|
|
2465
|
+
? identifier
|
|
2466
|
+
: await (async (): Promise<string> => {
|
|
2467
|
+
resolvedCircle = await this.get(identifier)
|
|
2468
|
+
return resolvedCircle.id
|
|
2469
|
+
})()
|
|
2470
|
+
|
|
2471
|
+
try {
|
|
2472
|
+
const response = await this.client.get(`${this.baseURL}/${bootstrapIdentifier}/management/bootstrap`)
|
|
2473
|
+
const data = readRecord(response.data, 'data')
|
|
2474
|
+
const bootstrapCircle = data?.circle
|
|
2475
|
+
const bootstrapManagementSections = normalizeManagementSections(data?.management_sections)
|
|
2476
|
+
const directCircle = normalizeCircle(bootstrapCircle ?? data)
|
|
2477
|
+
const mergedCircleSource = directCircle === null
|
|
2478
|
+
? {
|
|
2479
|
+
...(resolvedCircle ?? await this.get(bootstrapIdentifier)),
|
|
2480
|
+
...(bootstrapCircle ?? {}),
|
|
2481
|
+
actor_capabilities: data?.actor_capabilities,
|
|
2482
|
+
management_sections: bootstrapManagementSections.length > 0
|
|
2483
|
+
? bootstrapManagementSections
|
|
2484
|
+
: undefined,
|
|
2485
|
+
}
|
|
2486
|
+
: {
|
|
2487
|
+
...directCircle,
|
|
2488
|
+
actor_capabilities: data?.actor_capabilities ?? directCircle.actor_capabilities,
|
|
2489
|
+
management_sections: bootstrapManagementSections.length > 0
|
|
2490
|
+
? bootstrapManagementSections
|
|
2491
|
+
: directCircle.management_sections,
|
|
2492
|
+
}
|
|
2493
|
+
const circle = normalizeCircle(mergedCircleSource)
|
|
2494
|
+
|
|
2495
|
+
if (circle !== null) {
|
|
2496
|
+
const actor = circle.actor ?? buildActorContext(circle)
|
|
2497
|
+
return {
|
|
2498
|
+
circle,
|
|
2499
|
+
actor,
|
|
2500
|
+
management_sections: bootstrapManagementSections.length > 0
|
|
2501
|
+
? bootstrapManagementSections
|
|
2502
|
+
: actor.managementSections,
|
|
2503
|
+
members: normalizeCollectionResponse(data?.members ?? {}, normalizeCircleMember),
|
|
2504
|
+
requests: normalizeCollectionResponse(data?.requests ?? {}, normalizeJoinRequest),
|
|
2505
|
+
roles: Array.isArray(data?.roles)
|
|
2506
|
+
? normalizeInlineCollection(data.roles, normalizeRoleDefinition)
|
|
2507
|
+
: normalizeCollectionResponse(data?.roles ?? {}, normalizeRoleDefinition),
|
|
2508
|
+
counts: normalizeManagementCounts(data?.counts),
|
|
2509
|
+
permission_catalog: Array.isArray(data?.permission_catalog)
|
|
2510
|
+
? data.permission_catalog
|
|
2511
|
+
.map((entry) => normalizePermissionCatalogEntry(entry))
|
|
2512
|
+
.filter((entry): entry is CirclePermissionCatalogEntry => entry !== null)
|
|
2513
|
+
: [],
|
|
2514
|
+
reports: normalizeCollectionResponse(data?.reports ?? {}, normalizeModerationReport),
|
|
2515
|
+
bans: normalizeCollectionResponse(data?.bans ?? {}, normalizeBanRecord),
|
|
2516
|
+
mutes: normalizeCollectionResponse(data?.mutes ?? {}, normalizeMuteRecord),
|
|
2517
|
+
audit: normalizeCollectionResponse(data?.audit ?? {}, normalizeAuditLogEntry),
|
|
2518
|
+
automod: normalizeCollectionResponse(data?.automod ?? {}, normalizeAutomodRule),
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
} catch (error: unknown) {
|
|
2522
|
+
if (!this.isHttpStatus(error, 404)) {
|
|
2523
|
+
return await this.handleError(error)
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
const circle = resolvedCircle ?? await this.get(identifier)
|
|
2528
|
+
const actor = circle.actor ?? buildActorContext(circle)
|
|
2529
|
+
|
|
2530
|
+
if (!actor.capabilities.canViewManagement) {
|
|
2531
|
+
return {
|
|
2532
|
+
circle,
|
|
2533
|
+
actor,
|
|
2534
|
+
management_sections: actor.managementSections,
|
|
2535
|
+
members: createEmptyPagination<CircleMember>(),
|
|
2536
|
+
requests: createEmptyPagination<JoinRequest>(),
|
|
2537
|
+
roles: createEmptyPagination<CircleRoleDefinition>(),
|
|
2538
|
+
counts: createEmptyManagementCounts(),
|
|
2539
|
+
permission_catalog: [],
|
|
2540
|
+
reports: createEmptyPagination<CircleModerationReport>(),
|
|
2541
|
+
bans: createEmptyPagination<CircleBanRecord>(),
|
|
2542
|
+
mutes: createEmptyPagination<CircleMuteRecord>(),
|
|
2543
|
+
audit: createEmptyPagination<CircleModerationAuditLogEntry>(),
|
|
2544
|
+
automod: createEmptyPagination<CircleAutomodRule>(),
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const requestsPromise = actor.capabilities.canReviewRequests
|
|
2549
|
+
? this.listJoinRequests(circle.id, null, 'pending')
|
|
2550
|
+
: Promise.resolve(createEmptyPagination<JoinRequest>())
|
|
2551
|
+
|
|
2552
|
+
const [
|
|
2553
|
+
members,
|
|
2554
|
+
requests,
|
|
2555
|
+
roles,
|
|
2556
|
+
reports,
|
|
2557
|
+
bans,
|
|
2558
|
+
mutes,
|
|
2559
|
+
audit,
|
|
2560
|
+
automod,
|
|
2561
|
+
] = await Promise.all([
|
|
2562
|
+
this.getMembers(circle.id),
|
|
2563
|
+
requestsPromise,
|
|
2564
|
+
this.listRoles(circle.id),
|
|
2565
|
+
this.listModerationReports(circle.id, null, 'pending'),
|
|
2566
|
+
this.listBans(circle.id),
|
|
2567
|
+
this.listMutes(circle.id),
|
|
2568
|
+
this.getModerationAuditLog(circle.id),
|
|
2569
|
+
this.listAutomodRules(circle.id),
|
|
2570
|
+
])
|
|
2571
|
+
|
|
2572
|
+
return {
|
|
2573
|
+
circle,
|
|
2574
|
+
actor,
|
|
2575
|
+
management_sections: actor.managementSections,
|
|
2576
|
+
members,
|
|
2577
|
+
requests,
|
|
2578
|
+
roles,
|
|
2579
|
+
counts: {
|
|
2580
|
+
members: circle.member_count ?? members.data.length,
|
|
2581
|
+
requests_pending: requests.data.length,
|
|
2582
|
+
reports_pending: reports.data.length,
|
|
2583
|
+
roles: roles.data.length,
|
|
2584
|
+
bans_active: bans.data.length,
|
|
2585
|
+
mutes_active: mutes.data.length,
|
|
2586
|
+
},
|
|
2587
|
+
permission_catalog: [],
|
|
2588
|
+
reports,
|
|
2589
|
+
bans,
|
|
2590
|
+
mutes,
|
|
2591
|
+
audit,
|
|
2592
|
+
automod,
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
public async searchInviteCandidates(circleId: string, query: string): Promise<CircleInviteSearchUser[]> {
|
|
2597
|
+
void circleId
|
|
2598
|
+
|
|
2599
|
+
try {
|
|
2600
|
+
const sanitizedQuery = query.trim()
|
|
2601
|
+
if (sanitizedQuery.length < 2) {
|
|
2602
|
+
return []
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
const response = await this.client.get('/v1/search/aggregate', {
|
|
2606
|
+
params: {
|
|
2607
|
+
q: sanitizedQuery,
|
|
2608
|
+
'types[]': 'user',
|
|
2609
|
+
limit: 10,
|
|
2610
|
+
},
|
|
2611
|
+
})
|
|
2612
|
+
|
|
2613
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
2614
|
+
const results = readRecord(data, 'results')
|
|
2615
|
+
const byType = readRecord(results, 'by_type')
|
|
2616
|
+
const users = Array.isArray(byType?.user) ? byType.user : []
|
|
2617
|
+
|
|
2618
|
+
return users
|
|
2619
|
+
.map((entry): CircleInviteSearchUser | null => {
|
|
2620
|
+
if (!isRecord(entry)) {
|
|
2621
|
+
return null
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
const id = readString(entry.id)
|
|
2625
|
+
const name = readString(entry.title)
|
|
2626
|
+
const meta = readRecord(entry, 'meta')
|
|
2627
|
+
const handle = readString(meta?.handle)
|
|
2628
|
+
|
|
2629
|
+
if (id === null || name === null || handle === null) {
|
|
2630
|
+
return null
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
return {
|
|
2634
|
+
id,
|
|
2635
|
+
name,
|
|
2636
|
+
handle,
|
|
2637
|
+
avatar_url: readNullableString(meta?.avatar_url),
|
|
2638
|
+
}
|
|
2639
|
+
})
|
|
2640
|
+
.filter((entry): entry is CircleInviteSearchUser => entry !== null)
|
|
2641
|
+
} catch (error: unknown) {
|
|
2642
|
+
return await this.handleError(error)
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
public async createInviteLink(
|
|
2647
|
+
circleId: string,
|
|
2648
|
+
options?: CircleInviteLinkOptions,
|
|
2649
|
+
requestOptions?: CircleInviteLinkRequestOptions,
|
|
2650
|
+
): Promise<InviteLink> {
|
|
2651
|
+
try {
|
|
2652
|
+
const body: Record<string, number | string> = {}
|
|
2653
|
+
if (typeof options?.max_uses === 'number') {
|
|
2654
|
+
body.max_uses = options.max_uses
|
|
2655
|
+
}
|
|
2656
|
+
if (typeof options?.expires_in_minutes === 'number') {
|
|
2657
|
+
body.expires_in_minutes = options.expires_in_minutes
|
|
2658
|
+
}
|
|
2659
|
+
if (typeof options?.target_user_id === 'string' && options.target_user_id.trim() !== '') {
|
|
2660
|
+
body.target_user_id = options.target_user_id
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
const response = await this.client.post(`${this.baseURL}/${circleId}/invite-links`, body)
|
|
2664
|
+
const data = readRecord(response.data, 'data') ?? (isRecord(response.data) ? response.data : null)
|
|
2665
|
+
const normalized = normalizeInviteLink(data)
|
|
2666
|
+
|
|
2667
|
+
if (normalized !== null) {
|
|
2668
|
+
if (requestOptions?.showSuccessToast !== false) {
|
|
2669
|
+
this.emitNotification({
|
|
2670
|
+
description: `Invite link created for ${this.getCircleTerm()}!`,
|
|
2671
|
+
color: 'success',
|
|
2672
|
+
})
|
|
2673
|
+
}
|
|
2674
|
+
return normalized
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
throw new Error('Invalid invite link payload')
|
|
2678
|
+
} catch (error: unknown) {
|
|
2679
|
+
return await this.handleError(error)
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
public async joinViaInvite(token: string): Promise<{ circle_id: string; requested: boolean }> {
|
|
2684
|
+
try {
|
|
2685
|
+
const response = await this.client.post(`/v1/circle-invites/${encodeURIComponent(token)}/join`)
|
|
2686
|
+
const data = readRecord(response.data, 'data') ?? {}
|
|
2687
|
+
const circleId = readString(data.circle_id)
|
|
2688
|
+
const requested = readBoolean(data.requested)
|
|
2689
|
+
|
|
2690
|
+
if (circleId === null || requested === null) {
|
|
2691
|
+
throw new Error('Invalid invite accept payload')
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
return {
|
|
2695
|
+
circle_id: circleId,
|
|
2696
|
+
requested,
|
|
2697
|
+
}
|
|
2698
|
+
} catch (error: unknown) {
|
|
2699
|
+
return await this.handleError(error)
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
public async declineInvite(token: string): Promise<void> {
|
|
2704
|
+
try {
|
|
2705
|
+
await this.client.post(`/v1/circle-invites/${encodeURIComponent(token)}/decline`)
|
|
2706
|
+
} catch (error: unknown) {
|
|
2707
|
+
return await this.handleError(error)
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
public async checkSlugAvailability(slug: string): Promise<boolean> {
|
|
2712
|
+
try {
|
|
2713
|
+
const sanitizedSlug = this.sanitizeInput(slug)
|
|
2714
|
+
const response = await this.client.get(`${this.baseURL}/check-slug`, {
|
|
2715
|
+
params: { slug: sanitizedSlug },
|
|
2716
|
+
})
|
|
2717
|
+
|
|
2718
|
+
const data = readRecord(response.data, 'data')
|
|
2719
|
+
const payloadAvailability = readBoolean(data?.available)
|
|
2720
|
+
const rootAvailability = readBoolean(isRecord(response.data) ? response.data.available : undefined)
|
|
2721
|
+
|
|
2722
|
+
return payloadAvailability ?? rootAvailability ?? true
|
|
2723
|
+
} catch (error: unknown) {
|
|
2724
|
+
if (this.isHttpStatus(error, 404)) {
|
|
2725
|
+
return true
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
return await this.handleError(error)
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
public generateSlug(name: string): string {
|
|
2733
|
+
if (name.trim().length === 0) {
|
|
2734
|
+
return ''
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
return this.sanitizeInput(name)
|
|
2738
|
+
.toLowerCase()
|
|
2739
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
2740
|
+
.replace(/\s+/g, '-')
|
|
2741
|
+
.replace(/-+/g, '-')
|
|
2742
|
+
.replace(/^-|-$/g, '')
|
|
2743
|
+
.substring(0, 140)
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
export function createCirclesService(config: CirclesServiceConfig) {
|
|
2748
|
+
const {
|
|
2749
|
+
client,
|
|
2750
|
+
sanitizeString = defaultSanitizeString,
|
|
2751
|
+
onNotify,
|
|
2752
|
+
onUnauthorized,
|
|
2753
|
+
getCircleTerm = defaultGetCircleTerm,
|
|
2754
|
+
onError,
|
|
2755
|
+
} = config
|
|
2756
|
+
|
|
2757
|
+
return new CirclesService(
|
|
2758
|
+
client,
|
|
2759
|
+
sanitizeString,
|
|
2760
|
+
onNotify,
|
|
2761
|
+
onUnauthorized,
|
|
2762
|
+
getCircleTerm,
|
|
2763
|
+
onError,
|
|
2764
|
+
)
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
export type CirclesServiceInstance = ReturnType<typeof createCirclesService>
|