Conversation
…write actions Shared helper `buildTwitterArticleScopeSource(tweetId)` emits the canonical exact-match scoping bindings (tweetId, __twTweetPathRe, __twGetStatusIdFromHref, __twHasLinkToTarget, findTargetArticle) used by every write-action IIFE. Why --- PR #1400 (P0) review caught two silent failure classes that were previously hand-rolled in each adapter: 1. Substring match on tweet id: `pathname.includes('/status/' + tweetId)` would silently match `/status/1234567` when the requested id was `123`. 2. Loose article match: bare `[data-testid="like"]` on conversation pages silently grabs the first article (parent tweet), liking the wrong tweet. Five sibling adapters (like, bookmark, unbookmark, hide-reply, delete) had inherited these patterns from delete.js. Four newly-merged P0 adapters (unlike, retweet, unretweet, quote) inlined a fresh copy of the corrected helper. Centralising the helper eliminates drift risk and makes the exact-match guarantee a single review surface. What changed ------------ - shared.js: export `buildTwitterArticleScopeSource(tweetId)` returning a string fragment for embedding inside `page.evaluate(...)` IIFEs. - shared.test.js: add 7 JSDOM-based tests proving the regex anchors reject substring (`123` vs `1234567`) and path-suffix (`/photo/1`) attacks, and that `__twHasLinkToTarget` works for the quote-card use case (no <article> ancestor). - like.js, bookmark.js, unbookmark.js, hide-reply.js, delete.js, unlike.js, retweet.js, unretweet.js, quote.js: refactored to use the shared helper. - delete.js: also drops the local `extractTweetId` (which accepted any host) in favour of `parseTweetUrl` from shared.js (typed ArgumentError). - like.test.js, bookmark.test.js, unbookmark.test.js, hide-reply.test.js: new test files (these adapters had no tests before). - delete.test.js, unlike.test.js, retweet.test.js, unretweet.test.js, quote.test.js: updated assertions to verify the helper is embedded. Test ---- All 124 twitter tests pass (was 92 before this change, +32 from the four new test files and shared.js JSDOM coverage). typed-error-lint: 189 (unchanged from baseline). silent-column-drop: 103 (unchanged from baseline).
…s.js, add quote --image
P4 of WAWQAQ's twitter-cli enrichment series — surgical dedupe of code that
sibling adapters were already copy-pasting verbatim, plus a single feature add
(quote-tweet image attach).
Why scope is narrow:
The composer-text-insertion / submit-and-verify flows differ between post,
reply, and quote in real ways (different toast patterns, post-submit
verifications, quote has the additional quoted-card-rendered guard). Per
WAWQAQ's "no over-engineering" feedback, only true verbatim duplication is
extracted. FEATURES dicts differ per GraphQL endpoint and stay per-file.
What moves into clis/twitter/utils.js
- TWITTER_BEARER_TOKEN — was duplicated across 12 GraphQL adapters (article,
bookmarks, following, likes, list-add, list-remove, list-tweets, lists,
profile, thread, timeline, tweets). All 12 now import the single source.
- COMPOSER_FILE_INPUT_SELECTOR / SUPPORTED_IMAGE_EXTENSIONS / MAX_IMAGE_SIZE_BYTES
constants used by the /compose/post route.
- resolveImagePath / resolveImageExtension — already-validated path resolution
with typed ArgumentError on bad input.
- downloadRemoteImage — now returns { absPath, cleanupDir } so the caller can
rmSync the per-call tmp dir in finally. (Was an opaque path before; reply.js
cleanup path is the same, just explicit.)
- attachComposerImage — extracted from reply.js attachReplyImage. Native CDP
setFileInput first; falls back to base64 DataTransfer shim; polls preview
thumbnail to confirm upload actually rendered (without this, a 200 from
setFileInput could mask silent-no-attachment).
reply.js hardening (security: replaces loose substring match)
- buildReplyComposerUrl now uses parseTweetUrl(rawUrl).id from shared.js
instead of a local /\\/status\\/(\\d+)/ that would (a) match substrings like
/status/123 in a longer id and (b) accept any host.
- The combined --image + --image-url throw is now a typed CommandExecutionError
instead of a generic Error.
quote.js feature add
- New --image and --image-url flags (matches reply / post symmetry).
- Same upfront validation: parseTweetUrl runs before any browser interaction;
--image and --image-url are mutually exclusive (CommandExecutionError).
- Reuses the same attachComposerImage helper; cleanup tmp dir in finally.
Tests
- reply.test.js: ports image-helper tests to import from utils.js __test__,
drops the removed extractTweetId test, replaces it with parseTweetUrl-driven
buildReplyComposerUrl tests that prove the security tightening.
- quote.test.js: adds --image / --image-url upload coverage matching reply's
shape (mocks setFileInput, fetch, asserts goto / wait / row shape).
- All 22 twitter test files / 128 tests pass.
Audits
- typed-error-lint: 189 → 188 (resolved one silent-sentinel: reply.js no
longer has the throw new Error('Unsupported remote image format ...')
branch, since resolveImageExtension lives in utils.js as ArgumentError).
Baseline shrunk by one entry.
- silent-column-drop / listing-id-pairing: unchanged.
Out of scope (intentionally)
- post.js / post-thread.js do not yet share the composer helpers — they have
multi-tweet thread orchestration and a different set of preview-confirmation
selectors. Folding them in would be a separate, larger change.
…t help Triggered by WAWQAQ DM (msg=3e88a6bb): "推特的有些 help 文档是不对的, 譬如说没有说明当后面的 opencli followers [user] [options] 如果都没填的时候, 获取的是什么?" The runtime fallback for omitted positionals was correct in code, but the help text was silent about it. Now both the description (visible in top-level `--help` output) and the per-arg help (visible in command help) state what happens when the positional is omitted. Optional-positional defaults — now documented: - twitter/followers <user> → defaults to logged-in user - twitter/following <user> → defaults to logged-in user - twitter/likes <username> → defaults to logged-in user (was already documented at arg level; description now mirrors it) - twitter/profile <username> → defaults to logged-in user (same) download required-but-mutually-exclusive — clarified: - twitter/download requires <username> OR --tweet-url (the runtime return-row error message already said this; description and per-arg help now say it upfront). Self-data commands (no positional, but description was vague about whose data): - twitter/bookmarks → "Fetch your Twitter/X bookmarks (the logged-in user's saved tweets, newest first)" - twitter/notifications → "Get your Twitter/X notifications (the logged-in user's likes/replies/follows feed, newest first)" - twitter/timeline → "Fetch the logged-in user's home timeline (for-you algorithmic feed by default; pass --type following for the chronological feed of accounts you follow)" Also flesh out --limit help across the touched adapters so each shows its default and unit (was empty string in the manifest): - followers / following / likes / lists / bookmarks / notifications / timeline / download / + the timeline --type help. cli-manifest.json regenerated via `npm run build-manifest` (no schema change, only description / args[].help string updates). Audits: typed-error-lint 188 → 188, silent-column-drop 103 → 103, all 22 twitter test files / 128 tests pass.
…er flags
Adds four filter flags that map onto X's public search-operator surface so
callers don't have to memorise the operator strings:
--from <user> appends `from:<user>` (leading @ is stripped)
--has <kind> appends `filter:<media|images|videos|links|replies>`
--exclude <kind> appends `-filter:<replies|retweets|media|links>`
(retweets is an alias for X's `-filter:nativeretweets`)
--product <tab> picks the X result tab (top|live|photos|videos)
via the f= URL param. photos/videos use the singular
form Twitter expects (image/video).
Backwards compatible: the legacy `--filter top|live` flag is kept and still
works unchanged. When both `--product` and `--filter` are set, --product wins.
ArgumentError is thrown when the composed query is empty (i.e. <query> blank
and no filters supplied).
Implementation:
- buildSearchQuery() and resolveSearchFParam() are pure helpers exported via
__test__ for unit coverage. 21 new helper tests + 3 new e2e tests cover:
query composition order, @ stripping, retweets→nativeretweets aliasing,
--product winning over --filter, and ArgumentError on empty query.
- After the search-input fallback path lands on /search the URL has no f=
param, so clickProductTabIfNeeded() clicks the matching tab (Latest /
Photos / Videos with i18n labels) so the SearchTimeline call reads the
right product. No-op for top.
35 search tests pass (was 6); full twitter suite 157/157 green.
silent-column-drop and typed-error-lint baselines unchanged.
…ommands
Adds a `--top-by-engagement N` flag to twitter/{search, timeline, likes,
bookmarks, list-tweets, tweets, thread}. When N>0 the rows are re-ranked
by weighted engagement and trimmed to the top N; when 0 (the default) the
adapter behaves exactly as before so existing callers see no change.
Formula (single source of truth in clis/twitter/utils.js):
score = likes×1
+ retweets×3
+ replies×2
+ bookmarks×5
+ log10(views + 1) × 0.5
Rationale:
- Active engagement (bookmarks → retweets → replies → likes) is weighted
above passive (views). Bookmarks are the strongest signal because they
cost real intent — only 1–2% of viewers bookmark.
- Views are log-dampened because viral tweets routinely have 4 orders of
magnitude more views than likes/retweets, which would otherwise drown
out the active signals entirely.
- log10(views + 1) so views=0 maps to 0 (not -Infinity).
- Missing fields coerce to 0 — search returns no replies/bookmarks,
bookmarks returns no views, etc. The formula stays well-defined across
every read command's row shape without per-adapter special cases.
Implementation:
- Two pure helpers in utils.js — computeEngagementScore(row) and
applyTopByEngagement(rows, topN). Both exported via __test__.
- applyTopByEngagement() is a no-op when topN <= 0 / non-numeric, so
every existing test that doesn't pass the flag still gets the original
array reference (and ordering) back unchanged.
- Sort is stable for ties: rows with equal scores keep their original
order (Array.prototype.sort is guaranteed stable in V8 since 2018).
24 new helper tests cover: per-signal weights, log-dampening, string
coercion, NaN guard, negative-value clamp, real search/bookmarks row
shapes, no-op semantics for default flag, stable tie-break, and the
mixed-signal case where a bookmark-heavy row beats a like-heavy row.
Full twitter suite: 181/181 (was 157/157, +24 utils tests).
silent-column-drop and typed-error-lint baselines unchanged.
X has supported user-created bookmark folders (Premium feature) since
2023. The web UI exposes them under the Bookmarks page; we previously
had no way to read them from the CLI. Adds two commands that match the
shape of lists / list-tweets:
bookmark-folders List your bookmark folders. Returns
id, name, item count, created_at.
bookmark-folder <folder-id> List the tweets inside one folder.
Same row shape as bookmarks (id, author,
text, likes, retweets, bookmarks,
created_at, url) plus --limit and the
new --top-by-engagement N flag.
GraphQL plumbing:
- bookmarkFoldersSlice op for the folder list (FALLBACK_QUERY_ID
i78YDd0Tza-dWKw5H2Y7WA).
- BookmarkFolderTimeline op for folder contents (FALLBACK_QUERY_ID
13H7EUATwethsj_jZ6QQAQ), variables.bookmark_collection_id scopes
the existing bookmark_timeline_v2 envelope to one folder.
- Both adapters resolve queryId dynamically: try fa0311/twitter-openapi
placeholder.json first → scrape client-web bundles second → fall back
to the pinned constants. Same pattern as bookmarks.js / lists.js.
Parser robustness:
- parseBookmarkFolders walks 3 envelope shapes (viewer.bookmark_collections_slice,
viewer_v2.user_results.result.bookmark_collections_slice, flat
bookmark_collections_slice) and 4 item shapes (bookmarkCollection,
content.bookmarkCollection, content.itemContent.bookmark_collection,
flat). Tested.
- parseBookmarkFolderTimeline walks 3 envelope shapes
(bookmark_collection_timeline, bookmark_timeline_v2, bookmark_timeline)
and the standard tweet result shape with note_tweet long-form fallback.
- bookmark-folder validates folder-id with /^\d+$/ via ArgumentError before
any browser interaction (typed fail-fast).
20 new helper tests cover envelope variants, item dedup, missing fields,
ArgumentError on non-numeric folder-id, AuthRequiredError on missing
ct0 cookie, and URL builder shape (variables, cursor, includePromotedContent).
Test counts: full twitter suite 201/201 (was 181, +20).
Manifest 799 → 801 (+2 commands).
Baseline note: bookmark-folder.js inherits two pre-existing patterns
from sibling adapters (silent-clamp on the over-fetch Math.min and
silent-sentinel 'unknown' for missing screen_name). Both patterns are
already in the baseline for 7+ sibling adapters (bookmarks/likes/
list-tweets/search/timeline/tweets/thread). This commit adds 2 entries
to typed-error-lint-baseline.json for parity. The line-number drift on
5 other entries is automatic refresh from --update-baseline picking up
imports added in P3/P4 commits earlier on this branch.
7f93779 to
2a29ecc
Compare
2a29ecc to
df4dcd7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1400 (P0 write-action symmetry). Combines P1+P2+P3+P4+P5 plus a help-doc pass into one PR per WAWQAQ direction (msg=02d700c6 / msg=882c5ad9).
Scope (6 commits, all on this branch)
buildTwitterArticleScopeSourceTWITTER_BEARER_TOKEN+ composer image helpers inclis/twitter/utils.js(12 GraphQL adapters dedup'd, reply hardened, quote gains--image)followers/following/profile/likes, expanddownload/bookmarks/notifications/timeline/listsdescriptionstwitter searchadds--from / --has / --exclude / --productfilter flags mapped onto X's search operators (from:/filter:/-filter:/f=URL param)--top-by-engagement Non all 7 tweet-shaped read commands. Weighted formulalikes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5, single-source helper inutils.js, no-op when N=0twitter bookmark-folders(list folders) +twitter bookmark-folder <id>(read folder contents). Premium-only X feature, GraphQL plumbing with dynamic queryId resolutionWhy this layout
WAWQAQ asked to compare with public-clis/twitter-cli (msg=c497cd99). Five gaps surfaced:
P5 (sibling hardening) was also packaged here per the same direction.
Tests / audits / manifest
silent-column-droptyped-error-lintBehavioural compatibility
--top-by-engagement,--from,--has,--exclude,--product) defaults to off / no-op so existing callers see no change.--filter top|live(legacy search) is preserved;--productwins when both set, documented in help.description/helpstrings — no behaviour change, no schema change.applyTopByEngagement(rows, 0)returns the same array reference, so 157 pre-existing twitter tests still pass identical assertions without any churn.Notes for the reviewer
Math.min(100, limit-…+10)over-fetch and silent-sentinel'unknown'for missingscreen_name). Already grandfathered in baseline; refactoring those would diverge from sibling parity. Baseline has 2 new entries forbookmark-folder.jsmatching the sibling pattern verbatim.--update-baselineline-drift on 5 other entries is automatic refresh — P3/P4 added one import line at the top of those files which shifted line numbers by 1, and search.js drifted further from P1 helper code at the top.__test__exports follow the existing twitter convention (no__test__index file, each adapter exports its own helper namespace).--imageis parity-only with reply/post/dm--image;--image-urlfor quote was deferred per "no over-engineering" — single-image quote was the highest-value gap, batch / video can come later if asked.