@jackwener/opencli 0.7.9 → 0.7.10
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/dist/cli-manifest.json +7 -88
- package/dist/clis/twitter/bookmarks.js +171 -0
- package/dist/clis/twitter/delete.js +0 -1
- package/dist/clis/twitter/followers.js +5 -16
- package/dist/clis/twitter/following.js +3 -4
- package/dist/clis/twitter/like.js +0 -1
- package/dist/clis/twitter/notifications.js +17 -7
- package/dist/clis/twitter/search.js +14 -6
- package/dist/clis/twitter/trending.yaml +8 -2
- package/dist/main.js +0 -0
- package/package.json +1 -1
- package/src/clis/twitter/bookmarks.ts +201 -0
- package/src/clis/twitter/delete.ts +0 -1
- package/src/clis/twitter/followers.ts +5 -16
- package/src/clis/twitter/following.ts +3 -5
- package/src/clis/twitter/like.ts +0 -1
- package/src/clis/twitter/notifications.ts +18 -9
- package/src/clis/twitter/search.ts +14 -7
- package/src/clis/twitter/trending.yaml +8 -2
- package/dist/_debug.js +0 -7
- package/dist/browser-tab.d.ts +0 -2
- package/dist/browser-tab.js +0 -30
- package/dist/browser.d.ts +0 -105
- package/dist/browser.js +0 -644
- package/dist/clis/github/search.d.ts +0 -1
- package/dist/clis/github/search.js +0 -20
- package/dist/clis/index.d.ts +0 -27
- package/dist/clis/index.js +0 -41
- package/dist/clis/twitter/bookmarks.yaml +0 -85
- package/dist/clis/xiaohongshu/me.d.ts +0 -1
- package/dist/clis/xiaohongshu/me.js +0 -86
- package/dist/pipeline/_debug.d.ts +0 -1
- package/dist/pipeline/_debug.js +0 -7
- package/dist/promote.d.ts +0 -1
- package/dist/promote.js +0 -3
- package/dist/register.d.ts +0 -2
- package/dist/register.js +0 -2
- package/dist/scaffold.d.ts +0 -2
- package/dist/scaffold.js +0 -2
- package/dist/smoke.d.ts +0 -2
- package/dist/smoke.js +0 -2
- package/src/clis/twitter/bookmarks.yaml +0 -85
- /package/dist/{_debug.d.ts → clis/twitter/bookmarks.d.ts} +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
4
|
+
const BOOKMARKS_QUERY_ID = 'Fy0QMy4q_aZCpkO0PnyLYw';
|
|
5
|
+
|
|
6
|
+
const FEATURES = {
|
|
7
|
+
rweb_video_screen_enabled: false,
|
|
8
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
9
|
+
responsive_web_profile_redirect_enabled: false,
|
|
10
|
+
rweb_tipjar_consumption_enabled: false,
|
|
11
|
+
verified_phone_label_enabled: false,
|
|
12
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
13
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
14
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
15
|
+
premium_content_api_read_enabled: false,
|
|
16
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
17
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
18
|
+
articles_preview_enabled: true,
|
|
19
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
20
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
21
|
+
view_counts_everywhere_api_enabled: true,
|
|
22
|
+
longform_notetweets_consumption_enabled: true,
|
|
23
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
24
|
+
tweet_awards_web_tipping_enabled: false,
|
|
25
|
+
content_disclosure_indicator_enabled: true,
|
|
26
|
+
content_disclosure_ai_generated_indicator_enabled: true,
|
|
27
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
28
|
+
standardized_nudges_misinfo: true,
|
|
29
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
30
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
31
|
+
longform_notetweets_inline_media_enabled: false,
|
|
32
|
+
responsive_web_enhance_cards_enabled: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface BookmarkTweet {
|
|
36
|
+
id: string;
|
|
37
|
+
author: string;
|
|
38
|
+
name: string;
|
|
39
|
+
text: string;
|
|
40
|
+
likes: number;
|
|
41
|
+
retweets: number;
|
|
42
|
+
created_at: string;
|
|
43
|
+
url: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildBookmarksUrl(count: number, cursor?: string | null): string {
|
|
47
|
+
const vars: Record<string, any> = {
|
|
48
|
+
count,
|
|
49
|
+
includePromotedContent: false,
|
|
50
|
+
};
|
|
51
|
+
if (cursor) vars.cursor = cursor;
|
|
52
|
+
|
|
53
|
+
return `/i/api/graphql/${BOOKMARKS_QUERY_ID}/Bookmarks`
|
|
54
|
+
+ `?variables=${encodeURIComponent(JSON.stringify(vars))}`
|
|
55
|
+
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractBookmarkTweet(result: any, seen: Set<string>): BookmarkTweet | null {
|
|
59
|
+
if (!result) return null;
|
|
60
|
+
const tw = result.tweet || result;
|
|
61
|
+
const legacy = tw.legacy || {};
|
|
62
|
+
if (!tw.rest_id || seen.has(tw.rest_id)) return null;
|
|
63
|
+
seen.add(tw.rest_id);
|
|
64
|
+
|
|
65
|
+
const user = tw.core?.user_results?.result;
|
|
66
|
+
const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';
|
|
67
|
+
const displayName = user?.legacy?.name || user?.core?.name || '';
|
|
68
|
+
const noteText = tw.note_tweet?.note_tweet_results?.result?.text;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
id: tw.rest_id,
|
|
72
|
+
author: screenName,
|
|
73
|
+
name: displayName,
|
|
74
|
+
text: noteText || legacy.full_text || '',
|
|
75
|
+
likes: legacy.favorite_count || 0,
|
|
76
|
+
retweets: legacy.retweet_count || 0,
|
|
77
|
+
created_at: legacy.created_at || '',
|
|
78
|
+
url: `https://x.com/${screenName}/status/${tw.rest_id}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseBookmarks(data: any, seen: Set<string>): { tweets: BookmarkTweet[]; nextCursor: string | null } {
|
|
83
|
+
const tweets: BookmarkTweet[] = [];
|
|
84
|
+
let nextCursor: string | null = null;
|
|
85
|
+
|
|
86
|
+
const instructions =
|
|
87
|
+
data?.data?.bookmark_timeline_v2?.timeline?.instructions
|
|
88
|
+
|| data?.data?.bookmark_timeline?.timeline?.instructions
|
|
89
|
+
|| [];
|
|
90
|
+
|
|
91
|
+
for (const inst of instructions) {
|
|
92
|
+
for (const entry of inst.entries || []) {
|
|
93
|
+
const content = entry.content;
|
|
94
|
+
|
|
95
|
+
if (content?.entryType === 'TimelineTimelineCursor' || content?.__typename === 'TimelineTimelineCursor') {
|
|
96
|
+
if (content.cursorType === 'Bottom' || content.cursorType === 'ShowMore') nextCursor = content.value;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (entry.entryId?.startsWith('cursor-bottom-') || entry.entryId?.startsWith('cursor-showMore-')) {
|
|
100
|
+
nextCursor = content?.value || content?.itemContent?.value || nextCursor;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const direct = extractBookmarkTweet(content?.itemContent?.tweet_results?.result, seen);
|
|
105
|
+
if (direct) {
|
|
106
|
+
tweets.push(direct);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const item of content?.items || []) {
|
|
111
|
+
const nested = extractBookmarkTweet(item.item?.itemContent?.tweet_results?.result, seen);
|
|
112
|
+
if (nested) tweets.push(nested);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { tweets, nextCursor };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cli({
|
|
121
|
+
site: 'twitter',
|
|
122
|
+
name: 'bookmarks',
|
|
123
|
+
description: 'Fetch Twitter/X bookmarks',
|
|
124
|
+
domain: 'x.com',
|
|
125
|
+
strategy: Strategy.COOKIE,
|
|
126
|
+
browser: true,
|
|
127
|
+
args: [
|
|
128
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
129
|
+
],
|
|
130
|
+
columns: ['author', 'text', 'likes', 'url'],
|
|
131
|
+
func: async (page, kwargs) => {
|
|
132
|
+
const limit = kwargs.limit || 20;
|
|
133
|
+
|
|
134
|
+
await page.goto('https://x.com');
|
|
135
|
+
await page.wait(3);
|
|
136
|
+
|
|
137
|
+
const ct0 = await page.evaluate(`() => {
|
|
138
|
+
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
139
|
+
}`);
|
|
140
|
+
if (!ct0) throw new Error('Not logged into x.com (no ct0 cookie)');
|
|
141
|
+
|
|
142
|
+
const queryId = await page.evaluate(`async () => {
|
|
143
|
+
try {
|
|
144
|
+
const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
|
|
145
|
+
if (ghResp.ok) {
|
|
146
|
+
const data = await ghResp.json();
|
|
147
|
+
const entry = data['Bookmarks'];
|
|
148
|
+
if (entry && entry.queryId) return entry.queryId;
|
|
149
|
+
}
|
|
150
|
+
} catch {}
|
|
151
|
+
try {
|
|
152
|
+
const scripts = performance.getEntriesByType('resource')
|
|
153
|
+
.filter(r => r.name.includes('client-web') && r.name.endsWith('.js'))
|
|
154
|
+
.map(r => r.name);
|
|
155
|
+
for (const scriptUrl of scripts.slice(0, 15)) {
|
|
156
|
+
try {
|
|
157
|
+
const text = await (await fetch(scriptUrl)).text();
|
|
158
|
+
const re = /queryId:"([A-Za-z0-9_-]+)"[^}]{0,200}operationName:"Bookmarks"/;
|
|
159
|
+
const m = text.match(re);
|
|
160
|
+
if (m) return m[1];
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
return null;
|
|
165
|
+
}`) || BOOKMARKS_QUERY_ID;
|
|
166
|
+
|
|
167
|
+
const headers = JSON.stringify({
|
|
168
|
+
'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
|
|
169
|
+
'X-Csrf-Token': ct0,
|
|
170
|
+
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
171
|
+
'X-Twitter-Active-User': 'yes',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const allTweets: BookmarkTweet[] = [];
|
|
175
|
+
const seen = new Set<string>();
|
|
176
|
+
let cursor: string | null = null;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < 5 && allTweets.length < limit; i++) {
|
|
179
|
+
const fetchCount = Math.min(100, limit - allTweets.length + 10);
|
|
180
|
+
const apiUrl = buildBookmarksUrl(fetchCount, cursor).replace(BOOKMARKS_QUERY_ID, queryId);
|
|
181
|
+
|
|
182
|
+
const data = await page.evaluate(`async () => {
|
|
183
|
+
const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
|
|
184
|
+
return r.ok ? await r.json() : { error: r.status };
|
|
185
|
+
}`);
|
|
186
|
+
|
|
187
|
+
if (data?.error) {
|
|
188
|
+
if (allTweets.length === 0) throw new Error(`HTTP ${data.error}: Failed to fetch bookmarks. queryId may have expired.`);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { tweets, nextCursor } = parseBookmarks(data, seen);
|
|
193
|
+
allTweets.push(...tweets);
|
|
194
|
+
|
|
195
|
+
if (!nextCursor || nextCursor === cursor) break;
|
|
196
|
+
cursor = nextCursor;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return allTweets.slice(0, limit);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
@@ -15,7 +15,6 @@ cli({
|
|
|
15
15
|
func: async (page: IPage | null, kwargs: any) => {
|
|
16
16
|
if (!page) throw new Error('Requires browser');
|
|
17
17
|
|
|
18
|
-
console.log(`Navigating to tweet: ${kwargs.url}`);
|
|
19
18
|
await page.goto(kwargs.url);
|
|
20
19
|
await page.wait(5); // Wait for tweet to load completely
|
|
21
20
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
2
|
|
|
4
3
|
cli({
|
|
5
4
|
site: 'twitter',
|
|
@@ -37,8 +36,8 @@ cli({
|
|
|
37
36
|
await page.goto(`https://x.com/${targetUser}`);
|
|
38
37
|
await page.wait(3);
|
|
39
38
|
|
|
40
|
-
// 2. Inject interceptor for
|
|
41
|
-
await page.installInterceptor('
|
|
39
|
+
// 2. Inject interceptor for the followers GraphQL API
|
|
40
|
+
await page.installInterceptor('Followers');
|
|
42
41
|
|
|
43
42
|
// 3. Click the followers link inside the profile page
|
|
44
43
|
await page.evaluate(`() => {
|
|
@@ -53,24 +52,14 @@ cli({
|
|
|
53
52
|
|
|
54
53
|
// 4. Retrieve data from opencli's registered interceptors
|
|
55
54
|
const allRequests = await page.getInterceptedRequests();
|
|
55
|
+
const requestList = Array.isArray(allRequests) ? allRequests : [];
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
if (!allRequests || allRequests.length === 0) {
|
|
59
|
-
console.log('No GraphQL requests captured by the interceptor backend.');
|
|
57
|
+
if (requestList.length === 0) {
|
|
60
58
|
return [];
|
|
61
59
|
}
|
|
62
|
-
|
|
63
|
-
console.log('Intercepted keys:', allRequests.map((r: any) => {
|
|
64
|
-
try {
|
|
65
|
-
const u = new URL(r.url); return u.pathname;
|
|
66
|
-
} catch (e) {
|
|
67
|
-
return r.url;
|
|
68
|
-
}
|
|
69
|
-
}));
|
|
70
60
|
|
|
71
|
-
const requests =
|
|
61
|
+
const requests = requestList.filter((r: any) => r?.url?.includes('Followers'));
|
|
72
62
|
if (!requests || requests.length === 0) {
|
|
73
|
-
console.log('No specific Followers requests captured. Check keys printed above.');
|
|
74
63
|
return [];
|
|
75
64
|
}
|
|
76
65
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
2
|
|
|
4
3
|
cli({
|
|
5
4
|
site: 'twitter',
|
|
@@ -53,15 +52,14 @@ cli({
|
|
|
53
52
|
|
|
54
53
|
// 4. Retrieve data from opencli's registered interceptors
|
|
55
54
|
const requests = await page.getInterceptedRequests();
|
|
55
|
+
const requestList = Array.isArray(requests) ? requests : [];
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
if (!requests || requests.length === 0) {
|
|
59
|
-
console.log('No Following requests captured by the interceptor backend.');
|
|
57
|
+
if (requestList.length === 0) {
|
|
60
58
|
return [];
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
let results: any[] = [];
|
|
64
|
-
for (const req of
|
|
62
|
+
for (const req of requestList) {
|
|
65
63
|
try {
|
|
66
64
|
let instructions = req.data?.data?.user?.result?.timeline?.timeline?.instructions;
|
|
67
65
|
if (!instructions) continue;
|
package/src/clis/twitter/like.ts
CHANGED
|
@@ -15,7 +15,6 @@ cli({
|
|
|
15
15
|
func: async (page: IPage | null, kwargs: any) => {
|
|
16
16
|
if (!page) throw new Error('Requires browser');
|
|
17
17
|
|
|
18
|
-
console.log(`Navigating to tweet: ${kwargs.url}`);
|
|
19
18
|
await page.goto(kwargs.url);
|
|
20
19
|
await page.wait(5); // Wait for tweet to load completely
|
|
21
20
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
2
|
|
|
4
3
|
cli({
|
|
5
4
|
site: 'twitter',
|
|
@@ -13,13 +12,16 @@ cli({
|
|
|
13
12
|
],
|
|
14
13
|
columns: ['id', 'action', 'author', 'text', 'url'],
|
|
15
14
|
func: async (page, kwargs) => {
|
|
15
|
+
// Install the interceptor before loading the notifications page so we
|
|
16
|
+
// capture the initial timeline request triggered during page load.
|
|
17
|
+
await page.goto('https://x.com');
|
|
18
|
+
await page.wait(2);
|
|
19
|
+
await page.installInterceptor('NotificationsTimeline');
|
|
20
|
+
|
|
16
21
|
// 1. Navigate to notifications
|
|
17
22
|
await page.goto('https://x.com/notifications');
|
|
18
23
|
await page.wait(5);
|
|
19
24
|
|
|
20
|
-
// 2. Inject interceptor
|
|
21
|
-
await page.installInterceptor('NotificationsTimeline');
|
|
22
|
-
|
|
23
25
|
// 3. Trigger API by scrolling (if we need to load more)
|
|
24
26
|
await page.autoScroll({ times: 2, delayMs: 2000 });
|
|
25
27
|
|
|
@@ -28,6 +30,7 @@ cli({
|
|
|
28
30
|
if (!requests || requests.length === 0) return [];
|
|
29
31
|
|
|
30
32
|
let results: any[] = [];
|
|
33
|
+
const seen = new Set<string>();
|
|
31
34
|
for (const req of requests) {
|
|
32
35
|
try {
|
|
33
36
|
let instructions: any[] = [];
|
|
@@ -75,14 +78,16 @@ cli({
|
|
|
75
78
|
if (item.__typename === 'TimelineNotification') {
|
|
76
79
|
// Greet likes, retweet, mentions
|
|
77
80
|
text = item.rich_message?.text || item.message?.text || '';
|
|
78
|
-
|
|
81
|
+
const fromUser = item.template?.from_users?.[0]?.user_results?.result;
|
|
82
|
+
author = fromUser?.legacy?.screen_name || fromUser?.core?.screen_name || 'unknown';
|
|
79
83
|
urlStr = item.notification_url?.url || '';
|
|
80
84
|
actionText = item.notification_icon || 'Activity';
|
|
81
85
|
|
|
82
86
|
// If there's an attached tweet
|
|
83
87
|
const targetTweet = item.template?.target_objects?.[0]?.tweet_results?.result;
|
|
84
88
|
if (targetTweet) {
|
|
85
|
-
|
|
89
|
+
const targetText = targetTweet.note_tweet?.note_tweet_results?.result?.text || targetTweet.legacy?.full_text || '';
|
|
90
|
+
text += text && targetText ? ' | ' + targetText : targetText;
|
|
86
91
|
if (!urlStr) {
|
|
87
92
|
urlStr = `https://x.com/i/status/${targetTweet.rest_id}`;
|
|
88
93
|
}
|
|
@@ -91,18 +96,22 @@ cli({
|
|
|
91
96
|
// Direct mention/reply
|
|
92
97
|
const tweet = item.tweet_result?.result;
|
|
93
98
|
author = tweet?.core?.user_results?.result?.legacy?.screen_name || 'unknown';
|
|
94
|
-
text = tweet?.legacy?.full_text || item.message?.text || '';
|
|
99
|
+
text = tweet?.note_tweet?.note_tweet_results?.result?.text || tweet?.legacy?.full_text || item.message?.text || '';
|
|
95
100
|
actionText = 'Mention/Reply';
|
|
96
101
|
urlStr = `https://x.com/i/status/${tweet?.rest_id}`;
|
|
97
102
|
} else if (item.__typename === 'Tweet') {
|
|
98
103
|
author = item.core?.user_results?.result?.legacy?.screen_name || 'unknown';
|
|
99
|
-
text = item.legacy?.full_text || '';
|
|
104
|
+
text = item.note_tweet?.note_tweet_results?.result?.text || item.legacy?.full_text || '';
|
|
100
105
|
actionText = 'Mention';
|
|
101
106
|
urlStr = `https://x.com/i/status/${item.rest_id}`;
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
const id = item.id || item.rest_id || entryId;
|
|
110
|
+
if (seen.has(id)) return;
|
|
111
|
+
seen.add(id);
|
|
112
|
+
|
|
104
113
|
results.push({
|
|
105
|
-
id
|
|
114
|
+
id,
|
|
106
115
|
action: actionText,
|
|
107
116
|
author: author,
|
|
108
117
|
text: text,
|
|
@@ -13,14 +13,17 @@ cli({
|
|
|
13
13
|
],
|
|
14
14
|
columns: ['id', 'author', 'text', 'likes', 'views', 'url'],
|
|
15
15
|
func: async (page, kwargs) => {
|
|
16
|
+
// Install the interceptor before opening the target page so we don't miss
|
|
17
|
+
// the initial SearchTimeline request fired during hydration.
|
|
18
|
+
await page.goto('https://x.com');
|
|
19
|
+
await page.wait(2);
|
|
20
|
+
await page.installInterceptor('SearchTimeline');
|
|
21
|
+
|
|
16
22
|
// 1. Navigate to the search page
|
|
17
23
|
const q = encodeURIComponent(kwargs.query);
|
|
18
24
|
await page.goto(`https://x.com/search?q=${q}&f=top`);
|
|
19
25
|
await page.wait(5);
|
|
20
26
|
|
|
21
|
-
// 2. Inject XHR interceptor
|
|
22
|
-
await page.installInterceptor('SearchTimeline');
|
|
23
|
-
|
|
24
27
|
// 3. Trigger API by scrolling
|
|
25
28
|
await page.autoScroll({ times: 3, delayMs: 2000 });
|
|
26
29
|
|
|
@@ -29,11 +32,13 @@ cli({
|
|
|
29
32
|
if (!requests || requests.length === 0) return [];
|
|
30
33
|
|
|
31
34
|
let results: any[] = [];
|
|
35
|
+
const seen = new Set<string>();
|
|
32
36
|
for (const req of requests) {
|
|
33
37
|
try {
|
|
34
|
-
const insts = req.data
|
|
35
|
-
const addEntries = insts.find((i: any) => i.type === 'TimelineAddEntries')
|
|
36
|
-
|
|
38
|
+
const insts = req.data?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions || [];
|
|
39
|
+
const addEntries = insts.find((i: any) => i.type === 'TimelineAddEntries')
|
|
40
|
+
|| insts.find((i: any) => i.entries && Array.isArray(i.entries));
|
|
41
|
+
if (!addEntries?.entries) continue;
|
|
37
42
|
|
|
38
43
|
for (const entry of addEntries.entries) {
|
|
39
44
|
if (!entry.entryId.startsWith('tweet-')) continue;
|
|
@@ -45,11 +50,13 @@ cli({
|
|
|
45
50
|
if (tweet.__typename === 'TweetWithVisibilityResults' && tweet.tweet) {
|
|
46
51
|
tweet = tweet.tweet;
|
|
47
52
|
}
|
|
53
|
+
if (!tweet.rest_id || seen.has(tweet.rest_id)) continue;
|
|
54
|
+
seen.add(tweet.rest_id);
|
|
48
55
|
|
|
49
56
|
results.push({
|
|
50
57
|
id: tweet.rest_id,
|
|
51
58
|
author: tweet.core?.user_results?.result?.legacy?.screen_name || 'unknown',
|
|
52
|
-
text: tweet.legacy?.full_text || '',
|
|
59
|
+
text: tweet.note_tweet?.note_tweet_results?.result?.text || tweet.legacy?.full_text || '',
|
|
53
60
|
likes: tweet.legacy?.favorite_count || 0,
|
|
54
61
|
views: tweet.views?.count || '0',
|
|
55
62
|
url: `https://x.com/i/status/${tweet.rest_id}`
|
|
@@ -25,9 +25,15 @@ pipeline:
|
|
|
25
25
|
credentials: 'include',
|
|
26
26
|
headers: { 'x-twitter-active-user': 'yes', 'x-csrf-token': csrfToken, 'authorization': 'Bearer ' + bearerToken }
|
|
27
27
|
});
|
|
28
|
+
if (!res.ok) throw new Error('HTTP ' + res.status + '. Hint: trending endpoint may require login or API shape changed.');
|
|
28
29
|
const data = await res.json();
|
|
29
|
-
const
|
|
30
|
-
|
|
30
|
+
const instructions = data?.timeline?.instructions || [];
|
|
31
|
+
const entries = instructions.flatMap(inst => inst?.addEntries?.entries || inst?.entries || []);
|
|
32
|
+
return entries
|
|
33
|
+
.filter(e => e.content?.timelineModule)
|
|
34
|
+
.flatMap(e => e.content.timelineModule.items || [])
|
|
35
|
+
.map(t => t?.item?.content?.trend)
|
|
36
|
+
.filter(Boolean);
|
|
31
37
|
})()
|
|
32
38
|
|
|
33
39
|
- map:
|
package/dist/_debug.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { render, evalExpr } from '../pipeline/template.js';
|
|
2
|
-
const ctx = { item: { first: 'X', second: 'Y' } };
|
|
3
|
-
console.log('evalExpr item.first:', JSON.stringify(evalExpr('item.first', ctx)));
|
|
4
|
-
console.log('evalExpr item.second:', JSON.stringify(evalExpr('item.second', ctx)));
|
|
5
|
-
const template = '$' + '{{ item.first }}-$' + '{{ item.second }}';
|
|
6
|
-
console.log('template:', JSON.stringify(template));
|
|
7
|
-
console.log('render result:', JSON.stringify(render(template, ctx)));
|
package/dist/browser-tab.d.ts
DELETED
package/dist/browser-tab.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
function tabCount(tabs) {
|
|
2
|
-
if (Array.isArray(tabs))
|
|
3
|
-
return tabs.length;
|
|
4
|
-
if (typeof tabs === 'string') {
|
|
5
|
-
const matches = tabs.match(/Tab \d+/g);
|
|
6
|
-
return matches ? matches.length : 0;
|
|
7
|
-
}
|
|
8
|
-
return 0;
|
|
9
|
-
}
|
|
10
|
-
export async function withTemporaryTab(page, fn) {
|
|
11
|
-
let closeIndex = null;
|
|
12
|
-
try {
|
|
13
|
-
const before = tabCount(await page.tabs());
|
|
14
|
-
await page.newTab();
|
|
15
|
-
const after = tabCount(await page.tabs());
|
|
16
|
-
closeIndex = Math.max(after - 1, before);
|
|
17
|
-
if (closeIndex >= 0) {
|
|
18
|
-
await page.selectTab(closeIndex);
|
|
19
|
-
}
|
|
20
|
-
return await fn();
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
if (closeIndex != null && closeIndex >= 0) {
|
|
24
|
-
try {
|
|
25
|
-
await page.closeTab(closeIndex);
|
|
26
|
-
}
|
|
27
|
-
catch { }
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
package/dist/browser.d.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
|
-
* Connects to an existing Chrome browser through the extension.
|
|
4
|
-
*/
|
|
5
|
-
import { withTimeoutMs } from './runtime.js';
|
|
6
|
-
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
7
|
-
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
8
|
-
type ConnectFailureInput = {
|
|
9
|
-
kind: ConnectFailureKind;
|
|
10
|
-
timeout: number;
|
|
11
|
-
hasExtensionToken: boolean;
|
|
12
|
-
tokenFingerprint?: string | null;
|
|
13
|
-
stderr?: string;
|
|
14
|
-
exitCode?: number | null;
|
|
15
|
-
rawMessage?: string;
|
|
16
|
-
};
|
|
17
|
-
export declare function getTokenFingerprint(token: string | undefined): string | null;
|
|
18
|
-
export declare function formatBrowserConnectError(input: ConnectFailureInput): Error;
|
|
19
|
-
declare function createJsonRpcRequest(method: string, params?: Record<string, any>): {
|
|
20
|
-
id: number;
|
|
21
|
-
message: string;
|
|
22
|
-
};
|
|
23
|
-
import type { IPage } from './types.js';
|
|
24
|
-
/**
|
|
25
|
-
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
26
|
-
*/
|
|
27
|
-
export declare class Page implements IPage {
|
|
28
|
-
private _request;
|
|
29
|
-
constructor(_request: (method: string, params?: Record<string, any>) => Promise<any>);
|
|
30
|
-
call(method: string, params?: Record<string, any>): Promise<any>;
|
|
31
|
-
goto(url: string): Promise<void>;
|
|
32
|
-
evaluate(js: string): Promise<any>;
|
|
33
|
-
snapshot(opts?: {
|
|
34
|
-
interactive?: boolean;
|
|
35
|
-
compact?: boolean;
|
|
36
|
-
maxDepth?: number;
|
|
37
|
-
raw?: boolean;
|
|
38
|
-
}): Promise<any>;
|
|
39
|
-
click(ref: string): Promise<void>;
|
|
40
|
-
typeText(ref: string, text: string): Promise<void>;
|
|
41
|
-
pressKey(key: string): Promise<void>;
|
|
42
|
-
wait(options: number | {
|
|
43
|
-
text?: string;
|
|
44
|
-
time?: number;
|
|
45
|
-
timeout?: number;
|
|
46
|
-
}): Promise<void>;
|
|
47
|
-
tabs(): Promise<any>;
|
|
48
|
-
closeTab(index?: number): Promise<void>;
|
|
49
|
-
newTab(): Promise<void>;
|
|
50
|
-
selectTab(index: number): Promise<void>;
|
|
51
|
-
networkRequests(includeStatic?: boolean): Promise<any>;
|
|
52
|
-
consoleMessages(level?: string): Promise<any>;
|
|
53
|
-
scroll(direction?: string, _amount?: number): Promise<void>;
|
|
54
|
-
autoScroll(options?: {
|
|
55
|
-
times?: number;
|
|
56
|
-
delayMs?: number;
|
|
57
|
-
}): Promise<void>;
|
|
58
|
-
installInterceptor(pattern: string): Promise<void>;
|
|
59
|
-
getInterceptedRequests(): Promise<any[]>;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Playwright MCP process manager.
|
|
63
|
-
*/
|
|
64
|
-
export declare class PlaywrightMCP {
|
|
65
|
-
private static _activeInsts;
|
|
66
|
-
private static _cleanupRegistered;
|
|
67
|
-
private static _registerGlobalCleanup;
|
|
68
|
-
private _proc;
|
|
69
|
-
private _buffer;
|
|
70
|
-
private _pending;
|
|
71
|
-
private _initialTabIdentities;
|
|
72
|
-
private _closingPromise;
|
|
73
|
-
private _state;
|
|
74
|
-
private _page;
|
|
75
|
-
get state(): PlaywrightMCPState;
|
|
76
|
-
private _sendRequest;
|
|
77
|
-
private _rejectPendingRequests;
|
|
78
|
-
private _resetAfterFailedConnect;
|
|
79
|
-
connect(opts?: {
|
|
80
|
-
timeout?: number;
|
|
81
|
-
}): Promise<Page>;
|
|
82
|
-
close(): Promise<void>;
|
|
83
|
-
}
|
|
84
|
-
declare function extractTabEntries(raw: any): Array<{
|
|
85
|
-
index: number;
|
|
86
|
-
identity: string;
|
|
87
|
-
}>;
|
|
88
|
-
declare function diffTabIndexes(initialIdentities: string[], currentTabs: Array<{
|
|
89
|
-
index: number;
|
|
90
|
-
identity: string;
|
|
91
|
-
}>): number[];
|
|
92
|
-
declare function appendLimited(current: string, chunk: string, limit: number): string;
|
|
93
|
-
declare function buildMcpArgs(input: {
|
|
94
|
-
mcpPath: string;
|
|
95
|
-
executablePath?: string | null;
|
|
96
|
-
}): string[];
|
|
97
|
-
export declare const __test__: {
|
|
98
|
-
createJsonRpcRequest: typeof createJsonRpcRequest;
|
|
99
|
-
extractTabEntries: typeof extractTabEntries;
|
|
100
|
-
diffTabIndexes: typeof diffTabIndexes;
|
|
101
|
-
appendLimited: typeof appendLimited;
|
|
102
|
-
buildMcpArgs: typeof buildMcpArgs;
|
|
103
|
-
withTimeoutMs: typeof withTimeoutMs;
|
|
104
|
-
};
|
|
105
|
-
export {};
|