@jackwener/opencli 1.7.7 → 1.7.9
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/README.md +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +3 -22
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
export function parseToutiaoArticlesText(text) {
|
|
4
|
+
const NON_TITLE_LINES = new Set([
|
|
5
|
+
'展现', '阅读', '点赞', '评论',
|
|
6
|
+
'查看数据', '查看评论', '修改', '更多', '首发',
|
|
7
|
+
'已发布', '定时发布', '定时发布中', '由文章生成', '审核中',
|
|
8
|
+
]);
|
|
9
|
+
const lines = String(text || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
10
|
+
const results = [];
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
const line = lines[i];
|
|
14
|
+
if (!/^\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(line)) continue;
|
|
15
|
+
|
|
16
|
+
const date = line;
|
|
17
|
+
let title = '';
|
|
18
|
+
let status = '';
|
|
19
|
+
let stats = null;
|
|
20
|
+
|
|
21
|
+
for (let back = 3; back >= 1; back--) {
|
|
22
|
+
const prev = lines[i - back] || '';
|
|
23
|
+
if (!prev || prev.length >= 100 || /^\d+$/.test(prev) || NON_TITLE_LINES.has(prev)) continue;
|
|
24
|
+
title = prev;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (let fwd = 1; fwd < 8; fwd++) {
|
|
29
|
+
const fwdLine = lines[i + fwd] || '';
|
|
30
|
+
if (fwdLine === '已发布' || fwdLine === '定时发布中' || fwdLine === '审核中' || fwdLine === '由文章生成') {
|
|
31
|
+
status = fwdLine;
|
|
32
|
+
}
|
|
33
|
+
if (fwdLine.includes('展现') && fwdLine.includes('阅读')) {
|
|
34
|
+
const match = fwdLine.match(/展现\s*([\d,]+)\s*阅读\s*([\d,]+)\s*点赞\s*([\d,]+)\s*评论\s*([\d,]*)/);
|
|
35
|
+
if (match) {
|
|
36
|
+
stats = {
|
|
37
|
+
'展现': match[1],
|
|
38
|
+
'阅读': match[2],
|
|
39
|
+
'点赞': match[3],
|
|
40
|
+
'评论': match[4] || '0',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (title && stats) results.push({ title, date, status, ...stats });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cli({
|
|
53
|
+
site: 'toutiao',
|
|
54
|
+
name: 'articles',
|
|
55
|
+
description: '获取头条号创作者后台文章列表及数据',
|
|
56
|
+
domain: 'mp.toutiao.com',
|
|
57
|
+
args: [
|
|
58
|
+
{ name: 'page', type: 'int', default: 1, help: '页码 (1-4)' },
|
|
59
|
+
],
|
|
60
|
+
columns: ['title', 'date', 'status', '展现', '阅读', '点赞', '评论'],
|
|
61
|
+
pipeline: [
|
|
62
|
+
{ navigate: 'https://mp.toutiao.com/profile_v4/manage/content/all?page=${{ args.page }}' },
|
|
63
|
+
{ wait: 'networkidle' },
|
|
64
|
+
{ wait: 3000 },
|
|
65
|
+
{
|
|
66
|
+
evaluate: `
|
|
67
|
+
(async () => {
|
|
68
|
+
// Wait for content to load
|
|
69
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
70
|
+
const parse = ${parseToutiaoArticlesText.toString()};
|
|
71
|
+
return parse(document.body.innerText || '');
|
|
72
|
+
})()
|
|
73
|
+
`
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const __test__ = {
|
|
79
|
+
parseToutiaoArticlesText,
|
|
80
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './articles.js';
|
|
3
|
+
|
|
4
|
+
describe('toutiao articles parser', () => {
|
|
5
|
+
const articleText = [
|
|
6
|
+
'短标题',
|
|
7
|
+
'04-20 20:30',
|
|
8
|
+
'已发布',
|
|
9
|
+
'展现 8 阅读 0 点赞 0 评论 0',
|
|
10
|
+
].join('\n');
|
|
11
|
+
const parsedArticle = {
|
|
12
|
+
title: '短标题',
|
|
13
|
+
date: '04-20 20:30',
|
|
14
|
+
status: '已发布',
|
|
15
|
+
'展现': '8',
|
|
16
|
+
'阅读': '0',
|
|
17
|
+
'点赞': '0',
|
|
18
|
+
'评论': '0',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it('keeps short chinese titles instead of silently dropping the row', () => {
|
|
22
|
+
expect(__test__.parseToutiaoArticlesText(articleText)).toEqual([parsedArticle]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps parsing when serialized into the browser evaluate context', () => {
|
|
26
|
+
const parse = Function(`return (${__test__.parseToutiaoArticlesText.toString()})`)();
|
|
27
|
+
|
|
28
|
+
expect(parse(articleText)).toEqual([parsedArticle]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthRequiredError,
|
|
1
|
+
import { AuthRequiredError, selectorError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
3
|
cli({
|
|
4
4
|
site: 'twitter',
|
|
@@ -49,7 +49,7 @@ cli({
|
|
|
49
49
|
return false;
|
|
50
50
|
}`);
|
|
51
51
|
if (!clicked) {
|
|
52
|
-
throw
|
|
52
|
+
throw selectorError('Twitter followers link', 'Twitter may have changed the layout.');
|
|
53
53
|
}
|
|
54
54
|
await page.waitForCapture(5);
|
|
55
55
|
// 4. Scroll to trigger pagination API calls
|
|
@@ -1,11 +1,142 @@
|
|
|
1
|
-
import { AuthRequiredError, SelectorError } from '@jackwener/opencli/errors';
|
|
2
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { resolveTwitterQueryId, sanitizeQueryId } from './shared.js';
|
|
4
|
+
|
|
5
|
+
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
6
|
+
const FOLLOWING_QUERY_ID = 'zx6e-TLzRkeDO_a7p4b3JQ'; // Following fallback
|
|
7
|
+
const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
|
|
8
|
+
|
|
9
|
+
const FEATURES = {
|
|
10
|
+
rweb_video_screen_enabled: false,
|
|
11
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
12
|
+
responsive_web_profile_redirect_enabled: false,
|
|
13
|
+
rweb_tipjar_consumption_enabled: false,
|
|
14
|
+
verified_phone_label_enabled: false,
|
|
15
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
16
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
17
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
18
|
+
premium_content_api_read_enabled: false,
|
|
19
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
20
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
21
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
22
|
+
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
23
|
+
responsive_web_jetfuel_frame: true,
|
|
24
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
25
|
+
responsive_web_grok_annotations_enabled: true,
|
|
26
|
+
articles_preview_enabled: true,
|
|
27
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
28
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
29
|
+
view_counts_everywhere_api_enabled: true,
|
|
30
|
+
longform_notetweets_consumption_enabled: true,
|
|
31
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
32
|
+
tweet_awards_web_tipping_enabled: false,
|
|
33
|
+
content_disclosure_indicator_enabled: true,
|
|
34
|
+
content_disclosure_ai_generated_indicator_enabled: true,
|
|
35
|
+
responsive_web_grok_show_grok_translated_post: false,
|
|
36
|
+
responsive_web_grok_analysis_button_from_backend: true,
|
|
37
|
+
post_ctas_fetch_enabled: false,
|
|
38
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
39
|
+
standardized_nudges_misinfo: true,
|
|
40
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
41
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
42
|
+
longform_notetweets_inline_media_enabled: false,
|
|
43
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
44
|
+
responsive_web_grok_imagine_annotation_enabled: true,
|
|
45
|
+
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
|
46
|
+
responsive_web_enhance_cards_enabled: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function buildFollowingUrl(queryId, userId, count, cursor) {
|
|
50
|
+
const vars = {
|
|
51
|
+
userId,
|
|
52
|
+
count,
|
|
53
|
+
includePromotedContent: false,
|
|
54
|
+
withClientEventToken: false,
|
|
55
|
+
withBirdwatchNotes: false,
|
|
56
|
+
withVoice: true,
|
|
57
|
+
withV2Timeline: true,
|
|
58
|
+
};
|
|
59
|
+
if (cursor)
|
|
60
|
+
vars.cursor = cursor;
|
|
61
|
+
return `/i/api/graphql/${queryId}/Following`
|
|
62
|
+
+ `?variables=${encodeURIComponent(JSON.stringify(vars))}`
|
|
63
|
+
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildUserByScreenNameUrl(queryId, screenName) {
|
|
67
|
+
const vars = JSON.stringify({ screen_name: screenName, withSafetyModeUserFields: true });
|
|
68
|
+
const feats = JSON.stringify({
|
|
69
|
+
hidden_profile_subscriptions_enabled: true,
|
|
70
|
+
rweb_tipjar_consumption_enabled: true,
|
|
71
|
+
responsive_web_graphql_exclude_directive_enabled: true,
|
|
72
|
+
verified_phone_label_enabled: false,
|
|
73
|
+
subscriptions_verification_info_is_identity_verified_enabled: true,
|
|
74
|
+
subscriptions_verification_info_verified_since_enabled: true,
|
|
75
|
+
highlights_tweets_tab_ui_enabled: true,
|
|
76
|
+
responsive_web_twitter_article_notes_tab_enabled: true,
|
|
77
|
+
subscriptions_feature_can_gift_premium: true,
|
|
78
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
79
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
80
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
81
|
+
});
|
|
82
|
+
return `/i/api/graphql/${queryId}/UserByScreenName`
|
|
83
|
+
+ `?variables=${encodeURIComponent(vars)}`
|
|
84
|
+
+ `&features=${encodeURIComponent(feats)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractUser(result) {
|
|
88
|
+
if (!result || result.__typename !== 'User')
|
|
89
|
+
return null;
|
|
90
|
+
const core = result.core || {};
|
|
91
|
+
const legacy = result.legacy || {};
|
|
92
|
+
return {
|
|
93
|
+
screen_name: core.screen_name || legacy.screen_name || 'unknown',
|
|
94
|
+
name: core.name || legacy.name || 'unknown',
|
|
95
|
+
bio: legacy.description || result.profile_bio?.description || '',
|
|
96
|
+
followers: legacy.followers_count || legacy.normal_followers_count || 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseFollowing(data) {
|
|
101
|
+
const users = [];
|
|
102
|
+
let nextCursor = null;
|
|
103
|
+
const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions
|
|
104
|
+
|| data?.data?.user?.result?.timeline?.timeline?.instructions
|
|
105
|
+
|| [];
|
|
106
|
+
for (const inst of instructions) {
|
|
107
|
+
for (const entry of inst.entries || []) {
|
|
108
|
+
const content = entry.content;
|
|
109
|
+
// Extract cursor
|
|
110
|
+
if (content?.entryType === 'TimelineTimelineCursor' || content?.__typename === 'TimelineTimelineCursor') {
|
|
111
|
+
if (content.cursorType === 'Bottom' || content.cursorType === 'ShowMore')
|
|
112
|
+
nextCursor = content.value;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.entryId?.startsWith('cursor-bottom-') || entry.entryId?.startsWith('cursor-showMore-')) {
|
|
116
|
+
nextCursor = content?.value || content?.itemContent?.value || nextCursor;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Extract user
|
|
120
|
+
if (entry.entryId?.startsWith('user-')) {
|
|
121
|
+
const user = extractUser(content?.itemContent?.user_results?.result);
|
|
122
|
+
if (user)
|
|
123
|
+
users.push(user);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { users, nextCursor };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeScreenName(value) {
|
|
131
|
+
return String(value || '').trim().replace(/^\/+/, '').replace(/^@+/, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
3
134
|
cli({
|
|
4
135
|
site: 'twitter',
|
|
5
136
|
name: 'following',
|
|
6
137
|
description: 'Get accounts a Twitter/X user is following',
|
|
7
138
|
domain: 'x.com',
|
|
8
|
-
strategy: Strategy.
|
|
139
|
+
strategy: Strategy.COOKIE,
|
|
9
140
|
browser: true,
|
|
10
141
|
args: [
|
|
11
142
|
{ name: 'user', positional: true, type: 'string', required: false },
|
|
@@ -13,83 +144,103 @@ cli({
|
|
|
13
144
|
],
|
|
14
145
|
columns: ['screen_name', 'name', 'bio', 'followers'],
|
|
15
146
|
func: async (page, kwargs) => {
|
|
16
|
-
|
|
17
|
-
|
|
147
|
+
const limit = kwargs.limit === undefined || kwargs.limit === null ? 50 : Number(kwargs.limit);
|
|
148
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
149
|
+
throw new ArgumentError('twitter following --limit must be a positive integer', 'Example: opencli twitter following @elonmusk --limit 200');
|
|
150
|
+
}
|
|
151
|
+
let targetUser = normalizeScreenName(kwargs.user);
|
|
152
|
+
|
|
153
|
+
await page.goto('https://x.com');
|
|
154
|
+
await page.wait(3);
|
|
155
|
+
|
|
156
|
+
const ct0 = await page.evaluate(`() => {
|
|
157
|
+
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
158
|
+
}`);
|
|
159
|
+
if (!ct0)
|
|
160
|
+
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
161
|
+
|
|
18
162
|
if (!targetUser) {
|
|
19
|
-
await page.goto('https://x.com/home');
|
|
20
|
-
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
21
163
|
const href = await page.evaluate(`() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!href)
|
|
26
|
-
throw new AuthRequiredError('x.com', 'Could not
|
|
27
|
-
|
|
28
|
-
targetUser = href.replace('/', '');
|
|
164
|
+
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
165
|
+
return link ? link.getAttribute('href') : null;
|
|
166
|
+
}`);
|
|
167
|
+
if (!href)
|
|
168
|
+
throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?');
|
|
169
|
+
targetUser = normalizeScreenName(href.replace('/', ''));
|
|
29
170
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await page.wait(3);
|
|
33
|
-
// 2. Install interceptor BEFORE SPA navigation.
|
|
34
|
-
// goto() resets JS context, but SPA click preserves it.
|
|
35
|
-
await page.installInterceptor('Following');
|
|
36
|
-
// 3. Click the following link via SPA navigation (preserves interceptor)
|
|
37
|
-
const safeUser = JSON.stringify(targetUser);
|
|
38
|
-
const clicked = await page.evaluate(`() => {
|
|
39
|
-
const target = ${safeUser};
|
|
40
|
-
const link = document.querySelector('a[href="/' + target + '/following"]');
|
|
41
|
-
if (link) { link.click(); return true; }
|
|
42
|
-
return false;
|
|
43
|
-
}`);
|
|
44
|
-
if (!clicked) {
|
|
45
|
-
throw new SelectorError('Twitter following link', 'Twitter may have changed the layout.');
|
|
171
|
+
if (!targetUser) {
|
|
172
|
+
throw new ArgumentError('twitter following user cannot be empty', 'Example: opencli twitter following @elonmusk --limit 200');
|
|
46
173
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
174
|
+
|
|
175
|
+
const followingQueryId = await resolveTwitterQueryId(page, 'Following', FOLLOWING_QUERY_ID);
|
|
176
|
+
const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
177
|
+
const headers = JSON.stringify({
|
|
178
|
+
'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
|
|
179
|
+
'X-Csrf-Token': ct0,
|
|
180
|
+
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
181
|
+
'X-Twitter-Active-User': 'yes',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Get userId from screen_name
|
|
185
|
+
const userLookup = await page.evaluate(`async () => {
|
|
186
|
+
const url = ${JSON.stringify(buildUserByScreenNameUrl(userByScreenNameQueryId, targetUser))};
|
|
187
|
+
const resp = await fetch(url, { headers: ${headers}, credentials: 'include' });
|
|
188
|
+
if (!resp.ok) return { error: resp.status };
|
|
189
|
+
const d = await resp.json();
|
|
190
|
+
return { userId: d.data?.user?.result?.rest_id || null };
|
|
191
|
+
}`);
|
|
192
|
+
if (userLookup?.error === 401 || userLookup?.error === 403) {
|
|
193
|
+
throw new AuthRequiredError('x.com', `Twitter user lookup failed (HTTP ${userLookup.error})`);
|
|
55
194
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name: core.name || legacy.name || 'unknown',
|
|
80
|
-
bio: legacy.description || item.profile_bio?.description || '',
|
|
81
|
-
followers: legacy.followers_count || legacy.normal_followers_count || 0
|
|
82
|
-
});
|
|
83
|
-
}
|
|
195
|
+
if (userLookup?.error) {
|
|
196
|
+
throw new CommandExecutionError(`HTTP ${userLookup.error}: Failed to resolve Twitter user @${targetUser}`);
|
|
197
|
+
}
|
|
198
|
+
const userId = userLookup?.userId || null;
|
|
199
|
+
if (!userId)
|
|
200
|
+
throw new CommandExecutionError(`Could not find user @${targetUser}`);
|
|
201
|
+
|
|
202
|
+
const allUsers = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
let cursor = null;
|
|
205
|
+
|
|
206
|
+
const maxPages = Math.ceil(limit / 50) + 2;
|
|
207
|
+
for (let i = 0; i < maxPages && allUsers.length < limit; i++) {
|
|
208
|
+
const fetchCount = Math.min(50, limit - allUsers.length + 10);
|
|
209
|
+
const apiUrl = buildFollowingUrl(followingQueryId, userId, fetchCount, cursor);
|
|
210
|
+
const data = await page.evaluate(`async () => {
|
|
211
|
+
const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
|
|
212
|
+
return r.ok ? await r.json() : { error: r.status };
|
|
213
|
+
}`);
|
|
214
|
+
if (data?.error) {
|
|
215
|
+
if (data.error === 401 || data.error === 403)
|
|
216
|
+
throw new AuthRequiredError('x.com', `Twitter following request failed (HTTP ${data.error})`);
|
|
217
|
+
throw new CommandExecutionError(`HTTP ${data.error}: Failed to fetch following list. queryId may have expired.`);
|
|
84
218
|
}
|
|
85
|
-
|
|
86
|
-
|
|
219
|
+
const { users, nextCursor } = parseFollowing(data);
|
|
220
|
+
for (const u of users) {
|
|
221
|
+
if (!seen.has(u.screen_name)) {
|
|
222
|
+
seen.add(u.screen_name);
|
|
223
|
+
allUsers.push(u);
|
|
224
|
+
}
|
|
87
225
|
}
|
|
226
|
+
if (!nextCursor || nextCursor === cursor)
|
|
227
|
+
break;
|
|
228
|
+
cursor = nextCursor;
|
|
88
229
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
230
|
+
|
|
231
|
+
if (allUsers.length === 0) {
|
|
232
|
+
throw new EmptyResultError('twitter following', `No following accounts found for @${targetUser}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return allUsers.slice(0, limit);
|
|
236
|
+
},
|
|
95
237
|
});
|
|
238
|
+
|
|
239
|
+
export const __test__ = {
|
|
240
|
+
sanitizeQueryId,
|
|
241
|
+
buildFollowingUrl,
|
|
242
|
+
buildUserByScreenNameUrl,
|
|
243
|
+
extractUser,
|
|
244
|
+
normalizeScreenName,
|
|
245
|
+
parseFollowing,
|
|
246
|
+
};
|