feat: anti-detection hardening, transaction cache, article parsing, structured write output
Anti-detection:
- Add 6 sec-ch-ua-* Client Hints headers (arch, bitness, full-version, etc.)
- POST requests now send Referer: x.com/compose/post + Priority: u=1, i
- follow/unfollow REST adds include_profile_interstitial_type param
Performance:
- Transaction ID cache with 1h TTL (~/.twitter-cli/transaction_cache.json)
- resolve_user_id: auto-detect screen_name vs numeric user_id
Features:
- Twitter Article parsing: extract long-form content as Markdown
- Write operations emit structured JSON/YAML when piped or OUTPUT env set
ActionResult: {success, action, id, url, ...}
84 tests passing
This commit is contained in:
@@ -44,6 +44,10 @@ def tweet_to_dict(tweet: Tweet) -> Dict[str, Any]:
|
||||
"lang": tweet.lang,
|
||||
"score": tweet.score,
|
||||
}
|
||||
if tweet.article_title is not None:
|
||||
data["articleTitle"] = tweet.article_title
|
||||
if tweet.article_text is not None:
|
||||
data["articleText"] = tweet.article_text
|
||||
if tweet.quoted_tweet:
|
||||
data["quotedTweet"] = {
|
||||
"id": tweet.quoted_tweet.id,
|
||||
@@ -113,6 +117,8 @@ def tweet_from_dict(data: Dict[str, Any]) -> Tweet:
|
||||
retweeted_by=_optional_str(data.get("retweetedBy")),
|
||||
quoted_tweet=quoted_tweet,
|
||||
score=float(data["score"]) if data.get("score") is not None else None,
|
||||
article_title=_optional_str(data.get("articleTitle")),
|
||||
article_text=_optional_str(data.get("articleText")),
|
||||
)
|
||||
|
||||
|
||||
@@ -129,6 +135,11 @@ def tweets_to_json(tweets: Iterable[Tweet]) -> str:
|
||||
return json.dumps([tweet_to_dict(tweet) for tweet in tweets], ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def tweets_to_data(tweets: Iterable[Tweet]) -> List[Dict[str, Any]]:
|
||||
"""Serialize Tweet objects to Python dicts."""
|
||||
return [tweet_to_dict(tweet) for tweet in tweets]
|
||||
|
||||
|
||||
def tweet_to_compact_dict(tweet: Tweet) -> Dict[str, Any]:
|
||||
"""Convert a Tweet into a compact dict with minimal fields for LLM consumption."""
|
||||
text = tweet.text.replace("\n", " ").strip()
|
||||
@@ -187,6 +198,11 @@ def users_to_json(users: Iterable[UserProfile]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def users_to_data(users: Iterable[UserProfile]) -> List[Dict[str, Any]]:
|
||||
"""Serialize UserProfile objects to Python dicts."""
|
||||
return [user_profile_to_dict(user) for user in users]
|
||||
|
||||
|
||||
def _optional_int(value: Any) -> Optional[int]:
|
||||
"""Parse an optional integer value."""
|
||||
if value is None:
|
||||
|
||||
Reference in New Issue
Block a user