@codybrom/denim 1.3.6 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/publish.yml +17 -7
  2. package/.vscode/settings.json +34 -9
  3. package/CHANGELOG.md +137 -0
  4. package/deno.json +22 -8
  5. package/deno.lock +17 -59
  6. package/examples/edge-function.ts +171 -177
  7. package/mod.ts +138 -635
  8. package/mod_test.ts +1287 -431
  9. package/package.json +22 -22
  10. package/readme.md +155 -191
  11. package/src/api/createCarouselItem.ts +86 -0
  12. package/src/api/createThreadsContainer.ts +122 -0
  13. package/src/api/debugToken.ts +35 -0
  14. package/src/api/deleteThread.ts +36 -0
  15. package/src/api/exchangeCodeForToken.ts +50 -0
  16. package/src/api/exchangeToken.ts +36 -0
  17. package/src/api/getAppAccessToken.ts +35 -0
  18. package/src/api/getConversation.ts +51 -0
  19. package/src/api/getGhostPosts.ts +50 -0
  20. package/src/api/getLocation.ts +38 -0
  21. package/src/api/getMediaInsights.ts +39 -0
  22. package/src/api/getMentions.ts +57 -0
  23. package/src/api/getOEmbed.ts +41 -0
  24. package/src/api/getProfile.ts +46 -0
  25. package/src/api/getProfilePosts.ts +53 -0
  26. package/src/api/getPublishingLimit.ts +59 -0
  27. package/src/api/getReplies.ts +51 -0
  28. package/src/api/getSingleThread.ts +37 -0
  29. package/src/api/getThreadsList.ts +49 -0
  30. package/src/api/getUserInsights.ts +54 -0
  31. package/src/api/getUserReplies.ts +54 -0
  32. package/src/api/lookupProfile.ts +53 -0
  33. package/src/api/manageReply.ts +41 -0
  34. package/src/api/publishThreadsContainer.ts +107 -0
  35. package/src/api/refreshToken.ts +33 -0
  36. package/src/api/repost.ts +38 -0
  37. package/src/api/searchKeyword.ts +86 -0
  38. package/src/api/searchLocations.ts +46 -0
  39. package/src/constants.ts +80 -0
  40. package/src/types.ts +925 -0
  41. package/src/utils/checkContainerStatus.ts +39 -0
  42. package/src/utils/getAPI.ts +13 -0
  43. package/src/utils/mock_threads_api.ts +582 -0
  44. package/src/utils/validateRequest.ts +166 -0
  45. package/mock_threads_api.ts +0 -174
  46. package/types.ts +0 -235
package/src/types.ts ADDED
@@ -0,0 +1,925 @@
1
+ // types.ts
2
+
3
+ // ─── Media & Reply Control ───────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Media types used when creating posts.
7
+ */
8
+ export type MediaType = "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL";
9
+
10
+ /**
11
+ * All media type values that can appear in API responses.
12
+ * Responses use TEXT_POST (not TEXT) and CAROUSEL_ALBUM (not CAROUSEL).
13
+ */
14
+ export type ResponseMediaType =
15
+ | "TEXT_POST"
16
+ | "IMAGE"
17
+ | "VIDEO"
18
+ | "CAROUSEL_ALBUM"
19
+ | "GIF"
20
+ | "REPOST_FACADE"
21
+ | "AUDIO";
22
+
23
+ /**
24
+ * Represents the options for controlling who can reply to a post.
25
+ */
26
+ export type ReplyControl =
27
+ | "everyone"
28
+ | "accounts_you_follow"
29
+ | "mentioned_only"
30
+ | "parent_post_author_only"
31
+ | "followers_only";
32
+
33
+ // ─── Input Types (what callers pass in) ──────────────────────────────────────
34
+
35
+ /**
36
+ * Options for poll attachments when creating a post.
37
+ * Properties use snake_case to match the Threads API payload format directly.
38
+ */
39
+ export interface PollAttachmentInput {
40
+ /** First poll option (required) */
41
+ option_a: string;
42
+ /** Second poll option (required) */
43
+ option_b: string;
44
+ /** Third poll option (optional) */
45
+ option_c?: string;
46
+ /** Fourth poll option (optional) */
47
+ option_d?: string;
48
+ }
49
+
50
+ /**
51
+ * Represents a text entity for spoiler/styling annotations.
52
+ * Properties use snake_case to match the Threads API payload format directly.
53
+ */
54
+ export interface TextEntity {
55
+ /** The type of entity (e.g., "spoiler") */
56
+ entity_type: string;
57
+ /** Character offset where the entity starts */
58
+ offset: number;
59
+ /** Length of the entity in characters */
60
+ length: number;
61
+ }
62
+
63
+ /**
64
+ * Input for text attachments (long-form text posts).
65
+ * Properties use snake_case to match the Threads API payload format directly.
66
+ */
67
+ export interface TextAttachmentInput {
68
+ /** The plaintext content */
69
+ plaintext: string;
70
+ /** Optional URL to attach */
71
+ link_attachment_url?: string;
72
+ /** Optional styled text with formatting info */
73
+ text_with_styling_info?: Array<{
74
+ offset: number;
75
+ length: number;
76
+ styling_info: string[];
77
+ }>;
78
+ }
79
+
80
+ /**
81
+ * Input for GIF attachments.
82
+ * Properties use snake_case to match the Threads API payload format directly.
83
+ */
84
+ export interface GifAttachment {
85
+ /** The GIF ID from the provider */
86
+ gif_id: string;
87
+ /** The GIF provider */
88
+ provider: "TENOR";
89
+ }
90
+
91
+ /**
92
+ * Represents a request to post content on Threads.
93
+ */
94
+ export interface ThreadsPostRequest {
95
+ /** The user ID of the Threads account */
96
+ userId: string;
97
+ /** The access token for authentication */
98
+ accessToken: string;
99
+ /** The type of media being posted */
100
+ mediaType: MediaType;
101
+ /** The text content of the post (optional) */
102
+ text?: string;
103
+ /** The URL of the image to be posted (optional, for IMAGE type) */
104
+ imageUrl?: string;
105
+ /** The URL of the video to be posted (optional, for VIDEO type) */
106
+ videoUrl?: string;
107
+ /** The accessibility text for the image or video (optional) */
108
+ altText?: string;
109
+ /** The URL to be attached as a link to the post (optional, for text posts only) */
110
+ linkAttachment?: string;
111
+ /** List of country codes where the post should be visible (optional) */
112
+ allowlistedCountryCodes?: string[];
113
+ /** Controls who can reply to the post (optional) */
114
+ replyControl?: ReplyControl;
115
+ /** Array of carousel item IDs (required for CAROUSEL type) */
116
+ children?: string[];
117
+ /** ID of the post to reply to (optional) */
118
+ replyToId?: string;
119
+ /** ID of the post to quote (optional) */
120
+ quotePostId?: string;
121
+ /** Poll options (optional, TEXT posts only) */
122
+ pollAttachment?: PollAttachmentInput;
123
+ /** Auto-publish text posts without the separate publish step (optional, TEXT only) */
124
+ autoPublishText?: boolean;
125
+ /** Topic tag for the post (optional, no periods or ampersands) */
126
+ topicTag?: string;
127
+ /** Whether the media is a spoiler (optional) */
128
+ isSpoilerMedia?: boolean;
129
+ /** Text entity annotations for spoilers/styling (optional, max 10) */
130
+ textEntities?: TextEntity[];
131
+ /** Long-form text attachment (optional, TEXT posts only) */
132
+ textAttachment?: TextAttachmentInput;
133
+ /** GIF attachment (optional, TEXT posts only) */
134
+ gifAttachment?: GifAttachment;
135
+ /** Whether the post is a ghost post (optional, TEXT only, cannot be used with replyToId) */
136
+ isGhostPost?: boolean;
137
+ /** Location ID to tag (optional) */
138
+ locationId?: string;
139
+ }
140
+
141
+ /**
142
+ * Pagination options shared across list endpoints.
143
+ */
144
+ export interface PaginationOptions {
145
+ /** Start date (Unix timestamp or strtotime-parseable string) */
146
+ since?: string | number;
147
+ /** End date (Unix timestamp or strtotime-parseable string) */
148
+ until?: string | number;
149
+ /** Maximum number of results (default 25, max 100) */
150
+ limit?: number;
151
+ /** Cursor for previous page */
152
+ before?: string;
153
+ /** Cursor for next page */
154
+ after?: string;
155
+ }
156
+
157
+ /**
158
+ * Cursor-based pagination options for endpoints that only support before/after.
159
+ * Used by media-level reply and conversation endpoints.
160
+ */
161
+ export interface CursorPaginationOptions {
162
+ /** Cursor for previous page */
163
+ before?: string;
164
+ /** Cursor for next page */
165
+ after?: string;
166
+ }
167
+
168
+ // ─── Response Types (snake_case — matching API reality) ──────────────────────
169
+
170
+ /**
171
+ * Poll attachment as returned by the API.
172
+ */
173
+ export interface PollAttachment {
174
+ /** First poll option text */
175
+ option_a?: string;
176
+ /** Second poll option text */
177
+ option_b?: string;
178
+ /** Third poll option text */
179
+ option_c?: string;
180
+ /** Fourth poll option text */
181
+ option_d?: string;
182
+ /** Percentage of votes for option A (0-1) */
183
+ option_a_votes_percentage?: number;
184
+ /** Percentage of votes for option B (0-1) */
185
+ option_b_votes_percentage?: number;
186
+ /** Percentage of votes for option C (0-1) */
187
+ option_c_votes_percentage?: number;
188
+ /** Percentage of votes for option D (0-1) */
189
+ option_d_votes_percentage?: number;
190
+ /** Total number of votes */
191
+ total_votes?: number;
192
+ /** Timestamp when the poll expires (ISO 8601) */
193
+ expiration_timestamp?: string;
194
+ }
195
+
196
+ /**
197
+ * Text attachment as returned by the API.
198
+ */
199
+ export interface TextAttachment {
200
+ /** The plaintext content */
201
+ plaintext?: string;
202
+ /** Attached link URL */
203
+ link_attachment_url?: string;
204
+ /** Styled text with formatting info */
205
+ text_with_styling_info?: Array<{
206
+ /** Character offset */
207
+ offset: number;
208
+ /** Length of styled range */
209
+ length: number;
210
+ /** Styling type (e.g., "bold", "italic") */
211
+ styling_info: string[];
212
+ }>;
213
+ }
214
+
215
+ /**
216
+ * Location as returned by the API.
217
+ */
218
+ export interface ThreadsLocation {
219
+ /** Location ID */
220
+ id: string;
221
+ /** Location name */
222
+ name?: string;
223
+ /** Street address */
224
+ address?: string;
225
+ /** City */
226
+ city?: string;
227
+ /** Country */
228
+ country?: string;
229
+ /** Latitude */
230
+ latitude?: number;
231
+ /** Longitude */
232
+ longitude?: number;
233
+ /** Postal code */
234
+ postal_code?: string;
235
+ }
236
+
237
+ /**
238
+ * Represents a single Threads media object.
239
+ * All fields use snake_case to match the API response format.
240
+ */
241
+ export interface ThreadsPost {
242
+ /** Unique identifier for the media object */
243
+ id: string;
244
+ /** Type of product where the media is published (e.g., "THREADS") */
245
+ media_product_type?: string;
246
+ /** Type of media */
247
+ media_type?: ResponseMediaType;
248
+ /** URL of the media content */
249
+ media_url?: string;
250
+ /** Permanent link to the post */
251
+ permalink?: string;
252
+ /** Information about the owner of the post */
253
+ owner?: { id: string };
254
+ /** Username of the account that created the post */
255
+ username?: string;
256
+ /** Text content of the post */
257
+ text?: string;
258
+ /** Timestamp of when the post was created (ISO 8601 format) */
259
+ timestamp?: string;
260
+ /** Short code identifier for the media */
261
+ shortcode?: string;
262
+ /** URL of the thumbnail image (for video posts) */
263
+ thumbnail_url?: string;
264
+ /** List of child posts (for carousel posts) */
265
+ children?: { data: Array<{ id: string }> };
266
+ /** Indicates if the post is a quote of another post */
267
+ is_quote_post?: boolean;
268
+ /** Accessibility text for the image or video */
269
+ alt_text?: string;
270
+ /** URL of the attached link */
271
+ link_attachment_url?: string;
272
+ /** Indicates if the post has replies */
273
+ has_replies?: boolean;
274
+ /** Indicates if the post is a reply to another post */
275
+ is_reply?: boolean;
276
+ /** Indicates if the reply is owned by the current user */
277
+ is_reply_owned_by_me?: boolean;
278
+ /** Information about the root post (for replies) */
279
+ root_post?: { id: string };
280
+ /** Information about the post being replied to */
281
+ replied_to?: { id: string };
282
+ /** Visibility status of the post */
283
+ hide_status?:
284
+ | "NOT_HUSHED"
285
+ | "UNHUSHED"
286
+ | "HIDDEN"
287
+ | "COVERED"
288
+ | "BLOCKED"
289
+ | "RESTRICTED";
290
+ /** Controls who can reply to the post */
291
+ reply_audience?: ReplyControl;
292
+ /** The quoted post (for quote posts) */
293
+ quoted_post?: { id: string };
294
+ /** The reposted post (for reposts) */
295
+ reposted_post?: { id: string };
296
+ /** URL of the GIF */
297
+ gif_url?: string;
298
+ /** Poll attachment data */
299
+ poll_attachment?: PollAttachment;
300
+ /** Topic tag */
301
+ topic_tag?: string;
302
+ /** Whether the media is a spoiler */
303
+ is_spoiler_media?: boolean;
304
+ /** Text entity annotations */
305
+ text_entities?: TextEntity[];
306
+ /** Text attachment for long-form posts */
307
+ text_attachment?: TextAttachment;
308
+ /** Location ID */
309
+ location_id?: string;
310
+ /** Location data */
311
+ location?: ThreadsLocation;
312
+ /** Whether the user is verified */
313
+ is_verified?: boolean;
314
+ /** URL of the user's profile picture */
315
+ profile_picture_url?: string;
316
+ /** Ghost post status */
317
+ ghost_post_status?: "ACTIVE" | "ARCHIVED";
318
+ /** Ghost post expiration timestamp (ISO 8601) */
319
+ ghost_post_expiration_timestamp?: string;
320
+ /** List of country codes where the post is visible */
321
+ allowlisted_country_codes?: string[];
322
+ }
323
+
324
+ /**
325
+ * Represents the response structure when retrieving a list of Threads.
326
+ */
327
+ export interface ThreadsListResponse {
328
+ /** Array of ThreadsPost representing the retrieved posts */
329
+ data: ThreadsPost[];
330
+ /** Pagination information */
331
+ paging?: {
332
+ /** Cursors for navigating through pages of results */
333
+ cursors: {
334
+ /** Cursor for the previous page */
335
+ before: string;
336
+ /** Cursor for the next page */
337
+ after: string;
338
+ };
339
+ };
340
+ }
341
+
342
+ /**
343
+ * Quota configuration.
344
+ */
345
+ export interface QuotaConfig {
346
+ /** Total allowed quota */
347
+ quota_total: number;
348
+ /** Duration of the quota period in seconds */
349
+ quota_duration: number;
350
+ }
351
+
352
+ /**
353
+ * Represents the publishing limit information for a user.
354
+ */
355
+ export interface PublishingLimit {
356
+ /** Current post quota usage */
357
+ quota_usage?: number;
358
+ /** Post quota configuration */
359
+ config?: QuotaConfig;
360
+ /** Current reply quota usage */
361
+ reply_quota_usage?: number;
362
+ /** Reply quota configuration */
363
+ reply_config?: QuotaConfig;
364
+ /** Current delete quota usage */
365
+ delete_quota_usage?: number;
366
+ /** Delete quota configuration */
367
+ delete_config?: QuotaConfig;
368
+ /** Current location search quota usage */
369
+ location_search_quota_usage?: number;
370
+ /** Location search quota configuration */
371
+ location_search_config?: QuotaConfig;
372
+ }
373
+
374
+ /**
375
+ * Represents a Threads media container.
376
+ */
377
+ export interface ThreadsContainer {
378
+ /** Unique identifier for the container */
379
+ id: string;
380
+ /** Permanent link to the container */
381
+ permalink?: string;
382
+ /** Status of the container */
383
+ status?: "EXPIRED" | "ERROR" | "FINISHED" | "IN_PROGRESS" | "PUBLISHED";
384
+ /** Error message if the container failed */
385
+ error_message?: string;
386
+ }
387
+
388
+ /**
389
+ * Represents a Threads user profile (own profile via GET /me or GET /{user-id}).
390
+ */
391
+ export interface ThreadsProfile {
392
+ /** Unique identifier for the user */
393
+ id: string;
394
+ /** Username of the account */
395
+ username?: string;
396
+ /** Display name of the user */
397
+ name?: string;
398
+ /** URL of the user's profile picture */
399
+ threads_profile_picture_url?: string;
400
+ /** Biography text of the user */
401
+ threads_biography?: string;
402
+ /** Whether the user is verified */
403
+ is_verified?: boolean;
404
+ /** Recently searched keywords */
405
+ recently_searched_keywords?: Array<{ query: string; timestamp: number }>;
406
+ /** Whether the user is eligible for geo-gating */
407
+ is_eligible_for_geo_gating?: boolean;
408
+ }
409
+
410
+ /**
411
+ * Represents a public profile as returned by profile_lookup.
412
+ */
413
+ export interface PublicProfile {
414
+ /** Unique identifier */
415
+ id: string;
416
+ /** Username */
417
+ username?: string;
418
+ /** Display name */
419
+ name?: string;
420
+ /** Profile picture URL */
421
+ profile_picture_url?: string;
422
+ /** Biography */
423
+ biography?: string;
424
+ /** Whether the user is verified */
425
+ is_verified?: boolean;
426
+ /** Follower count */
427
+ follower_count?: number;
428
+ /** Likes count (past 7 days) */
429
+ likes_count?: number;
430
+ /** Quotes count (past 7 days) */
431
+ quotes_count?: number;
432
+ /** Replies count (past 7 days) */
433
+ replies_count?: number;
434
+ /** Reposts count (past 7 days) */
435
+ reposts_count?: number;
436
+ /** Views count (past 7 days) */
437
+ views_count?: number;
438
+ }
439
+
440
+ // ─── Insights Types ──────────────────────────────────────────────────────────
441
+
442
+ /**
443
+ * A single insight metric value.
444
+ */
445
+ export interface InsightValue {
446
+ /** The metric value */
447
+ value: number | Record<string, number>;
448
+ /** End time for the period (ISO 8601) */
449
+ end_time?: string;
450
+ }
451
+
452
+ /**
453
+ * Media insight metric.
454
+ */
455
+ export interface MediaInsight {
456
+ /** Metric name */
457
+ name: string;
458
+ /** Time period (e.g., "lifetime") */
459
+ period: string;
460
+ /** Metric values */
461
+ values: InsightValue[];
462
+ /** Human-readable title */
463
+ title: string;
464
+ /** Description of the metric */
465
+ description: string;
466
+ /** Unique identifier */
467
+ id: string;
468
+ }
469
+
470
+ /**
471
+ * Response from media insights endpoint.
472
+ */
473
+ export interface MediaInsightsResponse {
474
+ /** Array of insight metrics */
475
+ data: MediaInsight[];
476
+ }
477
+
478
+ /**
479
+ * User insight metric (supports both time series and total value formats).
480
+ */
481
+ export interface UserInsight {
482
+ /** Metric name */
483
+ name: string;
484
+ /** Time period (e.g., "day", "lifetime") */
485
+ period: string;
486
+ /** Metric values (time series) */
487
+ values: InsightValue[];
488
+ /** Human-readable title */
489
+ title: string;
490
+ /** Description of the metric */
491
+ description: string;
492
+ /** Unique identifier */
493
+ id: string;
494
+ /** Total value (for lifetime metrics) */
495
+ total_value?: { value: number | Record<string, number> };
496
+ /** Link total values (for clicks metric) */
497
+ link_total_values?: Array<{ value: number; link_url: string }>;
498
+ }
499
+
500
+ /**
501
+ * Response from user insights endpoint.
502
+ */
503
+ export interface UserInsightsResponse {
504
+ /** Array of insight metrics */
505
+ data: UserInsight[];
506
+ }
507
+
508
+ /**
509
+ * Options for user insights request.
510
+ */
511
+ export interface UserInsightsOptions {
512
+ /** Start of the time range (Unix timestamp, required for time series) */
513
+ since?: number;
514
+ /** End of the time range (Unix timestamp, required for time series) */
515
+ until?: number;
516
+ /** Breakdown dimension */
517
+ breakdown?: "country" | "city" | "age" | "gender";
518
+ }
519
+
520
+ // ─── Search Types ────────────────────────────────────────────────────────────
521
+
522
+ /**
523
+ * Options for keyword/tag search.
524
+ * Properties use snake_case to match the Threads API payload format directly.
525
+ */
526
+ export interface KeywordSearchOptions extends PaginationOptions {
527
+ /** The search query string (required) */
528
+ q: string;
529
+ /** Search behavior: TOP (popular) or RECENT (chronological) */
530
+ search_type?: "TOP" | "RECENT";
531
+ /** Search mode: KEYWORD (default) or TAG (topic tag search) */
532
+ search_mode?: "KEYWORD" | "TAG";
533
+ /** Filter by media type */
534
+ media_type?: "TEXT" | "IMAGE" | "VIDEO";
535
+ /** Filter by author username */
536
+ author_username?: string;
537
+ }
538
+
539
+ /**
540
+ * Options for location search.
541
+ */
542
+ export interface LocationSearchOptions {
543
+ /** Search query for location name */
544
+ query?: string;
545
+ /** Latitude for proximity search (must be used with longitude) */
546
+ latitude?: number;
547
+ /** Longitude for proximity search (must be used with latitude) */
548
+ longitude?: number;
549
+ }
550
+
551
+ // ─── Token Types ─────────────────────────────────────────────────────────────
552
+
553
+ /**
554
+ * Response from exchanging an OAuth authorization code for a short-lived token.
555
+ */
556
+ export interface AuthCodeResponse {
557
+ /** The short-lived access token */
558
+ access_token: string;
559
+ /** The user ID of the authenticated user */
560
+ user_id: string;
561
+ }
562
+
563
+ /**
564
+ * Response from token exchange/refresh endpoints.
565
+ */
566
+ export interface TokenResponse {
567
+ /** The access token */
568
+ access_token: string;
569
+ /** Token type (e.g., "bearer") */
570
+ token_type: string;
571
+ /** Expiration time in seconds */
572
+ expires_in?: number;
573
+ }
574
+
575
+ /**
576
+ * Response from debug_token endpoint.
577
+ */
578
+ export interface DebugTokenInfo {
579
+ /** Token data */
580
+ data: {
581
+ /** Token type (e.g., "USER") */
582
+ type?: string;
583
+ /** Application name */
584
+ application?: string;
585
+ /** Data access expiration timestamp */
586
+ data_access_expires_at?: number;
587
+ /** Token expiration timestamp */
588
+ expires_at?: number;
589
+ /** Whether the token is valid */
590
+ is_valid?: boolean;
591
+ /** Issued timestamp */
592
+ issued_at?: number;
593
+ /** Granted scopes */
594
+ scopes?: string[];
595
+ /** User ID */
596
+ user_id?: string;
597
+ };
598
+ }
599
+
600
+ // ─── oEmbed Types ────────────────────────────────────────────────────────────
601
+
602
+ /**
603
+ * Response from the oEmbed endpoint.
604
+ */
605
+ export interface OEmbedResponse {
606
+ /** HTML embed code */
607
+ html: string;
608
+ /** Provider name (e.g., "Threads") */
609
+ provider_name?: string;
610
+ /** Provider URL */
611
+ provider_url?: string;
612
+ /** oEmbed type (e.g., "rich") */
613
+ type?: string;
614
+ /** oEmbed version */
615
+ version?: string;
616
+ /** Width of the embed */
617
+ width?: number;
618
+ }
619
+
620
+ // ─── Webhook Types ───────────────────────────────────────────────────────────
621
+
622
+ /**
623
+ * Base webhook payload structure.
624
+ */
625
+ export interface WebhookPayload {
626
+ /** The Threads App ID */
627
+ app_id: string;
628
+ /** Webhook topic ("moderate" or "interaction") */
629
+ topic: "moderate" | "interaction";
630
+ /** Target ID (media ID or mentioned user ID) */
631
+ target_id: string;
632
+ /** Timestamp when the notification was sent */
633
+ time: number;
634
+ /** Subscription ID */
635
+ subscription_id: string;
636
+ /** Whether the payload has a UID field */
637
+ has_uid_field: boolean;
638
+ /** Webhook values */
639
+ values: {
640
+ /** The webhook data */
641
+ value: Record<string, unknown>;
642
+ /** The subscribed field name */
643
+ field: "replies" | "mentions" | "delete" | "publish";
644
+ };
645
+ }
646
+
647
+ /**
648
+ * Reply webhook payload values.
649
+ */
650
+ export interface WebhookReplyValue {
651
+ /** Reply media ID */
652
+ id: string;
653
+ /** Username of the replier */
654
+ username: string;
655
+ /** Reply text content */
656
+ text?: string;
657
+ /** Media type */
658
+ media_type: string;
659
+ /** Permalink to the reply */
660
+ permalink: string;
661
+ /** The post being replied to */
662
+ replied_to?: { id: string };
663
+ /** The root post of the conversation */
664
+ root_post?: {
665
+ id: string;
666
+ owner_id?: string;
667
+ username?: string;
668
+ };
669
+ /** Short code */
670
+ shortcode: string;
671
+ /** Timestamp (ISO 8601) */
672
+ timestamp: string;
673
+ /** Whether the user is verified */
674
+ is_verified?: boolean;
675
+ /** Profile picture URL */
676
+ profile_picture_url?: string;
677
+ }
678
+
679
+ /**
680
+ * Mention webhook payload values.
681
+ */
682
+ export interface WebhookMentionValue {
683
+ /** Media ID */
684
+ id: string;
685
+ /** Alt text */
686
+ alt_text?: string;
687
+ /** GIF URL */
688
+ gif_url?: string;
689
+ /** Whether the post has replies */
690
+ has_replies?: boolean;
691
+ /** Whether it's a quote post */
692
+ is_quote_post?: boolean;
693
+ /** Whether it's a reply */
694
+ is_reply?: boolean;
695
+ /** Media product type */
696
+ media_product_type?: string;
697
+ /** Media type */
698
+ media_type: string;
699
+ /** Permalink */
700
+ permalink: string;
701
+ /** Short code */
702
+ shortcode?: string;
703
+ /** Text content */
704
+ text?: string;
705
+ /** Timestamp (ISO 8601) */
706
+ timestamp: string;
707
+ /** Username */
708
+ username: string;
709
+ /** Whether the user is verified */
710
+ is_verified?: boolean;
711
+ /** Profile picture URL */
712
+ profile_picture_url?: string;
713
+ }
714
+
715
+ /**
716
+ * Delete webhook payload values.
717
+ */
718
+ export interface WebhookDeleteValue {
719
+ /** Deleted media ID */
720
+ id: string;
721
+ /** Owner information */
722
+ owner?: { owner_id: string };
723
+ /** When the post was deleted (ISO 8601) */
724
+ deleted_at: string;
725
+ /** When the post was originally published (ISO 8601) */
726
+ timestamp: string;
727
+ /** Username */
728
+ username: string;
729
+ }
730
+
731
+ /**
732
+ * Publish webhook payload values.
733
+ */
734
+ export interface WebhookPublishValue {
735
+ /** Published media ID */
736
+ id: string;
737
+ /** Media type */
738
+ media_type: string;
739
+ /** Permalink */
740
+ permalink: string;
741
+ /** Timestamp (ISO 8601) */
742
+ timestamp: string;
743
+ /** Username */
744
+ username: string;
745
+ }
746
+
747
+ // ─── Mock API Interface ──────────────────────────────────────────────────────
748
+
749
+ /**
750
+ * Represents the mock API for Threads operations.
751
+ */
752
+ export interface MockThreadsAPI {
753
+ // Existing methods
754
+ createThreadsContainer(
755
+ request: ThreadsPostRequest,
756
+ ): Promise<string>;
757
+
758
+ publishThreadsContainer(
759
+ userId: string,
760
+ accessToken: string,
761
+ containerId: string,
762
+ getPermalink?: boolean,
763
+ ): Promise<string | { id: string; permalink: string }>;
764
+
765
+ createCarouselItem(
766
+ request: Omit<ThreadsPostRequest, "mediaType"> & {
767
+ mediaType: "IMAGE" | "VIDEO";
768
+ },
769
+ ): Promise<string>;
770
+
771
+ getPublishingLimit(
772
+ userId: string,
773
+ accessToken: string,
774
+ fields?: string[],
775
+ ): Promise<PublishingLimit>;
776
+
777
+ getThreadsList(
778
+ userId: string,
779
+ accessToken: string,
780
+ options?: PaginationOptions,
781
+ fields?: string[],
782
+ ): Promise<ThreadsListResponse>;
783
+
784
+ getSingleThread(
785
+ mediaId: string,
786
+ accessToken: string,
787
+ fields?: string[],
788
+ ): Promise<ThreadsPost>;
789
+
790
+ // New methods
791
+ repost(
792
+ mediaId: string,
793
+ accessToken: string,
794
+ ): Promise<{ id: string }>;
795
+
796
+ deleteThread(
797
+ mediaId: string,
798
+ accessToken: string,
799
+ ): Promise<{ success: boolean; deleted_id?: string }>;
800
+
801
+ getProfile(
802
+ userId: string,
803
+ accessToken: string,
804
+ fields?: string[],
805
+ ): Promise<ThreadsProfile>;
806
+
807
+ lookupProfile(
808
+ accessToken: string,
809
+ username: string,
810
+ fields?: string[],
811
+ ): Promise<PublicProfile>;
812
+
813
+ getProfilePosts(
814
+ accessToken: string,
815
+ username: string,
816
+ options?: PaginationOptions,
817
+ fields?: string[],
818
+ ): Promise<ThreadsListResponse>;
819
+
820
+ getGhostPosts(
821
+ userId: string,
822
+ accessToken: string,
823
+ options?: PaginationOptions,
824
+ fields?: string[],
825
+ ): Promise<ThreadsListResponse>;
826
+
827
+ getUserReplies(
828
+ userId: string,
829
+ accessToken: string,
830
+ options?: PaginationOptions,
831
+ fields?: string[],
832
+ ): Promise<ThreadsListResponse>;
833
+
834
+ getReplies(
835
+ mediaId: string,
836
+ accessToken: string,
837
+ options?: CursorPaginationOptions,
838
+ fields?: string[],
839
+ reverse?: boolean,
840
+ ): Promise<ThreadsListResponse>;
841
+
842
+ getConversation(
843
+ mediaId: string,
844
+ accessToken: string,
845
+ options?: CursorPaginationOptions,
846
+ fields?: string[],
847
+ reverse?: boolean,
848
+ ): Promise<ThreadsListResponse>;
849
+
850
+ manageReply(
851
+ replyId: string,
852
+ accessToken: string,
853
+ hide: boolean,
854
+ ): Promise<{ success: boolean }>;
855
+
856
+ getMentions(
857
+ userId: string,
858
+ accessToken: string,
859
+ options?: PaginationOptions,
860
+ fields?: string[],
861
+ ): Promise<ThreadsListResponse>;
862
+
863
+ getMediaInsights(
864
+ mediaId: string,
865
+ accessToken: string,
866
+ metrics: string[],
867
+ ): Promise<MediaInsightsResponse>;
868
+
869
+ getUserInsights(
870
+ userId: string,
871
+ accessToken: string,
872
+ metrics: string[],
873
+ options?: UserInsightsOptions,
874
+ ): Promise<UserInsightsResponse>;
875
+
876
+ searchKeyword(
877
+ accessToken: string,
878
+ options: KeywordSearchOptions,
879
+ fields?: string[],
880
+ ): Promise<ThreadsListResponse>;
881
+
882
+ searchLocations(
883
+ accessToken: string,
884
+ options: LocationSearchOptions,
885
+ fields?: string[],
886
+ ): Promise<{ data: ThreadsLocation[] }>;
887
+
888
+ getLocation(
889
+ locationId: string,
890
+ accessToken: string,
891
+ fields?: string[],
892
+ ): Promise<ThreadsLocation>;
893
+
894
+ exchangeCodeForToken(
895
+ clientId: string,
896
+ clientSecret: string,
897
+ code: string,
898
+ redirectUri: string,
899
+ ): Promise<AuthCodeResponse>;
900
+
901
+ getAppAccessToken(
902
+ clientId: string,
903
+ clientSecret: string,
904
+ ): Promise<TokenResponse>;
905
+
906
+ exchangeToken(
907
+ clientSecret: string,
908
+ accessToken: string,
909
+ ): Promise<TokenResponse>;
910
+
911
+ refreshToken(
912
+ accessToken: string,
913
+ ): Promise<TokenResponse>;
914
+
915
+ debugToken(
916
+ accessToken: string,
917
+ inputToken: string,
918
+ ): Promise<DebugTokenInfo>;
919
+
920
+ getOEmbed(
921
+ accessToken: string,
922
+ url: string,
923
+ maxWidth?: number,
924
+ ): Promise<OEmbedResponse>;
925
+ }