新增 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: 类型定义
1055 lines
42 KiB
Python
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)}
|