feat(telegram): add options to disable bot commands in business chats
- Modified Telegram settings to include business mode and commands enable options - Implemented logic to handle business messages and commands in telegram.go - Added tests for business commands handling in telegram_dispatch_test.go - Updated configuration tests to include new business commands settings - Enhanced frontend form to allow configuration of business commands - Added translations for business commands in multiple languages - Updated README to include information about Telegram Business mode
This commit is contained in:
parent
0a8408fff4
commit
84164e47f3
10 changed files with 189 additions and 21 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Telegram
|
||||
|
||||
The Telegram channel uses long polling via the Telegram Bot API for bot-based communication. It supports text messages, media attachments (photos, voice, audio, documents), voice transcription ([setup](../../guides/providers.md#voice-transcription)), and built-in command handling.
|
||||
The Telegram channel uses long polling via the Telegram Bot API for bot-based communication. It supports text messages, media attachments (photos, voice, audio, documents), voice transcription ([setup](../../guides/providers.md#voice-transcription)), built-in command handling, and optional Telegram Business chats.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -14,8 +14,13 @@ The Telegram channel uses long polling via the Telegram Bot API for bot-based co
|
|||
"type": "telegram",
|
||||
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
|
||||
"allow_from": ["123456789"],
|
||||
"proxy": "",
|
||||
"use_markdown_v2": false
|
||||
"settings": {
|
||||
"proxy": "",
|
||||
"use_markdown_v2": false,
|
||||
"business_mode": false,
|
||||
"business_owner": "123456789",
|
||||
"business_commands_enable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +66,34 @@ Examples:
|
|||
explain how to squash the last 3 commits
|
||||
```
|
||||
|
||||
## Telegram Business Mode
|
||||
|
||||
Set `settings.business_mode: true` to receive and reply to Telegram Business messages from connected business accounts. Business replies are sent with the incoming `business_connection_id`, and incoming business messages are marked as read when the bot has the `can_read_messages` business right. If marking a message as read fails, PicoClaw still processes the message.
|
||||
|
||||
Use `settings.business_owner` to store the Telegram user ID of the business account owner. Business messages from that user are skipped, which prevents the bot from responding to messages you send manually from the connected business account.
|
||||
|
||||
By default, bot commands in business chats are ignored. Set `settings.business_commands_enable: true` if you want commands such as `/new`, `/help`, `/show`, `/list`, and `/use` to be handled in Telegram Business chats.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"type": "telegram",
|
||||
"allow_from": ["123456789"],
|
||||
"settings": {
|
||||
"token": "YOUR_BOT_TOKEN",
|
||||
"business_mode": true,
|
||||
"business_owner": "123456789",
|
||||
"business_commands_enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Formatting
|
||||
|
||||
You can set `use_markdown_v2: true` to enable enhanced formatting options. This allows the bot to utilize the full range of Telegram MarkdownV2 features, including nested styles, spoilers, and custom fixed-width blocks.
|
||||
|
|
|
|||
|
|
@ -794,10 +794,36 @@ func (c *TelegramChannel) handleBusinessMessage(ctx context.Context, message *te
|
|||
})
|
||||
return nil
|
||||
}
|
||||
if c.isDisabledBusinessCommand(message) {
|
||||
logger.DebugCF("telegram", "Business bot command ignored because business_commands_enable is false", map[string]any{
|
||||
"business_connection_id": businessConnectionID,
|
||||
"user_id": fmt.Sprintf("%d", message.From.ID),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
c.markBusinessMessageRead(ctx, businessConnectionID, message.Chat.ID, message.MessageID)
|
||||
return c.handleTelegramMessage(ctx, message, businessConnectionID)
|
||||
}
|
||||
|
||||
func (c *TelegramChannel) isDisabledBusinessCommand(message *telego.Message) bool {
|
||||
if c == nil || c.tgCfg == nil || c.tgCfg.BusinessCommandsEnable || message == nil {
|
||||
return false
|
||||
}
|
||||
return isTelegramBotCommandMessage(message)
|
||||
}
|
||||
|
||||
func isTelegramBotCommandMessage(message *telego.Message) bool {
|
||||
if message == nil {
|
||||
return false
|
||||
}
|
||||
for _, entity := range message.Entities {
|
||||
if entity.Type == telego.EntityTypeBotCommand {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return strings.HasPrefix(strings.TrimSpace(message.Text), "/")
|
||||
}
|
||||
|
||||
func (c *TelegramChannel) isBusinessOwnerMessage(message *telego.Message) bool {
|
||||
if c == nil || c.tgCfg == nil || message == nil || message.From == nil {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -121,6 +121,92 @@ func TestHandleBusinessMessage_BusinessOwnerIgnoresMessage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleBusinessMessage_DisabledBusinessCommandsIgnoresCommand(t *testing.T) {
|
||||
messageBus := bus.NewMessageBus()
|
||||
ch := &TelegramChannel{
|
||||
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil),
|
||||
chatIDs: make(map[string]int64),
|
||||
ctx: context.Background(),
|
||||
tgCfg: &config.TelegramSettings{
|
||||
BusinessMode: true,
|
||||
BusinessCommandsEnable: false,
|
||||
},
|
||||
}
|
||||
|
||||
msg := &telego.Message{
|
||||
Text: "/new",
|
||||
MessageID: 20,
|
||||
BusinessConnectionID: "biz-conn-1",
|
||||
Entities: []telego.MessageEntity{{
|
||||
Type: telego.EntityTypeBotCommand,
|
||||
Offset: 0,
|
||||
Length: len("/new"),
|
||||
}},
|
||||
Chat: telego.Chat{
|
||||
ID: 777,
|
||||
Type: "private",
|
||||
},
|
||||
From: &telego.User{
|
||||
ID: 42,
|
||||
FirstName: "Alice",
|
||||
},
|
||||
}
|
||||
|
||||
if err := ch.handleBusinessMessage(context.Background(), msg); err != nil {
|
||||
t.Fatalf("handleBusinessMessage error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case inbound := <-messageBus.InboundChan():
|
||||
t.Fatalf("expected disabled business commands to ignore message, got %#v", inbound)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleBusinessMessage_EnabledBusinessCommandsForwardsCommand(t *testing.T) {
|
||||
messageBus := bus.NewMessageBus()
|
||||
ch := &TelegramChannel{
|
||||
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil),
|
||||
chatIDs: make(map[string]int64),
|
||||
ctx: context.Background(),
|
||||
tgCfg: &config.TelegramSettings{
|
||||
BusinessMode: true,
|
||||
BusinessCommandsEnable: true,
|
||||
},
|
||||
}
|
||||
|
||||
msg := &telego.Message{
|
||||
Text: "/new",
|
||||
MessageID: 21,
|
||||
BusinessConnectionID: "biz-conn-1",
|
||||
Entities: []telego.MessageEntity{{
|
||||
Type: telego.EntityTypeBotCommand,
|
||||
Offset: 0,
|
||||
Length: len("/new"),
|
||||
}},
|
||||
Chat: telego.Chat{
|
||||
ID: 777,
|
||||
Type: "private",
|
||||
},
|
||||
From: &telego.User{
|
||||
ID: 42,
|
||||
FirstName: "Alice",
|
||||
},
|
||||
}
|
||||
|
||||
if err := ch.handleBusinessMessage(context.Background(), msg); err != nil {
|
||||
t.Fatalf("handleBusinessMessage error: %v", err)
|
||||
}
|
||||
|
||||
inbound, ok := <-messageBus.InboundChan()
|
||||
if !ok {
|
||||
t.Fatal("expected inbound message to be forwarded")
|
||||
}
|
||||
if inbound.Content != "/new" {
|
||||
t.Fatalf("content=%q", inbound.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramAllowedUpdates_BusinessMode(t *testing.T) {
|
||||
disabled := strings.Join(telegramAllowedUpdates(false), ",")
|
||||
if strings.Contains(disabled, telego.BusinessMessageUpdates) {
|
||||
|
|
|
|||
|
|
@ -359,13 +359,14 @@ type WhatsAppSettings struct {
|
|||
}
|
||||
|
||||
type TelegramSettings struct {
|
||||
Token SecureString `json:"token,omitzero" yaml:"token,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||
BaseURL string `json:"base_url" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"`
|
||||
Proxy string `json:"proxy" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||
Streaming StreamingConfig `json:"streaming,omitempty" yaml:"-"`
|
||||
BusinessMode bool `json:"business_mode" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_MODE"`
|
||||
BusinessOwner string `json:"business_owner" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_OWNER"`
|
||||
UseMarkdownV2 bool `json:"use_markdown_v2" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
|
||||
Token SecureString `json:"token,omitzero" yaml:"token,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||
BaseURL string `json:"base_url" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"`
|
||||
Proxy string `json:"proxy" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||
Streaming StreamingConfig `json:"streaming,omitempty" yaml:"-"`
|
||||
BusinessMode bool `json:"business_mode" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_MODE"`
|
||||
BusinessOwner string `json:"business_owner" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_OWNER"`
|
||||
BusinessCommandsEnable bool `json:"business_commands_enable" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_COMMANDS_ENABLE"`
|
||||
UseMarkdownV2 bool `json:"use_markdown_v2" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
|
||||
}
|
||||
|
||||
type FeishuSettings struct {
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ import (
|
|||
// ─── Test extend structs (simplified, settings + secure in one struct) ───
|
||||
|
||||
type testTelegramConfig struct {
|
||||
BaseURL string `json:"base_url" yaml:"-"`
|
||||
Proxy string `json:"proxy" yaml:"-"`
|
||||
BusinessMode bool `json:"business_mode" yaml:"-"`
|
||||
BusinessOwner string `json:"business_owner" yaml:"-"`
|
||||
UseMarkdownV2 bool `json:"use_markdown_v2" yaml:"-"`
|
||||
Streaming StreamingConfig `json:"streaming,omitempty" yaml:"-"`
|
||||
Token SecureString `json:"token,omitzero" yaml:"token,omitempty"`
|
||||
BaseURL string `json:"base_url" yaml:"-"`
|
||||
Proxy string `json:"proxy" yaml:"-"`
|
||||
BusinessMode bool `json:"business_mode" yaml:"-"`
|
||||
BusinessOwner string `json:"business_owner" yaml:"-"`
|
||||
BusinessCommandsEnable bool `json:"business_commands_enable" yaml:"-"`
|
||||
UseMarkdownV2 bool `json:"use_markdown_v2" yaml:"-"`
|
||||
Streaming StreamingConfig `json:"streaming,omitempty" yaml:"-"`
|
||||
Token SecureString `json:"token,omitzero" yaml:"token,omitempty"`
|
||||
}
|
||||
|
||||
type testDiscordConfig struct {
|
||||
|
|
@ -111,6 +112,7 @@ func TestChannel_JSON_Unmarshal(t *testing.T) {
|
|||
"base_url": "https://custom-api.example.com",
|
||||
"business_mode": true,
|
||||
"business_owner": "42",
|
||||
"business_commands_enable": true,
|
||||
"use_markdown_v2": true,
|
||||
"streaming": {"enabled": true, "throttle_seconds": 2},
|
||||
"token": "[NOT_HERE]"
|
||||
|
|
@ -132,6 +134,7 @@ func TestChannel_JSON_Unmarshal(t *testing.T) {
|
|||
assert.Equal(t, "https://custom-api.example.com", cfg.BaseURL)
|
||||
assert.True(t, cfg.BusinessMode)
|
||||
assert.Equal(t, "42", cfg.BusinessOwner)
|
||||
assert.True(t, cfg.BusinessCommandsEnable)
|
||||
assert.True(t, cfg.UseMarkdownV2)
|
||||
assert.True(t, cfg.Streaming.Enabled)
|
||||
assert.Equal(t, 2, cfg.Streaming.ThrottleSeconds)
|
||||
|
|
|
|||
|
|
@ -496,10 +496,11 @@ func defaultChannels() ChannelsConfig {
|
|||
"typing": map[string]any{"enabled": true},
|
||||
"placeholder": map[string]any{"enabled": true, "text": []string{"Thinking... 💭"}},
|
||||
"settings": map[string]any{
|
||||
"streaming": map[string]any{"enabled": true, "throttle_seconds": 3, "min_growth_chars": 200},
|
||||
"business_mode": false,
|
||||
"business_owner": "",
|
||||
"use_markdown_v2": false,
|
||||
"streaming": map[string]any{"enabled": true, "throttle_seconds": 3, "min_growth_chars": 200},
|
||||
"business_mode": false,
|
||||
"business_owner": "",
|
||||
"business_commands_enable": false,
|
||||
"use_markdown_v2": false,
|
||||
},
|
||||
},
|
||||
"feishu": map[string]any{},
|
||||
|
|
|
|||
|
|
@ -107,6 +107,18 @@ export function TelegramForm({
|
|||
placeholder="123456789"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div>
|
||||
<SwitchCardField
|
||||
label={t("channels.field.businessCommandsEnable")}
|
||||
hint={t("channels.form.desc.businessCommandsEnable")}
|
||||
checked={asBool(config.business_commands_enable)}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange("business_commands_enable", checked)
|
||||
}
|
||||
ariaLabel={t("channels.field.businessCommandsEnable")}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@
|
|||
"baseUrl": "API Base URL",
|
||||
"businessMode": "Business Mode",
|
||||
"businessOwner": "Business Owner",
|
||||
"businessCommandsEnable": "Business Commands",
|
||||
"proxy": "HTTP Proxy",
|
||||
"mentionOnly": "Mention Only",
|
||||
"typingEnabled": "Typing Indicator",
|
||||
|
|
@ -418,6 +419,7 @@
|
|||
"baseUrl": "Platform API base URL. Official endpoint is used by default.",
|
||||
"businessMode": "Receive and reply to Telegram Business messages for connected business accounts.",
|
||||
"businessOwner": "Telegram user ID of the business account owner. Business messages from this user are ignored.",
|
||||
"businessCommandsEnable": "Allow bot commands such as /new in Telegram Business chats.",
|
||||
"proxy": "HTTP proxy address for outbound network access.",
|
||||
"mentionOnly": "Only respond when the bot is explicitly mentioned in group chats.",
|
||||
"typingEnabled": "Display typing status while the assistant is generating a response.",
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@
|
|||
"baseUrl": "URL Base da API",
|
||||
"businessMode": "Modo Business",
|
||||
"businessOwner": "Proprietário Business",
|
||||
"businessCommandsEnable": "Comandos Business",
|
||||
"proxy": "Proxy HTTP",
|
||||
"mentionOnly": "Apenas com Menção",
|
||||
"typingEnabled": "Indicador de Digitação",
|
||||
|
|
@ -410,6 +411,7 @@
|
|||
"baseUrl": "URL base da API da plataforma. O endpoint oficial é usado por padrão.",
|
||||
"businessMode": "Receber e responder mensagens do Telegram Business para contas comerciais conectadas.",
|
||||
"businessOwner": "ID de usuário do Telegram do proprietário da conta comercial. Mensagens Business desse usuário são ignoradas.",
|
||||
"businessCommandsEnable": "Permitir comandos do bot, como /new, em chats do Telegram Business.",
|
||||
"proxy": "Endereço de proxy HTTP para acesso de rede de saída.",
|
||||
"mentionOnly": "Responder apenas quando o bot for explicitamente mencionado em chats em grupo.",
|
||||
"typingEnabled": "Exibir status de digitação enquanto o assistente está gerando uma resposta.",
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@
|
|||
"baseUrl": "API Base URL",
|
||||
"businessMode": "Business Mode",
|
||||
"businessOwner": "Business Owner",
|
||||
"businessCommandsEnable": "Business Commands",
|
||||
"proxy": "HTTP 代理",
|
||||
"mentionOnly": "仅提及时响应",
|
||||
"typingEnabled": "输入中提示",
|
||||
|
|
@ -418,6 +419,7 @@
|
|||
"baseUrl": "平台 API 地址,默认使用官方地址",
|
||||
"businessMode": "接收并回复已连接商业账号的 Telegram Business 消息",
|
||||
"businessOwner": "商业账号所有者的 Telegram 用户 ID。来自该用户的 Business 消息会被忽略。",
|
||||
"businessCommandsEnable": "允许在 Telegram Business 聊天中处理 /new 等机器人命令。",
|
||||
"proxy": "HTTP 代理地址,用于网络访问",
|
||||
"mentionOnly": "在群聊中仅当明确提及时才响应",
|
||||
"typingEnabled": "在生成回复时显示“正在输入”状态",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue