@jackwener/opencli 1.7.14 → 1.7.16
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 +9 -6
- package/README.zh-CN.md +9 -6
- package/cli-manifest.json +374 -74
- package/clis/bilibili/subtitle.js +1 -1
- package/clis/chatgpt/ask.js +2 -1
- package/clis/chatgpt/detail.js +6 -1
- package/clis/chatgpt/read.js +2 -1
- package/clis/chatgpt/send.js +2 -1
- package/clis/chatgpt/utils.js +54 -12
- package/clis/chatgpt/utils.test.js +36 -1
- package/clis/claude/ask.js +22 -7
- package/clis/claude/detail.js +9 -2
- package/clis/claude/new.js +8 -2
- package/clis/claude/read.js +2 -1
- package/clis/claude/send.js +8 -3
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +21 -8
- package/clis/deepseek/detail.js +9 -1
- package/clis/deepseek/new.js +13 -2
- package/clis/deepseek/read.js +2 -1
- package/clis/deepseek/utils.js +8 -1
- package/clis/dianping/cityResolver.js +185 -0
- package/clis/dianping/dianping.test.js +154 -0
- package/clis/dianping/search.js +6 -3
- package/clis/douyin/_shared/browser-fetch.js +14 -2
- package/clis/douyin/_shared/browser-fetch.test.js +13 -0
- package/clis/douyin/stats.js +1 -1
- package/clis/douyin/update.js +1 -1
- package/clis/jike/search.js +1 -1
- package/clis/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- 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 +2 -1
- package/clis/reddit/subreddit.js +2 -1
- 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 +2 -1
- package/clis/reddit/user-posts.js +2 -1
- package/clis/reddit/user.js +2 -1
- package/clis/twitter/article.js +9 -5
- package/clis/twitter/bookmark-folder.js +187 -0
- package/clis/twitter/bookmark-folder.test.js +337 -0
- package/clis/twitter/bookmark-folders.js +115 -0
- package/clis/twitter/bookmark-folders.test.js +152 -0
- package/clis/twitter/bookmark.js +15 -6
- package/clis/twitter/bookmark.test.js +74 -0
- package/clis/twitter/bookmarks.js +10 -10
- package/clis/twitter/delete.js +11 -35
- package/clis/twitter/delete.test.js +21 -9
- package/clis/twitter/download.js +6 -5
- package/clis/twitter/followers.js +10 -3
- package/clis/twitter/following.js +14 -11
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/hide-reply.js +24 -5
- package/clis/twitter/hide-reply.test.js +76 -0
- package/clis/twitter/like.js +21 -11
- package/clis/twitter/like.test.js +73 -0
- package/clis/twitter/likes.js +11 -11
- package/clis/twitter/list-add.js +8 -7
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +8 -7
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +9 -9
- package/clis/twitter/lists.js +6 -8
- package/clis/twitter/notifications.js +3 -2
- package/clis/twitter/profile.js +11 -7
- package/clis/twitter/quote.js +60 -32
- package/clis/twitter/quote.test.js +96 -8
- package/clis/twitter/reply.js +24 -178
- package/clis/twitter/reply.test.js +29 -11
- package/clis/twitter/retweet.js +9 -14
- package/clis/twitter/retweet.test.js +5 -1
- package/clis/twitter/search.js +176 -23
- package/clis/twitter/search.test.js +266 -1
- package/clis/twitter/shared.js +43 -0
- package/clis/twitter/shared.test.js +107 -1
- package/clis/twitter/thread.js +11 -11
- package/clis/twitter/timeline.js +13 -13
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +8 -9
- package/clis/twitter/unbookmark.js +13 -6
- package/clis/twitter/unbookmark.test.js +73 -0
- package/clis/twitter/unlike.js +6 -13
- package/clis/twitter/unlike.test.js +5 -2
- package/clis/twitter/unretweet.js +9 -14
- package/clis/twitter/unretweet.test.js +5 -1
- package/clis/twitter/utils.js +286 -0
- package/clis/twitter/utils.test.js +169 -0
- 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/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +51 -0
- package/dist/src/browser/base-page.js +545 -2
- package/dist/src/browser/base-page.test.js +520 -4
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +5 -0
- package/dist/src/browser/cdp.test.js +1 -0
- package/dist/src/browser/daemon-client.d.ts +5 -3
- package/dist/src/browser/daemon-client.js +6 -3
- package/dist/src/browser/daemon-client.test.js +10 -0
- package/dist/src/browser/find.d.ts +9 -1
- package/dist/src/browser/find.js +219 -0
- package/dist/src/browser/find.test.js +61 -1
- package/dist/src/browser/page.d.ts +4 -2
- package/dist/src/browser/page.js +18 -1
- package/dist/src/browser/page.test.js +28 -0
- package/dist/src/browser/target-errors.d.ts +3 -1
- package/dist/src/browser/target-errors.js +2 -0
- package/dist/src/browser/target-resolver.d.ts +14 -0
- package/dist/src/browser/target-resolver.js +28 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/build-manifest.d.ts +23 -0
- package/dist/src/build-manifest.js +34 -0
- package/dist/src/build-manifest.test.js +108 -1
- package/dist/src/cli.js +630 -60
- package/dist/src/cli.test.js +731 -1
- package/dist/src/commanderAdapter.js +7 -0
- package/dist/src/doctor.js +2 -2
- package/dist/src/doctor.test.js +4 -4
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +31 -6
- package/dist/src/execution.test.js +43 -16
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +33 -0
- package/dist/src/help.js +174 -0
- package/dist/src/main.js +4 -14
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +83 -1
- package/package.json +1 -1
- package/scripts/typed-error-lint-baseline.json +18 -18
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import './like.js';
|
|
5
|
+
import { createPageMock } from '../test-utils.js';
|
|
6
|
+
|
|
7
|
+
describe('twitter like command', () => {
|
|
8
|
+
it('navigates to the tweet URL and reports success when the like script confirms', async () => {
|
|
9
|
+
const cmd = getRegistry().get('twitter/like');
|
|
10
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
11
|
+
const page = createPageMock([
|
|
12
|
+
{ ok: true, message: 'Tweet successfully liked.' },
|
|
13
|
+
]);
|
|
14
|
+
const result = await cmd.func(page, {
|
|
15
|
+
url: 'https://x.com/alice/status/2040254679301718161',
|
|
16
|
+
});
|
|
17
|
+
expect(page.goto).toHaveBeenCalledWith('https://x.com/alice/status/2040254679301718161');
|
|
18
|
+
expect(page.wait).toHaveBeenNthCalledWith(1, { selector: '[data-testid="primaryColumn"]' });
|
|
19
|
+
expect(page.wait).toHaveBeenNthCalledWith(2, 2);
|
|
20
|
+
const script = page.evaluate.mock.calls[0][0];
|
|
21
|
+
// Idempotency: looks for the unlike button (already-liked path) before clicking.
|
|
22
|
+
expect(script).toContain("targetArticle?.querySelector('[data-testid=\"like\"]')");
|
|
23
|
+
expect(script).toContain("targetArticle?.querySelector('[data-testid=\"unlike\"]')");
|
|
24
|
+
expect(script).toContain('likeBtn.click()');
|
|
25
|
+
// Article scoping comes from the shared helper (buildTwitterArticleScopeSource):
|
|
26
|
+
// emits __twHasLinkToTarget + __twGetStatusIdFromHref + the anchored
|
|
27
|
+
// tweet-path regex. JSDOM-level coverage lives in shared.test.js.
|
|
28
|
+
expect(script).toContain('__twHasLinkToTarget');
|
|
29
|
+
expect(script).toContain('__twGetStatusIdFromHref');
|
|
30
|
+
expect(script).toContain("document.querySelectorAll('article')");
|
|
31
|
+
expect(result).toEqual([
|
|
32
|
+
{ status: 'success', message: 'Tweet successfully liked.' },
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns a failed row without re-waiting when the like script reports a UI mismatch', async () => {
|
|
37
|
+
const cmd = getRegistry().get('twitter/like');
|
|
38
|
+
const page = createPageMock([
|
|
39
|
+
{
|
|
40
|
+
ok: false,
|
|
41
|
+
message: 'Could not find the Like button on this tweet after waiting 10 seconds. Are you logged in?',
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
const result = await cmd.func(page, {
|
|
45
|
+
url: 'https://x.com/alice/status/2040254679301718161',
|
|
46
|
+
});
|
|
47
|
+
expect(result).toEqual([
|
|
48
|
+
{
|
|
49
|
+
status: 'failed',
|
|
50
|
+
message: 'Could not find the Like button on this tweet after waiting 10 seconds. Are you logged in?',
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
// Only the primaryColumn wait should run when ok is false.
|
|
54
|
+
expect(page.wait).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('throws CommandExecutionError when no page is provided', async () => {
|
|
58
|
+
const cmd = getRegistry().get('twitter/like');
|
|
59
|
+
await expect(cmd.func(undefined, {
|
|
60
|
+
url: 'https://x.com/alice/status/2040254679301718161',
|
|
61
|
+
})).rejects.toThrow(CommandExecutionError);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects invalid tweet URLs before navigation', async () => {
|
|
65
|
+
const cmd = getRegistry().get('twitter/like');
|
|
66
|
+
const page = createPageMock([]);
|
|
67
|
+
await expect(cmd.func(page, {
|
|
68
|
+
url: 'https://x.com/alice/status/2040254679301718161/photo/1',
|
|
69
|
+
})).rejects.toThrow(ArgumentError);
|
|
70
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
71
|
+
expect(page.evaluate).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
});
|
package/clis/twitter/likes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
2
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { resolveTwitterQueryId, sanitizeQueryId, extractMedia } from './shared.js';
|
|
4
|
-
|
|
4
|
+
import { TWITTER_BEARER_TOKEN, applyTopByEngagement } from './utils.js';
|
|
5
5
|
const LIKES_QUERY_ID = 'RozQdCp4CilQzrcuU0NY5w';
|
|
6
6
|
const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
|
|
7
7
|
const FEATURES = {
|
|
@@ -138,23 +138,22 @@ cli({
|
|
|
138
138
|
site: 'twitter',
|
|
139
139
|
name: 'likes',
|
|
140
140
|
access: 'read',
|
|
141
|
-
description: 'Fetch liked tweets of a Twitter user',
|
|
141
|
+
description: 'Fetch liked tweets of a Twitter user (defaults to the logged-in user when no username is given)',
|
|
142
142
|
domain: 'x.com',
|
|
143
143
|
strategy: Strategy.COOKIE,
|
|
144
144
|
browser: true,
|
|
145
|
+
browserSession: { reuse: 'site' },
|
|
145
146
|
args: [
|
|
146
|
-
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (without @). Defaults to logged-in user.' },
|
|
147
|
-
{ name: 'limit', type: 'int', default: 20 },
|
|
147
|
+
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
148
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of liked tweets to return (default 20).' },
|
|
149
|
+
{ name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the liked tweets 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 (recency) ordering.' },
|
|
148
150
|
],
|
|
149
151
|
columns: ['id', 'author', 'name', 'text', 'likes', 'retweets', 'created_at', 'url', 'has_media', 'media_urls'],
|
|
150
152
|
func: async (page, kwargs) => {
|
|
151
153
|
const limit = kwargs.limit || 20;
|
|
152
154
|
let username = (kwargs.username || '').replace(/^@/, '');
|
|
153
|
-
await page.
|
|
154
|
-
|
|
155
|
-
const ct0 = await page.evaluate(`() => {
|
|
156
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
157
|
-
}`);
|
|
155
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
156
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
158
157
|
if (!ct0)
|
|
159
158
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
160
159
|
// If no username provided, detect the logged-in user
|
|
@@ -170,7 +169,7 @@ cli({
|
|
|
170
169
|
const likesQueryId = await resolveTwitterQueryId(page, 'Likes', LIKES_QUERY_ID);
|
|
171
170
|
const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
172
171
|
const headers = JSON.stringify({
|
|
173
|
-
'Authorization': `Bearer ${decodeURIComponent(
|
|
172
|
+
'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
|
|
174
173
|
'X-Csrf-Token': ct0,
|
|
175
174
|
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
176
175
|
'X-Twitter-Active-User': 'yes',
|
|
@@ -208,7 +207,8 @@ cli({
|
|
|
208
207
|
break;
|
|
209
208
|
cursor = nextCursor;
|
|
210
209
|
}
|
|
211
|
-
|
|
210
|
+
const trimmed = allTweets.slice(0, limit);
|
|
211
|
+
return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);
|
|
212
212
|
},
|
|
213
213
|
});
|
|
214
214
|
export const __test__ = {
|
package/clis/twitter/list-add.js
CHANGED
|
@@ -2,8 +2,8 @@ import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
|
2
2
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { resolveTwitterQueryId } from './shared.js';
|
|
4
4
|
import { parseListsManagement } from './lists.js';
|
|
5
|
+
import { TWITTER_BEARER_TOKEN } from './utils.js';
|
|
5
6
|
|
|
6
|
-
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
7
7
|
const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
|
|
8
8
|
const LISTS_MANAGEMENT_QUERY_ID = '78UbkyXwXBD98IgUWXOy9g';
|
|
9
9
|
|
|
@@ -71,8 +71,8 @@ cli({
|
|
|
71
71
|
strategy: Strategy.UI,
|
|
72
72
|
browser: true,
|
|
73
73
|
args: [
|
|
74
|
-
{ name: 'listId', positional: true, type: 'string', required: true },
|
|
75
|
-
{ name: 'username', positional: true, type: 'string', required: true },
|
|
74
|
+
{ name: 'listId', positional: true, type: 'string', required: true, help: 'Numeric ID of the list you own (e.g. from `opencli twitter lists`)' },
|
|
75
|
+
{ name: 'username', positional: true, type: 'string', required: true, help: 'Twitter/X handle to add (with or without @)' },
|
|
76
76
|
],
|
|
77
77
|
columns: ['listId', 'username', 'userId', 'status', 'message'],
|
|
78
78
|
func: async (page, kwargs) => {
|
|
@@ -84,17 +84,18 @@ 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);
|
|
95
96
|
|
|
96
97
|
const headers = JSON.stringify({
|
|
97
|
-
'Authorization': `Bearer ${decodeURIComponent(
|
|
98
|
+
'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
|
|
98
99
|
'X-Csrf-Token': ct0,
|
|
99
100
|
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
100
101
|
'X-Twitter-Active-User': 'yes',
|
|
@@ -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
|
});
|
|
@@ -2,8 +2,8 @@ import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
|
2
2
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { resolveTwitterQueryId } from './shared.js';
|
|
4
4
|
import { parseListsManagement } from './lists.js';
|
|
5
|
+
import { TWITTER_BEARER_TOKEN } from './utils.js';
|
|
5
6
|
|
|
6
|
-
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
7
7
|
const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
|
|
8
8
|
const LISTS_MANAGEMENT_QUERY_ID = '78UbkyXwXBD98IgUWXOy9g';
|
|
9
9
|
|
|
@@ -80,8 +80,8 @@ cli({
|
|
|
80
80
|
strategy: Strategy.UI,
|
|
81
81
|
browser: true,
|
|
82
82
|
args: [
|
|
83
|
-
{ name: 'listId', positional: true, type: 'string', required: true },
|
|
84
|
-
{ name: 'username', positional: true, type: 'string', required: true },
|
|
83
|
+
{ name: 'listId', positional: true, type: 'string', required: true, help: 'Numeric ID of the list you own (e.g. from `opencli twitter lists`)' },
|
|
84
|
+
{ name: 'username', positional: true, type: 'string', required: true, help: 'Twitter/X handle to remove (with or without @)' },
|
|
85
85
|
],
|
|
86
86
|
columns: ['listId', 'username', 'userId', 'status', 'message'],
|
|
87
87
|
func: async (page, kwargs) => {
|
|
@@ -92,16 +92,17 @@ 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);
|
|
103
104
|
const headers = JSON.stringify({
|
|
104
|
-
'Authorization': `Bearer ${decodeURIComponent(
|
|
105
|
+
'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
|
|
105
106
|
'X-Csrf-Token': ct0,
|
|
106
107
|
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
107
108
|
'X-Twitter-Active-User': 'yes',
|
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
2
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { TWITTER_BEARER_TOKEN, applyTopByEngagement } from './utils.js';
|
|
3
4
|
|
|
4
|
-
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
5
5
|
const LIST_TWEETS_QUERY_ID = 'RlZzktZY_9wJynoepm8ZsA';
|
|
6
6
|
const OPERATION_NAME = 'ListLatestTweetsTimeline';
|
|
7
7
|
|
|
@@ -112,9 +112,11 @@ cli({
|
|
|
112
112
|
domain: 'x.com',
|
|
113
113
|
strategy: Strategy.COOKIE,
|
|
114
114
|
browser: true,
|
|
115
|
+
browserSession: { reuse: 'site' },
|
|
115
116
|
args: [
|
|
116
|
-
{ name: 'listId', positional: true, type: 'string', required: true },
|
|
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 },
|
|
119
|
+
{ name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the list timeline 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 list\'s native (recency) ordering.' },
|
|
118
120
|
],
|
|
119
121
|
columns: ['id', 'author', 'text', 'likes', 'retweets', 'replies', 'created_at', 'url'],
|
|
120
122
|
func: async (page, kwargs) => {
|
|
@@ -123,11 +125,8 @@ cli({
|
|
|
123
125
|
throw new CommandExecutionError(`Invalid listId: ${JSON.stringify(kwargs.listId)}. Expected a numeric ID (see \`opencli twitter lists\`).`);
|
|
124
126
|
}
|
|
125
127
|
const limit = kwargs.limit || 50;
|
|
126
|
-
await page.
|
|
127
|
-
|
|
128
|
-
const ct0 = await page.evaluate(`() => {
|
|
129
|
-
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
130
|
-
}`);
|
|
128
|
+
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
129
|
+
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
|
|
131
130
|
if (!ct0)
|
|
132
131
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
133
132
|
const queryId = await page.evaluate(`async () => {
|
|
@@ -155,7 +154,7 @@ cli({
|
|
|
155
154
|
return null;
|
|
156
155
|
}`) || LIST_TWEETS_QUERY_ID;
|
|
157
156
|
const headers = JSON.stringify({
|
|
158
|
-
'Authorization': `Bearer ${decodeURIComponent(
|
|
157
|
+
'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
|
|
159
158
|
'X-Csrf-Token': ct0,
|
|
160
159
|
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
161
160
|
'X-Twitter-Active-User': 'yes',
|
|
@@ -181,6 +180,7 @@ cli({
|
|
|
181
180
|
break;
|
|
182
181
|
cursor = nextCursor;
|
|
183
182
|
}
|
|
184
|
-
|
|
183
|
+
const trimmed = allTweets.slice(0, limit);
|
|
184
|
+
return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);
|
|
185
185
|
},
|
|
186
186
|
});
|
package/clis/twitter/lists.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
2
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { TWITTER_BEARER_TOKEN } from './utils.js';
|
|
3
4
|
|
|
4
|
-
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
5
5
|
const LISTS_QUERY_ID = '78UbkyXwXBD98IgUWXOy9g';
|
|
6
6
|
const OPERATION_NAME = 'ListsManagementPageTimeline';
|
|
7
7
|
|
|
@@ -92,17 +92,15 @@ export const command = cli({
|
|
|
92
92
|
domain: 'x.com',
|
|
93
93
|
strategy: Strategy.COOKIE,
|
|
94
94
|
browser: true,
|
|
95
|
+
browserSession: { reuse: 'site' },
|
|
95
96
|
args: [
|
|
96
|
-
{ name: 'limit', type: 'int', default: 50 },
|
|
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 () => {
|
|
@@ -130,7 +128,7 @@ export const command = cli({
|
|
|
130
128
|
return null;
|
|
131
129
|
}`) || LISTS_QUERY_ID;
|
|
132
130
|
const headers = JSON.stringify({
|
|
133
|
-
'Authorization': `Bearer ${decodeURIComponent(
|
|
131
|
+
'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
|
|
134
132
|
'X-Csrf-Token': ct0,
|
|
135
133
|
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
136
134
|
'X-Twitter-Active-User': 'yes',
|
|
@@ -4,12 +4,13 @@ cli({
|
|
|
4
4
|
site: 'twitter',
|
|
5
5
|
name: 'notifications',
|
|
6
6
|
access: 'read',
|
|
7
|
-
description: 'Get Twitter/X notifications',
|
|
7
|
+
description: 'Get your Twitter/X notifications (the logged-in user\'s likes/replies/follows feed, newest first)',
|
|
8
8
|
domain: 'x.com',
|
|
9
9
|
strategy: Strategy.INTERCEPT,
|
|
10
10
|
browser: true,
|
|
11
|
+
browserSession: { reuse: 'site' },
|
|
11
12
|
args: [
|
|
12
|
-
{ name: 'limit', type: 'int', default: 20 },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of notifications to return (default 20).' },
|
|
13
14
|
],
|
|
14
15
|
columns: ['id', 'action', 'author', 'text', 'url'],
|
|
15
16
|
func: async (page, kwargs) => {
|
package/clis/twitter/profile.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
3
|
import { resolveTwitterQueryId } from './shared.js';
|
|
4
|
+
import { TWITTER_BEARER_TOKEN } from './utils.js';
|
|
4
5
|
const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
|
|
5
6
|
cli({
|
|
6
7
|
site: 'twitter',
|
|
7
8
|
name: 'profile',
|
|
8
9
|
access: 'read',
|
|
9
|
-
description: 'Fetch a Twitter user profile
|
|
10
|
+
description: 'Fetch a Twitter user profile — bio, stats, etc. (defaults to the logged-in user when no username is given)',
|
|
10
11
|
domain: 'x.com',
|
|
11
12
|
strategy: Strategy.COOKIE,
|
|
12
13
|
browser: true,
|
|
14
|
+
browserSession: { reuse: 'site' },
|
|
13
15
|
args: [
|
|
14
|
-
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (without @). Defaults to logged-in user.' },
|
|
16
|
+
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
15
17
|
],
|
|
16
18
|
columns: ['screen_name', 'name', 'bio', 'location', 'url', 'followers', 'following', 'tweets', 'likes', 'verified', 'created_at'],
|
|
17
19
|
func: async (page, kwargs) => {
|
|
@@ -31,14 +33,18 @@ cli({
|
|
|
31
33
|
// Navigate directly to the user's profile page (gives us cookie context)
|
|
32
34
|
await page.goto(`https://x.com/${username}`);
|
|
33
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)');
|
|
34
41
|
const queryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
35
42
|
const result = await page.evaluate(`
|
|
36
43
|
async () => {
|
|
37
44
|
const screenName = "${username}";
|
|
38
|
-
const ct0 =
|
|
39
|
-
if (!ct0) return {error: 'No ct0 cookie — not logged into x.com'};
|
|
45
|
+
const ct0 = ${JSON.stringify(ct0)};
|
|
40
46
|
|
|
41
|
-
const bearer =
|
|
47
|
+
const bearer = ${JSON.stringify(TWITTER_BEARER_TOKEN)};
|
|
42
48
|
const headers = {
|
|
43
49
|
'Authorization': 'Bearer ' + decodeURIComponent(bearer),
|
|
44
50
|
'X-Csrf-Token': ct0,
|
|
@@ -95,8 +101,6 @@ cli({
|
|
|
95
101
|
}
|
|
96
102
|
`);
|
|
97
103
|
if (result?.error) {
|
|
98
|
-
if (String(result.error).includes('No ct0 cookie'))
|
|
99
|
-
throw new AuthRequiredError('x.com', result.error);
|
|
100
104
|
throw new CommandExecutionError(result.error + (result.hint ? ` (${result.hint})` : ''));
|
|
101
105
|
}
|
|
102
106
|
return result || [];
|
package/clis/twitter/quote.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
1
2
|
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
-
import { parseTweetUrl } from './shared.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { parseTweetUrl, buildTwitterArticleScopeSource } from './shared.js';
|
|
5
|
+
import {
|
|
6
|
+
COMPOSER_FILE_INPUT_SELECTOR,
|
|
7
|
+
attachComposerImage,
|
|
8
|
+
downloadRemoteImage,
|
|
9
|
+
resolveImagePath,
|
|
10
|
+
} from './utils.js';
|
|
8
11
|
|
|
9
12
|
function buildQuoteComposerUrl(url) {
|
|
10
13
|
// Twitter/X quote-tweet compose URL: the `url` param attaches the source
|
|
@@ -17,15 +20,8 @@ function buildQuoteComposerUrl(url) {
|
|
|
17
20
|
async function submitQuote(page, text, tweetId) {
|
|
18
21
|
return page.evaluate(`(async () => {
|
|
19
22
|
try {
|
|
23
|
+
${buildTwitterArticleScopeSource(tweetId)}
|
|
20
24
|
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
|
|
21
|
-
const getStatusId = (href) => {
|
|
22
|
-
try {
|
|
23
|
-
const match = new URL(href, window.location.origin).pathname.match(/^\\/(?:[^/]+|i)\\/status\\/(\\d+)\\/?$/);
|
|
24
|
-
return match?.[1] || null;
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
25
|
const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
|
|
30
26
|
const box = boxes.find(visible) || boxes[0];
|
|
31
27
|
if (!box) {
|
|
@@ -34,7 +30,6 @@ async function submitQuote(page, text, tweetId) {
|
|
|
34
30
|
|
|
35
31
|
box.focus();
|
|
36
32
|
const textToInsert = ${JSON.stringify(text)};
|
|
37
|
-
const tweetId = ${JSON.stringify(tweetId)};
|
|
38
33
|
// execCommand('insertText') is more reliable with Twitter's Draft.js editor.
|
|
39
34
|
if (!document.execCommand('insertText', false, textToInsert)) {
|
|
40
35
|
// Fallback to paste event if execCommand fails.
|
|
@@ -49,13 +44,16 @@ async function submitQuote(page, text, tweetId) {
|
|
|
49
44
|
|
|
50
45
|
await new Promise(r => setTimeout(r, 1000));
|
|
51
46
|
|
|
52
|
-
// Confirm the quoted card is rendered before submitting; otherwise
|
|
53
|
-
// accidentally post a plain tweet without the quote
|
|
47
|
+
// Confirm the quoted card is rendered before submitting; otherwise
|
|
48
|
+
// we may accidentally post a plain tweet without the quote
|
|
49
|
+
// attachment. The compose page does not wrap the card in an
|
|
50
|
+
// <article>, so we probe the document for any link whose path
|
|
51
|
+
// exactly matches the requested status id (uses __twHasLinkToTarget
|
|
52
|
+
// from buildTwitterArticleScopeSource).
|
|
54
53
|
let cardAttempts = 0;
|
|
55
54
|
let hasQuoteCard = false;
|
|
56
55
|
while (cardAttempts < 20) {
|
|
57
|
-
hasQuoteCard =
|
|
58
|
-
.some((link) => getStatusId(link.href) === tweetId);
|
|
56
|
+
hasQuoteCard = __twHasLinkToTarget(document);
|
|
59
57
|
if (hasQuoteCard) break;
|
|
60
58
|
await new Promise(r => setTimeout(r, 250));
|
|
61
59
|
cardAttempts++;
|
|
@@ -102,38 +100,68 @@ cli({
|
|
|
102
100
|
site: 'twitter',
|
|
103
101
|
name: 'quote',
|
|
104
102
|
access: 'write',
|
|
105
|
-
description: 'Quote-tweet a specific tweet with your own text',
|
|
103
|
+
description: 'Quote-tweet a specific tweet with your own text, optionally with a local or remote image',
|
|
106
104
|
domain: 'x.com',
|
|
107
105
|
strategy: Strategy.UI,
|
|
108
106
|
browser: true,
|
|
109
107
|
args: [
|
|
110
108
|
{ name: 'url', type: 'string', required: true, positional: true, help: 'The URL of the tweet to quote' },
|
|
111
109
|
{ name: 'text', type: 'string', required: true, positional: true, help: 'The text content of your quote' },
|
|
110
|
+
{ name: 'image', help: 'Optional local image path to attach to the quote tweet' },
|
|
111
|
+
{ name: 'image-url', help: 'Optional remote image URL to download and attach to the quote tweet' },
|
|
112
112
|
],
|
|
113
113
|
columns: ['status', 'message', 'text'],
|
|
114
114
|
func: async (page, kwargs) => {
|
|
115
115
|
if (!page)
|
|
116
116
|
throw new CommandExecutionError('Browser session required for twitter quote');
|
|
117
|
+
if (kwargs.image && kwargs['image-url']) {
|
|
118
|
+
throw new CommandExecutionError('Use either --image or --image-url, not both.');
|
|
119
|
+
}
|
|
117
120
|
|
|
118
|
-
//
|
|
121
|
+
// Validate URL (typed ArgumentError on malformed/off-domain inputs)
|
|
122
|
+
// before any browser interaction or remote image download.
|
|
119
123
|
const target = parseTweetUrl(kwargs.url);
|
|
120
|
-
await page.goto(`https://x.com/compose/post?url=${encodeURIComponent(target.url)}`, { waitUntil: 'load', settleMs: 2500 });
|
|
121
|
-
await page.wait({ selector: '[data-testid="tweetTextarea_0"]', timeout: 15 });
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
let localImagePath;
|
|
126
|
+
let cleanupDir;
|
|
127
|
+
try {
|
|
128
|
+
if (kwargs.image) {
|
|
129
|
+
localImagePath = resolveImagePath(kwargs.image);
|
|
130
|
+
} else if (kwargs['image-url']) {
|
|
131
|
+
const downloaded = await downloadRemoteImage(kwargs['image-url']);
|
|
132
|
+
localImagePath = downloaded.absPath;
|
|
133
|
+
cleanupDir = downloaded.cleanupDir;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Dedicated composer is more reliable than the inline quote-tweet button.
|
|
137
|
+
await page.goto(`https://x.com/compose/post?url=${encodeURIComponent(target.url)}`, { waitUntil: 'load', settleMs: 2500 });
|
|
138
|
+
await page.wait({ selector: '[data-testid="tweetTextarea_0"]', timeout: 15 });
|
|
139
|
+
|
|
140
|
+
if (localImagePath) {
|
|
141
|
+
await page.wait({ selector: COMPOSER_FILE_INPUT_SELECTOR, timeout: 20 });
|
|
142
|
+
await attachComposerImage(page, localImagePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await submitQuote(page, kwargs.text, target.id);
|
|
146
|
+
if (result.ok) {
|
|
147
|
+
// Wait for network submission to complete
|
|
148
|
+
await page.wait(3);
|
|
149
|
+
}
|
|
150
|
+
return [{
|
|
151
|
+
status: result.ok ? 'success' : 'failed',
|
|
152
|
+
message: result.message,
|
|
153
|
+
text: kwargs.text,
|
|
154
|
+
...(kwargs.image ? { image: kwargs.image } : {}),
|
|
155
|
+
...(kwargs['image-url'] ? { 'image-url': kwargs['image-url'] } : {}),
|
|
156
|
+
}];
|
|
157
|
+
} finally {
|
|
158
|
+
if (cleanupDir) {
|
|
159
|
+
fs.rmSync(cleanupDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
127
161
|
}
|
|
128
|
-
return [{
|
|
129
|
-
status: result.ok ? 'success' : 'failed',
|
|
130
|
-
message: result.message,
|
|
131
|
-
text: kwargs.text,
|
|
132
|
-
}];
|
|
133
162
|
}
|
|
134
163
|
});
|
|
135
164
|
|
|
136
165
|
export const __test__ = {
|
|
137
166
|
buildQuoteComposerUrl,
|
|
138
|
-
extractTweetId,
|
|
139
167
|
};
|