@vellumai/assistant 0.4.43 → 0.4.44
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/ARCHITECTURE.md +13 -14
- package/README.md +11 -12
- package/docs/architecture/integrations.md +75 -93
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -2
- package/src/__tests__/bundled-asset.test.ts +1 -1
- package/src/__tests__/checker.test.ts +31 -28
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
- package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
- package/src/__tests__/onboarding-template-contract.test.ts +0 -10
- package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
- package/src/__tests__/send-endpoint-busy.test.ts +0 -3
- package/src/__tests__/session-confirmation-signals.test.ts +7 -45
- package/src/__tests__/starter-task-flow.test.ts +9 -19
- package/src/__tests__/system-prompt.test.ts +3 -4
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
- package/src/cli/commands/amazon/index.ts +4 -39
- package/src/cli/commands/amazon/session.ts +18 -26
- package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
- package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
- package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
- package/src/cli/commands/twitter/index.ts +95 -835
- package/src/cli/commands/twitter/oauth-client.ts +1 -35
- package/src/cli/commands/twitter/router.ts +70 -115
- package/src/cli/commands/twitter/types.ts +30 -0
- package/src/cli/reference.ts +2 -2
- package/src/config/bundled-skills/amazon/SKILL.md +0 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
- package/src/config/bundled-skills/doordash/SKILL.md +0 -1
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
- package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
- package/src/config/bundled-skills/twitter/SKILL.md +53 -166
- package/src/config/feature-flag-registry.json +8 -0
- package/src/daemon/handlers/session-history.ts +41 -9
- package/src/daemon/lifecycle.ts +4 -17
- package/src/daemon/message-types/apps.ts +0 -25
- package/src/daemon/message-types/integrations.ts +1 -7
- package/src/daemon/message-types/sessions.ts +6 -1
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +33 -1
- package/src/daemon/seed-files.ts +3 -27
- package/src/daemon/server.ts +2 -18
- package/src/daemon/session-agent-loop-handlers.ts +24 -2
- package/src/daemon/session-runtime-assembly.ts +0 -7
- package/src/daemon/session-surfaces.ts +185 -33
- package/src/daemon/session.ts +2 -28
- package/src/memory/app-store.ts +0 -18
- package/src/memory/schema/infrastructure.ts +0 -8
- package/src/permissions/defaults.ts +3 -3
- package/src/prompts/system-prompt.ts +4 -5
- package/src/prompts/templates/BOOTSTRAP.md +0 -3
- package/src/providers/registry.ts +2 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/auth/scopes.ts +1 -0
- package/src/runtime/auth/token-service.ts +1 -1
- package/src/runtime/http-types.ts +10 -0
- package/src/runtime/middleware/error-handler.ts +14 -1
- package/src/runtime/routes/app-management-routes.ts +61 -64
- package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
- package/src/runtime/routes/brain-graph-routes.ts +4 -42
- package/src/runtime/routes/conversation-routes.ts +9 -6
- package/src/runtime/routes/diagnostics-routes.ts +91 -14
- package/src/runtime/routes/settings-routes.ts +3 -93
- package/src/tools/AGENTS.md +38 -0
- package/src/tools/apps/executors.ts +0 -6
- package/src/tools/document/editor-template.ts +10 -8
- package/src/twitter/platform-proxy-client.ts +6 -3
- package/src/util/errors.ts +12 -0
- package/src/__tests__/home-base-bootstrap.test.ts +0 -84
- package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
- package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
- package/src/cli/commands/twitter/client.ts +0 -989
- package/src/cli/commands/twitter/session.ts +0 -121
- package/src/home-base/app-link-store.ts +0 -78
- package/src/home-base/bootstrap.ts +0 -74
- package/src/home-base/prebuilt/brain-graph.html +0 -1483
- package/src/home-base/prebuilt/index.html +0 -702
- package/src/home-base/prebuilt/seed-metadata.json +0 -21
- package/src/home-base/prebuilt/seed.ts +0 -122
- package/src/home-base/prebuilt-home-base-updater.ts +0 -36
- package/src/util/cookie-session.ts +0 -98
|
@@ -1,989 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Twitter API client.
|
|
3
|
-
* Executes GraphQL queries through Chrome's CDP (Runtime.evaluate) so requests
|
|
4
|
-
* go through the browser's authenticated session.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { loadSession, type TwitterSession } from "./session.js";
|
|
8
|
-
|
|
9
|
-
class ProviderError extends Error {
|
|
10
|
-
constructor(
|
|
11
|
-
message: string,
|
|
12
|
-
public readonly provider: string,
|
|
13
|
-
public readonly statusCode?: number,
|
|
14
|
-
) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = "ProviderError";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const CDP_BASE = "http://localhost:9222";
|
|
21
|
-
|
|
22
|
-
/** Static bearer token used by x.com for all GraphQL requests. */
|
|
23
|
-
const BEARER_TOKEN =
|
|
24
|
-
"AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
|
|
25
|
-
|
|
26
|
-
// ─── Query IDs (captured from x.com) ─────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
const QUERY_IDS = {
|
|
29
|
-
CreateTweet: "Ah3G_byjEDs_HSlgU0PyZw",
|
|
30
|
-
UserByScreenName: "AWbeRIdkLtqTRN7yL_H8yw",
|
|
31
|
-
UserTweets: "N2tFDY-MlrLxXJ9F_ZxJGA",
|
|
32
|
-
TweetDetail: "YCNdW_ZytXfV9YR3cJK9kw",
|
|
33
|
-
SearchTimeline: "ML-n2SfAxx5S_9QMqNejbg",
|
|
34
|
-
Bookmarks: "toTC7lB_mQm5fuBE5yyEJw",
|
|
35
|
-
HomeTimeline: "nn16KxqX3E1OdE7WlHB5LA",
|
|
36
|
-
NotificationsTimeline: "saZw4lppu6QzMEiRUCYurg",
|
|
37
|
-
Likes: "Pcw-j9lrSeDMmkgnIejJiQ",
|
|
38
|
-
Followers: "P7m4Qr-rJEB8KUluOenU6A",
|
|
39
|
-
Following: "T5wihsMTYHncY7BB4YxHSg",
|
|
40
|
-
UserMedia: "xLCC9bG_VqHfXXgq8jPoCg",
|
|
41
|
-
} as const;
|
|
42
|
-
|
|
43
|
-
/** Feature flags shared by all GraphQL endpoints. */
|
|
44
|
-
const FEATURES: Record<string, boolean> = {
|
|
45
|
-
rweb_video_screen_enabled: false,
|
|
46
|
-
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
47
|
-
responsive_web_profile_redirect_enabled: false,
|
|
48
|
-
rweb_tipjar_consumption_enabled: false,
|
|
49
|
-
verified_phone_label_enabled: false,
|
|
50
|
-
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
51
|
-
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
52
|
-
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
53
|
-
premium_content_api_read_enabled: false,
|
|
54
|
-
communities_web_enable_tweet_community_results_fetch: true,
|
|
55
|
-
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
56
|
-
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
57
|
-
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
58
|
-
responsive_web_jetfuel_frame: true,
|
|
59
|
-
responsive_web_grok_share_attachment_enabled: true,
|
|
60
|
-
responsive_web_grok_annotations_enabled: true,
|
|
61
|
-
articles_preview_enabled: true,
|
|
62
|
-
responsive_web_edit_tweet_api_enabled: true,
|
|
63
|
-
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
64
|
-
view_counts_everywhere_api_enabled: true,
|
|
65
|
-
longform_notetweets_consumption_enabled: true,
|
|
66
|
-
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
67
|
-
tweet_awards_web_tipping_enabled: false,
|
|
68
|
-
responsive_web_grok_show_grok_translated_post: true,
|
|
69
|
-
responsive_web_grok_analysis_button_from_backend: true,
|
|
70
|
-
post_ctas_fetch_enabled: true,
|
|
71
|
-
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
72
|
-
standardized_nudges_misinfo: true,
|
|
73
|
-
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
74
|
-
longform_notetweets_rich_text_read_enabled: true,
|
|
75
|
-
longform_notetweets_inline_media_enabled: true,
|
|
76
|
-
responsive_web_grok_image_annotation_enabled: true,
|
|
77
|
-
responsive_web_grok_imagine_annotation_enabled: true,
|
|
78
|
-
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
|
79
|
-
responsive_web_enhance_cards_enabled: false,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// ─── Errors ──────────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
/** Thrown when the session is missing or expired. */
|
|
85
|
-
export class SessionExpiredError extends Error {
|
|
86
|
-
constructor(reason: string) {
|
|
87
|
-
super(reason);
|
|
88
|
-
this.name = "SessionExpiredError";
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function requireSession(): Promise<TwitterSession> {
|
|
93
|
-
const session = await loadSession();
|
|
94
|
-
if (!session) {
|
|
95
|
-
throw new SessionExpiredError("No Twitter session found.");
|
|
96
|
-
}
|
|
97
|
-
return session;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── CDP transport ───────────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
async function findTwitterTab(): Promise<string> {
|
|
103
|
-
const res = await fetch(`${CDP_BASE}/json/list`).catch(() => null);
|
|
104
|
-
if (!res?.ok) {
|
|
105
|
-
throw new SessionExpiredError(
|
|
106
|
-
"Chrome CDP not available. Run `assistant twitter refresh` first.",
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const targets = (await res.json()) as Array<{
|
|
110
|
-
type: string;
|
|
111
|
-
url: string;
|
|
112
|
-
webSocketDebuggerUrl: string;
|
|
113
|
-
}>;
|
|
114
|
-
const tab = targets.find(
|
|
115
|
-
(t) =>
|
|
116
|
-
t.type === "page" &&
|
|
117
|
-
(t.url.includes("x.com") || t.url.includes("twitter.com")),
|
|
118
|
-
);
|
|
119
|
-
if (!tab?.webSocketDebuggerUrl) {
|
|
120
|
-
throw new SessionExpiredError(
|
|
121
|
-
"No x.com tab found in Chrome. Open x.com and try again.",
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
return tab.webSocketDebuggerUrl;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Standard headers for X API requests (as a JS expression for Runtime.evaluate). */
|
|
128
|
-
const API_HEADERS_GET = `{
|
|
129
|
-
'authorization': 'Bearer ${BEARER_TOKEN}',
|
|
130
|
-
'x-csrf-token': csrf,
|
|
131
|
-
'x-twitter-auth-type': 'OAuth2Session',
|
|
132
|
-
'x-twitter-active-user': 'yes',
|
|
133
|
-
'x-twitter-client-language': 'en',
|
|
134
|
-
}`;
|
|
135
|
-
|
|
136
|
-
const API_HEADERS_POST = `{
|
|
137
|
-
'Content-Type': 'application/json',
|
|
138
|
-
'authorization': 'Bearer ${BEARER_TOKEN}',
|
|
139
|
-
'x-csrf-token': csrf,
|
|
140
|
-
'x-twitter-auth-type': 'OAuth2Session',
|
|
141
|
-
'x-twitter-active-user': 'yes',
|
|
142
|
-
'x-twitter-client-language': 'en',
|
|
143
|
-
}`;
|
|
144
|
-
|
|
145
|
-
/** Execute a POST fetch inside Chrome via CDP Runtime.evaluate. */
|
|
146
|
-
async function cdpFetch(
|
|
147
|
-
wsUrl: string,
|
|
148
|
-
url: string,
|
|
149
|
-
body: string,
|
|
150
|
-
): Promise<unknown> {
|
|
151
|
-
return cdpEval(
|
|
152
|
-
wsUrl,
|
|
153
|
-
`
|
|
154
|
-
(function() {
|
|
155
|
-
var csrf = (document.cookie.match(/ct0=([^;]+)/) || [])[1] || '';
|
|
156
|
-
return fetch(${JSON.stringify(url)}, {
|
|
157
|
-
method: 'POST',
|
|
158
|
-
headers: ${API_HEADERS_POST},
|
|
159
|
-
body: ${JSON.stringify(body)},
|
|
160
|
-
credentials: 'include',
|
|
161
|
-
})
|
|
162
|
-
.then(function(r) {
|
|
163
|
-
if (!r.ok) return r.text().then(function(t) {
|
|
164
|
-
return JSON.stringify({ __status: r.status, __error: true, __body: t.substring(0, 500) });
|
|
165
|
-
});
|
|
166
|
-
return r.text();
|
|
167
|
-
})
|
|
168
|
-
.catch(function(e) { return JSON.stringify({ __error: true, __message: e.message }); });
|
|
169
|
-
})()
|
|
170
|
-
`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Execute a GET fetch inside Chrome via CDP Runtime.evaluate. */
|
|
175
|
-
async function cdpGet(wsUrl: string, url: string): Promise<unknown> {
|
|
176
|
-
return cdpEval(
|
|
177
|
-
wsUrl,
|
|
178
|
-
`
|
|
179
|
-
(function() {
|
|
180
|
-
var csrf = (document.cookie.match(/ct0=([^;]+)/) || [])[1] || '';
|
|
181
|
-
return fetch(${JSON.stringify(url)}, {
|
|
182
|
-
method: 'GET',
|
|
183
|
-
headers: ${API_HEADERS_GET},
|
|
184
|
-
credentials: 'include',
|
|
185
|
-
})
|
|
186
|
-
.then(function(r) {
|
|
187
|
-
if (!r.ok) return r.text().then(function(t) {
|
|
188
|
-
return JSON.stringify({ __status: r.status, __error: true, __body: t.substring(0, 500) });
|
|
189
|
-
});
|
|
190
|
-
return r.text();
|
|
191
|
-
})
|
|
192
|
-
.catch(function(e) { return JSON.stringify({ __error: true, __message: e.message }); });
|
|
193
|
-
})()
|
|
194
|
-
`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Navigate Chrome to a URL and capture the response body of a specific GraphQL query.
|
|
200
|
-
* This works for endpoints that require X's client-generated transaction ID (e.g. Search, Followers)
|
|
201
|
-
* because the browser's own JavaScript generates the correct headers.
|
|
202
|
-
*/
|
|
203
|
-
async function cdpNavigateAndCapture(
|
|
204
|
-
wsUrl: string,
|
|
205
|
-
pageUrl: string,
|
|
206
|
-
queryName: string,
|
|
207
|
-
): Promise<unknown> {
|
|
208
|
-
return new Promise((resolve, reject) => {
|
|
209
|
-
const ws = new WebSocket(wsUrl);
|
|
210
|
-
let nextId = 1;
|
|
211
|
-
const callbacks = new Map<number, (v: unknown) => void>();
|
|
212
|
-
const pendingRequestIds = new Set<string>();
|
|
213
|
-
|
|
214
|
-
const timeout = setTimeout(() => {
|
|
215
|
-
ws.close();
|
|
216
|
-
reject(
|
|
217
|
-
new Error(`CDP navigate+capture timed out waiting for ${queryName}`),
|
|
218
|
-
);
|
|
219
|
-
}, 30000);
|
|
220
|
-
|
|
221
|
-
function send(
|
|
222
|
-
method: string,
|
|
223
|
-
params?: Record<string, unknown>,
|
|
224
|
-
): Promise<unknown> {
|
|
225
|
-
const id = nextId++;
|
|
226
|
-
return new Promise((r) => {
|
|
227
|
-
callbacks.set(id, r);
|
|
228
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
ws.onmessage = (event) => {
|
|
233
|
-
const msg = JSON.parse(typeof event.data === "string" ? event.data : "");
|
|
234
|
-
|
|
235
|
-
// Handle command responses
|
|
236
|
-
if (msg.id != null && callbacks.has(msg.id)) {
|
|
237
|
-
callbacks.get(msg.id)!(msg.result ?? msg.error);
|
|
238
|
-
callbacks.delete(msg.id);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Track GraphQL requests matching our query name
|
|
243
|
-
if (msg.method === "Network.requestWillBeSent") {
|
|
244
|
-
const req = msg.params?.request;
|
|
245
|
-
const url = req?.url as string | undefined;
|
|
246
|
-
if (url?.includes(`/graphql/`) && url?.includes(`/${queryName}`)) {
|
|
247
|
-
pendingRequestIds.add(msg.params.requestId as string);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Capture response when loading finishes
|
|
252
|
-
if (msg.method === "Network.loadingFinished") {
|
|
253
|
-
const requestId = msg.params?.requestId as string;
|
|
254
|
-
if (!pendingRequestIds.has(requestId)) return;
|
|
255
|
-
pendingRequestIds.delete(requestId);
|
|
256
|
-
|
|
257
|
-
send("Network.getResponseBody", { requestId })
|
|
258
|
-
.then((result) => {
|
|
259
|
-
const body = (result as Record<string, unknown>)?.body as string;
|
|
260
|
-
if (!body) return;
|
|
261
|
-
try {
|
|
262
|
-
const json = JSON.parse(body);
|
|
263
|
-
clearTimeout(timeout);
|
|
264
|
-
ws.close();
|
|
265
|
-
if (json.errors?.length) {
|
|
266
|
-
reject(
|
|
267
|
-
new Error(
|
|
268
|
-
`X API errors: ${json.errors.map((e: { message: string }) => e.message).join("; ")}`,
|
|
269
|
-
),
|
|
270
|
-
);
|
|
271
|
-
} else {
|
|
272
|
-
resolve(json);
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
/* not JSON, skip */
|
|
276
|
-
}
|
|
277
|
-
})
|
|
278
|
-
.catch(() => {
|
|
279
|
-
/* ignore */
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
ws.onerror = () => {
|
|
285
|
-
clearTimeout(timeout);
|
|
286
|
-
reject(new SessionExpiredError("CDP connection failed."));
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
ws.onopen = async () => {
|
|
290
|
-
await send("Network.enable");
|
|
291
|
-
await send("Page.enable");
|
|
292
|
-
await send("Page.navigate", { url: pageUrl });
|
|
293
|
-
};
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Shared CDP evaluate helper. */
|
|
298
|
-
async function cdpEval(wsUrl: string, expression: string): Promise<unknown> {
|
|
299
|
-
return new Promise((resolve, reject) => {
|
|
300
|
-
const ws = new WebSocket(wsUrl);
|
|
301
|
-
const id = 1;
|
|
302
|
-
|
|
303
|
-
const timeout = setTimeout(() => {
|
|
304
|
-
ws.close();
|
|
305
|
-
reject(new Error("CDP fetch timed out after 30s"));
|
|
306
|
-
}, 30000);
|
|
307
|
-
|
|
308
|
-
ws.onopen = () => {
|
|
309
|
-
ws.send(
|
|
310
|
-
JSON.stringify({
|
|
311
|
-
id,
|
|
312
|
-
method: "Runtime.evaluate",
|
|
313
|
-
params: { expression, awaitPromise: true, returnByValue: true },
|
|
314
|
-
}),
|
|
315
|
-
);
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
ws.onmessage = (event) => {
|
|
319
|
-
try {
|
|
320
|
-
const msg = JSON.parse(
|
|
321
|
-
typeof event.data === "string" ? event.data : "",
|
|
322
|
-
);
|
|
323
|
-
if (msg.id === id) {
|
|
324
|
-
clearTimeout(timeout);
|
|
325
|
-
ws.close();
|
|
326
|
-
|
|
327
|
-
if (msg.error) {
|
|
328
|
-
reject(new Error(`CDP error: ${msg.error.message}`));
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const value = msg.result?.result?.value;
|
|
333
|
-
if (!value) {
|
|
334
|
-
reject(new Error("Empty CDP response"));
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
339
|
-
if (parsed.__error) {
|
|
340
|
-
if (parsed.__status === 403 || parsed.__status === 401) {
|
|
341
|
-
reject(new SessionExpiredError("Twitter session has expired."));
|
|
342
|
-
} else {
|
|
343
|
-
reject(
|
|
344
|
-
new Error(
|
|
345
|
-
parsed.__message ??
|
|
346
|
-
`HTTP ${parsed.__status}: ${parsed.__body ?? ""}`,
|
|
347
|
-
),
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
resolve(parsed);
|
|
353
|
-
}
|
|
354
|
-
} catch (err) {
|
|
355
|
-
clearTimeout(timeout);
|
|
356
|
-
ws.close();
|
|
357
|
-
reject(err);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
ws.onerror = () => {
|
|
362
|
-
clearTimeout(timeout);
|
|
363
|
-
reject(new SessionExpiredError("CDP connection failed."));
|
|
364
|
-
};
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// ─── GraphQL helpers ─────────────────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
/** Build a GraphQL GET URL with encoded variables and features. */
|
|
371
|
-
function graphqlUrl(
|
|
372
|
-
queryId: string,
|
|
373
|
-
queryName: string,
|
|
374
|
-
variables: Record<string, unknown>,
|
|
375
|
-
): string {
|
|
376
|
-
const v = encodeURIComponent(JSON.stringify(variables));
|
|
377
|
-
const f = encodeURIComponent(JSON.stringify(FEATURES));
|
|
378
|
-
return `https://x.com/i/api/graphql/${queryId}/${queryName}?variables=${v}&features=${f}`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** Execute a GraphQL GET query and return the parsed response. */
|
|
382
|
-
async function graphqlGet(
|
|
383
|
-
queryId: string,
|
|
384
|
-
queryName: string,
|
|
385
|
-
variables: Record<string, unknown>,
|
|
386
|
-
): Promise<unknown> {
|
|
387
|
-
await requireSession();
|
|
388
|
-
const wsUrl = await findTwitterTab();
|
|
389
|
-
const url = graphqlUrl(queryId, queryName, variables);
|
|
390
|
-
const json = (await cdpGet(wsUrl, url)) as {
|
|
391
|
-
errors?: Array<{ message: string }>;
|
|
392
|
-
};
|
|
393
|
-
if (json.errors?.length) {
|
|
394
|
-
throw new ProviderError(
|
|
395
|
-
`X API errors: ${json.errors.map((e) => e.message).join("; ")}`,
|
|
396
|
-
"x",
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
return json;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// ─── Twitter API response types ──────────────────────────────────────────────
|
|
403
|
-
|
|
404
|
-
interface TwitterUserLegacy {
|
|
405
|
-
screen_name?: string;
|
|
406
|
-
name?: string;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
interface TwitterUserCore {
|
|
410
|
-
screen_name?: string;
|
|
411
|
-
name?: string;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
interface TwitterUserResult {
|
|
415
|
-
rest_id?: string;
|
|
416
|
-
legacy?: TwitterUserLegacy;
|
|
417
|
-
core?: TwitterUserCore;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
interface TwitterUserResults {
|
|
421
|
-
result?: TwitterUserResult;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
interface TweetLegacy {
|
|
425
|
-
full_text?: string;
|
|
426
|
-
created_at?: string;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
interface TweetResult {
|
|
430
|
-
__typename?: string;
|
|
431
|
-
rest_id?: string;
|
|
432
|
-
legacy?: TweetLegacy;
|
|
433
|
-
core?: { user_results?: TwitterUserResults };
|
|
434
|
-
tweet?: TweetResult;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
interface TweetResults {
|
|
438
|
-
result?: TweetResult;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
interface TimelineItemContent {
|
|
442
|
-
__typename?: string;
|
|
443
|
-
tweet_results?: TweetResults;
|
|
444
|
-
user_results?: TwitterUserResults;
|
|
445
|
-
// Notification-specific fields
|
|
446
|
-
id?: string;
|
|
447
|
-
rich_message?: { text?: string };
|
|
448
|
-
notification_text?: { text?: string };
|
|
449
|
-
timestamp_ms?: string;
|
|
450
|
-
notification_url?: { url?: string };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
interface TimelineModuleItem {
|
|
454
|
-
item?: { itemContent?: TimelineItemContent };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
interface TimelineEntryContent {
|
|
458
|
-
itemContent?: TimelineItemContent;
|
|
459
|
-
items?: TimelineModuleItem[];
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
interface TimelineEntry {
|
|
463
|
-
entryId?: string;
|
|
464
|
-
content?: TimelineEntryContent;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
interface TimelineInstruction {
|
|
468
|
-
entries?: TimelineEntry[];
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
interface TimelineContainer {
|
|
472
|
-
instructions?: TimelineInstruction[];
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
interface TimelineWrapper {
|
|
476
|
-
timeline?: TimelineContainer;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
interface TwitterApiError {
|
|
480
|
-
message: string;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** Response from CreateTweet mutation. */
|
|
484
|
-
interface CreateTweetResponse {
|
|
485
|
-
errors?: TwitterApiError[];
|
|
486
|
-
data?: {
|
|
487
|
-
create_tweet?: {
|
|
488
|
-
tweet_results?: TweetResults;
|
|
489
|
-
};
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/** Response from UserByScreenName query. */
|
|
494
|
-
interface UserByScreenNameResponse {
|
|
495
|
-
data?: {
|
|
496
|
-
user?: {
|
|
497
|
-
result?: TwitterUserResult;
|
|
498
|
-
};
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/** Response from UserTweets query. */
|
|
503
|
-
interface UserTweetsResponse {
|
|
504
|
-
data?: {
|
|
505
|
-
user?: {
|
|
506
|
-
result?: {
|
|
507
|
-
timeline_v2?: TimelineWrapper;
|
|
508
|
-
timeline?: TimelineWrapper;
|
|
509
|
-
};
|
|
510
|
-
};
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/** Response from TweetDetail query. */
|
|
515
|
-
interface TweetDetailResponse {
|
|
516
|
-
data?: {
|
|
517
|
-
threaded_conversation_with_injections_v2?: TimelineContainer;
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/** Response from SearchTimeline query. */
|
|
522
|
-
interface SearchTimelineResponse {
|
|
523
|
-
data?: {
|
|
524
|
-
search_by_raw_query?: {
|
|
525
|
-
search_timeline?: TimelineWrapper;
|
|
526
|
-
};
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/** Response from Bookmarks query. */
|
|
531
|
-
interface BookmarksResponse {
|
|
532
|
-
data?: {
|
|
533
|
-
bookmark_timeline_v2?: TimelineWrapper;
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/** Response from HomeTimeline query. */
|
|
538
|
-
interface HomeTimelineResponse {
|
|
539
|
-
data?: {
|
|
540
|
-
home?: {
|
|
541
|
-
home_timeline_urt?: TimelineContainer;
|
|
542
|
-
};
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/** Response from NotificationsTimeline query. */
|
|
547
|
-
interface NotificationsTimelineResponse {
|
|
548
|
-
data?: {
|
|
549
|
-
viewer_v2?: {
|
|
550
|
-
user_results?: {
|
|
551
|
-
result?: {
|
|
552
|
-
notification_timeline?: TimelineWrapper;
|
|
553
|
-
};
|
|
554
|
-
};
|
|
555
|
-
};
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/** Response from Likes / Following / Followers / UserMedia queries. */
|
|
560
|
-
interface UserTimelineResponse {
|
|
561
|
-
data?: {
|
|
562
|
-
user?: {
|
|
563
|
-
result?: {
|
|
564
|
-
timeline?: TimelineWrapper;
|
|
565
|
-
};
|
|
566
|
-
};
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// ─── Tweet extraction helpers ────────────────────────────────────────────────
|
|
571
|
-
|
|
572
|
-
function extractScreenName(tweetResult: TweetResult): string {
|
|
573
|
-
return (
|
|
574
|
-
tweetResult?.core?.user_results?.result?.legacy?.screen_name ??
|
|
575
|
-
tweetResult?.core?.user_results?.result?.core?.screen_name ??
|
|
576
|
-
"i"
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/** Extract tweets from a timeline instructions array (shared by most endpoints). */
|
|
581
|
-
function extractTweetsFromInstructions(
|
|
582
|
-
instructions: TimelineInstruction[],
|
|
583
|
-
): TweetEntry[] {
|
|
584
|
-
const tweets: TweetEntry[] = [];
|
|
585
|
-
for (const instruction of instructions) {
|
|
586
|
-
// Handle both array-style entries and direct entries
|
|
587
|
-
const entries = instruction.entries ?? [];
|
|
588
|
-
for (const entry of entries) {
|
|
589
|
-
// Standard tweet entry
|
|
590
|
-
let tweetResult = entry.content?.itemContent?.tweet_results?.result;
|
|
591
|
-
// Some results wrap in __typename: "TweetWithVisibilityResults"
|
|
592
|
-
if (tweetResult?.__typename === "TweetWithVisibilityResults") {
|
|
593
|
-
tweetResult = tweetResult.tweet;
|
|
594
|
-
}
|
|
595
|
-
if (tweetResult?.rest_id) {
|
|
596
|
-
tweets.push({
|
|
597
|
-
tweetId: tweetResult.rest_id,
|
|
598
|
-
text: tweetResult.legacy?.full_text ?? "",
|
|
599
|
-
url: `https://x.com/${extractScreenName(tweetResult)}/status/${tweetResult.rest_id}`,
|
|
600
|
-
createdAt: tweetResult.legacy?.created_at ?? "",
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Search results can have TimelineTimelineModule with nested items
|
|
605
|
-
if (entry.content?.items) {
|
|
606
|
-
for (const item of entry.content.items) {
|
|
607
|
-
let tr = item.item?.itemContent?.tweet_results?.result;
|
|
608
|
-
if (tr?.__typename === "TweetWithVisibilityResults") tr = tr.tweet;
|
|
609
|
-
if (tr?.rest_id) {
|
|
610
|
-
tweets.push({
|
|
611
|
-
tweetId: tr.rest_id,
|
|
612
|
-
text: tr.legacy?.full_text ?? "",
|
|
613
|
-
url: `https://x.com/${extractScreenName(tr)}/status/${tr.rest_id}`,
|
|
614
|
-
createdAt: tr.legacy?.created_at ?? "",
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return tweets;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/** Extract users from a timeline instructions array (Followers/Following). */
|
|
625
|
-
function extractUsersFromInstructions(
|
|
626
|
-
instructions: TimelineInstruction[],
|
|
627
|
-
): UserInfo[] {
|
|
628
|
-
const users: UserInfo[] = [];
|
|
629
|
-
for (const instruction of instructions) {
|
|
630
|
-
for (const entry of instruction.entries ?? []) {
|
|
631
|
-
const userResult = entry.content?.itemContent?.user_results?.result;
|
|
632
|
-
if (userResult?.rest_id) {
|
|
633
|
-
users.push({
|
|
634
|
-
userId: userResult.rest_id,
|
|
635
|
-
screenName:
|
|
636
|
-
userResult.legacy?.screen_name ??
|
|
637
|
-
userResult.core?.screen_name ??
|
|
638
|
-
"",
|
|
639
|
-
name: userResult.legacy?.name ?? userResult.core?.name ?? "",
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
return users;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ─── Public types ────────────────────────────────────────────────────────────
|
|
648
|
-
|
|
649
|
-
export interface PostTweetResult {
|
|
650
|
-
tweetId: string;
|
|
651
|
-
text: string;
|
|
652
|
-
url: string;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
export interface UserInfo {
|
|
656
|
-
userId: string;
|
|
657
|
-
screenName: string;
|
|
658
|
-
name: string;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
export interface TweetEntry {
|
|
662
|
-
tweetId: string;
|
|
663
|
-
text: string;
|
|
664
|
-
url: string;
|
|
665
|
-
createdAt: string;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export interface NotificationEntry {
|
|
669
|
-
id: string;
|
|
670
|
-
message: string;
|
|
671
|
-
timestamp: string;
|
|
672
|
-
url?: string;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ─── Write operations ────────────────────────────────────────────────────────
|
|
676
|
-
|
|
677
|
-
export async function postTweet(
|
|
678
|
-
text: string,
|
|
679
|
-
opts?: { inReplyToTweetId?: string },
|
|
680
|
-
): Promise<PostTweetResult> {
|
|
681
|
-
await requireSession();
|
|
682
|
-
|
|
683
|
-
const wsUrl = await findTwitterTab();
|
|
684
|
-
const url = `https://x.com/i/api/graphql/${QUERY_IDS.CreateTweet}/CreateTweet`;
|
|
685
|
-
const variables: Record<string, unknown> = {
|
|
686
|
-
tweet_text: text,
|
|
687
|
-
dark_request: false,
|
|
688
|
-
media: {
|
|
689
|
-
media_entities: [],
|
|
690
|
-
possibly_sensitive: false,
|
|
691
|
-
},
|
|
692
|
-
semantic_annotation_ids: [],
|
|
693
|
-
disallowed_reply_options: null,
|
|
694
|
-
};
|
|
695
|
-
if (opts?.inReplyToTweetId) {
|
|
696
|
-
variables.reply = {
|
|
697
|
-
in_reply_to_tweet_id: opts.inReplyToTweetId,
|
|
698
|
-
exclude_reply_user_ids: [],
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
const body = JSON.stringify({
|
|
702
|
-
variables,
|
|
703
|
-
features: FEATURES,
|
|
704
|
-
queryId: QUERY_IDS.CreateTweet,
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
const json = (await cdpFetch(wsUrl, url, body)) as CreateTweetResponse;
|
|
708
|
-
|
|
709
|
-
if (json.errors?.length) {
|
|
710
|
-
throw new ProviderError(
|
|
711
|
-
`X API errors: ${json.errors.map((e) => e.message).join("; ")}`,
|
|
712
|
-
"x",
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const tweetResults = json.data?.create_tweet?.tweet_results;
|
|
717
|
-
const result = tweetResults?.result;
|
|
718
|
-
if (!result?.rest_id) {
|
|
719
|
-
if (tweetResults && !result) {
|
|
720
|
-
throw new ProviderError(
|
|
721
|
-
"X rejected this post — it may be a duplicate of a recent post. Try different text.",
|
|
722
|
-
"x",
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
throw new ProviderError(
|
|
726
|
-
`Unexpected response from X API. Response: ${JSON.stringify(json).slice(0, 500)}`,
|
|
727
|
-
"x",
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return {
|
|
732
|
-
tweetId: result.rest_id,
|
|
733
|
-
text,
|
|
734
|
-
url: `https://x.com/${extractScreenName(result)}/status/${result.rest_id}`,
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ─── User lookup ─────────────────────────────────────────────────────────────
|
|
739
|
-
|
|
740
|
-
export async function getUserByScreenName(
|
|
741
|
-
screenName: string,
|
|
742
|
-
): Promise<UserInfo> {
|
|
743
|
-
const json = (await graphqlGet(
|
|
744
|
-
QUERY_IDS.UserByScreenName,
|
|
745
|
-
"UserByScreenName",
|
|
746
|
-
{
|
|
747
|
-
screen_name: screenName,
|
|
748
|
-
withGrokTranslatedBio: true,
|
|
749
|
-
},
|
|
750
|
-
)) as UserByScreenNameResponse;
|
|
751
|
-
|
|
752
|
-
const user = json.data?.user?.result;
|
|
753
|
-
if (!user?.rest_id) {
|
|
754
|
-
throw new ProviderError(`User @${screenName} not found`, "x");
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
return {
|
|
758
|
-
userId: user.rest_id,
|
|
759
|
-
screenName: user.legacy?.screen_name ?? screenName,
|
|
760
|
-
name: user.legacy?.name ?? screenName,
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// ─── User tweets ─────────────────────────────────────────────────────────────
|
|
765
|
-
|
|
766
|
-
export async function getUserTweets(
|
|
767
|
-
userId: string,
|
|
768
|
-
count = 20,
|
|
769
|
-
): Promise<TweetEntry[]> {
|
|
770
|
-
const json = (await graphqlGet(QUERY_IDS.UserTweets, "UserTweets", {
|
|
771
|
-
userId,
|
|
772
|
-
count,
|
|
773
|
-
includePromotedContent: true,
|
|
774
|
-
withQuickPromoteEligibilityTweetFields: true,
|
|
775
|
-
withVoice: true,
|
|
776
|
-
})) as UserTweetsResponse;
|
|
777
|
-
|
|
778
|
-
// Response path: data.user.result.timeline_v2.timeline.instructions[]
|
|
779
|
-
// Fallback to data.user.result.timeline.timeline.instructions[]
|
|
780
|
-
const timelineData =
|
|
781
|
-
json.data?.user?.result?.timeline_v2 ?? json.data?.user?.result?.timeline;
|
|
782
|
-
const instructions = timelineData?.timeline?.instructions ?? [];
|
|
783
|
-
return extractTweetsFromInstructions(instructions);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// ─── Tweet detail ────────────────────────────────────────────────────────────
|
|
787
|
-
|
|
788
|
-
export async function getTweetDetail(tweetId: string): Promise<TweetEntry[]> {
|
|
789
|
-
const json = (await graphqlGet(QUERY_IDS.TweetDetail, "TweetDetail", {
|
|
790
|
-
focalTweetId: tweetId,
|
|
791
|
-
referrer: "tweet",
|
|
792
|
-
with_rux_injections: false,
|
|
793
|
-
rankingMode: "Relevance",
|
|
794
|
-
includePromotedContent: true,
|
|
795
|
-
withCommunity: true,
|
|
796
|
-
withQuickPromoteEligibilityTweetFields: true,
|
|
797
|
-
withBirdwatchNotes: true,
|
|
798
|
-
withVoice: true,
|
|
799
|
-
})) as TweetDetailResponse;
|
|
800
|
-
|
|
801
|
-
// Response path: data.threaded_conversation_with_injections_v2.instructions[]
|
|
802
|
-
const instructions =
|
|
803
|
-
json.data?.threaded_conversation_with_injections_v2?.instructions ?? [];
|
|
804
|
-
return extractTweetsFromInstructions(instructions);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
808
|
-
|
|
809
|
-
export async function searchTweets(
|
|
810
|
-
query: string,
|
|
811
|
-
product: "Top" | "Latest" | "People" | "Media" = "Top",
|
|
812
|
-
): Promise<TweetEntry[]> {
|
|
813
|
-
await requireSession();
|
|
814
|
-
const wsUrl = await findTwitterTab();
|
|
815
|
-
|
|
816
|
-
// Search requires X's client-generated transaction ID, so we navigate Chrome
|
|
817
|
-
// to the search page and capture the response from network events.
|
|
818
|
-
const productParam = product === "Top" ? "" : `&f=${product.toLowerCase()}`;
|
|
819
|
-
const pageUrl = `https://x.com/search?q=${encodeURIComponent(query)}&src=typed_query${productParam}`;
|
|
820
|
-
const json = (await cdpNavigateAndCapture(
|
|
821
|
-
wsUrl,
|
|
822
|
-
pageUrl,
|
|
823
|
-
"SearchTimeline",
|
|
824
|
-
)) as SearchTimelineResponse;
|
|
825
|
-
|
|
826
|
-
const instructions =
|
|
827
|
-
json.data?.search_by_raw_query?.search_timeline?.timeline?.instructions ??
|
|
828
|
-
[];
|
|
829
|
-
return extractTweetsFromInstructions(instructions);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// ─── Bookmarks ───────────────────────────────────────────────────────────────
|
|
833
|
-
|
|
834
|
-
export async function getBookmarks(count = 20): Promise<TweetEntry[]> {
|
|
835
|
-
const json = (await graphqlGet(QUERY_IDS.Bookmarks, "Bookmarks", {
|
|
836
|
-
count,
|
|
837
|
-
includePromotedContent: true,
|
|
838
|
-
})) as BookmarksResponse;
|
|
839
|
-
|
|
840
|
-
// Response path: data.bookmark_timeline_v2.timeline.instructions[]
|
|
841
|
-
const instructions =
|
|
842
|
-
json.data?.bookmark_timeline_v2?.timeline?.instructions ?? [];
|
|
843
|
-
return extractTweetsFromInstructions(instructions);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// ─── Home timeline ───────────────────────────────────────────────────────────
|
|
847
|
-
|
|
848
|
-
export async function getHomeTimeline(count = 20): Promise<TweetEntry[]> {
|
|
849
|
-
const json = (await graphqlGet(QUERY_IDS.HomeTimeline, "HomeTimeline", {
|
|
850
|
-
count,
|
|
851
|
-
includePromotedContent: true,
|
|
852
|
-
requestContext: "launch",
|
|
853
|
-
withCommunity: true,
|
|
854
|
-
})) as HomeTimelineResponse;
|
|
855
|
-
|
|
856
|
-
// Response path: data.home.home_timeline_urt.instructions[]
|
|
857
|
-
const instructions = json.data?.home?.home_timeline_urt?.instructions ?? [];
|
|
858
|
-
return extractTweetsFromInstructions(instructions);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// ─── Notifications ───────────────────────────────────────────────────────────
|
|
862
|
-
|
|
863
|
-
export async function getNotifications(
|
|
864
|
-
count = 20,
|
|
865
|
-
): Promise<NotificationEntry[]> {
|
|
866
|
-
const json = (await graphqlGet(
|
|
867
|
-
QUERY_IDS.NotificationsTimeline,
|
|
868
|
-
"NotificationsTimeline",
|
|
869
|
-
{
|
|
870
|
-
timeline_type: "All",
|
|
871
|
-
count,
|
|
872
|
-
},
|
|
873
|
-
)) as NotificationsTimelineResponse;
|
|
874
|
-
|
|
875
|
-
// Response path: data.viewer_v2.user_results.result.notification_timeline.timeline.instructions[]
|
|
876
|
-
const instructions =
|
|
877
|
-
json.data?.viewer_v2?.user_results?.result?.notification_timeline?.timeline
|
|
878
|
-
?.instructions ?? [];
|
|
879
|
-
|
|
880
|
-
const notifications: NotificationEntry[] = [];
|
|
881
|
-
for (const instruction of instructions) {
|
|
882
|
-
for (const entry of instruction.entries ?? []) {
|
|
883
|
-
const ic = entry.content?.itemContent;
|
|
884
|
-
if (ic?.__typename !== "TimelineNotification") continue;
|
|
885
|
-
notifications.push({
|
|
886
|
-
id: ic.id ?? entry.entryId ?? "",
|
|
887
|
-
message: ic.rich_message?.text ?? ic.notification_text?.text ?? "",
|
|
888
|
-
timestamp: ic.timestamp_ms ?? "",
|
|
889
|
-
url: ic.notification_url?.url,
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return notifications;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// ─── Likes ───────────────────────────────────────────────────────────────────
|
|
897
|
-
|
|
898
|
-
export async function getLikes(
|
|
899
|
-
userId: string,
|
|
900
|
-
count = 20,
|
|
901
|
-
): Promise<TweetEntry[]> {
|
|
902
|
-
const json = (await graphqlGet(QUERY_IDS.Likes, "Likes", {
|
|
903
|
-
userId,
|
|
904
|
-
count,
|
|
905
|
-
includePromotedContent: false,
|
|
906
|
-
withClientEventToken: false,
|
|
907
|
-
withBirdwatchNotes: false,
|
|
908
|
-
withVoice: true,
|
|
909
|
-
})) as UserTimelineResponse;
|
|
910
|
-
|
|
911
|
-
// Response path: data.user.result.timeline.timeline.instructions[]
|
|
912
|
-
const instructions =
|
|
913
|
-
json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
914
|
-
return extractTweetsFromInstructions(instructions);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// ─── Followers ───────────────────────────────────────────────────────────────
|
|
918
|
-
|
|
919
|
-
export async function getFollowers(
|
|
920
|
-
userId: string,
|
|
921
|
-
screenName?: string,
|
|
922
|
-
): Promise<UserInfo[]> {
|
|
923
|
-
// Followers requires X's client-generated transaction ID.
|
|
924
|
-
// Navigate to the followers page and capture via CDP.
|
|
925
|
-
if (screenName) {
|
|
926
|
-
await requireSession();
|
|
927
|
-
const wsUrl = await findTwitterTab();
|
|
928
|
-
const json = (await cdpNavigateAndCapture(
|
|
929
|
-
wsUrl,
|
|
930
|
-
`https://x.com/${screenName}/followers`,
|
|
931
|
-
"Followers",
|
|
932
|
-
)) as UserTimelineResponse;
|
|
933
|
-
const instructions =
|
|
934
|
-
json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
935
|
-
return extractUsersFromInstructions(instructions);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const json = (await graphqlGet(QUERY_IDS.Followers, "Followers", {
|
|
939
|
-
userId,
|
|
940
|
-
count: 20,
|
|
941
|
-
includePromotedContent: false,
|
|
942
|
-
withGrokTranslatedBio: false,
|
|
943
|
-
})) as UserTimelineResponse;
|
|
944
|
-
|
|
945
|
-
const instructions =
|
|
946
|
-
json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
947
|
-
return extractUsersFromInstructions(instructions);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// ─── Following ───────────────────────────────────────────────────────────────
|
|
951
|
-
|
|
952
|
-
export async function getFollowing(
|
|
953
|
-
userId: string,
|
|
954
|
-
count = 20,
|
|
955
|
-
): Promise<UserInfo[]> {
|
|
956
|
-
const json = (await graphqlGet(QUERY_IDS.Following, "Following", {
|
|
957
|
-
userId,
|
|
958
|
-
count,
|
|
959
|
-
includePromotedContent: false,
|
|
960
|
-
withGrokTranslatedBio: false,
|
|
961
|
-
})) as UserTimelineResponse;
|
|
962
|
-
|
|
963
|
-
// Response path: data.user.result.timeline.timeline.instructions[]
|
|
964
|
-
const instructions =
|
|
965
|
-
json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
966
|
-
return extractUsersFromInstructions(instructions);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// ─── User media ──────────────────────────────────────────────────────────────
|
|
970
|
-
|
|
971
|
-
export async function getUserMedia(
|
|
972
|
-
userId: string,
|
|
973
|
-
count = 20,
|
|
974
|
-
): Promise<TweetEntry[]> {
|
|
975
|
-
const json = (await graphqlGet(QUERY_IDS.UserMedia, "UserMedia", {
|
|
976
|
-
userId,
|
|
977
|
-
count,
|
|
978
|
-
includePromotedContent: false,
|
|
979
|
-
withClientEventToken: false,
|
|
980
|
-
withBirdwatchNotes: false,
|
|
981
|
-
withVoice: true,
|
|
982
|
-
})) as UserTimelineResponse;
|
|
983
|
-
|
|
984
|
-
// Response path: data.user.result.timeline.timeline.instructions[]
|
|
985
|
-
// (same as Likes — contains tweets that have media)
|
|
986
|
-
const instructions =
|
|
987
|
-
json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
988
|
-
return extractTweetsFromInstructions(instructions);
|
|
989
|
-
}
|