@jackwener/opencli 1.7.15 → 1.7.17
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 +15 -13
- package/README.zh-CN.md +15 -12
- package/cli-manifest.json +165 -209
- package/clis/chatgpt/ask.js +3 -2
- package/clis/chatgpt/commands.test.js +2 -2
- package/clis/chatgpt/detail.js +7 -2
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/image.js +38 -4
- package/clis/chatgpt/image.test.js +68 -1
- package/clis/chatgpt/new.js +1 -1
- package/clis/chatgpt/read.js +3 -2
- package/clis/chatgpt/send.js +3 -2
- package/clis/chatgpt/status.js +1 -1
- package/clis/chatgpt/utils.js +259 -25
- package/clis/chatgpt/utils.test.js +166 -2
- package/clis/claude/ask.js +23 -8
- package/clis/claude/detail.js +10 -3
- package/clis/claude/history.js +1 -1
- package/clis/claude/new.js +9 -3
- package/clis/claude/read.js +3 -2
- package/clis/claude/send.js +9 -4
- package/clis/claude/status.js +1 -1
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +22 -9
- package/clis/deepseek/detail.js +10 -2
- package/clis/deepseek/history.js +1 -1
- package/clis/deepseek/new.js +14 -3
- package/clis/deepseek/read.js +3 -2
- package/clis/deepseek/send.js +1 -1
- package/clis/deepseek/status.js +1 -1
- package/clis/deepseek/utils.js +8 -1
- package/clis/doubao/ask.js +1 -1
- package/clis/doubao/detail.js +1 -1
- package/clis/doubao/history.js +1 -1
- package/clis/doubao/meeting-summary.js +1 -1
- package/clis/doubao/meeting-transcript.js +1 -1
- package/clis/doubao/new.js +1 -1
- package/clis/doubao/read.js +1 -1
- package/clis/doubao/send.js +1 -1
- package/clis/doubao/status.js +1 -1
- package/clis/gemini/ask.js +1 -1
- package/clis/gemini/deep-research-result.js +1 -1
- package/clis/gemini/deep-research.js +1 -1
- package/clis/gemini/image.js +1 -1
- package/clis/gemini/new.js +1 -1
- package/clis/grok/ask.js +1 -1
- package/clis/grok/detail.js +1 -1
- package/clis/grok/history.js +1 -1
- package/clis/grok/image.js +1 -1
- package/clis/grok/new.js +1 -1
- package/clis/grok/read.js +1 -1
- package/clis/grok/send.js +1 -1
- package/clis/grok/status.js +1 -1
- package/clis/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +2 -2
- package/clis/notebooklm/open.test.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- package/clis/qwen/ask.js +1 -1
- package/clis/qwen/detail.js +1 -1
- package/clis/qwen/history.js +1 -1
- package/clis/qwen/image.js +1 -1
- package/clis/qwen/new.js +1 -1
- package/clis/qwen/read.js +1 -1
- package/clis/qwen/send.js +1 -1
- package/clis/qwen/status.js +1 -1
- package/clis/reddit/comment.js +1 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +2 -0
- package/clis/reddit/read.test.js +4 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +1 -0
- package/clis/reddit/subreddit.js +1 -0
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +1 -0
- package/clis/reddit/user-posts.js +1 -0
- package/clis/reddit/user.js +1 -0
- package/clis/twitter/article.js +7 -4
- package/clis/twitter/bookmark-folder.js +3 -5
- package/clis/twitter/bookmark-folder.test.js +5 -2
- package/clis/twitter/bookmark-folders.js +3 -5
- package/clis/twitter/bookmark-folders.test.js +3 -1
- package/clis/twitter/bookmarks.js +3 -5
- package/clis/twitter/download.js +1 -0
- package/clis/twitter/followers.js +1 -0
- package/clis/twitter/following.js +3 -6
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/likes.js +3 -5
- package/clis/twitter/list-add.js +4 -3
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +4 -3
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +3 -5
- package/clis/twitter/lists.js +3 -5
- package/clis/twitter/notifications.js +1 -0
- package/clis/twitter/profile.js +7 -4
- package/clis/twitter/search.js +1 -0
- package/clis/twitter/thread.js +5 -7
- package/clis/twitter/timeline.js +5 -7
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +3 -6
- package/clis/youtube/like.js +6 -2
- package/clis/youtube/subscribe.js +6 -2
- package/clis/youtube/unlike.js +6 -2
- package/clis/youtube/unsubscribe.js +6 -2
- package/clis/youtube/utils.js +19 -13
- package/clis/youtube/utils.test.js +17 -1
- package/clis/yuanbao/ask.js +1 -1
- package/clis/yuanbao/detail.js +1 -1
- package/clis/yuanbao/history.js +1 -1
- package/clis/yuanbao/new.js +1 -1
- package/clis/yuanbao/read.js +1 -1
- package/clis/yuanbao/send.js +1 -1
- package/clis/yuanbao/status.js +1 -1
- package/dist/src/browser/bridge.d.ts +4 -1
- package/dist/src/browser/bridge.js +3 -1
- package/dist/src/browser/cdp.d.ts +4 -1
- package/dist/src/browser/daemon-client.d.ts +9 -16
- package/dist/src/browser/daemon-client.js +8 -9
- package/dist/src/browser/daemon-client.test.js +10 -0
- package/dist/src/browser/network-cache.d.ts +5 -5
- package/dist/src/browser/network-cache.js +8 -8
- package/dist/src/browser/network-cache.test.js +4 -4
- package/dist/src/browser/page.d.ts +9 -7
- package/dist/src/browser/page.js +27 -16
- package/dist/src/browser/page.test.js +60 -30
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli.js +91 -125
- package/dist/src/cli.test.js +293 -180
- package/dist/src/commanderAdapter.js +9 -0
- package/dist/src/discovery.js +1 -1
- package/dist/src/doctor.d.ts +0 -4
- package/dist/src/doctor.js +8 -72
- package/dist/src/doctor.test.js +26 -97
- package/dist/src/execution.d.ts +3 -0
- package/dist/src/execution.js +47 -23
- package/dist/src/execution.test.js +68 -45
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +1 -0
- package/dist/src/help.js +36 -1
- package/dist/src/main.js +0 -29
- package/dist/src/manifest-types.d.ts +2 -4
- package/dist/src/observation/artifact.js +1 -1
- package/dist/src/observation/artifact.test.js +3 -3
- package/dist/src/observation/events.d.ts +1 -1
- package/dist/src/observation/manager.js +1 -1
- package/dist/src/observation/manager.test.js +3 -3
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +3 -12
- package/dist/src/registry.js +6 -10
- package/dist/src/runtime.d.ts +10 -2
- package/dist/src/runtime.js +4 -1
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/types.d.ts +0 -15
- package/package.json +1 -1
package/clis/twitter/article.js
CHANGED
|
@@ -11,6 +11,7 @@ cli({
|
|
|
11
11
|
domain: 'x.com',
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
browser: true,
|
|
14
|
+
siteSession: 'persistent',
|
|
14
15
|
args: [
|
|
15
16
|
{ name: 'tweet-id', type: 'string', positional: true, required: true, help: 'Tweet ID or URL containing the article' },
|
|
16
17
|
],
|
|
@@ -51,12 +52,16 @@ cli({
|
|
|
51
52
|
// Navigate to the tweet page for cookie context
|
|
52
53
|
await page.goto(`https://x.com/i/status/${tweetId}`);
|
|
53
54
|
await page.wait(3);
|
|
55
|
+
// Read CSRF token directly from the cookie store via CDP — zero page.evaluate round-trip
|
|
56
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
57
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
58
|
+
if (!ct0)
|
|
59
|
+
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
54
60
|
const queryId = await resolveTwitterQueryId(page, 'TweetResultByRestId', TWEET_RESULT_BY_REST_ID_QUERY_ID);
|
|
55
61
|
const result = await page.evaluate(`
|
|
56
62
|
async () => {
|
|
57
63
|
const tweetId = "${tweetId}";
|
|
58
|
-
const ct0 =
|
|
59
|
-
if (!ct0) return {error: 'No ct0 cookie — not logged into x.com'};
|
|
64
|
+
const ct0 = ${JSON.stringify(ct0)};
|
|
60
65
|
|
|
61
66
|
const bearer = ${JSON.stringify(TWITTER_BEARER_TOKEN)};
|
|
62
67
|
const headers = {
|
|
@@ -156,8 +161,6 @@ cli({
|
|
|
156
161
|
}
|
|
157
162
|
`);
|
|
158
163
|
if (result?.error) {
|
|
159
|
-
if (String(result.error).includes('No ct0 cookie'))
|
|
160
|
-
throw new AuthRequiredError('x.com', result.error);
|
|
161
164
|
throw new CommandExecutionError(result.error + (result.hint ? ` (${result.hint})` : ''));
|
|
162
165
|
}
|
|
163
166
|
return result || [];
|
|
@@ -122,6 +122,7 @@ cli({
|
|
|
122
122
|
domain: 'x.com',
|
|
123
123
|
strategy: Strategy.COOKIE,
|
|
124
124
|
browser: true,
|
|
125
|
+
siteSession: 'persistent',
|
|
125
126
|
args: [
|
|
126
127
|
{ name: 'folder-id', positional: true, type: 'string', required: true, help: 'Folder id from `opencli twitter bookmark-folders`.' },
|
|
127
128
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of bookmarks to return (default 20).' },
|
|
@@ -140,11 +141,8 @@ cli({
|
|
|
140
141
|
throw new ArgumentError(`Invalid --limit: ${JSON.stringify(kwargs.limit)}. Expected a positive integer.`);
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
await page.
|
|
144
|
-
|
|
145
|
-
const ct0 = await page.evaluate(`() => {
|
|
146
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
147
|
-
}`);
|
|
144
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
145
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
148
146
|
if (!ct0)
|
|
149
147
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
150
148
|
|
|
@@ -309,11 +309,13 @@ describe('twitter bookmark-folder command (registry)', () => {
|
|
|
309
309
|
const page = {
|
|
310
310
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
311
311
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
312
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
312
313
|
evaluate: vi.fn().mockResolvedValue(null),
|
|
313
314
|
};
|
|
314
315
|
await expect(command.func(page, { 'folder-id': '12345', limit: 5 }))
|
|
315
316
|
.rejects
|
|
316
317
|
.toThrow(/Not logged into x.com/);
|
|
318
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
317
319
|
});
|
|
318
320
|
|
|
319
321
|
it('accepts an opaque safe folder-id and sends it in the GraphQL variables', async () => {
|
|
@@ -321,14 +323,15 @@ describe('twitter bookmark-folder command (registry)', () => {
|
|
|
321
323
|
const page = {
|
|
322
324
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
323
325
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
326
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'ct0-token' }]),
|
|
324
327
|
evaluate: vi.fn()
|
|
325
|
-
.mockResolvedValueOnce('ct0-token')
|
|
326
328
|
.mockResolvedValueOnce('queryX')
|
|
327
329
|
.mockResolvedValueOnce({ data: { bookmark_timeline_v2: { timeline: { instructions: [] } } } }),
|
|
328
330
|
};
|
|
329
331
|
const result = await command.func(page, { 'folder-id': 'folder_AbC-123', limit: 5 });
|
|
330
332
|
expect(result).toEqual([]);
|
|
331
|
-
|
|
333
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
334
|
+
const fetchScript = page.evaluate.mock.calls[1][0];
|
|
332
335
|
expect(decodeURIComponent(fetchScript)).toContain('"bookmark_collection_id":"folder_AbC-123"');
|
|
333
336
|
});
|
|
334
337
|
});
|
|
@@ -77,14 +77,12 @@ cli({
|
|
|
77
77
|
domain: 'x.com',
|
|
78
78
|
strategy: Strategy.COOKIE,
|
|
79
79
|
browser: true,
|
|
80
|
+
siteSession: 'persistent',
|
|
80
81
|
args: [],
|
|
81
82
|
columns: ['id', 'name', 'items', 'created_at'],
|
|
82
83
|
func: async (page) => {
|
|
83
|
-
await page.
|
|
84
|
-
|
|
85
|
-
const ct0 = await page.evaluate(`() => {
|
|
86
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
87
|
-
}`);
|
|
84
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
85
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
88
86
|
if (!ct0)
|
|
89
87
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
90
88
|
|
|
@@ -143,8 +143,10 @@ describe('twitter bookmark-folders command (registry)', () => {
|
|
|
143
143
|
const page = {
|
|
144
144
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
145
145
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
146
|
-
|
|
146
|
+
getCookies: vi.fn().mockResolvedValue([]), // no ct0 cookie → AuthRequired
|
|
147
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
147
148
|
};
|
|
148
149
|
await expect(command.func(page, {})).rejects.toThrow(/Not logged into x.com/);
|
|
150
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
149
151
|
});
|
|
150
152
|
});
|
|
@@ -105,6 +105,7 @@ cli({
|
|
|
105
105
|
domain: 'x.com',
|
|
106
106
|
strategy: Strategy.COOKIE,
|
|
107
107
|
browser: true,
|
|
108
|
+
siteSession: 'persistent',
|
|
108
109
|
args: [
|
|
109
110
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of bookmarks to return (default 20).' },
|
|
110
111
|
{ name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the bookmarks by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API\'s native (saved-time) ordering.' },
|
|
@@ -112,11 +113,8 @@ cli({
|
|
|
112
113
|
columns: ['id', 'author', 'text', 'likes', 'retweets', 'bookmarks', 'created_at', 'url'],
|
|
113
114
|
func: async (page, kwargs) => {
|
|
114
115
|
const limit = kwargs.limit || 20;
|
|
115
|
-
await page.
|
|
116
|
-
|
|
117
|
-
const ct0 = await page.evaluate(`() => {
|
|
118
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
119
|
-
}`);
|
|
116
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
117
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
120
118
|
if (!ct0)
|
|
121
119
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
122
120
|
const queryId = await page.evaluate(`async () => {
|
package/clis/twitter/download.js
CHANGED
|
@@ -15,6 +15,7 @@ cli({
|
|
|
15
15
|
description: 'Download Twitter/X media (images and videos). Provide either <username> to scan a profile\'s media tab, or --tweet-url to download a single tweet.',
|
|
16
16
|
domain: 'x.com',
|
|
17
17
|
strategy: Strategy.COOKIE,
|
|
18
|
+
siteSession: 'persistent',
|
|
18
19
|
args: [
|
|
19
20
|
{ name: 'username', positional: true, help: 'Twitter username (with or without @) to scan their /media tab. Either <username> or --tweet-url is required.' },
|
|
20
21
|
{ name: 'tweet-url', help: 'Single tweet URL to download. Use this OR <username>, not both required at once.' },
|
|
@@ -139,6 +139,7 @@ cli({
|
|
|
139
139
|
domain: 'x.com',
|
|
140
140
|
strategy: Strategy.COOKIE,
|
|
141
141
|
browser: true,
|
|
142
|
+
siteSession: 'persistent',
|
|
142
143
|
args: [
|
|
143
144
|
{
|
|
144
145
|
name: 'user',
|
|
@@ -157,12 +158,8 @@ cli({
|
|
|
157
158
|
}
|
|
158
159
|
let targetUser = normalizeScreenName(kwargs.user);
|
|
159
160
|
|
|
160
|
-
await page.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const ct0 = await page.evaluate(`() => {
|
|
164
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
165
|
-
}`);
|
|
161
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
162
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
166
163
|
if (!ct0)
|
|
167
164
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
168
165
|
|
|
@@ -205,8 +205,8 @@ function createFollowingPage(followingResponses, { ct0 = 'token', userLookup = {
|
|
|
205
205
|
const page = {
|
|
206
206
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
207
207
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
208
|
+
getCookies: vi.fn(async () => (ct0 ? [{ name: 'ct0', value: ct0 }] : [])),
|
|
208
209
|
evaluate: vi.fn(async (script) => {
|
|
209
|
-
if (script.includes('document.cookie')) return ct0;
|
|
210
210
|
if (script.includes('operationName')) return null;
|
|
211
211
|
if (script.includes('/UserByScreenName')) return userLookup;
|
|
212
212
|
if (script.includes('/Following')) return followingResponses.shift() || followingPayload([], null);
|
|
@@ -228,6 +228,7 @@ describe('twitter following command', () => {
|
|
|
228
228
|
const rows = await command.func(page, { user: '@elonmusk', limit: 3 });
|
|
229
229
|
|
|
230
230
|
expect(rows.map((row) => row.screen_name)).toEqual(['alice', 'bob', 'carol']);
|
|
231
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
231
232
|
const userLookupScript = page.evaluate.mock.calls.find(([script]) => script.includes('/UserByScreenName'))?.[0] || '';
|
|
232
233
|
expect(decodeURIComponent(userLookupScript)).toContain('"screen_name":"elonmusk"');
|
|
233
234
|
expect(decodeURIComponent(userLookupScript)).not.toContain('"screen_name":"@elonmusk"');
|
package/clis/twitter/likes.js
CHANGED
|
@@ -142,6 +142,7 @@ cli({
|
|
|
142
142
|
domain: 'x.com',
|
|
143
143
|
strategy: Strategy.COOKIE,
|
|
144
144
|
browser: true,
|
|
145
|
+
siteSession: 'persistent',
|
|
145
146
|
args: [
|
|
146
147
|
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
147
148
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of liked tweets to return (default 20).' },
|
|
@@ -151,11 +152,8 @@ cli({
|
|
|
151
152
|
func: async (page, kwargs) => {
|
|
152
153
|
const limit = kwargs.limit || 20;
|
|
153
154
|
let username = (kwargs.username || '').replace(/^@/, '');
|
|
154
|
-
await page.
|
|
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
|
-
}`);
|
|
155
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
156
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
159
157
|
if (!ct0)
|
|
160
158
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
161
159
|
// If no username provided, detect the logged-in user
|
package/clis/twitter/list-add.js
CHANGED
|
@@ -84,11 +84,12 @@ cli({
|
|
|
84
84
|
if (!username) {
|
|
85
85
|
throw new CommandExecutionError('Username is required');
|
|
86
86
|
}
|
|
87
|
+
// Strategy.UI does not get a domain URL pre-nav from the framework.
|
|
88
|
+
// This page context is load-bearing for pre-target GraphQL calls below.
|
|
87
89
|
await page.goto('https://x.com');
|
|
88
90
|
await page.wait(3);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
}`);
|
|
91
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
92
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
92
93
|
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
93
94
|
|
|
94
95
|
const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
3
|
import './list-add.js';
|
|
4
4
|
|
|
@@ -12,4 +12,26 @@ describe('twitter list-add registration', () => {
|
|
|
12
12
|
expect(listIdArg?.required).toBe(true);
|
|
13
13
|
expect(listIdArg?.positional).toBe(true);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
it('keeps the x.com root navigation before pre-target GraphQL calls', async () => {
|
|
17
|
+
const cmd = getRegistry().get('twitter/list-add');
|
|
18
|
+
const page = {
|
|
19
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
22
|
+
evaluate: vi.fn()
|
|
23
|
+
.mockResolvedValueOnce(null) // UserByScreenName queryId fallback
|
|
24
|
+
.mockResolvedValueOnce('user-1')
|
|
25
|
+
.mockResolvedValueOnce(null) // ListsManagement queryId fallback
|
|
26
|
+
.mockResolvedValueOnce({}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await expect(cmd.func(page, { listId: '123', username: 'alice' }))
|
|
30
|
+
.rejects
|
|
31
|
+
.toThrow(/List 123 not found/);
|
|
32
|
+
expect(page.goto).toHaveBeenCalledWith('https://x.com');
|
|
33
|
+
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(page.wait).toHaveBeenCalledWith(3);
|
|
35
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
36
|
+
});
|
|
15
37
|
});
|
|
@@ -92,11 +92,12 @@ cli({
|
|
|
92
92
|
}
|
|
93
93
|
if (!username) throw new CommandExecutionError('Username is required');
|
|
94
94
|
|
|
95
|
+
// Strategy.UI does not get a domain URL pre-nav from the framework.
|
|
96
|
+
// This page context is load-bearing for pre-target GraphQL calls below.
|
|
95
97
|
await page.goto('https://x.com');
|
|
96
98
|
await page.wait(3);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
}`);
|
|
99
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
100
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
100
101
|
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
101
102
|
|
|
102
103
|
const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
3
|
import './list-remove.js';
|
|
4
4
|
|
|
@@ -11,4 +11,26 @@ describe('twitter list-remove registration', () => {
|
|
|
11
11
|
expect(listIdArg).toBeTruthy();
|
|
12
12
|
expect(listIdArg?.required).toBe(true);
|
|
13
13
|
});
|
|
14
|
+
|
|
15
|
+
it('keeps the x.com root navigation before pre-target GraphQL calls', async () => {
|
|
16
|
+
const cmd = getRegistry().get('twitter/list-remove');
|
|
17
|
+
const page = {
|
|
18
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
21
|
+
evaluate: vi.fn()
|
|
22
|
+
.mockResolvedValueOnce(null) // UserByScreenName queryId fallback
|
|
23
|
+
.mockResolvedValueOnce('user-1')
|
|
24
|
+
.mockResolvedValueOnce(null) // ListsManagement queryId fallback
|
|
25
|
+
.mockResolvedValueOnce({}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
await expect(cmd.func(page, { listId: '123', username: 'alice' }))
|
|
29
|
+
.rejects
|
|
30
|
+
.toThrow(/List 123 not found/);
|
|
31
|
+
expect(page.goto).toHaveBeenCalledWith('https://x.com');
|
|
32
|
+
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
33
|
+
expect(page.wait).toHaveBeenCalledWith(3);
|
|
34
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
35
|
+
});
|
|
14
36
|
});
|
|
@@ -112,6 +112,7 @@ cli({
|
|
|
112
112
|
domain: 'x.com',
|
|
113
113
|
strategy: Strategy.COOKIE,
|
|
114
114
|
browser: true,
|
|
115
|
+
siteSession: 'persistent',
|
|
115
116
|
args: [
|
|
116
117
|
{ name: 'listId', positional: true, type: 'string', required: true, help: 'Numeric ID of a Twitter/X list (e.g. from `opencli twitter lists`)' },
|
|
117
118
|
{ name: 'limit', type: 'int', default: 50 },
|
|
@@ -124,11 +125,8 @@ cli({
|
|
|
124
125
|
throw new CommandExecutionError(`Invalid listId: ${JSON.stringify(kwargs.listId)}. Expected a numeric ID (see \`opencli twitter lists\`).`);
|
|
125
126
|
}
|
|
126
127
|
const limit = kwargs.limit || 50;
|
|
127
|
-
await page.
|
|
128
|
-
|
|
129
|
-
const ct0 = await page.evaluate(`() => {
|
|
130
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
131
|
-
}`);
|
|
128
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
129
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
132
130
|
if (!ct0)
|
|
133
131
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
134
132
|
const queryId = await page.evaluate(`async () => {
|
package/clis/twitter/lists.js
CHANGED
|
@@ -92,17 +92,15 @@ export const command = cli({
|
|
|
92
92
|
domain: 'x.com',
|
|
93
93
|
strategy: Strategy.COOKIE,
|
|
94
94
|
browser: true,
|
|
95
|
+
siteSession: 'persistent',
|
|
95
96
|
args: [
|
|
96
97
|
{ name: 'limit', type: 'int', default: 50, help: 'Maximum number of lists to return (default 50).' },
|
|
97
98
|
],
|
|
98
99
|
columns: ['id', 'name', 'members', 'followers', 'mode'],
|
|
99
100
|
func: async (page, kwargs) => {
|
|
100
101
|
const limit = kwargs.limit || 50;
|
|
101
|
-
await page.
|
|
102
|
-
|
|
103
|
-
const ct0 = await page.evaluate(`() => {
|
|
104
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
105
|
-
}`);
|
|
102
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
103
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
106
104
|
if (!ct0)
|
|
107
105
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
108
106
|
const queryId = await page.evaluate(`async () => {
|
package/clis/twitter/profile.js
CHANGED
|
@@ -11,6 +11,7 @@ cli({
|
|
|
11
11
|
domain: 'x.com',
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
browser: true,
|
|
14
|
+
siteSession: 'persistent',
|
|
14
15
|
args: [
|
|
15
16
|
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
16
17
|
],
|
|
@@ -32,12 +33,16 @@ cli({
|
|
|
32
33
|
// Navigate directly to the user's profile page (gives us cookie context)
|
|
33
34
|
await page.goto(`https://x.com/${username}`);
|
|
34
35
|
await page.wait(3);
|
|
36
|
+
// Read CSRF token directly from the cookie store via CDP — zero page.evaluate round-trip
|
|
37
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
38
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
39
|
+
if (!ct0)
|
|
40
|
+
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
35
41
|
const queryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
36
42
|
const result = await page.evaluate(`
|
|
37
43
|
async () => {
|
|
38
44
|
const screenName = "${username}";
|
|
39
|
-
const ct0 =
|
|
40
|
-
if (!ct0) return {error: 'No ct0 cookie — not logged into x.com'};
|
|
45
|
+
const ct0 = ${JSON.stringify(ct0)};
|
|
41
46
|
|
|
42
47
|
const bearer = ${JSON.stringify(TWITTER_BEARER_TOKEN)};
|
|
43
48
|
const headers = {
|
|
@@ -96,8 +101,6 @@ cli({
|
|
|
96
101
|
}
|
|
97
102
|
`);
|
|
98
103
|
if (result?.error) {
|
|
99
|
-
if (String(result.error).includes('No ct0 cookie'))
|
|
100
|
-
throw new AuthRequiredError('x.com', result.error);
|
|
101
104
|
throw new CommandExecutionError(result.error + (result.hint ? ` (${result.hint})` : ''));
|
|
102
105
|
}
|
|
103
106
|
return result || [];
|
package/clis/twitter/search.js
CHANGED
|
@@ -228,6 +228,7 @@ cli({
|
|
|
228
228
|
domain: 'x.com',
|
|
229
229
|
strategy: Strategy.INTERCEPT, // Use intercept strategy
|
|
230
230
|
browser: true,
|
|
231
|
+
siteSession: 'persistent',
|
|
231
232
|
args: [
|
|
232
233
|
{ name: 'query', type: 'string', required: true, positional: true, help: 'Search query. Raw X operators (e.g. "exact phrase", #tag, OR, lang:en, since:YYYY-MM-DD, from:, since:) are passed through unchanged.' },
|
|
233
234
|
{ name: 'filter', type: 'string', default: 'top', choices: ['top', 'live'], help: 'Legacy alias for --product. Kept for backwards compatibility; if --product is set it wins.' },
|
package/clis/twitter/thread.js
CHANGED
|
@@ -100,6 +100,7 @@ cli({
|
|
|
100
100
|
domain: 'x.com',
|
|
101
101
|
strategy: Strategy.COOKIE,
|
|
102
102
|
browser: true,
|
|
103
|
+
siteSession: 'persistent',
|
|
103
104
|
args: [
|
|
104
105
|
{ name: 'tweet-id', positional: true, type: 'string', required: true, help: 'Tweet numeric ID (e.g. 1234567890) or full status URL' },
|
|
105
106
|
{ name: 'limit', type: 'int', default: 50 },
|
|
@@ -111,13 +112,10 @@ cli({
|
|
|
111
112
|
const urlMatch = tweetId.match(/\/status\/(\d+)/);
|
|
112
113
|
if (urlMatch)
|
|
113
114
|
tweetId = urlMatch[1];
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
await page.
|
|
117
|
-
|
|
118
|
-
const ct0 = await page.evaluate(`() => {
|
|
119
|
-
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
120
|
-
}`);
|
|
115
|
+
// Cookie context auto-established by framework pre-nav (Strategy.COOKIE + domain).
|
|
116
|
+
// Read CSRF token directly from the cookie store via CDP — zero page.evaluate round-trip.
|
|
117
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
118
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
121
119
|
if (!ct0)
|
|
122
120
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
123
121
|
// Build auth headers in TypeScript
|
package/clis/twitter/timeline.js
CHANGED
|
@@ -141,6 +141,7 @@ cli({
|
|
|
141
141
|
domain: 'x.com',
|
|
142
142
|
strategy: Strategy.COOKIE,
|
|
143
143
|
browser: true,
|
|
144
|
+
siteSession: 'persistent',
|
|
144
145
|
args: [
|
|
145
146
|
{
|
|
146
147
|
name: 'type',
|
|
@@ -156,13 +157,10 @@ cli({
|
|
|
156
157
|
const limit = kwargs.limit || 20;
|
|
157
158
|
const timelineType = kwargs.type === 'following' ? 'following' : 'for-you';
|
|
158
159
|
const { endpoint, method, fallbackQueryId } = TIMELINE_ENDPOINTS[timelineType];
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
await page.
|
|
162
|
-
|
|
163
|
-
const ct0 = await page.evaluate(`() => {
|
|
164
|
-
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
165
|
-
}`);
|
|
160
|
+
// Cookie context auto-established by framework pre-nav (Strategy.COOKIE + domain).
|
|
161
|
+
// Read CSRF token directly from the cookie store via CDP — zero page.evaluate round-trip.
|
|
162
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
163
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
166
164
|
if (!ct0)
|
|
167
165
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
168
166
|
// Dynamically resolve queryId for the selected endpoint
|
package/clis/twitter/trending.js
CHANGED
|
@@ -17,6 +17,7 @@ cli({
|
|
|
17
17
|
domain: 'x.com',
|
|
18
18
|
strategy: Strategy.COOKIE,
|
|
19
19
|
browser: true,
|
|
20
|
+
siteSession: 'persistent',
|
|
20
21
|
args: [
|
|
21
22
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of trends to show' },
|
|
22
23
|
],
|
|
@@ -26,10 +27,9 @@ cli({
|
|
|
26
27
|
// Navigate to trending page
|
|
27
28
|
await page.goto('https://x.com/explore/tabs/trending');
|
|
28
29
|
await page.wait(3);
|
|
29
|
-
// Verify login via CSRF cookie
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
})()`);
|
|
30
|
+
// Verify login via CSRF cookie (read directly from cookie store via CDP)
|
|
31
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
32
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
33
33
|
if (!ct0)
|
|
34
34
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
35
35
|
await page.wait(2);
|
package/clis/twitter/tweets.js
CHANGED
|
@@ -149,6 +149,7 @@ cli({
|
|
|
149
149
|
domain: 'x.com',
|
|
150
150
|
strategy: Strategy.COOKIE,
|
|
151
151
|
browser: true,
|
|
152
|
+
siteSession: 'persistent',
|
|
152
153
|
args: [
|
|
153
154
|
{ name: 'username', type: 'string', positional: true, required: true, help: 'Twitter screen name (with or without @)' },
|
|
154
155
|
{ name: 'limit', type: 'int', default: 20, help: 'Max tweets to return' },
|
|
@@ -160,12 +161,8 @@ cli({
|
|
|
160
161
|
const username = String(kwargs.username || '').replace(/^@/, '').trim();
|
|
161
162
|
if (!username) throw new CommandExecutionError('username is required');
|
|
162
163
|
|
|
163
|
-
await page.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const ct0 = await page.evaluate(`() => {
|
|
167
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
168
|
-
}`);
|
|
164
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
165
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
169
166
|
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
170
167
|
|
|
171
168
|
const userTweetsQueryId = await resolveTwitterQueryId(page, 'UserTweets', USER_TWEETS_QUERY_ID);
|
package/clis/youtube/like.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* YouTube like — like a video via InnerTube like API (requires SAPISIDHASH auth).
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import { parseVideoId, prepareYoutubeApiPage, SAPISID_HASH_FN } from './utils.js';
|
|
5
|
+
import { parseVideoId, prepareYoutubeApiPage, readYoutubeSapisid, SAPISID_HASH_FN } from './utils.js';
|
|
6
6
|
import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors';
|
|
7
7
|
|
|
8
8
|
cli({
|
|
@@ -19,6 +19,10 @@ cli({
|
|
|
19
19
|
func: async (page, kwargs) => {
|
|
20
20
|
const videoId = parseVideoId(String(kwargs.url));
|
|
21
21
|
await prepareYoutubeApiPage(page);
|
|
22
|
+
// Read SAPISID directly from the cookie store via CDP — zero document.cookie round-trip
|
|
23
|
+
const sapisid = await readYoutubeSapisid(page);
|
|
24
|
+
if (!sapisid)
|
|
25
|
+
throw new AuthRequiredError('www.youtube.com', 'Not logged in (SAPISID cookie missing)');
|
|
22
26
|
const result = await page.evaluate(`
|
|
23
27
|
(async () => {
|
|
24
28
|
${SAPISID_HASH_FN}
|
|
@@ -28,7 +32,7 @@ cli({
|
|
|
28
32
|
const context = cfg.INNERTUBE_CONTEXT;
|
|
29
33
|
if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' };
|
|
30
34
|
|
|
31
|
-
const authHash = await getSapisidHash('https://www.youtube.com');
|
|
35
|
+
const authHash = await getSapisidHash(${JSON.stringify(sapisid)}, 'https://www.youtube.com');
|
|
32
36
|
if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' };
|
|
33
37
|
|
|
34
38
|
const resp = await fetch('/youtubei/v1/like/like?key=' + apiKey + '&prettyPrint=false', {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* YouTube subscribe — subscribe to a channel via InnerTube subscription API.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import { prepareYoutubeApiPage, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js';
|
|
5
|
+
import { prepareYoutubeApiPage, readYoutubeSapisid, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js';
|
|
6
6
|
import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors';
|
|
7
7
|
|
|
8
8
|
cli({
|
|
@@ -19,6 +19,10 @@ cli({
|
|
|
19
19
|
func: async (page, kwargs) => {
|
|
20
20
|
const channelInput = String(kwargs.channel);
|
|
21
21
|
await prepareYoutubeApiPage(page);
|
|
22
|
+
// Read SAPISID directly from the cookie store via CDP — zero document.cookie round-trip
|
|
23
|
+
const sapisid = await readYoutubeSapisid(page);
|
|
24
|
+
if (!sapisid)
|
|
25
|
+
throw new AuthRequiredError('www.youtube.com', 'Not logged in (SAPISID cookie missing)');
|
|
22
26
|
const result = await page.evaluate(`
|
|
23
27
|
(async () => {
|
|
24
28
|
${SAPISID_HASH_FN}
|
|
@@ -28,7 +32,7 @@ cli({
|
|
|
28
32
|
const context = cfg.INNERTUBE_CONTEXT;
|
|
29
33
|
if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' };
|
|
30
34
|
|
|
31
|
-
const authHash = await getSapisidHash('https://www.youtube.com');
|
|
35
|
+
const authHash = await getSapisidHash(${JSON.stringify(sapisid)}, 'https://www.youtube.com');
|
|
32
36
|
if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' };
|
|
33
37
|
|
|
34
38
|
${RESOLVE_CHANNEL_HANDLE_FN}
|
package/clis/youtube/unlike.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* YouTube unlike — remove like from a video via InnerTube like API.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import { parseVideoId, prepareYoutubeApiPage, SAPISID_HASH_FN } from './utils.js';
|
|
5
|
+
import { parseVideoId, prepareYoutubeApiPage, readYoutubeSapisid, SAPISID_HASH_FN } from './utils.js';
|
|
6
6
|
import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors';
|
|
7
7
|
|
|
8
8
|
cli({
|
|
@@ -19,6 +19,10 @@ cli({
|
|
|
19
19
|
func: async (page, kwargs) => {
|
|
20
20
|
const videoId = parseVideoId(String(kwargs.url));
|
|
21
21
|
await prepareYoutubeApiPage(page);
|
|
22
|
+
// Read SAPISID directly from the cookie store via CDP — zero document.cookie round-trip
|
|
23
|
+
const sapisid = await readYoutubeSapisid(page);
|
|
24
|
+
if (!sapisid)
|
|
25
|
+
throw new AuthRequiredError('www.youtube.com', 'Not logged in (SAPISID cookie missing)');
|
|
22
26
|
const result = await page.evaluate(`
|
|
23
27
|
(async () => {
|
|
24
28
|
${SAPISID_HASH_FN}
|
|
@@ -28,7 +32,7 @@ cli({
|
|
|
28
32
|
const context = cfg.INNERTUBE_CONTEXT;
|
|
29
33
|
if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' };
|
|
30
34
|
|
|
31
|
-
const authHash = await getSapisidHash('https://www.youtube.com');
|
|
35
|
+
const authHash = await getSapisidHash(${JSON.stringify(sapisid)}, 'https://www.youtube.com');
|
|
32
36
|
if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' };
|
|
33
37
|
|
|
34
38
|
const resp = await fetch('/youtubei/v1/like/removelike?key=' + apiKey + '&prettyPrint=false', {
|