fix: handle changed /account/multi/list.json response format

Twitter changed the response format from a list with nested 'user'
objects to {"users": [{user_id, name, screen_name, ...}]} with
minimal fields. Now extracts screen_name from the new format and
fetches the full profile via GraphQL UserByScreenName endpoint.
This commit is contained in:
jackwener
2026-03-11 00:40:07 +08:00
parent 8505428264
commit 5c1015f1fd
2 changed files with 47 additions and 18 deletions

View File

@@ -418,16 +418,38 @@ class TwitterClient:
def fetch_me(self): def fetch_me(self):
# type: () -> UserProfile # type: () -> UserProfile
"""Fetch the currently authenticated user's profile.""" """Fetch the currently authenticated user's profile.
Twitter's /account/multi/list.json endpoint changed its response format:
- Old: list of dicts with nested "user" objects (rich fields)
- New: {"users": [...]} with minimal fields (user_id, name, screen_name)
When the response only has minimal fields, we use the screen_name to
fetch the full profile via the GraphQL UserByScreenName endpoint.
"""
url = "https://x.com/i/api/1.1/account/multi/list.json" url = "https://x.com/i/api/1.1/account/multi/list.json"
data = self._api_get(url) data = self._api_get(url)
if isinstance(data, list) and data:
screen_name = None
# New format: {"users": [{"user_id": ..., "screen_name": ..., ...}]}
if isinstance(data, dict) and "users" in data:
users = data["users"]
if isinstance(users, list) and users:
user_data = users[0]
screen_name = user_data.get("screen_name")
# Old format: [{"user": {"id_str": ..., ...}}]
elif isinstance(data, list) and data:
user_data = data[0].get("user", {}) user_data = data[0].get("user", {})
if user_data: if user_data:
# Old format had rich fields — try to build profile directly
sn = user_data.get("screen_name", "")
if user_data.get("followers_count") is not None:
return UserProfile( return UserProfile(
id=str(user_data.get("id_str", "")), id=str(user_data.get("id_str", "")),
name=user_data.get("name", ""), name=user_data.get("name", ""),
screen_name=user_data.get("screen_name", ""), screen_name=sn,
bio=user_data.get("description", ""), bio=user_data.get("description", ""),
location=user_data.get("location", ""), location=user_data.get("location", ""),
url=_deep_get(user_data, "entities", "url", "urls", 0, "expanded_url") or "", url=_deep_get(user_data, "entities", "url", "urls", 0, "expanded_url") or "",
@@ -439,6 +461,13 @@ class TwitterClient:
profile_image_url=user_data.get("profile_image_url_https", ""), profile_image_url=user_data.get("profile_image_url_https", ""),
created_at=user_data.get("created_at", ""), created_at=user_data.get("created_at", ""),
) )
screen_name = sn
# Use screen_name to fetch full profile via GraphQL
if screen_name:
logger.info("Fetching full profile for @%s via GraphQL", screen_name)
return self.fetch_user(screen_name)
raise TwitterAPIError(0, "Failed to fetch current user info") raise TwitterAPIError(0, "Failed to fetch current user info")
def quote_tweet(self, tweet_id, text): def quote_tweet(self, tweet_id, text):

2
uv.lock generated
View File

@@ -1010,7 +1010,7 @@ wheels = [
[[package]] [[package]]
name = "twitter-cli" name = "twitter-cli"
version = "0.4.6" version = "0.5.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },