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:
@@ -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
2
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user