Files
goclaw/internal/tools/telegram_manager.go
T
nguyenha935andGoClaw Operator 4d3c6bcb05 feat: add Telegram manager channel permissions (#1339)
Co-authored-by: GoClaw Operator <operator@goclaw>
2026-07-04 00:25:00 +07:00

349 lines
12 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/channels"
)
// TelegramManagerFunc executes a whitelisted Telegram management action on a
// named channel instance. Implemented by channels.Manager.ManageTelegram.
type TelegramManagerFunc func(ctx context.Context, channel string, req channels.TelegramManagerRequest) (channels.TelegramManagerResult, error)
// TelegramManagerAware tools can receive a Telegram manager function.
type TelegramManagerAware interface {
SetTelegramManager(TelegramManagerFunc)
}
// TelegramManagerTool exposes selected Telegram Bot API management actions to agents.
type TelegramManagerTool struct {
manager TelegramManagerFunc
}
func NewTelegramManagerTool() *TelegramManagerTool { return &TelegramManagerTool{} }
func (t *TelegramManagerTool) SetTelegramManager(m TelegramManagerFunc) { t.manager = m }
func (t *TelegramManagerTool) RequiredChannelTypes() []string { return []string{"telegram"} }
func (t *TelegramManagerTool) Name() string { return "telegram_manager" }
func (t *TelegramManagerTool) Description() string {
return "Manage Telegram chats through whitelisted Bot API actions. Supports chat info, member admin actions, message pin/delete actions, and forum topic management when the bot has the required Telegram permissions."
}
func (t *TelegramManagerTool) Parameters() map[string]any {
actions := make([]string, 0, len(telegramManagerActions))
for action := range telegramManagerActions {
actions = append(actions, action)
}
return map[string]any{
"type": "object",
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"description": "Whitelisted Telegram management action, e.g. chat.get, member.ban, message.pin, topic.create.",
"enum": actions,
},
"channel": map[string]any{
"type": "string",
"description": "GoClaw Telegram channel instance name. Defaults to current channel.",
},
"chat_id": map[string]any{
"type": "string",
"description": "Telegram chat ID or @username. Defaults to current chat.",
},
"message_thread_id": map[string]any{"type": "integer", "description": "Telegram forum topic thread ID."},
"message_id": map[string]any{"type": "integer", "description": "Telegram message ID."},
"user_id": map[string]any{"type": "integer", "description": "Telegram user ID."},
"name": map[string]any{"type": "string", "description": "Topic name, general topic name, or other Telegram name parameter."},
"text": map[string]any{"type": "string", "description": "Text parameter for actions that need text."},
"invite_link": map[string]any{"type": "string", "description": "Telegram invite link for revoke actions."},
"icon_color": map[string]any{"type": "integer", "description": "Telegram forum topic icon color."},
"icon_custom_emoji_id": map[string]any{
"type": "string",
"description": "Telegram custom emoji ID for forum topic icon.",
},
"expire_date": map[string]any{"type": "integer", "description": "Unix timestamp when an invite link expires."},
"member_limit": map[string]any{"type": "integer", "description": "Maximum members for an invite link."},
"disable_notification": map[string]any{"type": "boolean", "description": "Disable notification for pin actions."},
"creates_join_request": map[string]any{"type": "boolean", "description": "Whether invite link joins require admin approval."},
"only_if_banned": map[string]any{"type": "boolean", "description": "Only unban if currently banned."},
"revoke_messages": map[string]any{"type": "boolean", "description": "Revoke prior messages when banning."},
"params": map[string]any{"type": "object", "description": "Reserved structured options for future whitelisted actions."},
},
"required": []string{"action"},
}
}
func (t *TelegramManagerTool) Execute(ctx context.Context, args map[string]any) *Result {
if ToolChannelTypeFromCtx(ctx) != "telegram" {
return ErrorResult("telegram_manager is only available inside Telegram channel runs")
}
permissions := TelegramManagerPermissionsFromCtx(ctx)
if len(permissions) == 0 {
return ErrorResult("telegram_manager is disabled for this Telegram channel")
}
if t.manager == nil {
return ErrorResult("telegram_manager: no Telegram manager available")
}
action := argString(args, "action")
if action == "" {
return ErrorResult("action is required")
}
if _, ok := telegramManagerActions[action]; !ok {
return ErrorResult(fmt.Sprintf("unsupported telegram_manager action: %s", action))
}
permissionGroup := telegramManagerPermissionGroup(action)
if permissionGroup == "" || !telegramManagerPermissionAllowed(permissions, permissionGroup) {
return ErrorResult(fmt.Sprintf("telegram_manager action %s is not allowed by this channel", action))
}
channel := argString(args, "channel")
if channel == "" {
channel = ToolChannelFromCtx(ctx)
}
if channel == "" {
return ErrorResult("channel is required (no current channel in context)")
}
chatID := argString(args, "chat_id")
if chatID == "" {
chatID = ToolChatIDFromCtx(ctx)
}
req := channels.TelegramManagerRequest{
Action: action,
ChatID: chatID,
MessageThreadID: argInt(args, "message_thread_id"),
MessageID: argInt(args, "message_id"),
UserID: argInt64(args, "user_id"),
Name: argString(args, "name"),
Text: argString(args, "text"),
InviteLink: argString(args, "invite_link"),
IconColor: argInt(args, "icon_color"),
IconCustomEmojiID: argString(args, "icon_custom_emoji_id"),
ExpireDate: argInt64(args, "expire_date"),
MemberLimit: argInt(args, "member_limit"),
DisableNotification: argBool(args, "disable_notification"),
CreatesJoinRequest: argBool(args, "creates_join_request"),
OnlyIfBanned: argBool(args, "only_if_banned"),
RevokeMessages: argBool(args, "revoke_messages"),
}
if rawParams, ok := args["params"].(map[string]any); ok {
req.Params = rawParams
}
if err := validateTelegramManagerRequest(req); err != nil {
return ErrorResult(err.Error())
}
result, err := t.manager(ctx, channel, req)
if err != nil {
return ErrorResult(fmt.Sprintf("telegram_manager %s failed: %v", action, err))
}
if result.Action == "" {
result.Action = action
}
data, _ := json.Marshal(map[string]any{
"ok": true,
"channel": channel,
"chat_id": req.ChatID,
"action": result.Action,
"result": result.Result,
})
return NewResult(string(data))
}
var telegramManagerActions = map[string]struct{}{
"chat.get": {},
"chat.get_administrators": {},
"chat.get_member_count": {},
"chat.get_member": {},
"chat.set_title": {},
"chat.set_description": {},
"chat.leave": {},
"member.ban": {},
"member.unban": {},
"member.set_custom_title": {},
"join_request.approve": {},
"join_request.decline": {},
"invite.export": {},
"invite.create": {},
"invite.revoke": {},
"message.delete": {},
"message.pin": {},
"message.unpin": {},
"message.unpin_all": {},
"topic.get_icon_stickers": {},
"topic.create": {},
"topic.edit": {},
"topic.close": {},
"topic.reopen": {},
"topic.delete": {},
"topic.unpin_all_messages": {},
"topic.general.edit": {},
"topic.general.close": {},
"topic.general.reopen": {},
"topic.general.hide": {},
"topic.general.unhide": {},
"topic.general.unpin_all_messages": {},
}
var telegramManagerPermissionGroups = map[string]struct{}{
"chat": {},
"member": {},
"join_request": {},
"invite": {},
"message": {},
"topic": {},
}
func telegramManagerPermissionGroup(action string) string {
prefix, _, _ := strings.Cut(action, ".")
if prefix == "join_request" {
return "join_request"
}
if _, ok := telegramManagerPermissionGroups[prefix]; ok {
return prefix
}
return ""
}
func telegramManagerPermissionAllowed(permissions []string, group string) bool {
for _, permission := range permissions {
if strings.EqualFold(strings.TrimSpace(permission), group) {
return true
}
}
return false
}
func validateTelegramManagerRequest(req channels.TelegramManagerRequest) error {
switch req.Action {
case "topic.get_icon_stickers":
return nil
case "chat.get", "chat.get_administrators", "chat.get_member_count", "message.unpin_all",
"chat.leave", "invite.export", "invite.create",
"topic.general.close", "topic.general.reopen", "topic.general.hide", "topic.general.unhide", "topic.general.unpin_all_messages":
return requireChatID(req)
case "chat.get_member", "member.ban", "member.unban", "join_request.approve", "join_request.decline":
if err := requireChatID(req); err != nil {
return err
}
if req.UserID == 0 {
return fmt.Errorf("user_id is required for %s", req.Action)
}
return nil
case "member.set_custom_title":
if err := requireChatID(req); err != nil {
return err
}
if req.UserID == 0 {
return fmt.Errorf("user_id is required for %s", req.Action)
}
if strings.TrimSpace(req.Text) == "" {
return fmt.Errorf("text is required for %s", req.Action)
}
return nil
case "chat.set_title", "chat.set_description":
if err := requireChatID(req); err != nil {
return err
}
if strings.TrimSpace(req.Text) == "" {
return fmt.Errorf("text is required for %s", req.Action)
}
return nil
case "invite.revoke":
if err := requireChatID(req); err != nil {
return err
}
if strings.TrimSpace(req.InviteLink) == "" {
return fmt.Errorf("invite_link is required for %s", req.Action)
}
return nil
case "message.delete", "message.pin":
if err := requireChatID(req); err != nil {
return err
}
if req.MessageID == 0 {
return fmt.Errorf("message_id is required for %s", req.Action)
}
return nil
case "message.unpin":
return requireChatID(req)
case "topic.create", "topic.general.edit":
if err := requireChatID(req); err != nil {
return err
}
if strings.TrimSpace(req.Name) == "" {
return fmt.Errorf("name is required for %s", req.Action)
}
return nil
case "topic.edit", "topic.close", "topic.reopen", "topic.delete", "topic.unpin_all_messages":
if err := requireChatID(req); err != nil {
return err
}
if req.MessageThreadID == 0 {
return fmt.Errorf("message_thread_id is required for %s", req.Action)
}
return nil
default:
return fmt.Errorf("unsupported telegram_manager action: %s", req.Action)
}
}
func requireChatID(req channels.TelegramManagerRequest) error {
if strings.TrimSpace(req.ChatID) == "" {
return fmt.Errorf("chat_id is required for %s (no current chat in context)", req.Action)
}
return nil
}
func argInt(m map[string]any, key string) int { return int(argInt64(m, key)) }
func argInt64(m map[string]any, key string) int64 {
v, ok := m[key]
if !ok || v == nil {
return 0
}
switch n := v.(type) {
case int:
return int64(n)
case int64:
return n
case float64:
return int64(n)
case json.Number:
parsed, _ := n.Int64()
return parsed
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
return parsed
default:
return 0
}
}
func argBool(m map[string]any, key string) bool {
v, ok := m[key]
if !ok || v == nil {
return false
}
switch b := v.(type) {
case bool:
return b
case string:
parsed, _ := strconv.ParseBool(strings.TrimSpace(b))
return parsed
default:
return false
}
}