@codybrom/denim 2.0.2 → 2.1.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/CHANGELOG.md +23 -0
- package/deno.json +1 -1
- package/mod.ts +8 -0
- package/package.json +1 -1
- package/readme.md +21 -3
- package/src/api/createThreadsContainer.ts +15 -0
- package/src/api/getOEmbed.ts +43 -8
- package/src/api/getPendingReplies.ts +69 -0
- package/src/api/managePendingReply.ts +42 -0
- package/src/api/publishThreadsContainer.ts +5 -0
- package/src/constants.ts +6 -0
- package/src/types.ts +63 -3
- package/src/utils/mock_threads_api.ts +40 -3
- package/src/utils/validateRequest.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [2.1.0] - 2026-09-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Reply Approvals (Feb 13, 2026 API): `enableReplyApprovals` on
|
|
14
|
+
`ThreadsPostRequest`, `getPendingReplies()` (`GET /{media-id}/pending_replies`
|
|
15
|
+
with `approval_status` filter), `managePendingReply()`
|
|
16
|
+
(`POST /{reply-id}/manage_pending_reply`), `reply_approval_status` on
|
|
17
|
+
`ThreadsPost`, and `ApprovalStatus` / `PendingRepliesOptions` types. Ghost
|
|
18
|
+
posts cannot combine with reply approvals.
|
|
19
|
+
- Share to Instagram Stories (Mar 25, 2026 API): `shareToIgStory` and
|
|
20
|
+
`shareToIgStoryDarkMode` on `ThreadsPostRequest` (`crossreshare_to_ig` /
|
|
21
|
+
`crossreshare_to_ig_dark_mode`, requires `threads_share_to_instagram`
|
|
22
|
+
permission). `publishThreadsContainer` with `getPermalink: true` now also
|
|
23
|
+
returns `crossreshare_to_ig_status`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `getOEmbed()` no longer requires an access token (Mar 3, 2026 API). Preferred
|
|
28
|
+
order is now `getOEmbed(postUrl, accessToken?, maxWidth?)`; legacy
|
|
29
|
+
`getOEmbed(accessToken, postUrl, maxWidth?)` still works.
|
|
30
|
+
- `AuthCodeResponse` now includes optional `token_type` (Aug 12, 2026 API).
|
|
31
|
+
|
|
9
32
|
## [2.0.2] - 2026-07-11
|
|
10
33
|
|
|
11
34
|
### Fixed
|
package/deno.json
CHANGED
package/mod.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
10
10
|
import type {
|
|
11
|
+
ApprovalStatus,
|
|
11
12
|
AuthCodeResponse,
|
|
12
13
|
CursorPaginationOptions,
|
|
13
14
|
DebugTokenInfo,
|
|
@@ -21,10 +22,12 @@ import type {
|
|
|
21
22
|
MockThreadsAPI,
|
|
22
23
|
OEmbedResponse,
|
|
23
24
|
PaginationOptions,
|
|
25
|
+
PendingRepliesOptions,
|
|
24
26
|
PollAttachment,
|
|
25
27
|
PollAttachmentInput,
|
|
26
28
|
PublicProfile,
|
|
27
29
|
PublishingLimit,
|
|
30
|
+
PublishResult,
|
|
28
31
|
QuotaConfig,
|
|
29
32
|
ReplyControl,
|
|
30
33
|
ResponseMediaType,
|
|
@@ -49,6 +52,7 @@ import type {
|
|
|
49
52
|
} from "./src/types.ts";
|
|
50
53
|
|
|
51
54
|
export type {
|
|
55
|
+
ApprovalStatus,
|
|
52
56
|
AuthCodeResponse,
|
|
53
57
|
CursorPaginationOptions,
|
|
54
58
|
DebugTokenInfo,
|
|
@@ -62,10 +66,12 @@ export type {
|
|
|
62
66
|
MockThreadsAPI,
|
|
63
67
|
OEmbedResponse,
|
|
64
68
|
PaginationOptions,
|
|
69
|
+
PendingRepliesOptions,
|
|
65
70
|
PollAttachment,
|
|
66
71
|
PollAttachmentInput,
|
|
67
72
|
PublicProfile,
|
|
68
73
|
PublishingLimit,
|
|
74
|
+
PublishResult,
|
|
69
75
|
QuotaConfig,
|
|
70
76
|
ReplyControl,
|
|
71
77
|
ResponseMediaType,
|
|
@@ -108,8 +114,10 @@ export { lookupProfile } from "./src/api/lookupProfile.ts";
|
|
|
108
114
|
|
|
109
115
|
// ─── Replies ─────────────────────────────────────────────────────────────────
|
|
110
116
|
export { getConversation } from "./src/api/getConversation.ts";
|
|
117
|
+
export { getPendingReplies } from "./src/api/getPendingReplies.ts";
|
|
111
118
|
export { getReplies } from "./src/api/getReplies.ts";
|
|
112
119
|
export { getUserReplies } from "./src/api/getUserReplies.ts";
|
|
120
|
+
export { managePendingReply } from "./src/api/managePendingReply.ts";
|
|
113
121
|
export { manageReply } from "./src/api/manageReply.ts";
|
|
114
122
|
|
|
115
123
|
// ─── Mentions ────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -115,6 +115,15 @@ made by a user.
|
|
|
115
115
|
`manageReply(replyId, accessToken, hide)` hides or unhides a reply. Pass `true`
|
|
116
116
|
to hide, `false` to unhide.
|
|
117
117
|
|
|
118
|
+
`getPendingReplies(mediaId, accessToken, options?, fields?, reverse?,
|
|
119
|
+
approvalStatus?)`
|
|
120
|
+
returns replies awaiting approval on posts created with
|
|
121
|
+
`enableReplyApprovals: true`. Filter with `"pending"` or `"ignored"` (default
|
|
122
|
+
returns both).
|
|
123
|
+
|
|
124
|
+
`managePendingReply(replyId, accessToken, approve)` approves (`true`) or ignores
|
|
125
|
+
(`false`) a pending reply. Ignored replies can still be approved later.
|
|
126
|
+
|
|
118
127
|
## Insights
|
|
119
128
|
|
|
120
129
|
`getMediaInsights(mediaId, accessToken, metrics)` returns metrics for a post.
|
|
@@ -150,7 +159,7 @@ objects with IDs you can pass to `createThreadsContainer` as `locationId`.
|
|
|
150
159
|
|
|
151
160
|
`exchangeCodeForToken(clientId, clientSecret, code, redirectUri)` exchanges an
|
|
152
161
|
OAuth authorization code for a short-lived access token. Returns
|
|
153
|
-
`{ access_token, user_id }
|
|
162
|
+
`{ access_token, user_id, token_type? }` (`token_type` present since Aug 2026).
|
|
154
163
|
|
|
155
164
|
`getAppAccessToken(clientId, clientSecret)` gets an app-level access token via
|
|
156
165
|
client credentials. Returns `{ access_token, token_type }`.
|
|
@@ -172,8 +181,17 @@ quota, reply quota, and remaining usage.
|
|
|
172
181
|
`getMentions(userId, accessToken, options?, fields?)` returns posts that mention
|
|
173
182
|
the authenticated user.
|
|
174
183
|
|
|
175
|
-
`getOEmbed(
|
|
176
|
-
post URL.
|
|
184
|
+
`getOEmbed(postUrl, accessToken?, maxWidth?)` returns embeddable HTML for a
|
|
185
|
+
Threads post URL. No access token required since Mar 2026. Legacy
|
|
186
|
+
`getOEmbed(accessToken, url, maxWidth?)` order still works. Returns
|
|
187
|
+
`{ html, provider_name, type, version, width }`.
|
|
188
|
+
|
|
189
|
+
`createThreadsContainer` accepts `enableReplyApprovals?: boolean` (cannot be
|
|
190
|
+
combined with `isGhostPost`), plus `shareToIgStory?: boolean` and
|
|
191
|
+
`shareToIgStoryDarkMode?: boolean` for cross-sharing to the linked Instagram
|
|
192
|
+
account as a Story (requires `threads_share_to_instagram` permission).
|
|
193
|
+
`publishThreadsContainer(..., getPermalink=true)` may also return
|
|
194
|
+
`crossreshare_to_ig_status: "SUCCESS" | "FAILED"`.
|
|
177
195
|
|
|
178
196
|
## Utilities
|
|
179
197
|
|
|
@@ -68,6 +68,21 @@ export async function createThreadsContainer(
|
|
|
68
68
|
if (request.isSpoilerMedia !== undefined) {
|
|
69
69
|
body.append("is_spoiler_media", String(request.isSpoilerMedia));
|
|
70
70
|
}
|
|
71
|
+
if (request.enableReplyApprovals !== undefined) {
|
|
72
|
+
body.append(
|
|
73
|
+
"enable_reply_approvals",
|
|
74
|
+
String(request.enableReplyApprovals),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (request.shareToIgStory !== undefined) {
|
|
78
|
+
body.append("crossreshare_to_ig", String(request.shareToIgStory));
|
|
79
|
+
}
|
|
80
|
+
if (request.shareToIgStoryDarkMode !== undefined) {
|
|
81
|
+
body.append(
|
|
82
|
+
"crossreshare_to_ig_dark_mode",
|
|
83
|
+
String(request.shareToIgStoryDarkMode),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
71
86
|
|
|
72
87
|
// JSON-serialized parameters
|
|
73
88
|
if (request.pollAttachment) {
|
package/src/api/getOEmbed.ts
CHANGED
|
@@ -2,31 +2,66 @@ import { THREADS_API_BASE_URL } from "../constants.ts";
|
|
|
2
2
|
import type { OEmbedResponse } from "../types.ts";
|
|
3
3
|
import { getAPI } from "../utils/getAPI.ts";
|
|
4
4
|
|
|
5
|
+
function isUrl(value: string): boolean {
|
|
6
|
+
return /^https?:\/\//i.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Retrieves oEmbed HTML for a Threads post.
|
|
11
|
+
* Since March 3, 2026 the oEmbed endpoint works without an access token.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* Supports both call orders for backwards compatibility:
|
|
14
|
+
* - `getOEmbed(accessToken, postUrl, maxWidth?)` (legacy)
|
|
15
|
+
* - `getOEmbed(postUrl, accessToken?, maxWidth?)` (preferred)
|
|
16
|
+
* - `getOEmbed(postUrl, maxWidth?)` — no token; pass maxWidth as second arg
|
|
17
|
+
* only via the 3-arg form `getOEmbed(postUrl, undefined, maxWidth)`
|
|
18
|
+
*
|
|
19
|
+
* @param accessTokenOrUrl - Legacy access token OR the post URL
|
|
20
|
+
* @param urlOrAccessTokenOrMaxWidth - Post URL, optional access token, or maxWidth
|
|
10
21
|
* @param maxWidth - Optional maximum width of the embed in pixels
|
|
11
22
|
* @returns A Promise that resolves to the OEmbedResponse
|
|
12
23
|
* @throws Will throw an error if the API request fails
|
|
13
24
|
*/
|
|
14
25
|
export async function getOEmbed(
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
accessTokenOrUrl: string,
|
|
27
|
+
urlOrAccessTokenOrMaxWidth?: string | number,
|
|
17
28
|
maxWidth?: number,
|
|
18
29
|
): Promise<OEmbedResponse> {
|
|
30
|
+
let accessToken: string | undefined;
|
|
31
|
+
let postUrl: string;
|
|
32
|
+
let resolvedMaxWidth = maxWidth;
|
|
33
|
+
|
|
34
|
+
if (typeof urlOrAccessTokenOrMaxWidth === "number") {
|
|
35
|
+
// getOEmbed(postUrl, maxWidth)
|
|
36
|
+
postUrl = accessTokenOrUrl;
|
|
37
|
+
resolvedMaxWidth = urlOrAccessTokenOrMaxWidth;
|
|
38
|
+
} else if (urlOrAccessTokenOrMaxWidth === undefined) {
|
|
39
|
+
// Single-arg: must be the post URL (no token)
|
|
40
|
+
postUrl = accessTokenOrUrl;
|
|
41
|
+
} else if (isUrl(accessTokenOrUrl) && !isUrl(urlOrAccessTokenOrMaxWidth)) {
|
|
42
|
+
// New order: getOEmbed(postUrl, accessToken?, maxWidth?)
|
|
43
|
+
postUrl = accessTokenOrUrl;
|
|
44
|
+
accessToken = urlOrAccessTokenOrMaxWidth || undefined;
|
|
45
|
+
} else {
|
|
46
|
+
// Legacy order: getOEmbed(accessToken, postUrl, maxWidth?)
|
|
47
|
+
accessToken = accessTokenOrUrl || undefined;
|
|
48
|
+
postUrl = urlOrAccessTokenOrMaxWidth;
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
const api = getAPI();
|
|
20
52
|
if (api) {
|
|
21
|
-
|
|
53
|
+
// Mock preserves legacy (token, url) ordering
|
|
54
|
+
return api.getOEmbed(accessToken ?? "", postUrl, resolvedMaxWidth);
|
|
22
55
|
}
|
|
23
56
|
|
|
24
57
|
const url = new URL(`${THREADS_API_BASE_URL}/oembed`);
|
|
25
58
|
url.searchParams.append("url", postUrl);
|
|
26
|
-
|
|
59
|
+
if (accessToken) {
|
|
60
|
+
url.searchParams.append("access_token", accessToken);
|
|
61
|
+
}
|
|
27
62
|
|
|
28
|
-
if (
|
|
29
|
-
url.searchParams.append("maxwidth",
|
|
63
|
+
if (resolvedMaxWidth !== undefined) {
|
|
64
|
+
url.searchParams.append("maxwidth", resolvedMaxWidth.toString());
|
|
30
65
|
}
|
|
31
66
|
|
|
32
67
|
const response = await fetch(url.toString());
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { PENDING_REPLY_FIELDS, THREADS_API_BASE_URL } from "../constants.ts";
|
|
2
|
+
import type {
|
|
3
|
+
ApprovalStatus,
|
|
4
|
+
CursorPaginationOptions,
|
|
5
|
+
ThreadsListResponse,
|
|
6
|
+
} from "../types.ts";
|
|
7
|
+
import { getAPI } from "../utils/getAPI.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves pending replies awaiting approval on a Threads post.
|
|
11
|
+
* Only applicable to posts created with reply approvals enabled.
|
|
12
|
+
*
|
|
13
|
+
* @param mediaId - The ID of the Threads media object
|
|
14
|
+
* @param accessToken - The access token for authentication
|
|
15
|
+
* @param options - Optional pagination parameters
|
|
16
|
+
* @param fields - Optional array of fields to return
|
|
17
|
+
* @param reverse - Optional boolean to sort in reverse chronological order (default: true)
|
|
18
|
+
* @param approvalStatus - Optional filter: `pending` or `ignored` (default returns both)
|
|
19
|
+
* @returns A Promise that resolves to the ThreadsListResponse
|
|
20
|
+
* @throws Will throw an error if the API request fails
|
|
21
|
+
*/
|
|
22
|
+
export async function getPendingReplies(
|
|
23
|
+
mediaId: string,
|
|
24
|
+
accessToken: string,
|
|
25
|
+
options?: CursorPaginationOptions,
|
|
26
|
+
fields?: string[],
|
|
27
|
+
reverse?: boolean,
|
|
28
|
+
approvalStatus?: ApprovalStatus,
|
|
29
|
+
): Promise<ThreadsListResponse> {
|
|
30
|
+
const api = getAPI();
|
|
31
|
+
if (api) {
|
|
32
|
+
return api.getPendingReplies(
|
|
33
|
+
mediaId,
|
|
34
|
+
accessToken,
|
|
35
|
+
options,
|
|
36
|
+
fields,
|
|
37
|
+
reverse,
|
|
38
|
+
approvalStatus,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fieldList = (fields ?? PENDING_REPLY_FIELDS).join(",");
|
|
43
|
+
const url = new URL(`${THREADS_API_BASE_URL}/${mediaId}/pending_replies`);
|
|
44
|
+
url.searchParams.append("fields", fieldList);
|
|
45
|
+
url.searchParams.append("access_token", accessToken);
|
|
46
|
+
|
|
47
|
+
if (reverse !== undefined) {
|
|
48
|
+
url.searchParams.append("reverse", String(reverse));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (approvalStatus !== undefined) {
|
|
52
|
+
url.searchParams.append("approval_status", approvalStatus);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options) {
|
|
56
|
+
if (options.after) url.searchParams.append("after", options.after);
|
|
57
|
+
if (options.before) url.searchParams.append("before", options.before);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const response = await fetch(url.toString());
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const errorBody = await response.text();
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Failed to get pending replies (${response.status}): ${errorBody}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return await response.json();
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { THREADS_API_BASE_URL } from "../constants.ts";
|
|
2
|
+
import { getAPI } from "../utils/getAPI.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Approves or ignores a pending reply on a Threads post with reply approvals enabled.
|
|
6
|
+
* Ignored replies can still be approved later.
|
|
7
|
+
*
|
|
8
|
+
* @param replyId - The ID of the pending reply to manage
|
|
9
|
+
* @param accessToken - The access token for authentication
|
|
10
|
+
* @param approve - Whether to approve (true) or ignore (false) the reply
|
|
11
|
+
* @returns A Promise that resolves to an object indicating success
|
|
12
|
+
* @throws Will throw an error if the API request fails
|
|
13
|
+
*/
|
|
14
|
+
export async function managePendingReply(
|
|
15
|
+
replyId: string,
|
|
16
|
+
accessToken: string,
|
|
17
|
+
approve: boolean,
|
|
18
|
+
): Promise<{ success: boolean }> {
|
|
19
|
+
const api = getAPI();
|
|
20
|
+
if (api) {
|
|
21
|
+
return api.managePendingReply(replyId, accessToken, approve);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const url = `${THREADS_API_BASE_URL}/${replyId}/manage_pending_reply`;
|
|
25
|
+
const body = new URLSearchParams({
|
|
26
|
+
access_token: accessToken,
|
|
27
|
+
approve: String(approve),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: body,
|
|
33
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const responseText = await response.text();
|
|
38
|
+
throw new Error(`Failed to manage pending reply: ${responseText}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await response.json();
|
|
42
|
+
}
|
|
@@ -92,6 +92,11 @@ export async function publishThreadsContainer(
|
|
|
92
92
|
return {
|
|
93
93
|
id: publishData.id,
|
|
94
94
|
permalink: threadData.permalink || "",
|
|
95
|
+
...(publishData.crossreshare_to_ig_status
|
|
96
|
+
? {
|
|
97
|
+
crossreshare_to_ig_status: publishData.crossreshare_to_ig_status,
|
|
98
|
+
}
|
|
99
|
+
: {}),
|
|
95
100
|
};
|
|
96
101
|
}
|
|
97
102
|
|
package/src/constants.ts
CHANGED
|
@@ -65,6 +65,12 @@ export const REPLY_FIELDS = [
|
|
|
65
65
|
"reposted_post",
|
|
66
66
|
"gif_url",
|
|
67
67
|
"topic_tag",
|
|
68
|
+
"reply_approval_status",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
/** Fields for pending replies endpoint (same as replies, includes approval status) */
|
|
72
|
+
export const PENDING_REPLY_FIELDS = [
|
|
73
|
+
...REPLY_FIELDS,
|
|
68
74
|
] as const;
|
|
69
75
|
|
|
70
76
|
/** Fields for location endpoints */
|
package/src/types.ts
CHANGED
|
@@ -136,6 +136,12 @@ export interface ThreadsPostRequest {
|
|
|
136
136
|
isGhostPost?: boolean;
|
|
137
137
|
/** Location ID to tag (optional) */
|
|
138
138
|
locationId?: string;
|
|
139
|
+
/** Enable reply approvals — replies must be approved before publishing (optional, cannot be used with isGhostPost) */
|
|
140
|
+
enableReplyApprovals?: boolean;
|
|
141
|
+
/** Cross-share the post to the linked Instagram account as a Story (optional, any media type, requires `threads_share_to_instagram` permission) */
|
|
142
|
+
shareToIgStory?: boolean;
|
|
143
|
+
/** Cross-share the post to the linked Instagram account as a Story in dark mode (optional, any media type, requires `threads_share_to_instagram` permission) */
|
|
144
|
+
shareToIgStoryDarkMode?: boolean;
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
/**
|
|
@@ -165,6 +171,33 @@ export interface CursorPaginationOptions {
|
|
|
165
171
|
after?: string;
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Approval status filter for pending replies.
|
|
176
|
+
* `pending` shows only pending replies, `ignored` shows only ignored replies.
|
|
177
|
+
* Default (omitted) returns both.
|
|
178
|
+
*/
|
|
179
|
+
export type ApprovalStatus = "pending" | "ignored";
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Options for pending replies retrieval.
|
|
183
|
+
*/
|
|
184
|
+
export interface PendingRepliesOptions extends CursorPaginationOptions {
|
|
185
|
+
/** Filter by approval status (optional, default returns both) */
|
|
186
|
+
approval_status?: ApprovalStatus;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Result of publishing a Threads media container.
|
|
191
|
+
*/
|
|
192
|
+
export interface PublishResult {
|
|
193
|
+
/** The published Threads media ID */
|
|
194
|
+
id: string;
|
|
195
|
+
/** Permalink (only populated when requested via `getPermalink`) */
|
|
196
|
+
permalink?: string;
|
|
197
|
+
/** Cross-share to Instagram Story status (only when cross-sharing was requested) */
|
|
198
|
+
crossreshare_to_ig_status?: "SUCCESS" | "FAILED";
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
// ─── Response Types (snake_case — matching API reality) ──────────────────────
|
|
169
202
|
|
|
170
203
|
/**
|
|
@@ -319,6 +352,8 @@ export interface ThreadsPost {
|
|
|
319
352
|
ghost_post_expiration_timestamp?: string;
|
|
320
353
|
/** List of country codes where the post is visible */
|
|
321
354
|
allowlisted_country_codes?: string[];
|
|
355
|
+
/** Approval status of a pending reply (`pending` or `ignored`) */
|
|
356
|
+
reply_approval_status?: "pending" | "ignored";
|
|
322
357
|
}
|
|
323
358
|
|
|
324
359
|
/**
|
|
@@ -552,12 +587,15 @@ export interface LocationSearchOptions {
|
|
|
552
587
|
|
|
553
588
|
/**
|
|
554
589
|
* Response from exchanging an OAuth authorization code for a short-lived token.
|
|
590
|
+
* Since August 12, 2026 the API also returns `token_type`.
|
|
555
591
|
*/
|
|
556
592
|
export interface AuthCodeResponse {
|
|
557
593
|
/** The short-lived access token */
|
|
558
594
|
access_token: string;
|
|
559
595
|
/** The user ID of the authenticated user */
|
|
560
596
|
user_id: string;
|
|
597
|
+
/** Token type (e.g., "bearer"). Present since August 12, 2026. */
|
|
598
|
+
token_type?: string;
|
|
561
599
|
}
|
|
562
600
|
|
|
563
601
|
/**
|
|
@@ -760,7 +798,14 @@ export interface MockThreadsAPI {
|
|
|
760
798
|
accessToken: string,
|
|
761
799
|
containerId: string,
|
|
762
800
|
getPermalink?: boolean,
|
|
763
|
-
): Promise<
|
|
801
|
+
): Promise<
|
|
802
|
+
| string
|
|
803
|
+
| {
|
|
804
|
+
id: string;
|
|
805
|
+
permalink: string;
|
|
806
|
+
crossreshare_to_ig_status?: "SUCCESS" | "FAILED";
|
|
807
|
+
}
|
|
808
|
+
>;
|
|
764
809
|
|
|
765
810
|
createCarouselItem(
|
|
766
811
|
request: Omit<ThreadsPostRequest, "mediaType"> & {
|
|
@@ -853,6 +898,21 @@ export interface MockThreadsAPI {
|
|
|
853
898
|
hide: boolean,
|
|
854
899
|
): Promise<{ success: boolean }>;
|
|
855
900
|
|
|
901
|
+
getPendingReplies(
|
|
902
|
+
mediaId: string,
|
|
903
|
+
accessToken: string,
|
|
904
|
+
options?: CursorPaginationOptions,
|
|
905
|
+
fields?: string[],
|
|
906
|
+
reverse?: boolean,
|
|
907
|
+
approvalStatus?: ApprovalStatus,
|
|
908
|
+
): Promise<ThreadsListResponse>;
|
|
909
|
+
|
|
910
|
+
managePendingReply(
|
|
911
|
+
replyId: string,
|
|
912
|
+
accessToken: string,
|
|
913
|
+
approve: boolean,
|
|
914
|
+
): Promise<{ success: boolean }>;
|
|
915
|
+
|
|
856
916
|
getMentions(
|
|
857
917
|
userId: string,
|
|
858
918
|
accessToken: string,
|
|
@@ -918,8 +978,8 @@ export interface MockThreadsAPI {
|
|
|
918
978
|
): Promise<DebugTokenInfo>;
|
|
919
979
|
|
|
920
980
|
getOEmbed(
|
|
921
|
-
|
|
922
|
-
|
|
981
|
+
accessTokenOrUrl: string,
|
|
982
|
+
urlOrUndefined?: string,
|
|
923
983
|
maxWidth?: number,
|
|
924
984
|
): Promise<OEmbedResponse>;
|
|
925
985
|
}
|
|
@@ -101,7 +101,13 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
|
|
|
101
101
|
_accessToken: string,
|
|
102
102
|
containerId: string,
|
|
103
103
|
getPermalink: boolean = false,
|
|
104
|
-
): Promise<
|
|
104
|
+
): Promise<
|
|
105
|
+
string | {
|
|
106
|
+
id: string;
|
|
107
|
+
permalink: string;
|
|
108
|
+
crossreshare_to_ig_status?: "SUCCESS" | "FAILED";
|
|
109
|
+
}
|
|
110
|
+
> {
|
|
105
111
|
if (this.errorMode) {
|
|
106
112
|
return Promise.reject(new Error("Failed to publish Threads container"));
|
|
107
113
|
}
|
|
@@ -377,6 +383,36 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
|
|
|
377
383
|
return Promise.resolve({ success: true });
|
|
378
384
|
}
|
|
379
385
|
|
|
386
|
+
getPendingReplies(
|
|
387
|
+
_mediaId: string,
|
|
388
|
+
_accessToken: string,
|
|
389
|
+
_options?: CursorPaginationOptions,
|
|
390
|
+
_fields?: string[],
|
|
391
|
+
_reverse?: boolean,
|
|
392
|
+
_approvalStatus?: "pending" | "ignored",
|
|
393
|
+
): Promise<ThreadsListResponse> {
|
|
394
|
+
if (this.errorMode) {
|
|
395
|
+
return Promise.reject(new Error("Failed to get pending replies"));
|
|
396
|
+
}
|
|
397
|
+
return Promise.resolve({
|
|
398
|
+
data: Array.from(this.posts.values()).slice(0, 25),
|
|
399
|
+
paging: {
|
|
400
|
+
cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" },
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
managePendingReply(
|
|
406
|
+
_replyId: string,
|
|
407
|
+
_accessToken: string,
|
|
408
|
+
_approve: boolean,
|
|
409
|
+
): Promise<{ success: boolean }> {
|
|
410
|
+
if (this.errorMode) {
|
|
411
|
+
return Promise.reject(new Error("Failed to manage pending reply"));
|
|
412
|
+
}
|
|
413
|
+
return Promise.resolve({ success: true });
|
|
414
|
+
}
|
|
415
|
+
|
|
380
416
|
getMentions(
|
|
381
417
|
_userId: string,
|
|
382
418
|
_accessToken: string,
|
|
@@ -504,6 +540,7 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
|
|
|
504
540
|
return Promise.resolve({
|
|
505
541
|
access_token: "short_lived_token_abc123",
|
|
506
542
|
user_id: "12345",
|
|
543
|
+
token_type: "bearer",
|
|
507
544
|
});
|
|
508
545
|
}
|
|
509
546
|
|
|
@@ -564,8 +601,8 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
|
|
|
564
601
|
}
|
|
565
602
|
|
|
566
603
|
getOEmbed(
|
|
567
|
-
|
|
568
|
-
|
|
604
|
+
_accessTokenOrUrl: string,
|
|
605
|
+
_urlOrUndefined?: string,
|
|
569
606
|
_maxWidth?: number,
|
|
570
607
|
): Promise<OEmbedResponse> {
|
|
571
608
|
if (this.errorMode) {
|
|
@@ -51,6 +51,11 @@ export async function validateRequest(
|
|
|
51
51
|
if (request.replyToId) {
|
|
52
52
|
throw new Error("isGhostPost cannot be used together with replyToId");
|
|
53
53
|
}
|
|
54
|
+
if (request.enableReplyApprovals) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"enableReplyApprovals cannot be used together with isGhostPost",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// Text attachment can only be used with TEXT posts and not with polls
|