feat(telegram): support custom api_base_url for bot api
This commit is contained in:
parent
1265655ef0
commit
12f352420c
7 changed files with 161 additions and 5 deletions
|
|
@ -324,6 +324,7 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "YOUR_BOT_TOKEN",
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"base_url": "",
|
||||||
"allow_from": ["YOUR_USER_ID"]
|
"allow_from": ["YOUR_USER_ID"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +332,8 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We
|
||||||
```
|
```
|
||||||
|
|
||||||
> Get your user ID from `@userinfobot` on Telegram.
|
> Get your user ID from `@userinfobot` on Telegram.
|
||||||
|
>
|
||||||
|
> Set `base_url` when you need a custom Telegram Bot API endpoint (for example, local Bot API server or reverse proxy).
|
||||||
|
|
||||||
**3. Run**
|
**3. Run**
|
||||||
|
|
||||||
|
|
@ -1188,6 +1191,7 @@ picoclaw agent -m "Hello"
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "123456:ABC...",
|
"token": "123456:ABC...",
|
||||||
|
"base_url": "",
|
||||||
"allow_from": ["123456789"]
|
"allow_from": ["123456789"]
|
||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
|
|
|
||||||
|
|
@ -713,6 +713,7 @@ picoclaw agent -m "你好"
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "123456:ABC...",
|
"token": "123456:ABC...",
|
||||||
|
"base_url": "",
|
||||||
"allow_from": ["123456789"]
|
"allow_from": ["123456789"]
|
||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,14 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
||||||
var opts []telego.BotOption
|
var opts []telego.BotOption
|
||||||
telegramCfg := cfg.Channels.Telegram
|
telegramCfg := cfg.Channels.Telegram
|
||||||
|
|
||||||
|
baseURL, err := normalizeTelegramBaseURL(telegramCfg.BaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid telegram base_url %q: %w", telegramCfg.BaseURL, err)
|
||||||
|
}
|
||||||
|
if baseURL != "" {
|
||||||
|
opts = append(opts, telego.WithAPIServer(baseURL))
|
||||||
|
}
|
||||||
|
|
||||||
if telegramCfg.Proxy != "" {
|
if telegramCfg.Proxy != "" {
|
||||||
proxyURL, parseErr := url.Parse(telegramCfg.Proxy)
|
proxyURL, parseErr := url.Parse(telegramCfg.Proxy)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
|
|
@ -72,10 +80,6 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseURL := strings.TrimRight(strings.TrimSpace(telegramCfg.BaseURL), "/"); baseURL != "" {
|
|
||||||
opts = append(opts, telego.WithAPIServer(baseURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
bot, err := telego.NewBot(telegramCfg.Token, opts...)
|
bot, err := telego.NewBot(telegramCfg.Token, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
|
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
|
||||||
|
|
@ -100,6 +104,30 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeTelegramBaseURL(raw string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := strings.TrimRight(trimmed, "/")
|
||||||
|
parsed, err := url.Parse(normalized)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||||
|
return "", fmt.Errorf("scheme must be http or https")
|
||||||
|
}
|
||||||
|
if parsed.Host == "" {
|
||||||
|
return "", fmt.Errorf("host is required")
|
||||||
|
}
|
||||||
|
if parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||||
|
return "", fmt.Errorf("query and fragment are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) Start(ctx context.Context) error {
|
func (c *TelegramChannel) Start(ctx context.Context) error {
|
||||||
logger.InfoC("telegram", "Starting Telegram bot (polling mode)...")
|
logger.InfoC("telegram", "Starting Telegram bot (polling mode)...")
|
||||||
|
|
||||||
|
|
|
||||||
93
pkg/channels/telegram/telegram_baseurl_test.go
Normal file
93
pkg/channels/telegram/telegram_baseurl_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeTelegramBaseURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty value is allowed",
|
||||||
|
input: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trim spaces and trailing slash",
|
||||||
|
input: " https://telegram-proxy.example.com/custom/ ",
|
||||||
|
want: "https://telegram-proxy.example.com/custom",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing scheme",
|
||||||
|
input: "telegram-proxy.example.com",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported scheme",
|
||||||
|
input: "ftp://telegram-proxy.example.com",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query not allowed",
|
||||||
|
input: "https://telegram-proxy.example.com/api?x=1",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := normalizeTelegramBaseURL(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalizeTelegramBaseURL error: %v", err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("normalizeTelegramBaseURL() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTelegramChannel_UsesCustomBaseURL(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Channels.Telegram.Token = "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
|
||||||
|
cfg.Channels.Telegram.BaseURL = "https://telegram-proxy.example.com/custom/"
|
||||||
|
|
||||||
|
channel, err := NewTelegramChannel(cfg, bus.NewMessageBus())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTelegramChannel error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := channel.bot.FileDownloadURL("photos/abc.jpg")
|
||||||
|
wantPrefix := "https://telegram-proxy.example.com/custom/file/bot123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi/"
|
||||||
|
if !strings.HasPrefix(got, wantPrefix) {
|
||||||
|
t.Fatalf("FileDownloadURL prefix = %q, want prefix %q", got, wantPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTelegramChannel_InvalidCustomBaseURLErrors(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Channels.Telegram.Token = "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
|
||||||
|
cfg.Channels.Telegram.BaseURL = "telegram-proxy.example.com"
|
||||||
|
|
||||||
|
_, err := NewTelegramChannel(cfg, bus.NewMessageBus())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "invalid telegram base_url") {
|
||||||
|
t.Fatalf("error = %q, expected invalid base_url message", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -189,6 +189,31 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTelegramConfig_BaseURLParse(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"channels": {
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "123456:ABC",
|
||||||
|
"base_url": "https://telegram-proxy.example.com/custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Channels.Telegram.BaseURL != "https://telegram-proxy.example.com/custom" {
|
||||||
|
t.Errorf(
|
||||||
|
"Telegram.BaseURL = %q, want %q",
|
||||||
|
cfg.Channels.Telegram.BaseURL,
|
||||||
|
"https://telegram-proxy.example.com/custom",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default
|
// TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default
|
||||||
func TestDefaultConfig_HeartbeatEnabled(t *testing.T) {
|
func TestDefaultConfig_HeartbeatEnabled(t *testing.T) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
|
@ -277,6 +302,9 @@ func TestDefaultConfig_Channels(t *testing.T) {
|
||||||
if cfg.Channels.Telegram.Enabled {
|
if cfg.Channels.Telegram.Enabled {
|
||||||
t.Error("Telegram should be disabled by default")
|
t.Error("Telegram should be disabled by default")
|
||||||
}
|
}
|
||||||
|
if cfg.Channels.Telegram.BaseURL != "" {
|
||||||
|
t.Errorf("Telegram BaseURL should be empty by default, got %q", cfg.Channels.Telegram.BaseURL)
|
||||||
|
}
|
||||||
if cfg.Channels.Discord.Enabled {
|
if cfg.Channels.Discord.Enabled {
|
||||||
t.Error("Discord should be disabled by default")
|
t.Error("Discord should be disabled by default")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ func DefaultConfig() *Config {
|
||||||
Telegram: TelegramConfig{
|
Telegram: TelegramConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
Token: "",
|
Token: "",
|
||||||
|
BaseURL: "",
|
||||||
|
Proxy: "",
|
||||||
AllowFrom: FlexibleStringSlice{},
|
AllowFrom: FlexibleStringSlice{},
|
||||||
Typing: TypingConfig{Enabled: true},
|
Typing: TypingConfig{Enabled: true},
|
||||||
Placeholder: PlaceholderConfig{
|
Placeholder: PlaceholderConfig{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue