Skip to content

Commit 9d5728e

Browse files
authored
feat: implement structured Telegram command handling with a dedicated command service and telegohandler integration. (#164)
1 parent 32cb8fd commit 9d5728e

File tree

3 files changed

+212
-41
lines changed

3 files changed

+212
-41
lines changed

pkg/channels/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func (m *Manager) initChannels() error {
4848

4949
if m.config.Channels.Telegram.Enabled && m.config.Channels.Telegram.Token != "" {
5050
logger.DebugC("channels", "Attempting to initialize Telegram channel")
51-
telegram, err := NewTelegramChannel(m.config.Channels.Telegram, m.bus)
51+
telegram, err := NewTelegramChannel(m.config, m.bus)
5252
if err != nil {
5353
logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]interface{}{
5454
"error": err.Error(),

pkg/channels/telegram.go

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import (
1111
"sync"
1212
"time"
1313

14+
th "github.com/mymmrac/telego/telegohandler"
15+
1416
"github.com/mymmrac/telego"
17+
"github.com/mymmrac/telego/telegohandler"
1518
tu "github.com/mymmrac/telego/telegoutil"
1619

1720
"github.com/sipeed/picoclaw/pkg/bus"
@@ -24,7 +27,8 @@ import (
2427
type TelegramChannel struct {
2528
*BaseChannel
2629
bot *telego.Bot
27-
config config.TelegramConfig
30+
commands TelegramCommander
31+
config *config.Config
2832
chatIDs map[string]int64
2933
transcriber *voice.GroqTranscriber
3034
placeholders sync.Map // chatID -> messageID
@@ -41,13 +45,14 @@ func (c *thinkingCancel) Cancel() {
4145
}
4246
}
4347

44-
func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*TelegramChannel, error) {
48+
func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChannel, error) {
4549
var opts []telego.BotOption
50+
telegramCfg := cfg.Channels.Telegram
4651

47-
if cfg.Proxy != "" {
48-
proxyURL, parseErr := url.Parse(cfg.Proxy)
52+
if telegramCfg.Proxy != "" {
53+
proxyURL, parseErr := url.Parse(telegramCfg.Proxy)
4954
if parseErr != nil {
50-
return nil, fmt.Errorf("invalid proxy URL %q: %w", cfg.Proxy, parseErr)
55+
return nil, fmt.Errorf("invalid proxy URL %q: %w", telegramCfg.Proxy, parseErr)
5156
}
5257
opts = append(opts, telego.WithHTTPClient(&http.Client{
5358
Transport: &http.Transport{
@@ -56,15 +61,16 @@ func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*Telegr
5661
}))
5762
}
5863

59-
bot, err := telego.NewBot(cfg.Token, opts...)
64+
bot, err := telego.NewBot(telegramCfg.Token, opts...)
6065
if err != nil {
6166
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
6267
}
6368

64-
base := NewBaseChannel("telegram", cfg, bus, cfg.AllowFrom)
69+
base := NewBaseChannel("telegram", telegramCfg, bus, telegramCfg.AllowFrom)
6570

6671
return &TelegramChannel{
6772
BaseChannel: base,
73+
commands: NewTelegramCommands(bot, cfg),
6874
bot: bot,
6975
config: cfg,
7076
chatIDs: make(map[string]int64),
@@ -88,31 +94,45 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
8894
return fmt.Errorf("failed to start long polling: %w", err)
8995
}
9096

97+
bh, err := telegohandler.NewBotHandler(c.bot, updates)
98+
if err != nil {
99+
return fmt.Errorf("failed to create bot handler: %w", err)
100+
}
101+
102+
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
103+
c.commands.Help(ctx, message)
104+
return nil
105+
}, th.CommandEqual("help"))
106+
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
107+
return c.commands.Start(ctx, message)
108+
}, th.CommandEqual("start"))
109+
110+
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
111+
return c.commands.Show(ctx, message)
112+
}, th.CommandEqual("show"))
113+
114+
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
115+
return c.commands.List(ctx, message)
116+
}, th.CommandEqual("list"))
117+
118+
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
119+
return c.handleMessage(ctx, &message)
120+
}, th.AnyMessage())
121+
91122
c.setRunning(true)
92123
logger.InfoCF("telegram", "Telegram bot connected", map[string]interface{}{
93124
"username": c.bot.Username(),
94125
})
95126

127+
go bh.Start()
128+
96129
go func() {
97-
for {
98-
select {
99-
case <-ctx.Done():
100-
return
101-
case update, ok := <-updates:
102-
if !ok {
103-
logger.InfoC("telegram", "Updates channel closed, reconnecting...")
104-
return
105-
}
106-
if update.Message != nil {
107-
c.handleMessage(ctx, update)
108-
}
109-
}
110-
}
130+
<-ctx.Done()
131+
bh.Stop()
111132
}()
112133

113134
return nil
114135
}
115-
116136
func (c *TelegramChannel) Stop(ctx context.Context) error {
117137
logger.InfoC("telegram", "Stopping Telegram bot...")
118138
c.setRunning(false)
@@ -166,30 +186,27 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
166186
return nil
167187
}
168188

169-
func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Update) {
170-
message := update.Message
189+
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
171190
if message == nil {
172-
return
191+
return fmt.Errorf("message is nil")
173192
}
174193

175194
user := message.From
176195
if user == nil {
177-
return
196+
return fmt.Errorf("message sender (user) is nil")
178197
}
179198

180-
userID := fmt.Sprintf("%d", user.ID)
181-
senderID := userID
199+
senderID := fmt.Sprintf("%d", user.ID)
182200
if user.Username != "" {
183-
senderID = fmt.Sprintf("%s|%s", userID, user.Username)
201+
senderID = fmt.Sprintf("%d|%s", user.ID, user.Username)
184202
}
185203

186204
// 检查白名单,避免为被拒绝的用户下载附件
187-
if !c.IsAllowed(userID) && !c.IsAllowed(senderID) {
205+
if !c.IsAllowed(senderID) {
188206
logger.DebugCF("telegram", "Message rejected by allowlist", map[string]interface{}{
189-
"user_id": userID,
190-
"username": user.Username,
207+
"user_id": senderID,
191208
})
192-
return
209+
return nil
193210
}
194211

195212
chatID := message.Chat.ID
@@ -222,7 +239,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
222239
content += message.Caption
223240
}
224241

225-
if message.Photo != nil && len(message.Photo) > 0 {
242+
if len(message.Photo) > 0 {
226243
photo := message.Photo[len(message.Photo)-1]
227244
photoPath := c.downloadPhoto(ctx, photo.FileID)
228245
if photoPath != "" {
@@ -231,7 +248,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
231248
if content != "" {
232249
content += "\n"
233250
}
234-
content += fmt.Sprintf("[image: photo]")
251+
content += "[image: photo]"
235252
}
236253
}
237254

@@ -252,15 +269,15 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
252269
"error": err.Error(),
253270
"path": voicePath,
254271
})
255-
transcribedText = fmt.Sprintf("[voice (transcription failed)]")
272+
transcribedText = "[voice (transcription failed)]"
256273
} else {
257274
transcribedText = fmt.Sprintf("[voice transcription: %s]", result.Text)
258275
logger.InfoCF("telegram", "Voice transcribed successfully", map[string]interface{}{
259276
"text": result.Text,
260277
})
261278
}
262279
} else {
263-
transcribedText = fmt.Sprintf("[voice]")
280+
transcribedText = "[voice]"
264281
}
265282

266283
if content != "" {
@@ -278,7 +295,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
278295
if content != "" {
279296
content += "\n"
280297
}
281-
content += fmt.Sprintf("[audio]")
298+
content += "[audio]"
282299
}
283300
}
284301

@@ -290,7 +307,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
290307
if content != "" {
291308
content += "\n"
292309
}
293-
content += fmt.Sprintf("[file]")
310+
content += "[file]"
294311
}
295312
}
296313

@@ -338,7 +355,8 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
338355
"is_group": fmt.Sprintf("%t", message.Chat.Type != "private"),
339356
}
340357

341-
c.HandleMessage(senderID, fmt.Sprintf("%d", chatID), content, mediaPaths, metadata)
358+
c.HandleMessage(fmt.Sprintf("%d", user.ID), fmt.Sprintf("%d", chatID), content, mediaPaths, metadata)
359+
return nil
342360
}
343361

344362
func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string {

pkg/channels/telegram_commands.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package channels
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/mymmrac/telego"
9+
"github.com/sipeed/picoclaw/pkg/config"
10+
)
11+
12+
type TelegramCommander interface {
13+
Help(ctx context.Context, message telego.Message) error
14+
Start(ctx context.Context, message telego.Message) error
15+
Show(ctx context.Context, message telego.Message) error
16+
List(ctx context.Context, message telego.Message) error
17+
}
18+
19+
type cmd struct {
20+
bot *telego.Bot
21+
config *config.Config
22+
}
23+
24+
func NewTelegramCommands(bot *telego.Bot, cfg *config.Config) TelegramCommander {
25+
return &cmd{
26+
bot: bot,
27+
config: cfg,
28+
}
29+
}
30+
31+
func commandArgs(text string) string {
32+
parts := strings.SplitN(text, " ", 2)
33+
if len(parts) < 2 {
34+
return ""
35+
}
36+
return strings.TrimSpace(parts[1])
37+
}
38+
func (c *cmd) Help(ctx context.Context, message telego.Message) error {
39+
msg := `/start - Start the bot
40+
/help - Show this help message
41+
/show [model|channel] - Show current configuration
42+
/list [models|channels] - List available options
43+
`
44+
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
45+
ChatID: telego.ChatID{ID: message.Chat.ID},
46+
Text: msg,
47+
ReplyParameters: &telego.ReplyParameters{
48+
MessageID: message.MessageID,
49+
},
50+
})
51+
return err
52+
}
53+
54+
func (c *cmd) Start(ctx context.Context, message telego.Message) error {
55+
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
56+
ChatID: telego.ChatID{ID: message.Chat.ID},
57+
Text: "Hello! I am PicoClaw 🦞",
58+
ReplyParameters: &telego.ReplyParameters{
59+
MessageID: message.MessageID,
60+
},
61+
})
62+
return err
63+
}
64+
65+
func (c *cmd) Show(ctx context.Context, message telego.Message) error {
66+
args := commandArgs(message.Text)
67+
if args == "" {
68+
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
69+
ChatID: telego.ChatID{ID: message.Chat.ID},
70+
Text: "Usage: /show [model|channel]",
71+
ReplyParameters: &telego.ReplyParameters{
72+
MessageID: message.MessageID,
73+
},
74+
})
75+
return err
76+
}
77+
78+
var response string
79+
switch args {
80+
case "model":
81+
response = fmt.Sprintf("Current Model: %s (Provider: %s)",
82+
c.config.Agents.Defaults.Model,
83+
c.config.Agents.Defaults.Provider)
84+
case "channel":
85+
response = "Current Channel: telegram"
86+
default:
87+
response = fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args)
88+
}
89+
90+
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
91+
ChatID: telego.ChatID{ID: message.Chat.ID},
92+
Text: response,
93+
ReplyParameters: &telego.ReplyParameters{
94+
MessageID: message.MessageID,
95+
},
96+
})
97+
return err
98+
}
99+
func (c *cmd) List(ctx context.Context, message telego.Message) error {
100+
args := commandArgs(message.Text)
101+
if args == "" {
102+
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
103+
ChatID: telego.ChatID{ID: message.Chat.ID},
104+
Text: "Usage: /list [models|channels]",
105+
ReplyParameters: &telego.ReplyParameters{
106+
MessageID: message.MessageID,
107+
},
108+
})
109+
return err
110+
}
111+
112+
var response string
113+
switch args {
114+
case "models":
115+
provider := c.config.Agents.Defaults.Provider
116+
if provider == "" {
117+
provider = "configured default"
118+
}
119+
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.yaml",
120+
c.config.Agents.Defaults.Model, provider)
121+
122+
case "channels":
123+
var enabled []string
124+
if c.config.Channels.Telegram.Enabled {
125+
enabled = append(enabled, "telegram")
126+
}
127+
if c.config.Channels.WhatsApp.Enabled {
128+
enabled = append(enabled, "whatsapp")
129+
}
130+
if c.config.Channels.Feishu.Enabled {
131+
enabled = append(enabled, "feishu")
132+
}
133+
if c.config.Channels.Discord.Enabled {
134+
enabled = append(enabled, "discord")
135+
}
136+
if c.config.Channels.Slack.Enabled {
137+
enabled = append(enabled, "slack")
138+
}
139+
response = fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- "))
140+
141+
default:
142+
response = fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args)
143+
}
144+
145+
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
146+
ChatID: telego.ChatID{ID: message.Chat.ID},
147+
Text: response,
148+
ReplyParameters: &telego.ReplyParameters{
149+
MessageID: message.MessageID,
150+
},
151+
})
152+
return err
153+
}

0 commit comments

Comments
 (0)