@thenavidm/threads-mcp-cli 1.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/LICENSE +21 -0
- package/README.md +1019 -0
- package/SKILL.md +203 -0
- package/dist/api/client.d.ts +105 -0
- package/dist/api/client.js +305 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +92 -0
- package/dist/api/errors.js +195 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/identity.d.ts +33 -0
- package/dist/api/identity.js +52 -0
- package/dist/api/identity.js.map +1 -0
- package/dist/auth/login.d.ts +32 -0
- package/dist/auth/login.js +204 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/auth/store.d.ts +37 -0
- package/dist/auth/store.js +88 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/tokens.d.ts +54 -0
- package/dist/auth/tokens.js +96 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +444 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +98 -0
- package/dist/config.js +185 -0
- package/dist/config.js.map +1 -0
- package/dist/content/containers.d.ts +89 -0
- package/dist/content/containers.js +210 -0
- package/dist/content/containers.js.map +1 -0
- package/dist/content/media.d.ts +61 -0
- package/dist/content/media.js +125 -0
- package/dist/content/media.js.map +1 -0
- package/dist/content/text.d.ts +68 -0
- package/dist/content/text.js +106 -0
- package/dist/content/text.js.map +1 -0
- package/dist/doctor.d.ts +14 -0
- package/dist/doctor.js +218 -0
- package/dist/doctor.js.map +1 -0
- package/dist/format/posts.d.ts +41 -0
- package/dist/format/posts.js +153 -0
- package/dist/format/posts.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/dist/safety.d.ts +52 -0
- package/dist/safety.js +85 -0
- package/dist/safety.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +232 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/accounts.d.ts +27 -0
- package/dist/tools/accounts.js +162 -0
- package/dist/tools/accounts.js.map +1 -0
- package/dist/tools/discover.d.ts +56 -0
- package/dist/tools/discover.js +146 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insights.d.ts +55 -0
- package/dist/tools/insights.js +223 -0
- package/dist/tools/insights.js.map +1 -0
- package/dist/tools/kit.d.ts +90 -0
- package/dist/tools/kit.js +119 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/posts.d.ts +170 -0
- package/dist/tools/posts.js +312 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/read.d.ts +31 -0
- package/dist/tools/read.js +95 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replies.d.ts +92 -0
- package/dist/tools/replies.js +218 -0
- package/dist/tools/replies.js.map +1 -0
- package/dist/transport/http.d.ts +28 -0
- package/dist/transport/http.js +103 -0
- package/dist/transport/http.js.map +1 -0
- package/package.json +65 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving credentials, and the multi-account model.
|
|
3
|
+
*
|
|
4
|
+
* Threads is not like a platform with app passwords. Every credential here is
|
|
5
|
+
* an OAuth token minted against a Meta app, it is bound to one Threads profile,
|
|
6
|
+
* and it dies after 60 days unless something refreshes it. That shapes the
|
|
7
|
+
* whole design: tokens arrive from three places, and the one that matters most
|
|
8
|
+
* is the token store `threads-mcp login` writes, because that is the only one
|
|
9
|
+
* this server can keep alive on its own.
|
|
10
|
+
*
|
|
11
|
+
* Three sources, in priority order:
|
|
12
|
+
* 1. THREADS_ACCOUNTS a JSON array, for several profiles at once
|
|
13
|
+
* 2. THREADS_ACCESS_TOKEN the single-account variable
|
|
14
|
+
* 3. the token store ~/.threads-mcp/tokens.json, written by `login`
|
|
15
|
+
*
|
|
16
|
+
* The store is last rather than first so an explicit environment variable
|
|
17
|
+
* always wins. Someone who exports a token into one client's config expects
|
|
18
|
+
* that token to be the one used, not a stale one a login left on disk months
|
|
19
|
+
* ago.
|
|
20
|
+
*
|
|
21
|
+
* `user_id` is deliberately optional everywhere. Almost every Threads endpoint
|
|
22
|
+
* is keyed by the numeric profile id, and asking a person to find theirs before
|
|
23
|
+
* anything works is a setup step with no reason to exist: `GET /me` returns it,
|
|
24
|
+
* and the client resolves and caches it on first use.
|
|
25
|
+
*/
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
export const DEFAULT_GRAPH_HOST = "https://graph.threads.net";
|
|
29
|
+
/** Where `login` writes tokens, and where refreshed tokens are written back. */
|
|
30
|
+
export function defaultStorePath() {
|
|
31
|
+
return process.env.THREADS_TOKEN_STORE || join(homedir(), ".threads-mcp", "tokens.json");
|
|
32
|
+
}
|
|
33
|
+
/** Strip a leading @ and lowercase. */
|
|
34
|
+
export function normalizeUsername(raw) {
|
|
35
|
+
return raw.trim().replace(/^@/, "").toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
function normalizeHost(raw, fallback) {
|
|
38
|
+
const t = (raw ?? "").trim();
|
|
39
|
+
if (!t)
|
|
40
|
+
return fallback;
|
|
41
|
+
const withScheme = /^https?:\/\//i.test(t) ? t : `https://${t}`;
|
|
42
|
+
return withScheme.replace(/\/+$/, "");
|
|
43
|
+
}
|
|
44
|
+
function envFlag(name, fallback) {
|
|
45
|
+
const raw = process.env[name];
|
|
46
|
+
if (raw === undefined || raw === "")
|
|
47
|
+
return fallback;
|
|
48
|
+
return /^(1|true|yes|on)$/i.test(raw.trim());
|
|
49
|
+
}
|
|
50
|
+
function envInt(name, fallback) {
|
|
51
|
+
const raw = process.env[name];
|
|
52
|
+
if (!raw)
|
|
53
|
+
return fallback;
|
|
54
|
+
const n = Number(raw);
|
|
55
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
56
|
+
process.stderr.write(`[threads-mcp] ${name}="${raw}" is not a positive number. Using ${fallback}.\n`);
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read `THREADS_ACCOUNTS`, a JSON array.
|
|
63
|
+
*
|
|
64
|
+
* Both snake_case and camelCase keys are accepted, because the same JSON gets
|
|
65
|
+
* pasted between a shell export and a client config file, and the two
|
|
66
|
+
* conventions do not survive that trip intact.
|
|
67
|
+
*/
|
|
68
|
+
export function accountsFromJson(raw) {
|
|
69
|
+
if (!raw)
|
|
70
|
+
return [];
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(raw);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
process.stderr.write("[threads-mcp] THREADS_ACCOUNTS is not valid JSON. Ignoring it.\n");
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(parsed))
|
|
80
|
+
return [];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const entry of parsed) {
|
|
83
|
+
if (!entry || typeof entry !== "object")
|
|
84
|
+
continue;
|
|
85
|
+
const e = entry;
|
|
86
|
+
const token = e.access_token ?? e.accessToken ?? e.token;
|
|
87
|
+
if (typeof token !== "string" || !token.trim())
|
|
88
|
+
continue;
|
|
89
|
+
const userId = e.user_id ?? e.userId ?? e.id;
|
|
90
|
+
const username = e.username ?? e.account_name ?? e.handle;
|
|
91
|
+
const expires = e.expires_at ?? e.expiresAt;
|
|
92
|
+
out.push({
|
|
93
|
+
accessToken: token.trim(),
|
|
94
|
+
userId: typeof userId === "string" ? userId : typeof userId === "number" ? String(userId) : undefined,
|
|
95
|
+
username: typeof username === "string" ? normalizeUsername(username) : undefined,
|
|
96
|
+
expiresAt: typeof expires === "number" ? expires : undefined,
|
|
97
|
+
source: "env-json",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function accountFromSingleEnv() {
|
|
103
|
+
const token = process.env.THREADS_ACCESS_TOKEN;
|
|
104
|
+
if (!token || !token.trim())
|
|
105
|
+
return [];
|
|
106
|
+
const userId = process.env.THREADS_USER_ID;
|
|
107
|
+
const username = process.env.THREADS_USERNAME;
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
accessToken: token.trim(),
|
|
111
|
+
userId: userId?.trim() || undefined,
|
|
112
|
+
username: username ? normalizeUsername(username) : undefined,
|
|
113
|
+
source: "env",
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
export function loadConfig(stored = []) {
|
|
118
|
+
const fromJson = accountsFromJson(process.env.THREADS_ACCOUNTS);
|
|
119
|
+
const fromEnv = accountFromSingleEnv();
|
|
120
|
+
const accounts = fromJson.length > 0 ? fromJson : fromEnv.length > 0 ? fromEnv : stored;
|
|
121
|
+
const preferred = (process.env.THREADS_DEFAULT_ACCOUNT ?? "")
|
|
122
|
+
.split(",")
|
|
123
|
+
.map((s) => normalizeUsername(s))
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
return {
|
|
126
|
+
accounts,
|
|
127
|
+
preferred,
|
|
128
|
+
appId: process.env.THREADS_APP_ID?.trim() || undefined,
|
|
129
|
+
appSecret: process.env.THREADS_APP_SECRET?.trim() || undefined,
|
|
130
|
+
readOnly: envFlag("THREADS_READ_ONLY", false),
|
|
131
|
+
allowDestructive: envFlag("THREADS_ALLOW_DESTRUCTIVE", true),
|
|
132
|
+
refreshWindowDays: envInt("THREADS_REFRESH_WINDOW_DAYS", 20),
|
|
133
|
+
persistTokens: envFlag("THREADS_PERSIST_TOKENS", true),
|
|
134
|
+
storePath: defaultStorePath(),
|
|
135
|
+
requestTimeoutMs: envInt("THREADS_REQUEST_TIMEOUT_MS", 30_000),
|
|
136
|
+
minRequestIntervalMs: envInt("THREADS_MIN_REQUEST_INTERVAL_MS", 120),
|
|
137
|
+
maxRetries: envInt("THREADS_MAX_RETRIES", 3),
|
|
138
|
+
containerTimeoutMs: envInt("THREADS_CONTAINER_TIMEOUT_MS", 120_000),
|
|
139
|
+
graphHost: normalizeHost(process.env.THREADS_GRAPH_HOST, DEFAULT_GRAPH_HOST),
|
|
140
|
+
userAgent: process.env.THREADS_USER_AGENT || "threads-mcp",
|
|
141
|
+
auditPath: process.env.THREADS_AUDIT_LOG || undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Pick which profile a call acts as.
|
|
146
|
+
*
|
|
147
|
+
* With no hint: the first configured `THREADS_DEFAULT_ACCOUNT` that is actually
|
|
148
|
+
* connected, else the first account. Exact username match beats prefix match,
|
|
149
|
+
* because "navid" is a prefix of "navidmedia", and a prefix-first search would
|
|
150
|
+
* hand an unnamed post to the wrong profile whenever both are connected. A
|
|
151
|
+
* name that matches nothing fails and lists what is connected, rather than
|
|
152
|
+
* quietly posting somewhere else.
|
|
153
|
+
*/
|
|
154
|
+
export function selectAccount(config, hint) {
|
|
155
|
+
if (config.accounts.length === 0) {
|
|
156
|
+
throw new Error("No Threads account configured. Run `threads-mcp login` to authorise one, or set THREADS_ACCESS_TOKEN. Run `threads-mcp doctor` for details.");
|
|
157
|
+
}
|
|
158
|
+
if (!hint) {
|
|
159
|
+
for (const want of config.preferred) {
|
|
160
|
+
const exact = config.accounts.find((a) => a.username === want);
|
|
161
|
+
if (exact)
|
|
162
|
+
return exact;
|
|
163
|
+
const prefix = config.accounts.find((a) => a.username?.startsWith(want));
|
|
164
|
+
if (prefix)
|
|
165
|
+
return prefix;
|
|
166
|
+
}
|
|
167
|
+
return config.accounts[0];
|
|
168
|
+
}
|
|
169
|
+
const needle = normalizeUsername(hint);
|
|
170
|
+
// A numeric hint is a profile id, not a username.
|
|
171
|
+
if (/^\d+$/.test(needle)) {
|
|
172
|
+
const byId = config.accounts.find((a) => a.userId === needle);
|
|
173
|
+
if (byId)
|
|
174
|
+
return byId;
|
|
175
|
+
}
|
|
176
|
+
const exact = config.accounts.find((a) => a.username === needle);
|
|
177
|
+
if (exact)
|
|
178
|
+
return exact;
|
|
179
|
+
const prefix = config.accounts.find((a) => a.username?.startsWith(needle));
|
|
180
|
+
if (prefix)
|
|
181
|
+
return prefix;
|
|
182
|
+
const known = config.accounts.map((a) => a.username ?? a.userId ?? "(unresolved)").join(", ");
|
|
183
|
+
throw new Error(`No connected Threads account matches "${hint}". Connected: ${known || "(none)"}. Usernames are resolved on first use, so a freshly added token may need one call before it can be named.`);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishing: the two-step container dance, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Every write to Threads is the same shape.
|
|
5
|
+
*
|
|
6
|
+
* 1. POST /{user-id}/threads build a container. Nothing is public.
|
|
7
|
+
* 2. wait the container transcodes.
|
|
8
|
+
* 3. POST /{user-id}/threads_publish the post appears.
|
|
9
|
+
*
|
|
10
|
+
* Step 2 is the one everybody skips. A text container is usually ready by the
|
|
11
|
+
* time the next request lands, so code that omits the wait works during
|
|
12
|
+
* development and then fails the first time someone attaches a video. Meta's
|
|
13
|
+
* own guidance is to wait about 30 seconds; polling the container's status is
|
|
14
|
+
* better than sleeping, because it is both faster for text and correct for a
|
|
15
|
+
* five-minute video.
|
|
16
|
+
*
|
|
17
|
+
* The unpublished container is also the only draft state Threads has. It is
|
|
18
|
+
* invisible, it holds for 24 hours, and it can be published later by id. That
|
|
19
|
+
* is worth exposing as its own tool rather than hiding inside a helper, so an
|
|
20
|
+
* agent can stage a post for a human to look at before anything is public.
|
|
21
|
+
*
|
|
22
|
+
* A note on ordering in `publishChain`: every part is validated before the
|
|
23
|
+
* first one is posted. A thread is a chain of ordinary posts, each replying to
|
|
24
|
+
* the one before, so there is no transaction and no rollback. Discovering on
|
|
25
|
+
* part four that part five is 40 characters too long leaves four public posts
|
|
26
|
+
* and no way to finish, and deleting them costs four of the day's hundred
|
|
27
|
+
* deletions. Checking first costs nothing.
|
|
28
|
+
*/
|
|
29
|
+
import type { ThreadsClient } from "../api/client.js";
|
|
30
|
+
import type { Account } from "../config.js";
|
|
31
|
+
import { type MediaItem } from "./media.js";
|
|
32
|
+
export type ReplyControl = "everyone" | "accounts_you_follow" | "mentioned_only" | "parent_post_author_only" | "followers_only";
|
|
33
|
+
export type ContainerOptions = MediaItem & {
|
|
34
|
+
text?: string;
|
|
35
|
+
reply_to_id?: string;
|
|
36
|
+
quote_post_id?: string;
|
|
37
|
+
reply_control?: ReplyControl;
|
|
38
|
+
link_attachment?: string;
|
|
39
|
+
topic_tag?: string;
|
|
40
|
+
allowlisted_country_codes?: string[];
|
|
41
|
+
enable_reply_approvals?: boolean;
|
|
42
|
+
auto_publish_text?: boolean;
|
|
43
|
+
is_carousel_item?: boolean;
|
|
44
|
+
children?: string[];
|
|
45
|
+
media_type?: "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL";
|
|
46
|
+
};
|
|
47
|
+
export type PublishedPost = {
|
|
48
|
+
id: string;
|
|
49
|
+
permalink?: string;
|
|
50
|
+
timestamp?: string;
|
|
51
|
+
text?: string;
|
|
52
|
+
warnings?: string[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Everything checkable without a network call.
|
|
56
|
+
*
|
|
57
|
+
* Returns warnings, throws on anything that would certainly be refused. The
|
|
58
|
+
* split matters: a `.webp` URL is a warning because it might be served as JPEG,
|
|
59
|
+
* while a 600-character post is an error because Threads will never take it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function validate(options: ContainerOptions, label?: string): string[];
|
|
62
|
+
/** Build the parameter map Meta expects, dropping anything unset. */
|
|
63
|
+
export declare function containerParams(options: ContainerOptions): Record<string, unknown>;
|
|
64
|
+
/** Step one. Builds a container and returns its id. Nothing is public yet. */
|
|
65
|
+
export declare function createContainer(client: ThreadsClient, account: Account, options: ContainerOptions): Promise<string>;
|
|
66
|
+
/** Step three. Publishes a container that has finished processing. */
|
|
67
|
+
export declare function publishContainer(client: ThreadsClient, account: Account, containerId: string): Promise<PublishedPost>;
|
|
68
|
+
/** Create, wait, publish. The single-call path most tools want. */
|
|
69
|
+
export declare function publish(client: ThreadsClient, account: Account, options: ContainerOptions): Promise<PublishedPost>;
|
|
70
|
+
/**
|
|
71
|
+
* Publish several posts as a chain, each replying to the one before.
|
|
72
|
+
*
|
|
73
|
+
* Threads has no thread endpoint. A "thread" is exactly this: ordinary posts
|
|
74
|
+
* linked by `reply_to_id`. Which means it can half-publish, so every part is
|
|
75
|
+
* validated first and the failure report names what did go out.
|
|
76
|
+
*/
|
|
77
|
+
export declare function publishChain(client: ThreadsClient, account: Account, parts: ContainerOptions[], rootReplyTo?: string): Promise<{
|
|
78
|
+
posts: PublishedPost[];
|
|
79
|
+
warnings: string[];
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* Stage a carousel: every child first, then the parent binding them together.
|
|
83
|
+
*
|
|
84
|
+
* Children are created in order and then waited on together rather than one at
|
|
85
|
+
* a time, so twenty images transcode in parallel instead of in series.
|
|
86
|
+
*/
|
|
87
|
+
export declare function publishCarousel(client: ThreadsClient, account: Account, items: MediaItem[], options?: Omit<ContainerOptions, "children" | "media_type">): Promise<PublishedPost>;
|
|
88
|
+
/** Read a post back in full, for the tools that return one. */
|
|
89
|
+
export declare function readPost(client: ThreadsClient, account: Account, id: string): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishing: the two-step container dance, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Every write to Threads is the same shape.
|
|
5
|
+
*
|
|
6
|
+
* 1. POST /{user-id}/threads build a container. Nothing is public.
|
|
7
|
+
* 2. wait the container transcodes.
|
|
8
|
+
* 3. POST /{user-id}/threads_publish the post appears.
|
|
9
|
+
*
|
|
10
|
+
* Step 2 is the one everybody skips. A text container is usually ready by the
|
|
11
|
+
* time the next request lands, so code that omits the wait works during
|
|
12
|
+
* development and then fails the first time someone attaches a video. Meta's
|
|
13
|
+
* own guidance is to wait about 30 seconds; polling the container's status is
|
|
14
|
+
* better than sleeping, because it is both faster for text and correct for a
|
|
15
|
+
* five-minute video.
|
|
16
|
+
*
|
|
17
|
+
* The unpublished container is also the only draft state Threads has. It is
|
|
18
|
+
* invisible, it holds for 24 hours, and it can be published later by id. That
|
|
19
|
+
* is worth exposing as its own tool rather than hiding inside a helper, so an
|
|
20
|
+
* agent can stage a post for a human to look at before anything is public.
|
|
21
|
+
*
|
|
22
|
+
* A note on ordering in `publishChain`: every part is validated before the
|
|
23
|
+
* first one is posted. A thread is a chain of ordinary posts, each replying to
|
|
24
|
+
* the one before, so there is no transaction and no rollback. Discovering on
|
|
25
|
+
* part four that part five is 40 characters too long leaves four public posts
|
|
26
|
+
* and no way to finish, and deleting them costs four of the day's hundred
|
|
27
|
+
* deletions. Checking first costs nothing.
|
|
28
|
+
*/
|
|
29
|
+
import { POST_FIELDS } from "../api/client.js";
|
|
30
|
+
import { checkMedia, mediaTypeFor } from "./media.js";
|
|
31
|
+
import { countLinks, MAX_LINKS_PER_POST, normalizeTopicTag, overLimitMessage } from "./text.js";
|
|
32
|
+
import { TextTooLongError } from "../api/errors.js";
|
|
33
|
+
/**
|
|
34
|
+
* Everything checkable without a network call.
|
|
35
|
+
*
|
|
36
|
+
* Returns warnings, throws on anything that would certainly be refused. The
|
|
37
|
+
* split matters: a `.webp` URL is a warning because it might be served as JPEG,
|
|
38
|
+
* while a 600-character post is an error because Threads will never take it.
|
|
39
|
+
*/
|
|
40
|
+
export function validate(options, label = "This post") {
|
|
41
|
+
const warnings = [];
|
|
42
|
+
if (options.text !== undefined) {
|
|
43
|
+
const problem = overLimitMessage(options.text, label);
|
|
44
|
+
if (problem)
|
|
45
|
+
throw new TextTooLongError(problem);
|
|
46
|
+
const links = countLinks(options.text);
|
|
47
|
+
if (links > MAX_LINKS_PER_POST) {
|
|
48
|
+
warnings.push(`${label} contains ${links} distinct URLs. Threads accepts at most ${MAX_LINKS_PER_POST} and may refuse the post.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
warnings.push(...checkMedia(options));
|
|
52
|
+
if (options.link_attachment) {
|
|
53
|
+
if (options.image_url || options.video_url) {
|
|
54
|
+
throw new Error("A link attachment renders as a preview card and Threads only allows it on a text-only post. Drop the media, or put the URL in the text instead.");
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(options.link_attachment);
|
|
58
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
59
|
+
throw new Error("not http");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(`link_attachment "${options.link_attachment}" is not a valid URL.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (options.topic_tag)
|
|
67
|
+
normalizeTopicTag(options.topic_tag);
|
|
68
|
+
if (options.allowlisted_country_codes?.length) {
|
|
69
|
+
const bad = options.allowlisted_country_codes.filter((c) => !/^[A-Za-z]{2}$/.test(c.trim()));
|
|
70
|
+
if (bad.length) {
|
|
71
|
+
throw new Error(`Geo-gating takes ISO 3166-1 alpha-2 country codes, two letters each. These are not: ${bad.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (options.reply_to_id && options.quote_post_id) {
|
|
75
|
+
throw new Error("A post is either a reply or a quote, not both.");
|
|
76
|
+
}
|
|
77
|
+
return warnings;
|
|
78
|
+
}
|
|
79
|
+
/** Build the parameter map Meta expects, dropping anything unset. */
|
|
80
|
+
export function containerParams(options) {
|
|
81
|
+
const mediaType = options.media_type ?? mediaTypeFor(options);
|
|
82
|
+
return {
|
|
83
|
+
media_type: mediaType,
|
|
84
|
+
text: options.text,
|
|
85
|
+
image_url: options.image_url,
|
|
86
|
+
video_url: options.video_url,
|
|
87
|
+
alt_text: options.alt_text,
|
|
88
|
+
reply_to_id: options.reply_to_id,
|
|
89
|
+
quote_post_id: options.quote_post_id,
|
|
90
|
+
reply_control: options.reply_control,
|
|
91
|
+
link_attachment: options.link_attachment,
|
|
92
|
+
topic_tag: options.topic_tag ? normalizeTopicTag(options.topic_tag) : undefined,
|
|
93
|
+
allowlisted_country_codes: options.allowlisted_country_codes?.length
|
|
94
|
+
? options.allowlisted_country_codes.map((c) => c.trim().toUpperCase())
|
|
95
|
+
: undefined,
|
|
96
|
+
enable_reply_approvals: options.enable_reply_approvals ? "true" : undefined,
|
|
97
|
+
auto_publish_text: options.auto_publish_text ? "true" : undefined,
|
|
98
|
+
is_carousel_item: options.is_carousel_item ? "true" : undefined,
|
|
99
|
+
children: options.children?.length ? options.children.join(",") : undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Step one. Builds a container and returns its id. Nothing is public yet. */
|
|
103
|
+
export async function createContainer(client, account, options) {
|
|
104
|
+
const userId = await client.userId(account);
|
|
105
|
+
const created = (await client.call(account, `/${userId}/threads`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
params: containerParams(options),
|
|
108
|
+
}));
|
|
109
|
+
if (!created.id) {
|
|
110
|
+
throw new Error("Threads accepted the container request but returned no container id.");
|
|
111
|
+
}
|
|
112
|
+
return String(created.id);
|
|
113
|
+
}
|
|
114
|
+
/** Step three. Publishes a container that has finished processing. */
|
|
115
|
+
export async function publishContainer(client, account, containerId) {
|
|
116
|
+
const userId = await client.userId(account);
|
|
117
|
+
await client.awaitContainer(account, containerId);
|
|
118
|
+
const published = (await client.call(account, `/${userId}/threads_publish`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
params: { creation_id: containerId },
|
|
121
|
+
}));
|
|
122
|
+
if (!published.id) {
|
|
123
|
+
throw new Error(`Threads published container ${containerId} but returned no post id.`);
|
|
124
|
+
}
|
|
125
|
+
const detail = (await client.call(account, `/${published.id}`, {
|
|
126
|
+
params: { fields: "id,permalink,timestamp,text" },
|
|
127
|
+
}));
|
|
128
|
+
return {
|
|
129
|
+
id: String(published.id),
|
|
130
|
+
permalink: detail.permalink,
|
|
131
|
+
timestamp: detail.timestamp,
|
|
132
|
+
text: detail.text,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Create, wait, publish. The single-call path most tools want. */
|
|
136
|
+
export async function publish(client, account, options) {
|
|
137
|
+
const warnings = validate(options);
|
|
138
|
+
const containerId = await createContainer(client, account, options);
|
|
139
|
+
const post = await publishContainer(client, account, containerId);
|
|
140
|
+
return warnings.length ? { ...post, warnings } : post;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Publish several posts as a chain, each replying to the one before.
|
|
144
|
+
*
|
|
145
|
+
* Threads has no thread endpoint. A "thread" is exactly this: ordinary posts
|
|
146
|
+
* linked by `reply_to_id`. Which means it can half-publish, so every part is
|
|
147
|
+
* validated first and the failure report names what did go out.
|
|
148
|
+
*/
|
|
149
|
+
export async function publishChain(client, account, parts, rootReplyTo) {
|
|
150
|
+
if (parts.length === 0)
|
|
151
|
+
throw new Error("A thread needs at least one post.");
|
|
152
|
+
// Everything, before anything.
|
|
153
|
+
const warnings = [];
|
|
154
|
+
parts.forEach((part, index) => {
|
|
155
|
+
warnings.push(...validate(part, `Part ${index + 1} of ${parts.length}`));
|
|
156
|
+
});
|
|
157
|
+
const posts = [];
|
|
158
|
+
let replyTo = rootReplyTo;
|
|
159
|
+
for (const [index, part] of parts.entries()) {
|
|
160
|
+
try {
|
|
161
|
+
const containerId = await createContainer(client, account, { ...part, reply_to_id: replyTo });
|
|
162
|
+
const post = await publishContainer(client, account, containerId);
|
|
163
|
+
posts.push(post);
|
|
164
|
+
replyTo = post.id;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Say exactly how far it got. A partially published thread is recoverable
|
|
168
|
+
// by hand; one that reports only "failed" is not.
|
|
169
|
+
const done = posts.length;
|
|
170
|
+
const summary = done
|
|
171
|
+
? `Parts 1-${done} of ${parts.length} are published (last id ${posts[done - 1].id}). Part ${index + 1} failed.`
|
|
172
|
+
: `Nothing was published. Part ${index + 1} failed.`;
|
|
173
|
+
throw new Error(`${summary} ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { posts, warnings };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Stage a carousel: every child first, then the parent binding them together.
|
|
180
|
+
*
|
|
181
|
+
* Children are created in order and then waited on together rather than one at
|
|
182
|
+
* a time, so twenty images transcode in parallel instead of in series.
|
|
183
|
+
*/
|
|
184
|
+
export async function publishCarousel(client, account, items, options = {}) {
|
|
185
|
+
const warnings = validate({ ...options, image_url: undefined, video_url: undefined });
|
|
186
|
+
for (const [index, item] of items.entries()) {
|
|
187
|
+
warnings.push(...checkMedia(item).map((w) => `Item ${index + 1}: ${w}`));
|
|
188
|
+
}
|
|
189
|
+
const children = [];
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
children.push(await createContainer(client, account, {
|
|
192
|
+
...item,
|
|
193
|
+
media_type: mediaTypeFor(item) === "VIDEO" ? "VIDEO" : "IMAGE",
|
|
194
|
+
is_carousel_item: true,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
await Promise.all(children.map((id) => client.awaitContainer(account, id)));
|
|
198
|
+
const parentId = await createContainer(client, account, {
|
|
199
|
+
...options,
|
|
200
|
+
media_type: "CAROUSEL",
|
|
201
|
+
children,
|
|
202
|
+
});
|
|
203
|
+
const post = await publishContainer(client, account, parentId);
|
|
204
|
+
return warnings.length ? { ...post, warnings, ...{} } : post;
|
|
205
|
+
}
|
|
206
|
+
/** Read a post back in full, for the tools that return one. */
|
|
207
|
+
export async function readPost(client, account, id) {
|
|
208
|
+
return (await client.call(account, `/${id}`, { params: { fields: POST_FIELDS } }));
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=containers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"containers.js","sourceRoot":"","sources":["../../src/content/containers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media rules, checked before a container is created.
|
|
3
|
+
*
|
|
4
|
+
* Threads does not upload bytes. You hand it a public URL and it fetches the
|
|
5
|
+
* file itself, asynchronously, and reports the result minutes later as a
|
|
6
|
+
* container in state ERROR with a message like "Media download failed". By
|
|
7
|
+
* then the useful context is gone.
|
|
8
|
+
*
|
|
9
|
+
* So the checks that can be made locally are made locally, in the one place the
|
|
10
|
+
* numbers are written down. Everything here comes from Meta's published specs.
|
|
11
|
+
*/
|
|
12
|
+
export declare const IMAGE_SPEC: {
|
|
13
|
+
readonly formats: readonly ["jpeg", "jpg", "png"];
|
|
14
|
+
readonly maxBytes: number;
|
|
15
|
+
readonly minWidth: 320;
|
|
16
|
+
readonly maxWidth: 1440;
|
|
17
|
+
/** Widest and tallest Threads will accept, as width:height. */
|
|
18
|
+
readonly maxAspect: 10;
|
|
19
|
+
};
|
|
20
|
+
export declare const VIDEO_SPEC: {
|
|
21
|
+
readonly formats: readonly ["mp4", "mov"];
|
|
22
|
+
readonly maxBytes: number;
|
|
23
|
+
readonly maxSeconds: 300;
|
|
24
|
+
readonly codecs: readonly ["h264", "hevc"];
|
|
25
|
+
readonly recommendedAspect: "9:16";
|
|
26
|
+
};
|
|
27
|
+
export declare const CAROUSEL: {
|
|
28
|
+
readonly min: 2;
|
|
29
|
+
readonly max: 20;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Reject a media URL that cannot possibly work, before spending a container on
|
|
33
|
+
* it.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately narrow. It refuses what is certainly wrong (not a URL, not
|
|
36
|
+
* HTTPS, a local path, a `data:` URI) and lets everything else through, because
|
|
37
|
+
* a URL with no file extension is completely normal for a CDN and refusing it
|
|
38
|
+
* would break more than it fixed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertMediaUrl(url: string, kind: "image" | "video"): void;
|
|
41
|
+
/** The extension, lowercased, when the URL has one. */
|
|
42
|
+
export declare function extensionOf(url: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Warn about an extension Threads does not support.
|
|
45
|
+
*
|
|
46
|
+
* A warning rather than an error: the extension in a URL is a hint, not a
|
|
47
|
+
* content type, and plenty of perfectly good CDN URLs end in `.webp?format=jpg`
|
|
48
|
+
* or nothing at all. Being told up front that a `.webp` is likely to fail is
|
|
49
|
+
* worth more than being stopped by it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatWarning(url: string, kind: "image" | "video"): string | undefined;
|
|
52
|
+
export type MediaItem = {
|
|
53
|
+
image_url?: string;
|
|
54
|
+
video_url?: string;
|
|
55
|
+
alt_text?: string;
|
|
56
|
+
};
|
|
57
|
+
/** Which container type a set of arguments implies. */
|
|
58
|
+
export declare function mediaTypeFor(item: MediaItem): "TEXT" | "IMAGE" | "VIDEO";
|
|
59
|
+
/** Validate one carousel item or post attachment. Returns any warnings. */
|
|
60
|
+
export declare function checkMedia(item: MediaItem): string[];
|
|
61
|
+
export declare function assertCarouselSize(count: number): void;
|