@ apicity/ x (韩语)
X (原Twitter)社会API供应商发布内容 (api.x.com).
运行时间依赖性 :
zod@^4.6.6- 请求在每一个 POST 端点上附加计划.schema
安装
npm install @apicity/x
# or
pnpm add @apicity/x
快速启动
import { createX } from "@apicity/x";
const x = createX({ accessToken: process.env.X_ACCESS_TOKEN! });
设置
X 需要一个 OAuth 2.0 用户文本访问令牌 发送或上传 媒体。 仅 App 的 Bearer 令牌只读而被上传拒绝 和推特的终点.
1. 配置您的 X 应用程序
打开 控制台.x.com 并确保你的应用生活
在一个 按用途付费 项目——遗产 免费 项目折旧
而 v2 端点拒绝其标志 client-not-enrolled移动
如果需要, Apps 列表中的应用.
然后打开应用程序并点击 用户认证设置 – 设置数字 :
- 应用程序类型 : Web App, 自动 App 或 瓶 (这产生了客户机密)
- 应用权限 : 读写
- 回调 URI :
http://127.0.0.1:8765/callback - 网站 URL: 任何有效的 URL
保存并复制 OAuth 2.0 客户端标识 财务报告和审定财务报表 客户端机密。 。 。 .
2. 入计量
每笔按用途支付账单(~0.015美元/邮资). 打开 计费 —— 信贷
并装入最小值(通常为5美元)。 没有信用, 写出终点
返回时 402 Your enrolled account does not have any credits to fulfill this request 即使认证本身成功.
3. Mint 访问符号
将脚本另存为 mint-x-token.mjs 运行它:
X_CLIENT_ID=<your-client-id> \
X_CLIENT_SECRET=<your-client-secret> \
node mint-x-token.mjs
它打印一个授权 URL —— 打开, 点击 授权应用,则
帮助获取方向 127.0.0.1:8765 并打印访问权限
符号+刷新符号。 访问令牌持续2小时;刷新令牌
(通过) offline.access 瞄准镜),允许您将一个新的薄荷,没有
重新授权.
mint-x-token.mjs — OAuth 2.0 PKCE 帮助器
import http from "node:http";
import crypto from "node:crypto";
import { createXOAuth } from "@apicity/x";
const CLIENT_ID = process.env.X_CLIENT_ID;
const CLIENT_SECRET = process.env.X_CLIENT_SECRET;
const REDIRECT = "http://127.0.0.1:8765/callback";
const SCOPES = [
"tweet.read",
"tweet.write",
"media.write",
"users.read",
"offline.access",
].join(" ");
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error("Set X_CLIENT_ID and X_CLIENT_SECRET");
process.exit(1);
}
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
.createHash("sha256")
.update(verifier)
.digest("base64url");
const state = crypto.randomBytes(16).toString("hex");
const authURL = new URL("https://x.com/i/oauth2/authorize");
authURL.searchParams.set("response_type", "code");
authURL.searchParams.set("client_id", CLIENT_ID);
authURL.searchParams.set("redirect_uri", REDIRECT);
authURL.searchParams.set("scope", SCOPES);
authURL.searchParams.set("state", state);
authURL.searchParams.set("code_challenge", challenge);
authURL.searchParams.set("code_challenge_method", "S256");
console.log("Open this URL and click \"Authorize app\":\n" + authURL.toString());
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, REDIRECT);
if (!url.pathname.startsWith("/callback")) {
res.writeHead(404).end();
return;
}
const code = url.searchParams.get("code");
if (!code || url.searchParams.get("state") !== state) {
res.writeHead(400).end("bad state");
server.close();
process.exit(1);
}
const oauth = createXOAuth({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
});
const tok = await oauth.post.v2.oauth2.token({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT,
code_verifier: verifier,
});
console.log(JSON.stringify(tok, null, 2));
res.writeHead(200).end("Authorized — check your terminal.");
server.close();
});
server.listen(8765, "127.0.0.1");
4. 使用标志
import { createX } from "@apicity/x";
const x = createX({ accessToken: process.env.X_ACCESS_TOKEN! });
await x.post.v2.tweets({
text: "hello from @apicity/x",
});
5. 刷新标志
进入标志在2小时后失效. 使用 createXOAuth 与新鲜
从第3步到薄荷新鲜的,无需重新授权。 刷新
符号可旋转——持续 refresh_token 从 X 时的响应中
返回一个,在省略时保留旧的.
import { createXOAuth } from "@apicity/x";
declare const storedRefreshToken: string; // the refresh_token from step 3
const oauth = createXOAuth({
clientId: process.env.X_CLIENT_ID!,
clientSecret: process.env.X_CLIENT_SECRET!,
});
const tok = await oauth.post.v2.oauth2.token({
grant_type: "refresh_token",
refresh_token: storedRefreshToken,
});
真实世界的例子:发布视频
在X上张贴视频是四通八达的舞蹈
媒体上传, 附加字节, 最终启动转码,
民意调查,直到媒体准备好,然后附上结果 media_id
在微博。 以下信息系按来文照登,并不意味着
tests/integration/x-post-video.test.ts
并重玩对
tests/recordings/x_*/post-video_*/recording.har, (中文(简体) ),
因此反应形状与 X 实际返回的匹配.
import { readFileSync } from "node:fs";
import { createX } from "@apicity/x";
const x = createX({ accessToken: process.env.X_ACCESS_TOKEN! });
// 1. Initialize a chunked upload — declare the media type, total
// byte length, and category up-front. X reserves a media_id we'll
// thread through every later call.
const bytes = readFileSync("./jump.mp4"); // 1,318,021 bytes in the recording
const init = await x.post.v2.media.upload.initialize({
media_type: "video/mp4",
total_bytes: bytes.length,
media_category: "tweet_video",
});
const mediaId = init.data.id;
// → "2050123807214718976"
// 2. Append the bytes. For files >5MB slice the buffer into
// segments and call append once per chunk with segment_index 0..n.
await x.post.v2.media.upload.append(mediaId, {
media: new Blob([bytes], { type: "video/mp4" }),
segment_index: 0,
});
// 3. Finalize. X queues server-side transcoding and returns
// processing_info.state = "pending" while the worker is busy.
const fin = await x.post.v2.media.upload.finalize(mediaId);
// fin.data.processing_info → { state: "pending", check_after_secs: 1 }
// 4. Poll status until the media is ready. Honor
// `check_after_secs` so the loop respects X's pacing hint.
let state = fin.data.processing_info?.state ?? "succeeded";
let wait = fin.data.processing_info?.check_after_secs ?? 1;
while (state === "pending" || state === "in_progress") {
await new Promise((r) => setTimeout(r, wait * 1000));
const status = await x.get.v2.media.upload(mediaId);
state = status.data.processing_info?.state ?? "succeeded";
wait = status.data.processing_info?.check_after_secs ?? 1;
}
// status.data.processing_info → { state: "succeeded", progress_percent: 100 }
// 5. Post the tweet, attaching the now-ready media id.
const tweet = await x.post.v2.tweets({
text: "jump",
media: { media_ids: [mediaId] },
});
console.log(tweet.data.id);
// → "2050123819986378933"
console.log(tweet.data.text);
// → "jump https://t.co/X8cTIpcy3s"
// X auto-appends the attached media's t.co URL to the returned
// text — the literal request body just had "jump".
页:1
media_category必须与资产相匹配:tweet_video, (中文(简体) ),tweet_image, (中文(简体) ),tweet_gif,或amplify_video对于长形。 错位被拒绝 在定稿时,不初始化.- 上传于
data.expires_after_secs(24小时). 如果你最后决定 但永远不要提及media_id在推特上,它是垃圾收集. - 任何步骤投出错误
XError与status和被剖析出的身体 随附,故try { ... } catch (e) { if (e instanceof XError) ... }给你上游errors[0].message或者说detail直接说.
API 参考
7个终点跨出3组. 每个方法都映射出上游的 URL 路径 .
媒体
GET x.v2.media.upload
GET https://api.x.com/2/media/upload{query}
成本等级 : prohibitive
const res = await x.get.v2.media.upload({ /* ... */ });
POST x.v2.media.upload.append
POST https://api.x.com/2/media/upload/{id}/append
成本等级 : prohibitive
const res = await x.post.v2.media.upload.append({ /* ... */ });
POST x.v2.media.upload.finalize
POST https://api.x.com/2/media/upload/{id}/finalize
成本等级 : prohibitive
const res = await x.post.v2.media.upload.finalize({ /* ... */ });
POST x.v2.media.upload.initialize
POST https://api.x.com/2/media/upload/initialize
成本等级 : prohibitive
const res = await x.post.v2.media.upload.initialize({ /* ... */ });
微博
GET x.v2.tweets
GET https://api.x.com/2/tweets/{id}{query}
成本等级 : expensive
const res = await x.get.v2.tweets({ /* ... */ });
POST x.v2.tweets
POST https://api.x.com/2/tweets
成本等级 : expensive
const res = await x.post.v2.tweets({ /* ... */ });
用户
GET x.v2.users.me
GET https://api.x.com/2/users/me{query}
成本等级 : expensive
const res = await x.get.v2.users.me({ /* ... */ });
部分 辣点 单相机.
许可证
麻省理工学院——见 自由主义。 。 。 .