yappr 0.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/.env.example +115 -0
- package/config/context/personality.md +7 -0
- package/config/context/security.md +10 -0
- package/config/hooks/example.ts +47 -0
- package/config/hooks/holder.ts +154 -0
- package/config/hooks/user-memory.ts +102 -0
- package/config/skills/compute/handler.ts +6 -0
- package/config/skills/compute/skill.md +7 -0
- package/config/skills/cron/handler.ts +89 -0
- package/config/skills/cron/skill.md +36 -0
- package/config/skills/generate-image/handler.ts +133 -0
- package/config/skills/generate-image/skill.md +20 -0
- package/config/skills/generate-meme-prompt/handler.ts +40 -0
- package/config/skills/generate-meme-prompt/skill.md +23 -0
- package/config/skills/stats/handler.ts +76 -0
- package/config/skills/stats/skill.md +18 -0
- package/config/skills/wallet/handler.ts +56 -0
- package/config/skills/wallet/skill.md +17 -0
- package/config/skills/x/handler.ts +135 -0
- package/config/skills/x/skill.md +163 -0
- package/dist/config/hooks/example.d.ts +2 -0
- package/dist/config/hooks/example.js +37 -0
- package/dist/config/hooks/holder.d.ts +2 -0
- package/dist/config/hooks/holder.js +147 -0
- package/dist/config/hooks/user-memory.d.ts +2 -0
- package/dist/config/hooks/user-memory.js +79 -0
- package/dist/config/skills/compute/handler.d.ts +2 -0
- package/dist/config/skills/compute/handler.js +5 -0
- package/dist/config/skills/cron/handler.d.ts +2 -0
- package/dist/config/skills/cron/handler.js +84 -0
- package/dist/config/skills/generate-image/handler.d.ts +2 -0
- package/dist/config/skills/generate-image/handler.js +122 -0
- package/dist/config/skills/generate-meme/handler.d.ts +2 -0
- package/dist/config/skills/generate-meme/handler.js +121 -0
- package/dist/config/skills/generate-meme-prompt/handler.d.ts +2 -0
- package/dist/config/skills/generate-meme-prompt/handler.js +38 -0
- package/dist/config/skills/stats/handler.d.ts +2 -0
- package/dist/config/skills/stats/handler.js +71 -0
- package/dist/config/skills/wallet/handler.d.ts +2 -0
- package/dist/config/skills/wallet/handler.js +54 -0
- package/dist/config/skills/x/handler.d.ts +2 -0
- package/dist/config/skills/x/handler.js +115 -0
- package/dist/src/agent-prompt.d.ts +1 -0
- package/dist/src/agent-prompt.js +45 -0
- package/dist/src/bankr.d.ts +41 -0
- package/dist/src/bankr.js +76 -0
- package/dist/src/cli/backup.d.ts +7 -0
- package/dist/src/cli/backup.js +78 -0
- package/dist/src/cli/charts.d.ts +32 -0
- package/dist/src/cli/charts.js +222 -0
- package/dist/src/cli/config-sync.d.ts +7 -0
- package/dist/src/cli/config-sync.js +71 -0
- package/dist/src/cli/deploy.d.ts +2 -0
- package/dist/src/cli/deploy.js +1059 -0
- package/dist/src/cli/env.d.ts +4 -0
- package/dist/src/cli/env.js +50 -0
- package/dist/src/cli/host-key.d.ts +4 -0
- package/dist/src/cli/host-key.js +50 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +71 -0
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +51 -0
- package/dist/src/cli/ssh.d.ts +2 -0
- package/dist/src/cli/ssh.js +141 -0
- package/dist/src/cli/status.d.ts +7 -0
- package/dist/src/cli/status.js +1184 -0
- package/dist/src/cli/tui.d.ts +18 -0
- package/dist/src/cli/tui.js +115 -0
- package/dist/src/cli/ui.d.ts +30 -0
- package/dist/src/cli/ui.js +164 -0
- package/dist/src/cli/update.d.ts +1 -0
- package/dist/src/cli/update.js +263 -0
- package/dist/src/cli/x-login.d.ts +6 -0
- package/dist/src/cli/x-login.js +70 -0
- package/dist/src/compute.d.ts +11 -0
- package/dist/src/compute.js +109 -0
- package/dist/src/config-loader.d.ts +19 -0
- package/dist/src/config-loader.js +82 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +68 -0
- package/dist/src/cron/capability.d.ts +6 -0
- package/dist/src/cron/capability.js +66 -0
- package/dist/src/cron/runner.d.ts +2 -0
- package/dist/src/cron/runner.js +113 -0
- package/dist/src/cron/schedule.d.ts +19 -0
- package/dist/src/cron/schedule.js +154 -0
- package/dist/src/cron/store.d.ts +46 -0
- package/dist/src/cron/store.js +220 -0
- package/dist/src/db.d.ts +4 -0
- package/dist/src/db.js +53 -0
- package/dist/src/hooks/loader.d.ts +1 -0
- package/dist/src/hooks/loader.js +17 -0
- package/dist/src/hooks/registry.d.ts +17 -0
- package/dist/src/hooks/registry.js +78 -0
- package/dist/src/hooks/types.d.ts +45 -0
- package/dist/src/hooks/types.js +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +35 -0
- package/dist/src/llm/index.d.ts +23 -0
- package/dist/src/llm/index.js +213 -0
- package/dist/src/llm/prompts.d.ts +6 -0
- package/dist/src/llm/prompts.js +99 -0
- package/dist/src/log.d.ts +2 -0
- package/dist/src/log.js +30 -0
- package/dist/src/reply/agent.d.ts +20 -0
- package/dist/src/reply/agent.js +215 -0
- package/dist/src/reply/context-blocks.d.ts +12 -0
- package/dist/src/reply/context-blocks.js +22 -0
- package/dist/src/reply/gating.d.ts +3 -0
- package/dist/src/reply/gating.js +35 -0
- package/dist/src/reply/pipeline.d.ts +3 -0
- package/dist/src/reply/pipeline.js +144 -0
- package/dist/src/reply/poller.d.ts +5 -0
- package/dist/src/reply/poller.js +79 -0
- package/dist/src/skills/holder-access.d.ts +7 -0
- package/dist/src/skills/holder-access.js +53 -0
- package/dist/src/skills/loader.d.ts +2 -0
- package/dist/src/skills/loader.js +64 -0
- package/dist/src/skills/registry.d.ts +4 -0
- package/dist/src/skills/registry.js +10 -0
- package/dist/src/skills/types.d.ts +16 -0
- package/dist/src/skills/types.js +1 -0
- package/dist/src/state.d.ts +5 -0
- package/dist/src/state.js +26 -0
- package/dist/src/stats-cli.d.ts +1 -0
- package/dist/src/stats-cli.js +82 -0
- package/dist/src/stats.d.ts +41 -0
- package/dist/src/stats.js +236 -0
- package/dist/src/storage.d.ts +16 -0
- package/dist/src/storage.js +107 -0
- package/dist/src/treasury/abi.d.ts +99 -0
- package/dist/src/treasury/abi.js +71 -0
- package/dist/src/treasury/cycle.d.ts +16 -0
- package/dist/src/treasury/cycle.js +154 -0
- package/dist/src/treasury/index.d.ts +28 -0
- package/dist/src/treasury/index.js +222 -0
- package/dist/src/util.d.ts +3 -0
- package/dist/src/util.js +18 -0
- package/dist/src/wallet.d.ts +5 -0
- package/dist/src/wallet.js +241 -0
- package/dist/src/x/client.d.ts +74 -0
- package/dist/src/x/client.js +323 -0
- package/dist/src/x/types.d.ts +61 -0
- package/dist/src/x/types.js +1 -0
- package/dist/src/x402.d.ts +6 -0
- package/dist/src/x402.js +11 -0
- package/dist/src/yappr.d.ts +1 -0
- package/dist/src/yappr.js +85 -0
- package/package.json +52 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { config } from "./config.js";
|
|
2
|
+
import { log } from "./log.js";
|
|
3
|
+
import { bankrApi, bankrX402Pay } from "./bankr.js";
|
|
4
|
+
import { createBankrSigner, createPayFetch } from "./x402.js";
|
|
5
|
+
import { resolveEvmAddress } from "./compute.js";
|
|
6
|
+
import { recordSpend } from "./stats.js";
|
|
7
|
+
import { sleep, envNumber } from "./util.js";
|
|
8
|
+
let _walletAddress = null;
|
|
9
|
+
let _payFetch = null;
|
|
10
|
+
export async function initBankr() {
|
|
11
|
+
// `/wallet/me` returns the EVM wallet under `wallets[]` (the Bankr EIP-7702 wallet
|
|
12
|
+
// has no top-level `address`), so resolve it the same robust way as everywhere else
|
|
13
|
+
// — and throw clearly if it's missing, rather than silently storing `undefined`.
|
|
14
|
+
_walletAddress = await resolveEvmAddress(config.bankrApiKey);
|
|
15
|
+
return _walletAddress;
|
|
16
|
+
}
|
|
17
|
+
export function walletAddress() {
|
|
18
|
+
if (!_walletAddress)
|
|
19
|
+
throw new Error("initBankr() not called yet");
|
|
20
|
+
return _walletAddress;
|
|
21
|
+
}
|
|
22
|
+
function requestUrl(input) {
|
|
23
|
+
if (typeof input === "string")
|
|
24
|
+
return input;
|
|
25
|
+
if (input instanceof URL)
|
|
26
|
+
return input.toString();
|
|
27
|
+
return input.url;
|
|
28
|
+
}
|
|
29
|
+
// Redact credentials carried as query params (auth_token/ct0) before logging a URL.
|
|
30
|
+
function redactUrl(url) {
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(url);
|
|
33
|
+
for (const k of ["auth_token", "ct0"]) {
|
|
34
|
+
if (u.searchParams.has(k))
|
|
35
|
+
u.searchParams.set(k, "[redacted]");
|
|
36
|
+
}
|
|
37
|
+
return u.toString();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Atomic USDC amount paid for a given Response, captured from the X-PAYMENT header
|
|
44
|
+
// the x402 client attaches to its (paid) retry request. Keyed on the Response so
|
|
45
|
+
// concurrent paid calls can't clobber each other's amount. wrapFetchWithPayment
|
|
46
|
+
// returns the paid response object directly, so the lookup in payFetch hits.
|
|
47
|
+
const _paidAtomic = new WeakMap();
|
|
48
|
+
// Read the EIP-3009 `authorization.value` (atomic USDC) out of a base64 X-PAYMENT /
|
|
49
|
+
// PAYMENT-SIGNATURE header so we can report what each call actually cost.
|
|
50
|
+
function paymentAtomic(headers) {
|
|
51
|
+
const raw = headers.get("x-payment") ?? headers.get("payment-signature");
|
|
52
|
+
if (!raw)
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
const decoded = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
|
|
56
|
+
const value = decoded?.payload?.authorization?.value;
|
|
57
|
+
return value != null ? BigInt(value) : undefined;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Base fetch that records the paid amount per response, then delegates to fetch.
|
|
64
|
+
const tracedFetch = async (input, init) => {
|
|
65
|
+
const res = await fetch(input, init);
|
|
66
|
+
// wrapFetchWithPayment sends a Request object (headers on it, no init) on the
|
|
67
|
+
// paid retry; a first, unpaid attempt carries no payment header and is ignored.
|
|
68
|
+
const headers = input instanceof Request ? input.headers : new Headers(init?.headers);
|
|
69
|
+
const atomic = paymentAtomic(headers);
|
|
70
|
+
if (atomic != null)
|
|
71
|
+
_paidAtomic.set(res, atomic);
|
|
72
|
+
return res;
|
|
73
|
+
};
|
|
74
|
+
// Client-side x402 paid fetch: the @x402/fetch client signs the EIP-3009 payment
|
|
75
|
+
// authorization locally (via Bankr /wallet/sign) and sends the X-PAYMENT header
|
|
76
|
+
// itself — instead of delegating the whole flow to Bankr's /wallet/x402-pay
|
|
77
|
+
// gateway. Built lazily (after initBankr() resolves the wallet address) and reused.
|
|
78
|
+
function paidFetch() {
|
|
79
|
+
if (!_payFetch) {
|
|
80
|
+
const signer = createBankrSigner(config.bankrApiKey, walletAddress());
|
|
81
|
+
_payFetch = createPayFetch(signer, tracedFetch);
|
|
82
|
+
}
|
|
83
|
+
return _payFetch;
|
|
84
|
+
}
|
|
85
|
+
// Safety cap on what the /wallet/x402-pay fallback may authorize per call — used
|
|
86
|
+
// verbatim when the 402's payment requirements can't be parsed, and as a ceiling
|
|
87
|
+
// when they can (guards against an endpoint demanding an absurd amount).
|
|
88
|
+
const FALLBACK_MAX_USD = envNumber("X402_FALLBACK_MAX_USD", 5);
|
|
89
|
+
const FALLBACK_ATTEMPTS = 2;
|
|
90
|
+
// USD price asked by a 402 response: the base64 payment-required header (Bankr
|
|
91
|
+
// style) or the JSON body (Coinbase x402 style), whichever parses.
|
|
92
|
+
async function requiredUsd(res) {
|
|
93
|
+
let req;
|
|
94
|
+
const raw = res.headers.get("payment-required") ?? res.headers.get("x-payment-required");
|
|
95
|
+
if (raw) {
|
|
96
|
+
try {
|
|
97
|
+
req = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
|
|
98
|
+
}
|
|
99
|
+
catch { /* try body */ }
|
|
100
|
+
}
|
|
101
|
+
if (!req) {
|
|
102
|
+
try {
|
|
103
|
+
req = await res.clone().json();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const accept = req?.accepts?.[0];
|
|
110
|
+
const atomic = accept?.maxAmountRequired ?? accept?.amount;
|
|
111
|
+
const n = Number(atomic);
|
|
112
|
+
return atomic != null && Number.isFinite(n) ? n / 1e6 : undefined;
|
|
113
|
+
}
|
|
114
|
+
// Repackage a /wallet/x402-pay gateway result as a Response so callers see the
|
|
115
|
+
// same shape as the client-side path, including the paid amount for paidUsd().
|
|
116
|
+
function gatewayResponse(result) {
|
|
117
|
+
const body = typeof result.response === "string" ? result.response : JSON.stringify(result.response ?? null);
|
|
118
|
+
const res = new Response(body, {
|
|
119
|
+
status: result.status || (result.success ? 200 : 402),
|
|
120
|
+
headers: typeof result.response === "string" ? undefined : { "Content-Type": "application/json" },
|
|
121
|
+
});
|
|
122
|
+
if (result.paymentMade?.amountUsd) {
|
|
123
|
+
_paidAtomic.set(res, BigInt(Math.round(result.paymentMade.amountUsd * 1e6)));
|
|
124
|
+
}
|
|
125
|
+
return res;
|
|
126
|
+
}
|
|
127
|
+
// Which spend category an x402 call bills to, by host: the compute API
|
|
128
|
+
// (compute.x402layer.cc) and the X data endpoint (x402.twit.sh) keep their own lines;
|
|
129
|
+
// everything else a skill/hook pays for (image gen, other x402 APIs) rolls up under "x402".
|
|
130
|
+
function spendCategory(url) {
|
|
131
|
+
if (url.includes("compute.x402layer.cc"))
|
|
132
|
+
return "compute";
|
|
133
|
+
if (url.includes("twit.sh"))
|
|
134
|
+
return "x-api";
|
|
135
|
+
return "x402";
|
|
136
|
+
}
|
|
137
|
+
export async function payFetch(input, init = {}) {
|
|
138
|
+
const url = requestUrl(input);
|
|
139
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
140
|
+
let res;
|
|
141
|
+
let clientErr;
|
|
142
|
+
try {
|
|
143
|
+
res = await paidFetch()(input, init);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
clientErr = err;
|
|
147
|
+
}
|
|
148
|
+
// Fallback: a 402 left after the client-side payment retry (or a thrown payment
|
|
149
|
+
// error) is usually Bankr having EIP-7702-delegated the wallet to a smart account
|
|
150
|
+
// after a server-side op (transfer/swap/fee claim) — the EOA then has code, USDC
|
|
151
|
+
// validates EIP-3009 signatures via ERC-1271 on the delegate, and the plain
|
|
152
|
+
// /wallet/sign signature is rejected. Bankr's /wallet/x402-pay gateway clears the
|
|
153
|
+
// delegation and pays in one call, so route the same request through it.
|
|
154
|
+
if (!res || res.status === 402) {
|
|
155
|
+
const maxUsd = Math.min((res && (await requiredUsd(res))) ?? FALLBACK_MAX_USD, FALLBACK_MAX_USD);
|
|
156
|
+
log.warn({ url: redactUrl(url), method, status: res?.status, maxUsd, err: clientErr instanceof Error ? clientErr.message : clientErr }, "x402 client-side payment failed — falling back to Bankr /wallet/x402-pay");
|
|
157
|
+
for (let attempt = 1; attempt <= FALLBACK_ATTEMPTS; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
const result = await bankrX402Pay(config.bankrApiKey, url, method, typeof init.body === "string" ? init.body : undefined, maxUsd);
|
|
160
|
+
if (result.success) {
|
|
161
|
+
res = gatewayResponse(result);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
log.warn({ url: redactUrl(url), attempt, status: result.status, err: result.error }, "x402-pay fallback failed");
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
log.warn({ url: redactUrl(url), attempt, err: err instanceof Error ? err.message : String(err) }, "x402-pay fallback errored");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Both paths exhausted: surface the original client-side failure — the 402
|
|
171
|
+
// response if there was one, otherwise the thrown error.
|
|
172
|
+
if (!res)
|
|
173
|
+
throw clientErr;
|
|
174
|
+
}
|
|
175
|
+
// /tweets/mentions is polled constantly — skip its success log to keep logs
|
|
176
|
+
// clean. Failures are always logged. The per-call USD cost is captured per
|
|
177
|
+
// response (see paidUsd) and logged by the caller (x/client, treasury) on the
|
|
178
|
+
// line that's actually shown, so the amount appears next to the call itself.
|
|
179
|
+
const isMentions = url.includes("/tweets/mentions");
|
|
180
|
+
const safeUrl = redactUrl(url);
|
|
181
|
+
// warn, not error: this layer rethrows nothing itself but every caller throws on
|
|
182
|
+
// !res.ok and the failure is logged as ONE error where it's finally handled —
|
|
183
|
+
// logging error here too would count a single failure 2-3× in the stats ledger.
|
|
184
|
+
if (!res.ok)
|
|
185
|
+
log.warn({ url: safeUrl, method, status: res.status }, "x402 payFetch failed");
|
|
186
|
+
else if (!isMentions)
|
|
187
|
+
log.info({ url: safeUrl, method, status: res.status }, "x402 payFetch ok");
|
|
188
|
+
// Every x402 payment funnels through here, so it's the one place to record spend
|
|
189
|
+
// into the ledger. Categorise by host: the compute API, the X data endpoint, or any
|
|
190
|
+
// other x402 endpoint a skill/hook calls (image gen, etc.) → the generic "x402".
|
|
191
|
+
// (Inference isn't x402 — it's recorded separately from the credit balance.)
|
|
192
|
+
const paid = paidUsd(res);
|
|
193
|
+
if (paid != null && paid > 0)
|
|
194
|
+
recordSpend(spendCategory(url), paid);
|
|
195
|
+
return res;
|
|
196
|
+
}
|
|
197
|
+
// USD cost (USDC, 6 decimals) of the paid call that produced `res`, or undefined if
|
|
198
|
+
// the response wasn't a paid one. Callers log this on their own success line so the
|
|
199
|
+
// amount shows up where the call is reported (e.g. "x-api GET ... ok {usd}").
|
|
200
|
+
export function paidUsd(res) {
|
|
201
|
+
const atomic = _paidAtomic.get(res);
|
|
202
|
+
return atomic != null ? Number(atomic) / 1e6 : undefined;
|
|
203
|
+
}
|
|
204
|
+
// Fixed pause before each submit so rapid back-to-back treasury txs (dev cut → burn →
|
|
205
|
+
// swap → extend) don't trip the Bankr signer's in-flight limit; and attempts/backoff for
|
|
206
|
+
// retrying transient signer/provider errors (provider_inflight_limit, 5xx, timeouts, …).
|
|
207
|
+
const SUBMIT_PAUSE_MS = envNumber("TX_SUBMIT_PAUSE_MS", 1500);
|
|
208
|
+
const SUBMIT_MAX_ATTEMPTS = envNumber("TX_SUBMIT_MAX_ATTEMPTS", 5);
|
|
209
|
+
export async function submitTx(to, data) {
|
|
210
|
+
if (config.treasuryDryRun) {
|
|
211
|
+
log.info({ to, data: data.slice(0, 66) + "..." }, "bankr [dry run] submitTx");
|
|
212
|
+
return "0xdry000000000000000000000000000000000000000000000000000000000000";
|
|
213
|
+
}
|
|
214
|
+
// Space submissions out, then retry any failure with exponential backoff (2s, 4s, 8s,
|
|
215
|
+
// 16s) — gives a busy signer time to clear before the next attempt.
|
|
216
|
+
await sleep(SUBMIT_PAUSE_MS);
|
|
217
|
+
let lastErr;
|
|
218
|
+
for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt++) {
|
|
219
|
+
try {
|
|
220
|
+
// Bankr's /wallet/submit expects the tx nested under `transaction` (not flat) and
|
|
221
|
+
// returns `transactionHash`.
|
|
222
|
+
const res = await bankrApi(config.bankrApiKey, "/wallet/submit", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
transaction: { to, data, chainId: 8453 },
|
|
226
|
+
waitForConfirmation: true,
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
return res.transactionHash ?? res.txHash ?? "";
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
lastErr = err;
|
|
233
|
+
if (attempt < SUBMIT_MAX_ATTEMPTS) {
|
|
234
|
+
const delayMs = 2000 * 2 ** (attempt - 1); // 2s → 4s → 8s → 16s
|
|
235
|
+
log.warn({ to, attempt, maxAttempts: SUBMIT_MAX_ATTEMPTS, delayMs, err: err instanceof Error ? err.message : String(err) }, "submitTx failed — retrying after backoff");
|
|
236
|
+
await sleep(delayMs);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw lastErr;
|
|
241
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { SearchResponse, Tweet } from "./types.js";
|
|
2
|
+
export declare function extractTweetId(raw: string): string;
|
|
3
|
+
export declare function tweetImageUrls(tweet: Tweet): string[];
|
|
4
|
+
export declare function getTweetById(id: string): Promise<Tweet>;
|
|
5
|
+
export declare function getTweets(ids: string[]): Promise<Tweet[]>;
|
|
6
|
+
export declare function getUserTweets(username: string, next_token?: string): Promise<SearchResponse>;
|
|
7
|
+
export type TweetSearchParams = {
|
|
8
|
+
words?: string;
|
|
9
|
+
phrase?: string;
|
|
10
|
+
anyWords?: string;
|
|
11
|
+
noneWords?: string;
|
|
12
|
+
hashtags?: string;
|
|
13
|
+
from?: string;
|
|
14
|
+
to?: string;
|
|
15
|
+
mentioning?: string;
|
|
16
|
+
minReplies?: number;
|
|
17
|
+
minLikes?: number;
|
|
18
|
+
minReposts?: number;
|
|
19
|
+
since?: string;
|
|
20
|
+
until?: string;
|
|
21
|
+
next_token?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function searchTweets(params: TweetSearchParams): Promise<SearchResponse>;
|
|
24
|
+
export declare function searchMentions(handle: string): Promise<SearchResponse>;
|
|
25
|
+
export declare function getTweetReplies(id: string, next_token?: string): Promise<SearchResponse>;
|
|
26
|
+
export declare function getRetweetedBy(id: string, next_token?: string): Promise<SearchResponse>;
|
|
27
|
+
export declare function getQuoteTweets(id: string, next_token?: string): Promise<SearchResponse>;
|
|
28
|
+
export declare function postTweet(text: string, opts?: {
|
|
29
|
+
replyTo?: string;
|
|
30
|
+
quoteTweetId?: string;
|
|
31
|
+
mediaIds?: string[];
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
export declare function postReply(tweetId: string, text: string, mediaIds?: string[]): Promise<void>;
|
|
34
|
+
export declare function deleteTweet(id: string): Promise<void>;
|
|
35
|
+
export declare function likeTweet(id: string): Promise<void>;
|
|
36
|
+
export declare function unlikeTweet(id: string): Promise<void>;
|
|
37
|
+
export declare function retweetTweet(id: string): Promise<void>;
|
|
38
|
+
export declare function unretweetTweet(id: string): Promise<void>;
|
|
39
|
+
export declare function bookmarkTweet(id: string): Promise<void>;
|
|
40
|
+
export declare function unbookmarkTweet(id: string): Promise<void>;
|
|
41
|
+
export declare function uploadMedia(file: Blob | ArrayBuffer | Uint8Array, opts?: {
|
|
42
|
+
filename?: string;
|
|
43
|
+
contentType?: string;
|
|
44
|
+
}): Promise<string>;
|
|
45
|
+
export declare function uploadMediaFromUrl(url: string): Promise<string>;
|
|
46
|
+
export declare function getUserByUsername(username: string): Promise<unknown>;
|
|
47
|
+
export declare function getUserById(id: string): Promise<unknown>;
|
|
48
|
+
export declare function searchUsers(query: string, next_token?: string): Promise<unknown>;
|
|
49
|
+
export declare function getUsers(ids: string[]): Promise<unknown>;
|
|
50
|
+
export declare function getFollowers(id: string, next_token?: string): Promise<unknown>;
|
|
51
|
+
export declare function getFollowing(id: string, next_token?: string): Promise<unknown>;
|
|
52
|
+
export declare function followUser(opts: {
|
|
53
|
+
id?: string;
|
|
54
|
+
username?: string;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
export declare function unfollowUser(opts: {
|
|
57
|
+
id?: string;
|
|
58
|
+
username?: string;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
export declare function setProfile(opts: {
|
|
61
|
+
name?: string;
|
|
62
|
+
bio?: string;
|
|
63
|
+
location?: string;
|
|
64
|
+
url?: string;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
export declare function getArticle(id: string): Promise<unknown>;
|
|
67
|
+
export declare function getList(id: string): Promise<unknown>;
|
|
68
|
+
export declare function getListMembers(id: string, next_token?: string): Promise<unknown>;
|
|
69
|
+
export declare function getListFollowers(id: string, next_token?: string): Promise<unknown>;
|
|
70
|
+
export declare function getListTweets(id: string, next_token?: string): Promise<SearchResponse>;
|
|
71
|
+
export declare function getCommunity(id: string): Promise<unknown>;
|
|
72
|
+
export declare function getCommunityMembers(id: string, next_token?: string): Promise<unknown>;
|
|
73
|
+
export declare function getCommunityPosts(id: string, next_token?: string): Promise<SearchResponse>;
|
|
74
|
+
export declare function getUserInsights(username: string): Promise<unknown>;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { payFetch, paidUsd } from "../wallet.js";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
import { sleep } from "../util.js";
|
|
5
|
+
// Full X/Twitter API SDK over the x402-paid data endpoint. Not every export is
|
|
6
|
+
// wired to a skill — these are the building blocks skills (e.g. config/skills/x)
|
|
7
|
+
// compose into actions.
|
|
8
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
9
|
+
function base(path) {
|
|
10
|
+
return `${config.xApiBaseUrl}${path}`;
|
|
11
|
+
}
|
|
12
|
+
function auth() {
|
|
13
|
+
return { auth_token: config.twitterAuthToken, ct0: config.twitterCt0 };
|
|
14
|
+
}
|
|
15
|
+
// twit.sh endpoints that act on behalf of the authenticated user. They require
|
|
16
|
+
// auth_token + ct0, which we inject automatically (from env) on every call — these
|
|
17
|
+
// params are never exposed to the LLM/skill. Read endpoints don't need auth.
|
|
18
|
+
const AUTHENTICATED_PATHS = new Set([
|
|
19
|
+
"/tweets",
|
|
20
|
+
"/tweets/long",
|
|
21
|
+
"/tweets/like",
|
|
22
|
+
"/tweets/bookmark",
|
|
23
|
+
"/tweets/retweet",
|
|
24
|
+
"/users/following",
|
|
25
|
+
"/users/setProfile",
|
|
26
|
+
// (/tweets/mediaUpload is also authenticated, but uploadMedia injects auth itself —
|
|
27
|
+
// it's multipart/form-data and bypasses the JSON post()/withAuth() path.)
|
|
28
|
+
]);
|
|
29
|
+
function withAuth(path, params) {
|
|
30
|
+
return AUTHENTICATED_PATHS.has(path) ? { ...params, ...auth() } : params;
|
|
31
|
+
}
|
|
32
|
+
function buildUrl(path, params) {
|
|
33
|
+
const url = new URL(base(path));
|
|
34
|
+
for (const [k, v] of Object.entries(params)) {
|
|
35
|
+
if (v !== undefined)
|
|
36
|
+
url.searchParams.set(k, String(v));
|
|
37
|
+
}
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
async function get(path, params = {}) {
|
|
41
|
+
const t = Date.now();
|
|
42
|
+
log.info({ path, params: sanitizeParams(params) }, `x-api GET ${path}`);
|
|
43
|
+
const res = await payFetch(buildUrl(path, params));
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const body = await res.text();
|
|
46
|
+
// warn before throwing: the catch site logs the (counted) error — see log.ts.
|
|
47
|
+
log.warn({ path, status: res.status, ms: Date.now() - t }, `x-api GET ${path} failed`);
|
|
48
|
+
throw new Error(`GET ${path} failed: ${res.status} ${body}`);
|
|
49
|
+
}
|
|
50
|
+
log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res) }, `x-api GET ${path} ok`);
|
|
51
|
+
return res.json();
|
|
52
|
+
}
|
|
53
|
+
async function post(path, body) {
|
|
54
|
+
const payload = withAuth(path, body);
|
|
55
|
+
// twit.sh proxy endpoints read their params (including auth_token/ct0) from the
|
|
56
|
+
// QUERY STRING, not the body — confirmed: a correctly-sent body-only POST still
|
|
57
|
+
// returns "ct0 is null". So put the payload on the URL (and keep it in the body
|
|
58
|
+
// too for any endpoint that reads it, e.g. /tweets/long's `text`).
|
|
59
|
+
const url = buildUrl(path, payload);
|
|
60
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
61
|
+
const t = Date.now();
|
|
62
|
+
log.info({ path, attempt }, `x-api POST ${path}`);
|
|
63
|
+
const res = await payFetch(url, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
});
|
|
68
|
+
// twit.sh returns 2xx (often 201) even when the action failed — the real
|
|
69
|
+
// signal is an `errors` array embedded in the body (e.g. a created tweet has
|
|
70
|
+
// `data.id`, a failure has `data.errors` like the 186 length/auth error). So
|
|
71
|
+
// on a 2xx we inspect the body and treat an embedded error as a failure.
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
const json = await res.json().catch(() => undefined);
|
|
74
|
+
const apiErrors = json?.errors ?? json?.data?.errors;
|
|
75
|
+
if (Array.isArray(apiErrors) && apiErrors.length > 0) {
|
|
76
|
+
log.warn({ path, status: res.status, ms: Date.now() - t, errors: apiErrors }, `x-api POST ${path} returned errors`);
|
|
77
|
+
throw new Error(`POST ${path} failed: ${JSON.stringify(apiErrors)}`);
|
|
78
|
+
}
|
|
79
|
+
// Log the raw twit.sh response body (e.g. the created tweet on POST /tweets).
|
|
80
|
+
log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res), response: json }, `x-api POST ${path} ok`);
|
|
81
|
+
return json;
|
|
82
|
+
}
|
|
83
|
+
const text = await res.text();
|
|
84
|
+
if (res.status === 500 && attempt < 5) {
|
|
85
|
+
log.warn({ path, attempt, status: res.status }, `x-api POST ${path} 500, retrying`);
|
|
86
|
+
await sleep(attempt * 1000);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
log.warn({ path, status: res.status, ms: Date.now() - t, body: text }, `x-api POST ${path} failed`);
|
|
90
|
+
throw new Error(`POST ${path} failed: ${res.status} ${text}`);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`POST ${path} exhausted retries`);
|
|
93
|
+
}
|
|
94
|
+
async function del(path, params) {
|
|
95
|
+
const all = withAuth(path, params);
|
|
96
|
+
const t = Date.now();
|
|
97
|
+
log.info({ path }, `x-api DELETE ${path}`);
|
|
98
|
+
const res = await payFetch(buildUrl(path, all), { method: "DELETE" });
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const body = await res.text();
|
|
101
|
+
log.warn({ path, status: res.status, ms: Date.now() - t }, `x-api DELETE ${path} failed`);
|
|
102
|
+
throw new Error(`DELETE ${path} failed: ${res.status} ${body}`);
|
|
103
|
+
}
|
|
104
|
+
log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res) }, `x-api DELETE ${path} ok`);
|
|
105
|
+
return res.json();
|
|
106
|
+
}
|
|
107
|
+
function sanitizeParams(params) {
|
|
108
|
+
const safe = {};
|
|
109
|
+
for (const [k, v] of Object.entries(params)) {
|
|
110
|
+
if (k === "auth_token" || k === "ct0")
|
|
111
|
+
safe[k] = "[redacted]";
|
|
112
|
+
else
|
|
113
|
+
safe[k] = v;
|
|
114
|
+
}
|
|
115
|
+
return safe;
|
|
116
|
+
}
|
|
117
|
+
// ─── tweets ─────────────────────────────────────────────────────────────────
|
|
118
|
+
export function extractTweetId(raw) {
|
|
119
|
+
return raw.match(/\/status\/(\d+)/)?.[1] ?? raw.trim();
|
|
120
|
+
}
|
|
121
|
+
// Direct CDN URLs of the still images attached to a tweet (photos only — video /
|
|
122
|
+
// animated_gif are skipped). Reads entities.media first, falling back to the
|
|
123
|
+
// parallel media_metadata list, and dedupes. Used by the reply loop to attach the
|
|
124
|
+
// image to a vision model when a mention carries one.
|
|
125
|
+
export function tweetImageUrls(tweet) {
|
|
126
|
+
const urls = new Set();
|
|
127
|
+
for (const m of tweet.entities?.media ?? []) {
|
|
128
|
+
if ((m.type ?? "photo") === "photo" && m.media_url_https)
|
|
129
|
+
urls.add(m.media_url_https);
|
|
130
|
+
}
|
|
131
|
+
for (const m of tweet.media_metadata ?? []) {
|
|
132
|
+
if (m.media_url)
|
|
133
|
+
urls.add(m.media_url);
|
|
134
|
+
}
|
|
135
|
+
return [...urls];
|
|
136
|
+
}
|
|
137
|
+
export async function getTweetById(id) {
|
|
138
|
+
return get("/tweets/by/id", { id });
|
|
139
|
+
}
|
|
140
|
+
export async function getTweets(ids) {
|
|
141
|
+
if (ids.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
log.info({ ids }, "GET x-api /tweets");
|
|
144
|
+
const json = await get("/tweets", { ids: ids.join(",") });
|
|
145
|
+
return json.data ?? [];
|
|
146
|
+
}
|
|
147
|
+
export async function getUserTweets(username, next_token) {
|
|
148
|
+
return get("/tweets/user", { username, next_token });
|
|
149
|
+
}
|
|
150
|
+
export async function searchTweets(params) {
|
|
151
|
+
return get("/tweets/search", params);
|
|
152
|
+
}
|
|
153
|
+
// Poll the authenticated account's mentions. Uses the purpose-built
|
|
154
|
+
// /tweets/mentions endpoint, which returns the same shape as search but only
|
|
155
|
+
// needs auth (auth_token + ct0). `type` is an optional filter we leave unset.
|
|
156
|
+
// Poll for mentions using the method chosen in config (POLL_METHOD):
|
|
157
|
+
// - "search": /tweets/search filtered to tweets mentioning the agent handle.
|
|
158
|
+
// - "mentions": the dedicated /tweets/mentions endpoint (auth_token + ct0 only).
|
|
159
|
+
// Both return the same SearchResponse shape.
|
|
160
|
+
export async function searchMentions(handle) {
|
|
161
|
+
// No banner log here — the underlying get()/searchTweets already logs the request
|
|
162
|
+
// and its "ok" line (with cost), so a separate line would just triple the output.
|
|
163
|
+
if (config.pollMethod === "mentions") {
|
|
164
|
+
return get("/tweets/mentions", auth());
|
|
165
|
+
}
|
|
166
|
+
return searchTweets({ mentioning: handle });
|
|
167
|
+
}
|
|
168
|
+
export async function getTweetReplies(id, next_token) {
|
|
169
|
+
return get("/tweets/replies", { id, next_token });
|
|
170
|
+
}
|
|
171
|
+
export async function getRetweetedBy(id, next_token) {
|
|
172
|
+
return get("/tweets/retweeted_by", { id, next_token });
|
|
173
|
+
}
|
|
174
|
+
export async function getQuoteTweets(id, next_token) {
|
|
175
|
+
return get("/tweets/quote_tweets", { id, next_token });
|
|
176
|
+
}
|
|
177
|
+
export async function postTweet(text, opts = {}) {
|
|
178
|
+
// /tweets/long supports the full character limit; plain /tweets rejects longer text.
|
|
179
|
+
// Both accept `medias` — a comma-separated list of media IDs from uploadMedia() — to
|
|
180
|
+
// attach images to the post.
|
|
181
|
+
await post("/tweets/long", {
|
|
182
|
+
text,
|
|
183
|
+
...(opts.replyTo ? { in_reply_to_tweet_id: opts.replyTo } : {}),
|
|
184
|
+
...(opts.quoteTweetId ? { quote_tweet_id: opts.quoteTweetId } : {}),
|
|
185
|
+
...(opts.mediaIds?.length ? { medias: opts.mediaIds.join(",") } : {}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export async function postReply(tweetId, text, mediaIds) {
|
|
189
|
+
log.info({ tweetId, media: mediaIds?.length ?? 0 }, "POST x-api reply");
|
|
190
|
+
await postTweet(text, { replyTo: tweetId, mediaIds });
|
|
191
|
+
}
|
|
192
|
+
export async function deleteTweet(id) {
|
|
193
|
+
await del("/tweets", { id });
|
|
194
|
+
}
|
|
195
|
+
export async function likeTweet(id) {
|
|
196
|
+
await post("/tweets/like", { id });
|
|
197
|
+
}
|
|
198
|
+
export async function unlikeTweet(id) {
|
|
199
|
+
await del("/tweets/like", { id });
|
|
200
|
+
}
|
|
201
|
+
export async function retweetTweet(id) {
|
|
202
|
+
await post("/tweets/retweet", { id });
|
|
203
|
+
}
|
|
204
|
+
export async function unretweetTweet(id) {
|
|
205
|
+
await del("/tweets/retweet", { id });
|
|
206
|
+
}
|
|
207
|
+
export async function bookmarkTweet(id) {
|
|
208
|
+
await post("/tweets/bookmark", { id });
|
|
209
|
+
}
|
|
210
|
+
export async function unbookmarkTweet(id) {
|
|
211
|
+
await del("/tweets/bookmark", { id });
|
|
212
|
+
}
|
|
213
|
+
// ─── media ─────────────────────────────────────────────────────────────────
|
|
214
|
+
// Upload an image to X and return its media_id — attach it to a post via
|
|
215
|
+
// postTweet({ mediaIds: [id] }). This endpoint is multipart/form-data with auth on the
|
|
216
|
+
// query string (per the twit.sh spec), so it bypasses the JSON post() helper and builds
|
|
217
|
+
// the request itself. Accepts raw bytes (Blob / Uint8Array / ArrayBuffer).
|
|
218
|
+
export async function uploadMedia(file, opts = {}) {
|
|
219
|
+
const path = "/tweets/mediaUpload";
|
|
220
|
+
const url = buildUrl(path, auth()); // auth_token + ct0 on the query string
|
|
221
|
+
// Cast at the Blob boundary: TS 5.7's generic Uint8Array<ArrayBufferLike> doesn't
|
|
222
|
+
// structurally match lib.dom's BlobPart (ArrayBufferView<ArrayBuffer>), though the
|
|
223
|
+
// bytes are valid — see microsoft/TypeScript#59417.
|
|
224
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: opts.contentType ?? "application/octet-stream" });
|
|
225
|
+
const form = new FormData();
|
|
226
|
+
form.append("file", blob, opts.filename ?? "image.png");
|
|
227
|
+
const t = Date.now();
|
|
228
|
+
log.info({ path }, `x-api POST ${path}`);
|
|
229
|
+
// No Content-Type header: fetch sets multipart/form-data with the correct boundary.
|
|
230
|
+
const res = await payFetch(url, { method: "POST", body: form });
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
const body = await res.text();
|
|
233
|
+
log.warn({ path, status: res.status, ms: Date.now() - t }, `x-api POST ${path} failed`);
|
|
234
|
+
throw new Error(`POST ${path} failed: ${res.status} ${body}`);
|
|
235
|
+
}
|
|
236
|
+
const json = (await res.json().catch(() => undefined));
|
|
237
|
+
// The success body is undocumented in the spec; pull the id from the usual
|
|
238
|
+
// Twitter/twit.sh field names (media_id_string is the canonical Twitter one).
|
|
239
|
+
const id = json?.media_id_string ?? json?.media_id ?? json?.data?.media_id_string ?? json?.data?.media_id ?? json?.id ?? json?.data?.id;
|
|
240
|
+
log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res), mediaId: id }, `x-api POST ${path} ok`);
|
|
241
|
+
if (id == null)
|
|
242
|
+
throw new Error(`mediaUpload: no media id in response ${JSON.stringify(json)}`);
|
|
243
|
+
return String(id);
|
|
244
|
+
}
|
|
245
|
+
// Convenience: fetch an image by URL (plain fetch — public CDN, not x402) and upload it,
|
|
246
|
+
// returning the media_id. Pairs with image-generation skills that produce a hosted URL.
|
|
247
|
+
export async function uploadMediaFromUrl(url) {
|
|
248
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(20_000) });
|
|
249
|
+
if (!res.ok)
|
|
250
|
+
throw new Error(`fetch media ${url} failed: ${res.status}`);
|
|
251
|
+
const contentType = res.headers.get("content-type") ?? "image/png";
|
|
252
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
253
|
+
const filename = new URL(url).pathname.split("/").pop() || "image.png";
|
|
254
|
+
return uploadMedia(bytes, { filename, contentType });
|
|
255
|
+
}
|
|
256
|
+
// ─── users ──────────────────────────────────────────────────────────────────
|
|
257
|
+
export async function getUserByUsername(username) {
|
|
258
|
+
return get("/users/by/username", { username });
|
|
259
|
+
}
|
|
260
|
+
export async function getUserById(id) {
|
|
261
|
+
return get("/users/by/id", { id });
|
|
262
|
+
}
|
|
263
|
+
export async function searchUsers(query, next_token) {
|
|
264
|
+
return get("/users/search", { query, next_token });
|
|
265
|
+
}
|
|
266
|
+
// Batch-fetch several users in one call. Cheaper than N `getUserById` calls when
|
|
267
|
+
// you already have the numeric ids (e.g. resolving a list of authors/followers).
|
|
268
|
+
export async function getUsers(ids) {
|
|
269
|
+
return get("/users", { ids: ids.join(",") });
|
|
270
|
+
}
|
|
271
|
+
export async function getFollowers(id, next_token) {
|
|
272
|
+
return get("/users/followers", { id, next_token });
|
|
273
|
+
}
|
|
274
|
+
export async function getFollowing(id, next_token) {
|
|
275
|
+
return get("/users/following", { id, next_token });
|
|
276
|
+
}
|
|
277
|
+
export async function followUser(opts) {
|
|
278
|
+
await post("/users/following", opts);
|
|
279
|
+
}
|
|
280
|
+
export async function unfollowUser(opts) {
|
|
281
|
+
await del("/users/following", opts);
|
|
282
|
+
}
|
|
283
|
+
// Update the authenticated user's profile. Only the fields you pass are sent; an
|
|
284
|
+
// omitted field is left unchanged (pass an empty string to clear one).
|
|
285
|
+
export async function setProfile(opts) {
|
|
286
|
+
await post("/users/setProfile", {
|
|
287
|
+
...(opts.name !== undefined ? { name: opts.name } : {}),
|
|
288
|
+
...(opts.bio !== undefined ? { bio: opts.bio } : {}),
|
|
289
|
+
...(opts.location !== undefined ? { location: opts.location } : {}),
|
|
290
|
+
...(opts.url !== undefined ? { url: opts.url } : {}),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// ─── articles ───────────────────────────────────────────────────────────────
|
|
294
|
+
export async function getArticle(id) {
|
|
295
|
+
return get("/articles/by/id", { id });
|
|
296
|
+
}
|
|
297
|
+
// ─── lists ──────────────────────────────────────────────────────────────────
|
|
298
|
+
export async function getList(id) {
|
|
299
|
+
return get("/lists/by/id", { id });
|
|
300
|
+
}
|
|
301
|
+
export async function getListMembers(id, next_token) {
|
|
302
|
+
return get("/lists/members", { id, next_token });
|
|
303
|
+
}
|
|
304
|
+
export async function getListFollowers(id, next_token) {
|
|
305
|
+
return get("/lists/followers", { id, next_token });
|
|
306
|
+
}
|
|
307
|
+
export async function getListTweets(id, next_token) {
|
|
308
|
+
return get("/lists/tweets", { id, next_token });
|
|
309
|
+
}
|
|
310
|
+
// ─── communities ─────────────────────────────────────────────────────────────
|
|
311
|
+
export async function getCommunity(id) {
|
|
312
|
+
return get("/communities/by/id", { id });
|
|
313
|
+
}
|
|
314
|
+
export async function getCommunityMembers(id, next_token) {
|
|
315
|
+
return get("/communities/members", { id, next_token });
|
|
316
|
+
}
|
|
317
|
+
export async function getCommunityPosts(id, next_token) {
|
|
318
|
+
return get("/communities/posts", { id, next_token });
|
|
319
|
+
}
|
|
320
|
+
// ─── workflows ───────────────────────────────────────────────────────────────
|
|
321
|
+
export async function getUserInsights(username) {
|
|
322
|
+
return get("/workflows/userInsights", { username });
|
|
323
|
+
}
|