@jackwener/opencli 0.7.8 → 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 +17 -31
- package/dist/clis/twitter/bookmarks.d.ts +1 -0
- 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/timeline.js +174 -35
- package/dist/clis/twitter/trending.yaml +8 -2
- package/dist/setup.js +8 -2
- 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/timeline.ts +204 -36
- package/src/clis/twitter/trending.yaml +8 -2
- package/src/setup.ts +8 -2
- package/dist/clis/twitter/bookmarks.yaml +0 -85
- package/src/clis/twitter/bookmarks.yaml +0 -85
|
@@ -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}`
|
|
@@ -1,50 +1,218 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
|
|
3
|
+
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
6
|
+
const HOME_TIMELINE_QUERY_ID = 'c-CzHF1LboFilMpsx4ZCrQ';
|
|
7
|
+
|
|
8
|
+
const FEATURES = {
|
|
9
|
+
rweb_video_screen_enabled: false,
|
|
10
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
11
|
+
rweb_tipjar_consumption_enabled: true,
|
|
12
|
+
verified_phone_label_enabled: false,
|
|
13
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
14
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
15
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
16
|
+
premium_content_api_read_enabled: false,
|
|
17
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
18
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
19
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
20
|
+
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
21
|
+
responsive_web_jetfuel_frame: false,
|
|
22
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
23
|
+
articles_preview_enabled: true,
|
|
24
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
25
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
26
|
+
view_counts_everywhere_api_enabled: true,
|
|
27
|
+
longform_notetweets_consumption_enabled: true,
|
|
28
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
29
|
+
tweet_awards_web_tipping_enabled: false,
|
|
30
|
+
responsive_web_grok_show_grok_translated_post: false,
|
|
31
|
+
responsive_web_grok_analysis_button_from_backend: false,
|
|
32
|
+
creator_subscriptions_quote_tweet_preview_enabled: false,
|
|
33
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
34
|
+
standardized_nudges_misinfo: true,
|
|
35
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
36
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
37
|
+
longform_notetweets_inline_media_enabled: true,
|
|
38
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
39
|
+
responsive_web_enhance_cards_enabled: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ── Pure functions (type-safe, testable) ───────────────────────────────
|
|
43
|
+
|
|
44
|
+
interface TimelineTweet {
|
|
45
|
+
id: string;
|
|
46
|
+
author: string;
|
|
47
|
+
text: string;
|
|
48
|
+
likes: number;
|
|
49
|
+
retweets: number;
|
|
50
|
+
replies: number;
|
|
51
|
+
views: number;
|
|
52
|
+
created_at: string;
|
|
53
|
+
url: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildHomeTimelineUrl(count: number, cursor?: string | null): string {
|
|
57
|
+
const vars: Record<string, any> = {
|
|
58
|
+
count,
|
|
59
|
+
includePromotedContent: false,
|
|
60
|
+
latestControlAvailable: true,
|
|
61
|
+
requestContext: 'launch',
|
|
62
|
+
withCommunity: true,
|
|
63
|
+
};
|
|
64
|
+
if (cursor) vars.cursor = cursor;
|
|
65
|
+
|
|
66
|
+
return `/i/api/graphql/${HOME_TIMELINE_QUERY_ID}/HomeTimeline`
|
|
67
|
+
+ `?variables=${encodeURIComponent(JSON.stringify(vars))}`
|
|
68
|
+
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractTweet(result: any, seen: Set<string>): TimelineTweet | null {
|
|
72
|
+
if (!result) return null;
|
|
73
|
+
const tw = result.tweet || result;
|
|
74
|
+
const l = tw.legacy || {};
|
|
75
|
+
if (!tw.rest_id || seen.has(tw.rest_id)) return null;
|
|
76
|
+
seen.add(tw.rest_id);
|
|
77
|
+
|
|
78
|
+
const u = tw.core?.user_results?.result;
|
|
79
|
+
const screenName = u?.legacy?.screen_name || u?.core?.screen_name || 'unknown';
|
|
80
|
+
const noteText = tw.note_tweet?.note_tweet_results?.result?.text;
|
|
81
|
+
const views = tw.views?.count ? parseInt(tw.views.count, 10) : 0;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id: tw.rest_id,
|
|
85
|
+
author: screenName,
|
|
86
|
+
text: noteText || l.full_text || '',
|
|
87
|
+
likes: l.favorite_count || 0,
|
|
88
|
+
retweets: l.retweet_count || 0,
|
|
89
|
+
replies: l.reply_count || 0,
|
|
90
|
+
views,
|
|
91
|
+
created_at: l.created_at || '',
|
|
92
|
+
url: `https://x.com/${screenName}/status/${tw.rest_id}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseHomeTimeline(data: any, seen: Set<string>): { tweets: TimelineTweet[]; nextCursor: string | null } {
|
|
97
|
+
const tweets: TimelineTweet[] = [];
|
|
98
|
+
let nextCursor: string | null = null;
|
|
99
|
+
|
|
100
|
+
const instructions =
|
|
101
|
+
data?.data?.home?.home_timeline_urt?.instructions || [];
|
|
102
|
+
|
|
103
|
+
for (const inst of instructions) {
|
|
104
|
+
for (const entry of inst.entries || []) {
|
|
105
|
+
const c = entry.content;
|
|
106
|
+
|
|
107
|
+
// Cursor entries
|
|
108
|
+
if (c?.entryType === 'TimelineTimelineCursor' || c?.__typename === 'TimelineTimelineCursor') {
|
|
109
|
+
if (c.cursorType === 'Bottom') nextCursor = c.value;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (entry.entryId?.startsWith('cursor-bottom-')) {
|
|
113
|
+
nextCursor = c?.value || nextCursor;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Single tweet entry
|
|
118
|
+
const tweetResult = c?.itemContent?.tweet_results?.result;
|
|
119
|
+
if (tweetResult) {
|
|
120
|
+
// Skip promoted content
|
|
121
|
+
if (c?.itemContent?.promotedMetadata) continue;
|
|
122
|
+
const tw = extractTweet(tweetResult, seen);
|
|
123
|
+
if (tw) tweets.push(tw);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Conversation module (grouped tweets)
|
|
128
|
+
for (const item of c?.items || []) {
|
|
129
|
+
const nested = item.item?.itemContent?.tweet_results?.result;
|
|
130
|
+
if (nested) {
|
|
131
|
+
if (item.item?.itemContent?.promotedMetadata) continue;
|
|
132
|
+
const tw = extractTweet(nested, seen);
|
|
133
|
+
if (tw) tweets.push(tw);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { tweets, nextCursor };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── CLI definition ────────────────────────────────────────────────────
|
|
143
|
+
|
|
3
144
|
cli({
|
|
4
145
|
site: 'twitter',
|
|
5
146
|
name: 'timeline',
|
|
6
|
-
description: 'Twitter Home Timeline',
|
|
147
|
+
description: 'Fetch Twitter Home Timeline',
|
|
7
148
|
domain: 'x.com',
|
|
8
149
|
strategy: Strategy.COOKIE,
|
|
150
|
+
browser: true,
|
|
9
151
|
args: [
|
|
10
152
|
{ name: 'limit', type: 'int', default: 20 },
|
|
11
153
|
],
|
|
12
|
-
columns: ['
|
|
154
|
+
columns: ['id', 'author', 'text', 'likes', 'retweets', 'replies', 'views', 'created_at', 'url'],
|
|
13
155
|
func: async (page, kwargs) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
await page.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
156
|
+
const limit = kwargs.limit || 20;
|
|
157
|
+
|
|
158
|
+
// Navigate to x.com for cookie context
|
|
159
|
+
await page.goto('https://x.com');
|
|
160
|
+
await page.wait(3);
|
|
161
|
+
|
|
162
|
+
// Extract CSRF token
|
|
163
|
+
const ct0 = await page.evaluate(`() => {
|
|
164
|
+
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
165
|
+
}`);
|
|
166
|
+
if (!ct0) throw new Error('Not logged into x.com (no ct0 cookie)');
|
|
167
|
+
|
|
168
|
+
// Dynamically resolve queryId
|
|
169
|
+
const queryId = await page.evaluate(`async () => {
|
|
170
|
+
try {
|
|
171
|
+
const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
|
|
172
|
+
if (ghResp.ok) {
|
|
173
|
+
const data = await ghResp.json();
|
|
174
|
+
const entry = data['HomeTimeline'];
|
|
175
|
+
if (entry && entry.queryId) return entry.queryId;
|
|
176
|
+
}
|
|
177
|
+
} catch {}
|
|
178
|
+
return null;
|
|
179
|
+
}`) || HOME_TIMELINE_QUERY_ID;
|
|
180
|
+
|
|
181
|
+
// Build auth headers
|
|
182
|
+
const headers = JSON.stringify({
|
|
183
|
+
'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
|
|
184
|
+
'X-Csrf-Token': ct0,
|
|
185
|
+
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
186
|
+
'X-Twitter-Active-User': 'yes',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Paginate — fetch in browser, parse in TypeScript
|
|
190
|
+
const allTweets: TimelineTweet[] = [];
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
let cursor: string | null = null;
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < 5 && allTweets.length < limit; i++) {
|
|
195
|
+
const fetchCount = Math.min(40, limit - allTweets.length + 5); // over-fetch slightly for promoted filtering
|
|
196
|
+
const apiUrl = buildHomeTimelineUrl(fetchCount, cursor)
|
|
197
|
+
.replace(HOME_TIMELINE_QUERY_ID, queryId);
|
|
198
|
+
|
|
199
|
+
const data = await page.evaluate(`async () => {
|
|
200
|
+
const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
|
|
201
|
+
return r.ok ? await r.json() : { error: r.status };
|
|
202
|
+
}`);
|
|
203
|
+
|
|
204
|
+
if (data?.error) {
|
|
205
|
+
if (allTweets.length === 0) throw new Error(`HTTP ${data.error}: Failed to fetch timeline. queryId may have expired.`);
|
|
206
|
+
break;
|
|
35
207
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
208
|
+
|
|
209
|
+
const { tweets, nextCursor } = parseHomeTimeline(data, seen);
|
|
210
|
+
allTweets.push(...tweets);
|
|
211
|
+
|
|
212
|
+
if (!nextCursor || nextCursor === cursor) break;
|
|
213
|
+
cursor = nextCursor;
|
|
42
214
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!data || data.length === 0) return [{responseType: 'no data captured'}];
|
|
47
|
-
|
|
48
|
-
return [{responseType: `captured ${data.length} responses`, first: JSON.stringify(data[0]).substring(0,300)}];
|
|
49
|
-
}
|
|
215
|
+
|
|
216
|
+
return allTweets.slice(0, limit);
|
|
217
|
+
},
|
|
50
218
|
});
|
|
@@ -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/src/setup.ts
CHANGED
|
@@ -184,11 +184,17 @@ export async function runSetup(opts: { cliVersion?: string; token?: string } = {
|
|
|
184
184
|
if (result.ok) {
|
|
185
185
|
console.log(` ${chalk.green('✓')} Browser connected in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
186
186
|
} else {
|
|
187
|
+
console.log(` ${chalk.green('✓')} Token saved successfully.`);
|
|
187
188
|
console.log(` ${chalk.yellow('!')} Browser connectivity test failed: ${result.error ?? 'unknown'}`);
|
|
188
|
-
console.log(chalk.dim('
|
|
189
|
+
console.log(chalk.dim(' Token configuration is complete. To use opencli, make sure Chrome'));
|
|
190
|
+
console.log(chalk.dim(' is running with the Playwright MCP Bridge extension enabled.'));
|
|
191
|
+
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
|
|
189
192
|
}
|
|
190
193
|
} catch {
|
|
191
|
-
console.log(` ${chalk.
|
|
194
|
+
console.log(` ${chalk.green('✓')} Token saved successfully.`);
|
|
195
|
+
console.log(` ${chalk.yellow('!')} Browser connectivity test skipped (Chrome may not be running).`);
|
|
196
|
+
console.log(chalk.dim(' Token configuration is complete. Start Chrome to begin using opencli.'));
|
|
197
|
+
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
|
|
192
198
|
}
|
|
193
199
|
console.log();
|
|
194
200
|
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
site: twitter
|
|
2
|
-
name: bookmarks
|
|
3
|
-
description: 获取 Twitter 书签列表
|
|
4
|
-
domain: x.com
|
|
5
|
-
browser: true
|
|
6
|
-
|
|
7
|
-
args:
|
|
8
|
-
limit:
|
|
9
|
-
type: int
|
|
10
|
-
default: 20
|
|
11
|
-
description: Number of bookmarks to return (default 20)
|
|
12
|
-
|
|
13
|
-
pipeline:
|
|
14
|
-
- navigate: https://x.com/i/bookmarks
|
|
15
|
-
- wait: 2
|
|
16
|
-
- evaluate: |
|
|
17
|
-
(async () => {
|
|
18
|
-
const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1];
|
|
19
|
-
if (!ct0) throw new Error('No ct0 cookie. Hint: Not logged into x.com.');
|
|
20
|
-
const bearer = decodeURIComponent('AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA');
|
|
21
|
-
const _h = {'Authorization':'Bearer '+bearer, 'X-Csrf-Token':ct0, 'X-Twitter-Auth-Type':'OAuth2Session', 'X-Twitter-Active-User':'yes'};
|
|
22
|
-
|
|
23
|
-
const count = Math.min(${{ args.limit }}, 100);
|
|
24
|
-
const variables = JSON.stringify({count, includePromotedContent: false});
|
|
25
|
-
const features = JSON.stringify({
|
|
26
|
-
rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
27
|
-
responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: false,
|
|
28
|
-
verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true,
|
|
29
|
-
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
30
|
-
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
31
|
-
premium_content_api_read_enabled: false, communities_web_enable_tweet_community_results_fetch: true,
|
|
32
|
-
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
33
|
-
articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true,
|
|
34
|
-
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
35
|
-
view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true,
|
|
36
|
-
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
37
|
-
tweet_awards_web_tipping_enabled: false,
|
|
38
|
-
content_disclosure_indicator_enabled: true, content_disclosure_ai_generated_indicator_enabled: true,
|
|
39
|
-
freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true,
|
|
40
|
-
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
41
|
-
longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: false,
|
|
42
|
-
responsive_web_enhance_cards_enabled: false
|
|
43
|
-
});
|
|
44
|
-
const url = '/i/api/graphql/Fy0QMy4q_aZCpkO0PnyLYw/Bookmarks?variables=' + encodeURIComponent(variables) + '&features=' + encodeURIComponent(features);
|
|
45
|
-
const resp = await fetch(url, {headers: _h, credentials: 'include'});
|
|
46
|
-
if (!resp.ok) throw new Error('HTTP ' + resp.status + '. Hint: queryId may have changed.');
|
|
47
|
-
const d = await resp.json();
|
|
48
|
-
|
|
49
|
-
const instructions = d.data?.bookmark_timeline_v2?.timeline?.instructions || d.data?.bookmark_timeline?.timeline?.instructions || [];
|
|
50
|
-
let tweets = [], seen = new Set();
|
|
51
|
-
for (const inst of instructions) {
|
|
52
|
-
for (const entry of (inst.entries || [])) {
|
|
53
|
-
const r = entry.content?.itemContent?.tweet_results?.result;
|
|
54
|
-
if (!r) continue;
|
|
55
|
-
const tw = r.tweet || r;
|
|
56
|
-
const l = tw.legacy || {};
|
|
57
|
-
if (!tw.rest_id || seen.has(tw.rest_id)) continue;
|
|
58
|
-
seen.add(tw.rest_id);
|
|
59
|
-
const u = tw.core?.user_results?.result;
|
|
60
|
-
const nt = tw.note_tweet?.note_tweet_results?.result?.text;
|
|
61
|
-
const screenName = u?.legacy?.screen_name || u?.core?.screen_name;
|
|
62
|
-
tweets.push({
|
|
63
|
-
id: tw.rest_id,
|
|
64
|
-
author: screenName,
|
|
65
|
-
name: u?.legacy?.name || u?.core?.name,
|
|
66
|
-
url: 'https://x.com/' + (screenName || '_') + '/status/' + tw.rest_id,
|
|
67
|
-
text: nt || l.full_text || '',
|
|
68
|
-
likes: l.favorite_count,
|
|
69
|
-
retweets: l.retweet_count,
|
|
70
|
-
created_at: l.created_at
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return tweets;
|
|
75
|
-
})()
|
|
76
|
-
|
|
77
|
-
- map:
|
|
78
|
-
author: ${{ item.author }}
|
|
79
|
-
text: ${{ item.text }}
|
|
80
|
-
likes: ${{ item.likes }}
|
|
81
|
-
url: ${{ item.url }}
|
|
82
|
-
|
|
83
|
-
- limit: ${{ args.limit }}
|
|
84
|
-
|
|
85
|
-
columns: [author, text, likes, url]
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
site: twitter
|
|
2
|
-
name: bookmarks
|
|
3
|
-
description: 获取 Twitter 书签列表
|
|
4
|
-
domain: x.com
|
|
5
|
-
browser: true
|
|
6
|
-
|
|
7
|
-
args:
|
|
8
|
-
limit:
|
|
9
|
-
type: int
|
|
10
|
-
default: 20
|
|
11
|
-
description: Number of bookmarks to return (default 20)
|
|
12
|
-
|
|
13
|
-
pipeline:
|
|
14
|
-
- navigate: https://x.com/i/bookmarks
|
|
15
|
-
- wait: 2
|
|
16
|
-
- evaluate: |
|
|
17
|
-
(async () => {
|
|
18
|
-
const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1];
|
|
19
|
-
if (!ct0) throw new Error('No ct0 cookie. Hint: Not logged into x.com.');
|
|
20
|
-
const bearer = decodeURIComponent('AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA');
|
|
21
|
-
const _h = {'Authorization':'Bearer '+bearer, 'X-Csrf-Token':ct0, 'X-Twitter-Auth-Type':'OAuth2Session', 'X-Twitter-Active-User':'yes'};
|
|
22
|
-
|
|
23
|
-
const count = Math.min(${{ args.limit }}, 100);
|
|
24
|
-
const variables = JSON.stringify({count, includePromotedContent: false});
|
|
25
|
-
const features = JSON.stringify({
|
|
26
|
-
rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
27
|
-
responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: false,
|
|
28
|
-
verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true,
|
|
29
|
-
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
30
|
-
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
31
|
-
premium_content_api_read_enabled: false, communities_web_enable_tweet_community_results_fetch: true,
|
|
32
|
-
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
33
|
-
articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true,
|
|
34
|
-
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
35
|
-
view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true,
|
|
36
|
-
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
37
|
-
tweet_awards_web_tipping_enabled: false,
|
|
38
|
-
content_disclosure_indicator_enabled: true, content_disclosure_ai_generated_indicator_enabled: true,
|
|
39
|
-
freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true,
|
|
40
|
-
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
41
|
-
longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: false,
|
|
42
|
-
responsive_web_enhance_cards_enabled: false
|
|
43
|
-
});
|
|
44
|
-
const url = '/i/api/graphql/Fy0QMy4q_aZCpkO0PnyLYw/Bookmarks?variables=' + encodeURIComponent(variables) + '&features=' + encodeURIComponent(features);
|
|
45
|
-
const resp = await fetch(url, {headers: _h, credentials: 'include'});
|
|
46
|
-
if (!resp.ok) throw new Error('HTTP ' + resp.status + '. Hint: queryId may have changed.');
|
|
47
|
-
const d = await resp.json();
|
|
48
|
-
|
|
49
|
-
const instructions = d.data?.bookmark_timeline_v2?.timeline?.instructions || d.data?.bookmark_timeline?.timeline?.instructions || [];
|
|
50
|
-
let tweets = [], seen = new Set();
|
|
51
|
-
for (const inst of instructions) {
|
|
52
|
-
for (const entry of (inst.entries || [])) {
|
|
53
|
-
const r = entry.content?.itemContent?.tweet_results?.result;
|
|
54
|
-
if (!r) continue;
|
|
55
|
-
const tw = r.tweet || r;
|
|
56
|
-
const l = tw.legacy || {};
|
|
57
|
-
if (!tw.rest_id || seen.has(tw.rest_id)) continue;
|
|
58
|
-
seen.add(tw.rest_id);
|
|
59
|
-
const u = tw.core?.user_results?.result;
|
|
60
|
-
const nt = tw.note_tweet?.note_tweet_results?.result?.text;
|
|
61
|
-
const screenName = u?.legacy?.screen_name || u?.core?.screen_name;
|
|
62
|
-
tweets.push({
|
|
63
|
-
id: tw.rest_id,
|
|
64
|
-
author: screenName,
|
|
65
|
-
name: u?.legacy?.name || u?.core?.name,
|
|
66
|
-
url: 'https://x.com/' + (screenName || '_') + '/status/' + tw.rest_id,
|
|
67
|
-
text: nt || l.full_text || '',
|
|
68
|
-
likes: l.favorite_count,
|
|
69
|
-
retweets: l.retweet_count,
|
|
70
|
-
created_at: l.created_at
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return tweets;
|
|
75
|
-
})()
|
|
76
|
-
|
|
77
|
-
- map:
|
|
78
|
-
author: ${{ item.author }}
|
|
79
|
-
text: ${{ item.text }}
|
|
80
|
-
likes: ${{ item.likes }}
|
|
81
|
-
url: ${{ item.url }}
|
|
82
|
-
|
|
83
|
-
- limit: ${{ args.limit }}
|
|
84
|
-
|
|
85
|
-
columns: [author, text, likes, url]
|