Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
111 changes: 111 additions & 0 deletions ai_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.",
Expand Down
7 changes: 7 additions & 0 deletions docs/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"*).
Expand Down
158 changes: 158 additions & 0 deletions tt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading