from __future__ import annotations import asyncio import logging import tweepy from yuxi.channel.extensions.twitter.auth import create_tweepy_client from yuxi.channel.extensions.twitter.errors import MAX_RETRIES logger = logging.getLogger(__name__) TWEET_TEXT_LIMIT = 280 class TwitterTweets: @staticmethod async def create_tweet( account: dict, text: str, *, media_ids: list[str] | None = None, in_reply_to_tweet_id: str | None = None, quote_tweet_id: str | None = None, ) -> dict | None: client = create_tweepy_client(account) for attempt in range(MAX_RETRIES): try: payload: dict = {"text": text[:TWEET_TEXT_LIMIT]} if media_ids: payload["media"] = {"media_ids": [str(mid) for mid in media_ids]} if in_reply_to_tweet_id: payload["reply"] = {"in_reply_to_tweet_id": in_reply_to_tweet_id} if quote_tweet_id: payload["quote_tweet_id"] = quote_tweet_id response = await asyncio.to_thread(client.create_tweet, **payload) return response.get("data", {}) except tweepy.TooManyRequests: await asyncio.sleep(min(2**attempt * 60, 300)) continue except Exception as e: logger.warning( "Twitter create_tweet error (attempt %d): %s", attempt + 1, e ) if attempt >= MAX_RETRIES - 1: return None await asyncio.sleep(min(2**attempt, 10)) return None @staticmethod async def get_tweet(account: dict, tweet_id: str) -> dict | None: client = create_tweepy_client(account) try: response = await asyncio.to_thread( client.get_tweet, id=tweet_id, tweet_fields=["text", "created_at", "author_id", "public_metrics"], expansions=["author_id", "attachments.media_key"], ) return response except Exception as e: logger.warning("Twitter get_tweet error: %s", e) return None @staticmethod async def delete_tweet(account: dict, tweet_id: str) -> bool: client = create_tweepy_client(account) try: await asyncio.to_thread(client.delete_tweet, id=tweet_id) return True except Exception as e: logger.warning("Twitter delete_tweet error: %s", e) return False @staticmethod async def search_recent_tweets( account: dict, query: str, *, max_results: int = 10, ) -> list[dict] | None: client = create_tweepy_client(account) try: response = await asyncio.to_thread( client.search_recent_tweets, query=query, max_results=max_results, tweet_fields=["text", "created_at", "author_id", "public_metrics"], expansions=["author_id"], ) return response.get("data", []) except Exception as e: logger.warning("Twitter search_recent_tweets error: %s", e) return None @staticmethod async def get_user_timeline( account: dict, user_id: str, *, max_results: int = 10, ) -> list[dict] | None: client = create_tweepy_client(account) try: response = await asyncio.to_thread( client.get_users_tweets, id=user_id, max_results=max_results, tweet_fields=["text", "created_at", "author_id", "public_metrics"], expansions=["author_id", "attachments.media_key"], ) return response.get("data", []) except Exception as e: logger.warning("Twitter get_user_timeline error: %s", e) return None @staticmethod async def hide_reply(account: dict, tweet_id: str, hidden: bool = True) -> bool: client = create_tweepy_client(account) try: await asyncio.to_thread(client.hide_reply, id=tweet_id, hidden=hidden) return True except Exception as e: logger.warning("Twitter hide_reply error: %s", e) return False @staticmethod async def create_tweet_with_poll( account: dict, text: str, options: list[str], *, duration_minutes: int = 60, ) -> dict | None: client = create_tweepy_client(account) poll_payload = {"options": options, "duration_minutes": duration_minutes} for attempt in range(MAX_RETRIES): try: response = await asyncio.to_thread( client.create_tweet, text=text[:TWEET_TEXT_LIMIT], poll=poll_payload, ) return response.get("data", {}) except tweepy.TooManyRequests: await asyncio.sleep(min(2**attempt * 60, 300)) continue except Exception as e: logger.warning( "Twitter create_tweet_with_poll error (attempt %d): %s", attempt + 1, e, ) if attempt >= MAX_RETRIES - 1: return None await asyncio.sleep(min(2**attempt, 10)) return None @staticmethod async def get_tweet_counts( account: dict, query: str, ) -> dict | None: client = create_tweepy_client(account) try: response = await asyncio.to_thread( client.get_recent_tweets_count, query=query, ) return response except Exception as e: logger.warning("Twitter get_tweet_counts error: %s", e) return None