From 912022193f49e8136ecaabc826ae0744687f558b Mon Sep 17 00:00:00 2001 From: "fian@DESKTOP-JAO9LN0." Date: Mon, 5 Oct 2026 11:02:05 +0700 Subject: [PATCH] expanding function calling to be able to use sdk. --- .github/workflows/deploy.yml | 4 +- ai_chat.py | 111 ++++++++++++++++++++++++ docs/ai.md | 7 ++ tt_client.py | 158 +++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ee28cd2..e359c24 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,5 +55,5 @@ jobs: printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts # The server only lets this key run tools/deploy.sh ssh -i ~/.ssh/deploy_key -p "${DEPLOY_PORT:-22}" \ - -o BatchMode=yes -o StrictHostKeyChecking=yes \ - "$DEPLOY_USER@$DEPLOY_HOST" + -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=30 \ + -v "$DEPLOY_USER@$DEPLOY_HOST" diff --git a/ai_chat.py b/ai_chat.py index 94331b7..12eb331 100644 --- a/ai_chat.py +++ b/ai_chat.py @@ -51,6 +51,13 @@ - list_channels: List server channels. - find_user: Find where a user is located and their status. - check_channel_owner: Check who owns a channel. +- change_bot_nickname: Change the bot's nickname using SDK doChangeNickname (sensitive, admin only). +- join_channel: Make the bot join a specific channel using SDK doJoinChannelByID (sensitive, admin only). +- leave_channel: Make the bot leave the current channel and return to root/lobi (sensitive, admin only). +- set_channel_operator: Grant or revoke channel operator status ChanOp using SDK doChannelOpEx (sensitive, admin only). +- get_channel_info: View technical channel details like topic and max users from SDK getChannel. +- get_user_info: View user profile details from SDK getUser. +- broadcast_message: Broadcast an announcement to everyone on the server using SDK (sensitive, admin only). - get_bot_info: Get info about the bot and its features. Handling Commands & Actions: @@ -318,6 +325,110 @@ def clean_answer(text: str) -> str: "required": ["channel"], }, }, + { + "name": "change_bot_nickname", + "description": "Change the bot's nickname/display name on the server using SDK doChangeNickname. Sensitive: requires admin privileges.", + "parameters": { + "type": "object", + "properties": { + "nickname": { + "type": "string", + "description": "The new nickname for the bot.", + }, + }, + "required": ["nickname"], + }, + }, + { + "name": "join_channel", + "description": "Make the bot join a specific channel on the server using SDK doJoinChannelByID. Sensitive: requires admin privileges.", + "parameters": { + "type": "object", + "properties": { + "channel": { + "type": "string", + "description": "The target channel name or path.", + }, + "password": { + "type": "string", + "description": "Optional channel password.", + }, + }, + "required": ["channel"], + }, + }, + { + "name": "leave_channel", + "description": "Make the bot leave its current channel and return to the root channel using SDK. Sensitive: requires admin privileges.", + "parameters": { + "type": "object", + "properties": {}, + }, + }, + { + "name": "set_channel_operator", + "description": "Grant or revoke Channel Operator (ChanOp) status for a user in a channel using SDK doChannelOpEx. Sensitive: requires admin privileges.", + "parameters": { + "type": "object", + "properties": { + "nickname": { + "type": "string", + "description": "The nickname of the user.", + }, + "channel": { + "type": "string", + "description": "Optional channel name or path (defaults to user's current channel).", + }, + "is_operator": { + "type": "boolean", + "description": "True to grant operator status, False to revoke operator status.", + }, + }, + "required": ["nickname", "is_operator"], + }, + }, + { + "name": "get_channel_info", + "description": "Get detailed channel information and technical settings from SDK getChannel. Available to everyone.", + "parameters": { + "type": "object", + "properties": { + "channel": { + "type": "string", + "description": "The channel name or path.", + }, + }, + "required": ["channel"], + }, + }, + { + "name": "get_user_info", + "description": "Get detailed user info from SDK getUser (account username, channel, status, admin role). Sensitive data like IP only for admins.", + "parameters": { + "type": "object", + "properties": { + "nickname": { + "type": "string", + "description": "The nickname of the user.", + }, + }, + "required": ["nickname"], + }, + }, + { + "name": "broadcast_message", + "description": "Broadcast an announcement message to all users on the server using SDK. Sensitive: requires admin privileges.", + "parameters": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The announcement message to broadcast.", + }, + }, + "required": ["message"], + }, + }, { "name": "get_bot_info", "description": "Get general information about the bot, version, and supported capabilities.", diff --git a/docs/ai.md b/docs/ai.md index 2beff05..0e1c40b 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -137,6 +137,13 @@ The AI automatically invokes tools executed safely on the bot's main thread: - `list_channels`: list server channels (available to everyone). - `find_user`: locate a user and see what channel they are in and their status (available to everyone). - `check_channel_owner`: check the owner of a channel (available to everyone). +- `change_bot_nickname`: change the bot's nickname/display name on the server using SDK `doChangeNickname` (sensitive, requires admin). +- `join_channel`: make the bot join a specific channel using SDK `doJoinChannelByID` (sensitive, requires admin). +- `leave_channel`: make the bot leave its current channel and return to root using SDK (sensitive, requires admin). +- `set_channel_operator`: grant or revoke Channel Operator (ChanOp) status for a user using SDK `doChannelOpEx` (sensitive, requires admin). +- `get_channel_info`: view technical channel settings (topic, max users, password protection) from SDK `getChannel` (available to everyone). +- `get_user_info`: view user profile details from SDK `getUser` (available to everyone; sensitive fields like IP are admin only). +- `broadcast_message`: broadcast an announcement message across the server using SDK `TextMsgType.MSGTYPE_BROADCAST` (sensitive, requires admin). - `get_bot_info`: get bot version and supported features (available to everyone). **Admin permissions**: Sensitive actions strictly check the requester's `uUserType & UserType.USERTYPE_ADMIN`. If a non-admin requests a restricted action (like kicking, moving, or disabling moderation), permission is denied and the AI replies in its friendly Gen Z persona (for example: *"Woi kamu bukan admin bro, gak boleh aneh-aneh ya"*). diff --git a/tt_client.py b/tt_client.py index 50903d4..24d3d82 100644 --- a/tt_client.py +++ b/tt_client.py @@ -3282,6 +3282,164 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic "message": f"Pemilik channel {path} adalah {owner}.", } + elif tool_name == "change_bot_nickname": + if not is_admin: + return { + "status": "error", + "error": "PERMISSION_DENIED", + "message": "Kamu bukan admin, jadi gak boleh ganti nama bot.", + } + nickname = str(tool_args.get("nickname") or "").strip() + if not nickname: + return {"status": "error", "message": "Nama baru bot tidak boleh kosong."} + self.doChangeNickname(nickname) + return { + "status": "success", + "message": f"Nama bot berhasil diubah menjadi: '{nickname}'.", + } + + elif tool_name == "join_channel": + if not is_admin: + return { + "status": "error", + "error": "PERMISSION_DENIED", + "message": "Kamu bukan admin, jadi gak boleh mindahin bot ke channel lain.", + } + channel_target = str(tool_args.get("channel") or "").strip() + password = str(tool_args.get("password") or "").strip() + cid = self._find_channel_id(channel_target) + if not cid: + return { + "status": "error", + "error": "CHANNEL_NOT_FOUND", + "message": f"Channel '{channel_target}' tidak ditemukan di server.", + } + self.doJoinChannelByID(cid, password) + cpath = self.getChannelPath(cid) or channel_target + return { + "status": "success", + "message": f"Bot berhasil bergabung ke channel {cpath}.", + } + + elif tool_name == "leave_channel": + if not is_admin: + return { + "status": "error", + "error": "PERMISSION_DENIED", + "message": "Kamu bukan admin, jadi gak boleh nyuruh bot keluar channel.", + } + root_id = self.getRootChannelID() if hasattr(self, "getRootChannelID") else 1 + self.doJoinChannelByID(root_id or 1, "") + return { + "status": "success", + "message": "Bot sudah kembali ke channel utama (root/lobi).", + } + + elif tool_name == "set_channel_operator": + if not is_admin: + return { + "status": "error", + "error": "PERMISSION_DENIED", + "message": "Kamu bukan admin, jadi gak boleh ngatur status operator channel.", + } + nickname = str(tool_args.get("nickname") or "").strip() + channel_target = str(tool_args.get("channel") or "").strip() + is_op = bool(tool_args.get("is_operator", True)) + targets = self._find_users_by_nickname_fuzzy(nickname) + if not targets: + return { + "status": "error", + "error": "USER_NOT_FOUND", + "message": f"User '{nickname}' tidak ditemukan di server.", + } + u = targets[0] + cid = self._find_channel_id(channel_target) if channel_target else u.nChannelID + if not cid: + cid = u.nChannelID + self.doChannelOpEx(u.nUserID, cid, "", is_op) + nick = from_tt_char(u.szNickname) + cpath = self.getChannelPath(cid) or f"channel {cid}" + status_str = "dijadikan operator" if is_op else "dicabut status operatornya" + return { + "status": "success", + "message": f"User {nick} berhasil {status_str} di {cpath}.", + } + + elif tool_name == "get_channel_info": + channel_target = str(tool_args.get("channel") or "").strip() + cid = self._find_channel_id(channel_target) + if not cid: + return { + "status": "error", + "error": "CHANNEL_NOT_FOUND", + "message": f"Channel '{channel_target}' tidak ditemukan di server.", + } + cpath = self.getChannelPath(cid) or channel_target + try: + ch = self.getChannel(cid) + topic = from_tt_char(ch.szTopic).strip() if ch and hasattr(ch, "szTopic") else "" + max_u = ch.nMaxUsers if ch and hasattr(ch, "nMaxUsers") else 0 + has_pass = bool(ch.uChannelType & 1) if ch and hasattr(ch, "uChannelType") else False + except Exception: + topic, max_u, has_pass = "", 0, False + info_parts = [f"Channel: {cpath}"] + if topic: + info_parts.append(f"Topik: {topic}") + if max_u > 0: + info_parts.append(f"Kapasitas: {max_u} pengguna") + info_parts.append("Dilindungi password" if has_pass else "Tanpa password") + return { + "status": "success", + "message": f"Info {', '.join(info_parts)}.", + } + + elif tool_name == "get_user_info": + nickname = str(tool_args.get("nickname") or "").strip() + targets = self._find_users_by_nickname_fuzzy(nickname) + if not targets: + return { + "status": "error", + "error": "USER_NOT_FOUND", + "message": f"User '{nickname}' tidak ditemukan di server.", + } + u = targets[0] + nick = from_tt_char(u.szNickname) + uname = from_tt_char(u.szUsername) + ip = from_tt_char(u.szIPAddress) + cpath = self.getChannelPath(u.nChannelID) or "Root" + status_msg = from_tt_char(u.szStatusMsg).strip() + u_is_admin = bool(u.uUserType & UserType.USERTYPE_ADMIN) if hasattr(u, "uUserType") else False + role = "Admin" if u_is_admin else "User biasa" + details = [f"Nama: {nick}", f"Role: {role}", f"Di channel: {cpath}"] + if status_msg: + details.append(f"Status: '{status_msg}'") + if is_admin: + details.append(f"Username: '{uname}'") + details.append(f"IP: {ip}") + return { + "status": "success", + "message": f"Data pengguna: {', '.join(details)}.", + } + + elif tool_name == "broadcast_message": + if not is_admin: + return { + "status": "error", + "error": "PERMISSION_DENIED", + "message": "Kamu bukan admin, jadi gak boleh kirim broadcast.", + } + msg = str(tool_args.get("message") or "").strip() + if not msg: + return {"status": "error", "message": "Pesan broadcast tidak boleh kosong."} + my_id = self.getMyUserID() or 0 + parts = buildTextMessage(content=msg, nMsgType=TextMsgType.MSGTYPE_BROADCAST, nFromUserID=my_id) + for part in parts: + self.doTextMessage(part) + return { + "status": "success", + "message": f"Pesan siaran berhasil dikirim ke seluruh server: '{msg}'.", + } + elif tool_name == "get_bot_info": ver = getattr(config, "VERSION", "1.1.2") try: