新增 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: 媒体资源处理
176 lines
5.9 KiB
Python
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
|