ForcePilot/backend/package/yuxi/channel/extensions/twitch/actions.py
Kris 0babb1ca8e feat(channel): 添加 Tlon 渠道扩展
新增 Tlon 渠道扩展,支持在 Yuxi 平台中集成 Tlon/Urbit 去中心化通讯平台。

包含以下功能模块:
- tlon_api: Tlon API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- sse_client: SSE 客户端
- outbound: 外发消息管理
- send: 消息发送
- security: 安全校验
- auth: 认证管理
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- approval: 审批流程
- channel_mgmt: 频道管理
- channel_ops: 频道操作
- contacts: 联系人管理
- discovery: 服务发现
- doctor: 健康诊断
- expose: 服务暴露
- gallery: 图库管理
- history: 历史记录
- hooks: 钩子管理
- media: 媒体资源处理
- notebook: 笔记本功能
- settings_store: 设置存储
- setup: 初始化设置
- story: 故事功能
- targets: 目标管理
- cite_parser: 引用解析
- utils: 工具函数
- types: 类型定义
2026-05-21 11:55:25 +08:00

1055 lines
42 KiB
Python

import logging
from yuxi.channel.extensions.twitch.client import client_manager_registry
from yuxi.channel.extensions.twitch.config import get_account_config
from yuxi.channel.extensions.twitch.outbound import send_message_twitch_internal
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
from yuxi.channel.protocols import AgentTool, AgentToolParam
logger = logging.getLogger(__name__)
_ANNOUNCEMENT_COLORS = ["primary", "blue", "green", "orange", "purple"]
def _resolve_account_and_http(config, account_id):
account = get_account_config(config, account_id)
manager = client_manager_registry.get(account_id)
if manager is None:
return account, None, {"success": False, "error": "Twitch gateway not started"}
http = manager.get_http_client(account)
if http is None:
return (
account,
None,
{
"success": False,
"error": "HTTPClient not available, configure client_id and client_secret",
},
)
return account, http, None
async def _require_broadcaster_id(http, account, config, channel=None):
target = channel or account.channel
try:
users = await http.get_users(logins=[target])
if not users:
return None, None
return users[0].id, users
except Exception:
return None, None
def get_twitch_agent_tools() -> list[AgentTool]:
return [
AgentTool(
name="twitch_send_message",
description="Send a message to a Twitch channel",
parameters=[
AgentToolParam(name="to", type="string", description="Target channel name", required=True),
AgentToolParam(name="message", type="string", description="Message text to send", required=True),
],
),
AgentTool(
name="twitch_get_stream_info",
description=(
"Get the current stream status for a Twitch channel. "
"Returns live status, title, game, viewer count, and start time."
),
parameters=[
AgentToolParam(
name="channel",
type="string",
description="Channel name (lowercase). Defaults to configured channel.",
required=False,
),
],
),
AgentTool(
name="twitch_get_channel_info",
description="Get channel details including title, game, language, tags, and content classification labels.",
parameters=[
AgentToolParam(
name="channel",
type="string",
description="Channel name (lowercase). Defaults to configured channel.",
required=False,
),
],
),
AgentTool(
name="twitch_get_user_info",
description=(
"Get user profile: user ID, display name, avatar URL, description, broadcaster type, and creation date."
),
parameters=[
AgentToolParam(name="login", type="string", description="User login name to look up.", required=True),
],
),
AgentTool(
name="twitch_send_announcement",
description="Send a highlighted announcement in chat (requires moderator:manage:announcements scope).",
parameters=[
AgentToolParam(name="message", type="string", description="Announcement text", required=True),
AgentToolParam(
name="color",
type="string",
description="Announcement color: primary/blue/green/orange/purple",
required=False,
default="primary",
enum=_ANNOUNCEMENT_COLORS,
),
],
),
AgentTool(
name="twitch_send_shoutout",
description="Send a shoutout to another streamer (requires moderator:manage:shoutouts scope).",
parameters=[
AgentToolParam(
name="target_channel", type="string", description="Channel name to shout out.", required=True
),
],
),
AgentTool(
name="twitch_get_chatters",
description="Get the list of users currently connected to the chat room.",
parameters=[
AgentToolParam(
name="channel",
type="string",
description="Channel name. Defaults to configured channel.",
required=False,
),
],
),
AgentTool(
name="twitch_get_channel_emotes",
description="Get the custom emotes for a Twitch channel.",
parameters=[
AgentToolParam(
name="channel",
type="string",
description="Channel name. Defaults to configured channel.",
required=False,
),
],
),
AgentTool(
name="twitch_get_global_emotes",
description="Get all global Twitch emotes.",
parameters=[],
),
AgentTool(
name="twitch_get_chat_settings",
description=(
"Get chat room settings: slow mode, follower mode, subscriber mode, emote mode, unique chat mode."
),
parameters=[
AgentToolParam(
name="channel",
type="string",
description="Channel name. Defaults to configured channel.",
required=False,
),
],
),
AgentTool(
name="twitch_update_chat_settings",
description="Update chat room settings (requires moderator:manage:chat_settings scope).",
parameters=[
AgentToolParam(
name="slow_mode", type="boolean", description="Enable or disable slow mode", required=False
),
AgentToolParam(
name="slow_mode_wait_time",
type="integer",
description="Wait time in seconds for slow mode (3-120)",
required=False,
),
AgentToolParam(
name="follower_mode",
type="boolean",
description="Enable or disable follower-only mode",
required=False,
),
AgentToolParam(
name="follower_mode_duration",
type="integer",
description="Follower-only mode duration in minutes",
required=False,
),
AgentToolParam(
name="subscriber_mode",
type="boolean",
description="Enable or disable subscriber-only mode",
required=False,
),
AgentToolParam(
name="emote_mode", type="boolean", description="Enable or disable emote-only mode", required=False
),
AgentToolParam(
name="unique_chat_mode",
type="boolean",
description="Enable or disable unique chat mode",
required=False,
),
],
),
AgentTool(
name="twitch_search_categories",
description="Search for game categories on Twitch by name.",
parameters=[
AgentToolParam(
name="query", type="string", description="Search query for game/category name", required=True
),
AgentToolParam(
name="first",
type="integer",
description="Maximum number of results (1-100)",
required=False,
default=10,
),
],
),
AgentTool(
name="twitch_search_channels",
description="Search for live Twitch channels by name or description.",
parameters=[
AgentToolParam(name="query", type="string", description="Search query for channel name", required=True),
AgentToolParam(
name="first",
type="integer",
description="Maximum number of results (1-100)",
required=False,
default=10,
),
AgentToolParam(
name="live_only",
type="boolean",
description="Only return currently live channels",
required=False,
default=False,
),
],
),
AgentTool(
name="twitch_get_top_games",
description="Get the most popular games currently being streamed on Twitch.",
parameters=[
AgentToolParam(
name="first", type="integer", description="Number of top games (1-100)", required=False, default=10
),
],
),
AgentTool(
name="twitch_get_followed_streams",
description="Get live streams followed by a user (requires user:read:follows scope).",
parameters=[
AgentToolParam(
name="user_id",
type="string",
description="Twitch user ID to lookup. Defaults to configured broadcaster.",
required=False,
),
],
),
AgentTool(
name="twitch_delete_message",
description="Delete a chat message (requires moderator:manage:chat_messages scope).",
parameters=[
AgentToolParam(
name="message_id", type="string", description="ID of the message to delete", required=True
),
AgentToolParam(
name="channel",
type="string",
description="Channel name. Defaults to configured channel.",
required=False,
),
],
),
AgentTool(
name="twitch_timeout_user",
description="Timeout a user for a specified duration (requires moderator:manage:banned_users scope).",
parameters=[
AgentToolParam(name="user_id", type="string", description="Twitch user ID to timeout", required=True),
AgentToolParam(
name="duration",
type="integer",
description="Timeout duration in seconds (1-1209600, default 600)",
required=False,
default=600,
),
AgentToolParam(name="reason", type="string", description="Reason for timeout", required=False),
],
),
AgentTool(
name="twitch_ban_user",
description="Ban a user from the channel (requires moderator:manage:banned_users scope).",
parameters=[
AgentToolParam(name="user_id", type="string", description="Twitch user ID to ban", required=True),
AgentToolParam(name="reason", type="string", description="Reason for ban", required=False),
],
),
AgentTool(
name="twitch_unban_user",
description="Unban a user from the channel (requires moderator:manage:banned_users scope).",
parameters=[
AgentToolParam(name="user_id", type="string", description="Twitch user ID to unban", required=True),
],
),
AgentTool(
name="twitch_get_banned_users",
description="Get the list of banned users in a channel (requires moderator:read:banned_users scope).",
parameters=[
AgentToolParam(
name="channel",
type="string",
description="Channel name. Defaults to configured channel.",
required=False,
),
AgentToolParam(
name="first", type="integer", description="Maximum results (1-100)", required=False, default=20
),
],
),
AgentTool(
name="twitch_get_eventsub_subscriptions",
description="Get active EventSub subscriptions for this channel.",
parameters=[
AgentToolParam(
name="status",
type="string",
description=("Filter by status: enabled, revoked, user_removed, authorization_revoked"),
required=False,
),
],
),
AgentTool(
name="twitch_delete_eventsub_subscription",
description="Delete an EventSub subscription by its ID.",
parameters=[
AgentToolParam(
name="subscription_id",
type="string",
description="EventSub subscription ID to delete",
required=True,
),
],
),
]
async def execute_twitch_agent_tool(tool_name: str, params: dict, context: dict) -> dict:
config = context.get("config")
account_id = context.get("account_id", "default")
if tool_name == "twitch_send_message":
return await _handle_send_message(params, config, account_id)
if tool_name == "twitch_get_stream_info":
return await _handle_get_stream_info(params, config, account_id)
if tool_name == "twitch_get_channel_info":
return await _handle_get_channel_info(params, config, account_id)
if tool_name == "twitch_get_user_info":
return await _handle_get_user_info(params, config, account_id)
if tool_name == "twitch_send_announcement":
return await _handle_send_announcement(params, config, account_id)
if tool_name == "twitch_send_shoutout":
return await _handle_send_shoutout(params, config, account_id)
if tool_name == "twitch_get_chatters":
return await _handle_get_chatters(params, config, account_id)
if tool_name == "twitch_get_channel_emotes":
return await _handle_get_emotes(params, config, account_id)
if tool_name == "twitch_get_global_emotes":
return await _handle_get_global_emotes(params, config, account_id)
if tool_name == "twitch_get_chat_settings":
return await _handle_get_chat_settings(params, config, account_id)
if tool_name == "twitch_update_chat_settings":
return await _handle_update_chat_settings(params, config, account_id)
if tool_name == "twitch_search_categories":
return await _handle_search_categories(params, config, account_id)
if tool_name == "twitch_search_channels":
return await _handle_search_channels(params, config, account_id)
if tool_name == "twitch_get_top_games":
return await _handle_get_top_games(params, config, account_id)
if tool_name == "twitch_get_followed_streams":
return await _handle_get_followed_streams(params, config, account_id)
if tool_name == "twitch_delete_message":
return await _handle_delete_message(params, config, account_id)
if tool_name == "twitch_timeout_user":
return await _handle_timeout_user(params, config, account_id)
if tool_name == "twitch_ban_user":
return await _handle_ban_user(params, config, account_id)
if tool_name == "twitch_unban_user":
return await _handle_unban_user(params, config, account_id)
if tool_name == "twitch_get_banned_users":
return await _handle_get_banned_users(params, config, account_id)
if tool_name == "twitch_get_eventsub_subscriptions":
return await _handle_get_eventsub_subscriptions(params, config, account_id)
if tool_name == "twitch_delete_eventsub_subscription":
return await _handle_delete_eventsub_subscription(params, config, account_id)
return {"success": False, "error": f"Unsupported tool: {tool_name}"}
# ---- Handler implementations ----
async def _handle_send_message(params, config, account_id):
to = params.get("to", "")
message = params.get("message", "")
if not to:
account = get_account_config(config, account_id)
to = account.channel
result = await send_message_twitch_internal(
channel=to,
text=message,
cfg=config,
account_id=account_id,
strip_markdown=True,
)
if result.ok:
return {"success": True, "message_id": result.message_id}
return {"success": False, "error": result.error}
async def _handle_get_stream_info(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = params.get("channel", "") or account.channel
try:
streams = await http.get_streams(user_logins=[channel])
if streams:
s = streams[0]
return {
"success": True,
"data": {
"online": True,
"title": s.title,
"game_name": s.game_name,
"viewer_count": s.viewer_count,
"started_at": str(s.started_at) if s.started_at else None,
},
}
return {"success": True, "data": {"online": False}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_channel_info(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = params.get("channel", "") or account.channel
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
channels = await http.get_channels(broadcaster_ids=[broadcaster_id])
if channels:
c = channels[0]
return {
"success": True,
"data": {
"broadcaster_id": broadcaster_id,
"broadcaster_name": c.broadcaster_name,
"broadcaster_language": c.broadcaster_language,
"game_name": c.game_name,
"title": c.title,
"delay": c.delay,
},
}
return {"success": False, "error": f"Channel info not found for '{channel}'"}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_user_info(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
login = params.get("login", "")
if not login:
return {"success": False, "error": "login parameter is required"}
try:
users = await http.get_users(logins=[login])
if users:
u = users[0]
return {
"success": True,
"data": {
"id": u.id,
"login": u.login,
"display_name": u.display_name,
"type": u.type,
"broadcaster_type": u.broadcaster_type,
"description": u.description,
"profile_image_url": u.profile_image_url,
"offline_image_url": u.offline_image_url,
"created_at": str(u.created_at) if u.created_at else None,
},
}
return {"success": False, "error": f"User '{login}' not found"}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_send_announcement(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = account.channel
message = params.get("message", "")
color = params.get("color", "primary")
if color not in _ANNOUNCEMENT_COLORS:
color = "primary"
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
await http.post_chat_announcements(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
message=message,
color=color,
)
return {"success": True, "data": {"color": color, "message": message}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_send_shoutout(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
target_channel = params.get("target_channel", "")
if not target_channel:
return {"success": False, "error": "target_channel is required"}
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{account.channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
target_users = await http.get_users(logins=[target_channel])
if not target_users:
return {"success": False, "error": f"Target channel '{target_channel}' not found"}
to_broadcaster_id = target_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
await http.post_chat_shoutouts(
token=denormalize_token(token),
from_broadcaster_id=broadcaster_id,
to_broadcaster_id=to_broadcaster_id,
moderator_id=moderator_id,
)
return {"success": True, "data": {"target": target_channel}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_chatters(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = params.get("channel", "") or account.channel
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
chatters = await http.get_chatters(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
)
total_count = getattr(chatters, "total", 0) if chatters else 0
chatters_list = getattr(chatters, "data", []) if chatters else []
return {
"success": True,
"data": {
"total": total_count,
"chatters": [u.user_name for u in chatters_list] if chatters_list else [],
},
}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_emotes(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = params.get("channel", "") or account.channel
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
emotes = await http.get_chat_emotes(broadcaster_id=broadcaster_id)
if emotes:
emote_list = [{"name": e.name, "id": e.id, "emote_type": e.emote_type} for e in emotes]
return {"success": True, "data": {"count": len(emote_list), "emotes": emote_list}}
return {"success": True, "data": {"count": 0, "emotes": []}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_global_emotes(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
try:
emotes = await http.get_chat_emotes_global()
if emotes:
emote_list = [{"name": e.name, "id": e.id} for e in emotes]
return {"success": True, "data": {"count": len(emote_list), "emotes": emote_list}}
return {"success": True, "data": {"count": 0, "emotes": []}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_chat_settings(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = params.get("channel", "") or account.channel
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
settings = await http.get_chat_settings(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
)
if settings:
data = settings[0] if isinstance(settings, list) else settings
return {
"success": True,
"data": {
"slow_mode": getattr(data, "slow_mode", False),
"slow_mode_wait_time": getattr(data, "slow_mode_wait_time", None),
"follower_mode": getattr(data, "follower_mode", False),
"follower_mode_duration": getattr(data, "follower_mode_duration", None),
"subscriber_mode": getattr(data, "subscriber_mode", False),
"emote_mode": getattr(data, "emote_mode", False),
"unique_chat_mode": getattr(data, "unique_chat_mode", False),
},
}
return {"success": False, "error": "Failed to get chat settings"}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_update_chat_settings(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
kwargs = {}
bool_fields = {
"slow_mode": params.get("slow_mode"),
"follower_mode": params.get("follower_mode"),
"subscriber_mode": params.get("subscriber_mode"),
"emote_mode": params.get("emote_mode"),
"unique_chat_mode": params.get("unique_chat_mode"),
}
for field, val in bool_fields.items():
if val is not None:
kwargs[field] = val
if "slow_mode_wait_time" in params and params["slow_mode_wait_time"] is not None:
kwargs["slow_mode_wait_time"] = params["slow_mode_wait_time"]
if "follower_mode_duration" in params and params["follower_mode_duration"] is not None:
kwargs["follower_mode_duration"] = params["follower_mode_duration"]
if not kwargs:
return {"success": False, "error": "No settings to update"}
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{account.channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
await http.patch_chat_settings(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
**kwargs,
)
return {"success": True, "data": {"updated_fields": list(kwargs.keys())}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_search_categories(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
query = params.get("query", "")
first = params.get("first", 10)
try:
categories = await http.search_categories(query=query, first=min(first, 100))
results = [{"id": c.id, "name": c.name} for c in categories] if categories else []
return {"success": True, "data": {"count": len(results), "categories": results}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_search_channels(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
query = params.get("query", "")
first = params.get("first", 10)
live_only = params.get("live_only", False)
try:
channels = await http.search_channels(query=query, first=min(first, 100), live_only=live_only)
results = []
if channels:
for c in channels:
results.append(
{
"id": c.id,
"broadcaster_login": c.broadcaster_login,
"display_name": c.display_name,
"game_name": c.game_name,
"title": c.title,
"is_live": c.is_live,
"broadcaster_language": c.broadcaster_language,
}
)
return {"success": True, "data": {"count": len(results), "channels": results}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_top_games(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
first = params.get("first", 10)
try:
games = await http.get_top_games(first=min(first, 100))
results = []
if games:
for g in games:
results.append(
{
"id": g.id,
"name": g.name,
"box_art_url": g.box_art_url if hasattr(g, "box_art_url") else None,
}
)
return {"success": True, "data": {"count": len(results), "games": results}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_followed_streams(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
user_id = params.get("user_id")
if not user_id:
try:
users = await http.get_users(logins=[account.username])
if users:
user_id = users[0].id
except Exception:
pass
if not user_id:
return {"success": False, "error": "user_id is required and could not be resolved from account"}
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
try:
streams = await http.get_followed_streams(
token=denormalize_token(token),
user_id=user_id,
)
results = []
if streams:
for s in streams:
results.append(
{
"user_name": s.user_name,
"game_name": s.game_name,
"title": s.title,
"viewer_count": s.viewer_count,
}
)
return {"success": True, "data": {"count": len(results), "streams": results}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_delete_message(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
message_id = params.get("message_id", "")
channel = params.get("channel", "") or account.channel
if not message_id:
return {"success": False, "error": "message_id is required"}
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
await http.delete_chat_messages(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
message_id=message_id,
)
return {"success": True, "data": {"deleted_message_id": message_id}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_timeout_user(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
user_id = params.get("user_id", "")
duration = params.get("duration", 600)
reason = params.get("reason", "")
if not user_id:
return {"success": False, "error": "user_id is required"}
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{account.channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
body = {"user_id": user_id, "duration": duration}
if reason:
body["reason"] = reason
await http.post_moderation_bans(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
**body,
)
return {"success": True, "data": {"user_id": user_id, "duration": duration}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_ban_user(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
user_id = params.get("user_id", "")
reason = params.get("reason", "")
if not user_id:
return {"success": False, "error": "user_id is required"}
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{account.channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
body = {"user_id": user_id}
if reason:
body["reason"] = reason
await http.post_moderation_bans(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
**body,
)
return {"success": True, "data": {"user_id": user_id, "action": "ban"}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_unban_user(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
user_id = params.get("user_id", "")
if not user_id:
return {"success": False, "error": "user_id is required"}
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{account.channel}' not found"}
bot_users = await http.get_users(logins=[account.username])
if not bot_users:
return {"success": False, "error": f"Bot user '{account.username}' not found"}
moderator_id = bot_users[0].id
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
await http.delete_moderation_bans(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
moderator_id=moderator_id,
user_id=user_id,
)
return {"success": True, "data": {"user_id": user_id, "action": "unban"}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_banned_users(params, config, account_id):
account, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
channel = params.get("channel", "") or account.channel
first = params.get("first", 20)
try:
broadcaster_id, _ = await _require_broadcaster_id(http, account, config, channel)
if not broadcaster_id:
return {"success": False, "error": f"Channel '{channel}' not found"}
from yuxi.channel.extensions.twitch.token import denormalize_token, resolve_twitch_token
token, _ = resolve_twitch_token(account)
banned = await http.get_moderation_banned(
token=denormalize_token(token),
broadcaster_id=broadcaster_id,
first=min(first, 100),
)
results = []
if banned:
for b in banned:
results.append(
{
"user_id": b.user_id,
"user_name": getattr(b, "user_name", None),
"expires_at": str(b.expires_at) if getattr(b, "expires_at", None) else None,
"reason": getattr(b, "reason", None),
}
)
return {"success": True, "data": {"count": len(results), "banned_users": results}}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_get_eventsub_subscriptions(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
status = params.get("status")
try:
import aiohttp
account = get_account_config(config, account_id)
token, _ = resolve_twitch_token(account)
headers = {
"Client-Id": account.client_id,
"Authorization": f"Bearer {denormalize_token(token)}",
}
query = {}
if status:
query["status"] = status
async with aiohttp.ClientSession() as session:
async with session.get(
"https://api.twitch.tv/helix/eventsub/subscriptions",
headers=headers,
params=query,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
data = await resp.json()
if resp.status == 200:
subs = data.get("data", [])
results = []
for s in subs:
results.append(
{
"id": s.get("id"),
"type": s.get("type"),
"status": s.get("status"),
"created_at": s.get("created_at"),
"condition": s.get("condition"),
}
)
return {
"success": True,
"data": {
"total": data.get("total", 0),
"subscriptions": results,
},
}
return {"success": False, "error": f"HTTP {resp.status}: {data}"}
except Exception as e:
return {"success": False, "error": str(e)}
async def _handle_delete_eventsub_subscription(params, config, account_id):
_, http, err = _resolve_account_and_http(config, account_id)
if err:
return err
subscription_id = params.get("subscription_id", "")
if not subscription_id:
return {"success": False, "error": "subscription_id is required"}
try:
import aiohttp
account = get_account_config(config, account_id)
token, _ = resolve_twitch_token(account)
headers = {
"Client-Id": account.client_id,
"Authorization": f"Bearer {denormalize_token(token)}",
}
async with aiohttp.ClientSession() as session:
async with session.delete(
f"https://api.twitch.tv/helix/eventsub/subscriptions?id={subscription_id}",
headers=headers,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
if resp.status == 204:
return {"success": True, "data": {"deleted": subscription_id}}
return {"success": False, "error": f"HTTP {resp.status}"}
except Exception as e:
return {"success": False, "error": str(e)}