ForcePilot/backend/package/yuxi/channel/extensions/twitter/tweets.py
Kris 1c590097be feat(channel): 添加 Twitter 和 Viber 渠道扩展
新增 Twitter 和 Viber 两个渠道扩展。

Twitter 渠道扩展功能模块:
- auth: OAuth 认证管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- tweets: 推文管理
- social: 社交互动
- reactions: 表情反应
- media: 媒体资源处理

Viber 渠道扩展功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- rate_limiter: 速率限制
- media: 媒体资源处理
2026-05-21 11:57:22 +08:00

176 lines
5.9 KiB
Python

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