mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
feat(telegram): migrate to go-telegram/bot and add forum topic support (#321)
* feat(telegram): migrate to go-telegram/bot and add forum topic support
Migrate the Telegram platform from the unmaintained go-telegram-bot-api/v5
(last release 2021) to go-telegram/bot v1.20.0, which is actively maintained
and supports the full Telegram Bot API including forum topics.
Key changes:
- **Library migration**: Replace go-telegram-bot-api/v5 with go-telegram/bot.
Redesign the telegramBot interface around the new library's typed API methods
(SendMessage, SendPhoto, EditMessageText, etc.) instead of the old generic
Send/Request pattern. The *bot.Bot type satisfies the interface directly.
- **Forum topic support**: Add threadID to replyContext and session key.
Messages in forum groups (IsForum=true) now include the MessageThreadID
in the session key (telegram:{chatID}:{threadID}:{userID}), giving each
topic its own independent agent session. All send methods propagate
MessageThreadID so replies land in the correct topic. Non-forum groups
and private chats are unaffected.
- **Per-topic workspace binding**: Add ChannelKey field to core.Message so
platforms can provide an explicit channel identifier for workspace binding.
Telegram sets this to "chatID:threadID" for forum topics, enabling
multi-workspace mode to bind different topics to different project
directories. Add effectiveChannelID/effectiveWorkspaceChannelKey helpers
and update all workspace resolution call sites in the engine.
- **Skill command registration fix**: GetAllCommands now respects
disabled_commands for skills, matching the existing behavior for
built-in commands. This prevents disabled skills from being registered
as Telegram bot menu commands.
- **Connection management**: Simplify the reconnect architecture.
bot.New() handles initial validation, bot.Start(ctx) manages long
polling internally. Remove the stale bot detection (generation
comparison in handlers) since the new library's blocking Start()
ensures handlers are never called from a superseded bot instance.
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
* fix: resolve lint errors exposed by engine.go and telegram_test.go changes
- Remove unused assignment to `s` in cmdNew (SA4006)
- Remove unused stubBotFactory and failingBotFactory helpers in telegram tests
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
---------
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
This commit is contained in:
@@ -1225,8 +1225,8 @@ func (e *Engine) handleMessage(p Platform, msg *Message) {
|
||||
var wsSessions *SessionManager
|
||||
var resolvedWorkspace string
|
||||
if e.multiWorkspace {
|
||||
channelID := extractChannelID(msg.SessionKey)
|
||||
channelKey := extractWorkspaceChannelKey(msg.SessionKey)
|
||||
channelID := effectiveChannelID(msg)
|
||||
channelKey := effectiveWorkspaceChannelKey(msg)
|
||||
workspace, channelName, err := e.resolveWorkspace(p, channelID)
|
||||
if err != nil {
|
||||
slog.Error("workspace resolution failed", "err", err)
|
||||
@@ -2914,8 +2914,8 @@ func (e *Engine) handleCommand(p Platform, msg *Message, raw string) bool {
|
||||
}
|
||||
|
||||
func (e *Engine) handleWorkspaceCommand(p Platform, msg *Message, args []string) {
|
||||
channelID := extractChannelID(msg.SessionKey)
|
||||
channelKey := extractWorkspaceChannelKey(msg.SessionKey)
|
||||
channelID := effectiveChannelID(msg)
|
||||
channelKey := effectiveWorkspaceChannelKey(msg)
|
||||
projectKey := "project:" + e.name
|
||||
resolveChannelName := func() func() string {
|
||||
resolved := false
|
||||
@@ -3372,7 +3372,7 @@ func (e *Engine) cmdShell(p Platform, msg *Message, raw string) {
|
||||
// In multi-workspace mode, resolve workspace directory for this channel
|
||||
var workDir string
|
||||
if e.multiWorkspace {
|
||||
channelKey := extractWorkspaceChannelKey(msg.SessionKey)
|
||||
channelKey := effectiveWorkspaceChannelKey(msg)
|
||||
if b, _, usable := e.lookupEffectiveWorkspaceBinding(channelKey); usable {
|
||||
workDir = normalizeWorkspacePath(b.Workspace)
|
||||
}
|
||||
@@ -4631,10 +4631,14 @@ func (e *Engine) GetAllCommands() []BotCommandInfo {
|
||||
|
||||
// Collect skills
|
||||
for _, s := range e.skills.ListAll() {
|
||||
if seenCmds[strings.ToLower(s.Name)] {
|
||||
lowerName := strings.ToLower(s.Name)
|
||||
if seenCmds[lowerName] {
|
||||
continue
|
||||
}
|
||||
seenCmds[strings.ToLower(s.Name)] = true
|
||||
if disabledCmds[lowerName] {
|
||||
continue
|
||||
}
|
||||
seenCmds[lowerName] = true
|
||||
|
||||
desc := s.Description
|
||||
if desc == "" {
|
||||
@@ -9140,14 +9144,32 @@ func extractWorkspaceChannelKey(sessionKey string) string {
|
||||
return workspaceChannelKey(extractPlatformName(sessionKey), extractChannelID(sessionKey))
|
||||
}
|
||||
|
||||
// effectiveChannelID returns the channel identifier from a Message.
|
||||
// It prefers the platform-provided ChannelKey (e.g. "chatID:threadID" for forum topics)
|
||||
// and falls back to parsing the session key.
|
||||
func effectiveChannelID(msg *Message) string {
|
||||
if msg.ChannelKey != "" {
|
||||
return msg.ChannelKey
|
||||
}
|
||||
return extractChannelID(msg.SessionKey)
|
||||
}
|
||||
|
||||
// effectiveWorkspaceChannelKey returns the workspace binding key from a Message.
|
||||
func effectiveWorkspaceChannelKey(msg *Message) string {
|
||||
if msg.ChannelKey != "" {
|
||||
return workspaceChannelKey(msg.Platform, msg.ChannelKey)
|
||||
}
|
||||
return extractWorkspaceChannelKey(msg.SessionKey)
|
||||
}
|
||||
|
||||
// commandContext resolves the appropriate agent, session manager, and interactive key
|
||||
// for a command. In multi-workspace mode, it routes to the bound workspace if present.
|
||||
func (e *Engine) commandContext(p Platform, msg *Message) (Agent, *SessionManager, string, error) {
|
||||
if !e.multiWorkspace {
|
||||
return e.agent, e.sessions, msg.SessionKey, nil
|
||||
}
|
||||
channelID := extractChannelID(msg.SessionKey)
|
||||
channelKey := extractWorkspaceChannelKey(msg.SessionKey)
|
||||
channelID := effectiveChannelID(msg)
|
||||
channelKey := effectiveWorkspaceChannelKey(msg)
|
||||
if channelKey == "" || channelID == "" {
|
||||
return e.agent, e.sessions, msg.SessionKey, nil
|
||||
}
|
||||
@@ -9271,7 +9293,7 @@ func (e *Engine) resolveWorkspace(p Platform, channelID string) (string, string,
|
||||
// handleWorkspaceInitFlow manages the conversational workspace setup.
|
||||
// Returns true if the message was consumed by the init flow.
|
||||
func (e *Engine) handleWorkspaceInitFlow(p Platform, msg *Message, channelName string) bool {
|
||||
channelKey := extractWorkspaceChannelKey(msg.SessionKey)
|
||||
channelKey := effectiveWorkspaceChannelKey(msg)
|
||||
|
||||
e.initFlowsMu.Lock()
|
||||
flow, exists := e.initFlows[channelKey]
|
||||
|
||||
@@ -138,6 +138,7 @@ type Message struct {
|
||||
Images []ImageAttachment // attached images (if any)
|
||||
Files []FileAttachment // attached files (if any)
|
||||
Audio *AudioAttachment // voice message (if any)
|
||||
ChannelKey string // platform-provided channel identifier for workspace binding (optional)
|
||||
ReplyCtx any // platform-specific context needed for replying
|
||||
FromVoice bool // true if message originated from voice transcription
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -9,7 +9,7 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/go-telegram/bot v1.20.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
|
||||
github.com/line/line-bot-sdk-go/v8 v8.19.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -34,8 +34,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/go-telegram/bot v1.20.0 h1:4Pea/qTidSspr4WBJw9FbHUMNhYeqszBqQUfsQEyFbc=
|
||||
github.com/go-telegram/bot v1.20.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,9 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
tgbot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
)
|
||||
|
||||
type testLifecycleHandler struct {
|
||||
@@ -67,133 +69,174 @@ func (t *stubTypingTicker) C() <-chan time.Time {
|
||||
func (t *stubTypingTicker) Stop() {}
|
||||
|
||||
type stubTelegramBot struct {
|
||||
userName string
|
||||
userID int64
|
||||
token string
|
||||
mu sync.Mutex
|
||||
sendMessageCalls int
|
||||
sendPhotoCalls int
|
||||
sendDocumentCalls int
|
||||
sendVoiceCalls int
|
||||
sendAudioCalls int
|
||||
sendChatActionCalls int
|
||||
editMessageTextCalls int
|
||||
deleteMessageCalls int
|
||||
answerCallbackCalls int
|
||||
setMyCommandsCalls int
|
||||
getFileCalls int
|
||||
|
||||
mu sync.Mutex
|
||||
updates chan tgbotapi.Update
|
||||
sendErr error
|
||||
requestErr error
|
||||
fileErr error
|
||||
file tgbotapi.File
|
||||
stopCalls int
|
||||
sendCalls int
|
||||
requestCalls int
|
||||
getFileCalls int
|
||||
sendErr error
|
||||
getFileErr error
|
||||
file *models.File
|
||||
}
|
||||
|
||||
func newStubTelegramBot(userName string) *stubTelegramBot {
|
||||
func newStubTelegramBot() *stubTelegramBot {
|
||||
return &stubTelegramBot{
|
||||
userName: userName,
|
||||
userID: 42,
|
||||
token: "token",
|
||||
updates: make(chan tgbotapi.Update),
|
||||
file: tgbotapi.File{FilePath: "files/test.dat"},
|
||||
file: &models.File{FilePath: "files/test.dat"},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) SelfUser() tgbotapi.User {
|
||||
return tgbotapi.User{ID: b.userID, UserName: b.userName}
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) Token() string {
|
||||
return b.token
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) Send(tgbotapi.Chattable) (tgbotapi.Message, error) {
|
||||
func (b *stubTelegramBot) SendMessage(_ context.Context, _ *tgbot.SendMessageParams) (*models.Message, error) {
|
||||
b.mu.Lock()
|
||||
b.sendCalls++
|
||||
b.sendMessageCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return tgbotapi.Message{}, b.sendErr
|
||||
return nil, b.sendErr
|
||||
}
|
||||
return tgbotapi.Message{MessageID: 99}, nil
|
||||
return &models.Message{ID: 99}, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) Request(tgbotapi.Chattable) (*tgbotapi.APIResponse, error) {
|
||||
func (b *stubTelegramBot) SendPhoto(_ context.Context, _ *tgbot.SendPhotoParams) (*models.Message, error) {
|
||||
b.mu.Lock()
|
||||
b.requestCalls++
|
||||
b.sendPhotoCalls++
|
||||
b.mu.Unlock()
|
||||
if b.requestErr != nil {
|
||||
return nil, b.requestErr
|
||||
if b.sendErr != nil {
|
||||
return nil, b.sendErr
|
||||
}
|
||||
return &tgbotapi.APIResponse{Ok: true}, nil
|
||||
return &models.Message{ID: 99}, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) GetUpdates(tgbotapi.UpdateConfig) ([]tgbotapi.Update, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) GetUpdatesChan(tgbotapi.UpdateConfig) tgbotapi.UpdatesChannel {
|
||||
return b.updates
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) StopReceivingUpdates() {
|
||||
func (b *stubTelegramBot) SendDocument(_ context.Context, _ *tgbot.SendDocumentParams) (*models.Message, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.stopCalls++
|
||||
select {
|
||||
case <-b.updates:
|
||||
default:
|
||||
b.sendDocumentCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return nil, b.sendErr
|
||||
}
|
||||
select {
|
||||
case <-b.updates:
|
||||
default:
|
||||
}
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
close(b.updates)
|
||||
return &models.Message{ID: 99}, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) GetFile(tgbotapi.FileConfig) (tgbotapi.File, error) {
|
||||
func (b *stubTelegramBot) SendVoice(_ context.Context, _ *tgbot.SendVoiceParams) (*models.Message, error) {
|
||||
b.mu.Lock()
|
||||
b.sendVoiceCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return nil, b.sendErr
|
||||
}
|
||||
return &models.Message{ID: 99}, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) SendAudio(_ context.Context, _ *tgbot.SendAudioParams) (*models.Message, error) {
|
||||
b.mu.Lock()
|
||||
b.sendAudioCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return nil, b.sendErr
|
||||
}
|
||||
return &models.Message{ID: 99}, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) SendChatAction(_ context.Context, _ *tgbot.SendChatActionParams) (bool, error) {
|
||||
b.mu.Lock()
|
||||
b.sendChatActionCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return false, b.sendErr
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) EditMessageText(_ context.Context, _ *tgbot.EditMessageTextParams) (*models.Message, error) {
|
||||
b.mu.Lock()
|
||||
b.editMessageTextCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return nil, b.sendErr
|
||||
}
|
||||
return &models.Message{ID: 99}, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) DeleteMessage(_ context.Context, _ *tgbot.DeleteMessageParams) (bool, error) {
|
||||
b.mu.Lock()
|
||||
b.deleteMessageCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return false, b.sendErr
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) AnswerCallbackQuery(_ context.Context, _ *tgbot.AnswerCallbackQueryParams) (bool, error) {
|
||||
b.mu.Lock()
|
||||
b.answerCallbackCalls++
|
||||
b.mu.Unlock()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) SetMyCommands(_ context.Context, _ *tgbot.SetMyCommandsParams) (bool, error) {
|
||||
b.mu.Lock()
|
||||
b.setMyCommandsCalls++
|
||||
b.mu.Unlock()
|
||||
if b.sendErr != nil {
|
||||
return false, b.sendErr
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) GetFile(_ context.Context, _ *tgbot.GetFileParams) (*models.File, error) {
|
||||
b.mu.Lock()
|
||||
b.getFileCalls++
|
||||
b.mu.Unlock()
|
||||
if b.fileErr != nil {
|
||||
return tgbotapi.File{}, b.fileErr
|
||||
if b.getFileErr != nil {
|
||||
return nil, b.getFileErr
|
||||
}
|
||||
return b.file, nil
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) StopCalls() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.stopCalls
|
||||
func (b *stubTelegramBot) FileDownloadLink(f *models.File) string {
|
||||
return "https://test.example.com/file/" + f.FilePath
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) SendCalls() int {
|
||||
func (b *stubTelegramBot) SendMessageCallCount() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.sendCalls
|
||||
return b.sendMessageCalls
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) GetFileCalls() int {
|
||||
func (b *stubTelegramBot) SendChatActionCallCount() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.sendChatActionCalls
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) GetFileCallCount() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.getFileCalls
|
||||
}
|
||||
|
||||
func (b *stubTelegramBot) RequestCalls() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.requestCalls
|
||||
}
|
||||
|
||||
func TestPlatformStart_RetriesInBackgroundUntilConnected(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
readyCh := make(chan struct{}, 1)
|
||||
connectedBot := newStubTelegramBot("mybot")
|
||||
stubBot := newStubTelegramBot()
|
||||
me := &models.User{ID: 42, Username: "mybot"}
|
||||
|
||||
p := &Platform{
|
||||
token: "token",
|
||||
httpClient: &http.Client{},
|
||||
newBot: func(string, *http.Client) (telegramBot, error) { //nolint:nonamedreturns
|
||||
newBot: func(_ string, _ func(context.Context, *models.Update), _ *http.Client) (telegramBot, *models.User, func(context.Context), error) {
|
||||
if attempts.Add(1) == 1 {
|
||||
return nil, errors.New("dial failed")
|
||||
return nil, nil, nil, errors.New("dial failed")
|
||||
}
|
||||
return connectedBot, nil
|
||||
return stubBot, me, func(ctx context.Context) { <-ctx.Done() }, nil
|
||||
},
|
||||
newBackoffTimer: immediateTimer,
|
||||
}
|
||||
@@ -227,15 +270,17 @@ func TestPlatformStart_InitialConnectFailureEmitsUnavailableOnceBeforeReady(t *t
|
||||
var attempts atomic.Int32
|
||||
var unavailableCount atomic.Int32
|
||||
readyCh := make(chan struct{}, 1)
|
||||
stubBot := newStubTelegramBot()
|
||||
me := &models.User{ID: 42, Username: "mybot"}
|
||||
|
||||
p := &Platform{
|
||||
token: "token",
|
||||
httpClient: &http.Client{},
|
||||
newBot: func(string, *http.Client) (telegramBot, error) {
|
||||
newBot: func(_ string, _ func(context.Context, *models.Update), _ *http.Client) (telegramBot, *models.User, func(context.Context), error) {
|
||||
if attempts.Add(1) <= 2 {
|
||||
return nil, errors.New("dial failed")
|
||||
return nil, nil, nil, errors.New("dial failed")
|
||||
}
|
||||
return newStubTelegramBot("mybot"), nil
|
||||
return stubBot, me, func(ctx context.Context) { <-ctx.Done() }, nil
|
||||
},
|
||||
newBackoffTimer: immediateTimer,
|
||||
}
|
||||
@@ -271,7 +316,7 @@ func TestPlatformStart_InitialConnectFailureEmitsUnavailableOnceBeforeReady(t *t
|
||||
func TestPlatformDisconnectedSendPathsReturnNotConnected(t *testing.T) {
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
ctx := context.Background()
|
||||
rctx := replyContext{chatID: 1, messageID: 2}
|
||||
rctx := replyContext{chatID: 1, threadID: 0, messageID: 2}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -321,16 +366,17 @@ func TestPlatformLateReadyIgnoredAfterStop(t *testing.T) {
|
||||
connectDone := make(chan struct{})
|
||||
readyCh := make(chan struct{}, 1)
|
||||
unavailableCh := make(chan error, 1)
|
||||
lateBot := newStubTelegramBot("latebot")
|
||||
stubBot := newStubTelegramBot()
|
||||
me := &models.User{ID: 42, Username: "latebot"}
|
||||
|
||||
p := &Platform{
|
||||
token: "token",
|
||||
httpClient: &http.Client{},
|
||||
newBot: func(string, *http.Client) (telegramBot, error) {
|
||||
newBot: func(_ string, _ func(context.Context, *models.Update), _ *http.Client) (telegramBot, *models.User, func(context.Context), error) {
|
||||
close(connectStarted)
|
||||
defer close(connectDone)
|
||||
<-releaseConnect
|
||||
return lateBot, nil
|
||||
return stubBot, me, func(ctx context.Context) { <-ctx.Done() }, nil
|
||||
},
|
||||
newBackoffTimer: immediateTimer,
|
||||
}
|
||||
@@ -360,67 +406,11 @@ func TestPlatformLateReadyIgnoredAfterStop(t *testing.T) {
|
||||
t.Fatalf("unexpected unavailable callback after Stop: %v", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
if got := lateBot.StopCalls(); got != 1 {
|
||||
t.Fatalf("StopReceivingUpdates calls = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformStaleGenerationCleanupDoesNotClobberNewerBot(t *testing.T) {
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
oldBot := newStubTelegramBot("old")
|
||||
newBot := newStubTelegramBot("new")
|
||||
|
||||
oldGen, ok := p.publishConnectedBot(oldBot)
|
||||
if !ok {
|
||||
t.Fatal("failed to publish old bot")
|
||||
}
|
||||
newGen, ok := p.publishConnectedBot(newBot)
|
||||
if !ok {
|
||||
t.Fatal("failed to publish new bot")
|
||||
}
|
||||
|
||||
p.finishConnection(oldGen, oldBot, errors.New("old lost"), false)
|
||||
|
||||
gotBot, gotGen, err := p.currentBot()
|
||||
if err != nil {
|
||||
t.Fatalf("currentBot: %v", err)
|
||||
}
|
||||
if gotBot != newBot {
|
||||
t.Fatal("stale cleanup replaced current bot")
|
||||
}
|
||||
if gotGen != newGen {
|
||||
t.Fatalf("generation = %d, want %d", gotGen, newGen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformUnavailableEmittedExactlyOnceForCurrentLiveGeneration(t *testing.T) {
|
||||
var unavailableCount atomic.Int32
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
bot := newStubTelegramBot("live")
|
||||
|
||||
p.SetLifecycleHandler(testLifecycleHandler{
|
||||
onUnavailable: func(core.Platform, error) {
|
||||
unavailableCount.Add(1)
|
||||
},
|
||||
})
|
||||
|
||||
gen, ok := p.publishConnectedBot(bot)
|
||||
if !ok {
|
||||
t.Fatal("failed to publish bot")
|
||||
}
|
||||
|
||||
p.finishConnection(gen, bot, errors.New("lost"), false)
|
||||
p.finishConnection(gen, bot, errors.New("lost again"), false)
|
||||
|
||||
if got := unavailableCount.Load(); got != 1 {
|
||||
t.Fatalf("unavailable callbacks = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformStartTypingSwitchesToCurrentBotAfterReconnect(t *testing.T) {
|
||||
oldBot := newStubTelegramBot("old")
|
||||
newBot := newStubTelegramBot("new")
|
||||
oldBot := newStubTelegramBot()
|
||||
newBot := newStubTelegramBot()
|
||||
ticker := newStubTypingTicker()
|
||||
|
||||
p := &Platform{
|
||||
@@ -431,126 +421,31 @@ func TestPlatformStartTypingSwitchesToCurrentBotAfterReconnect(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if _, ok := p.publishConnectedBot(oldBot); !ok {
|
||||
t.Fatal("failed to publish old bot")
|
||||
}
|
||||
me := &models.User{ID: 42, Username: "old"}
|
||||
p.publishBot(oldBot, me)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
stop := p.StartTyping(ctx, replyContext{chatID: 1, messageID: 2})
|
||||
stop := p.StartTyping(ctx, replyContext{chatID: 1, threadID: 0, messageID: 2})
|
||||
defer func() {
|
||||
stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if got := oldBot.RequestCalls(); got != 1 {
|
||||
t.Fatalf("old bot request calls after initial typing = %d, want 1", got)
|
||||
if got := oldBot.SendChatActionCallCount(); got != 1 {
|
||||
t.Fatalf("old bot action calls after initial typing = %d, want 1", got)
|
||||
}
|
||||
|
||||
if _, ok := p.publishConnectedBot(newBot); !ok {
|
||||
t.Fatal("failed to publish new bot")
|
||||
}
|
||||
me2 := &models.User{ID: 42, Username: "new"}
|
||||
p.publishBot(newBot, me2)
|
||||
|
||||
ticker.ch <- time.Now()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
if got := oldBot.RequestCalls(); got != 1 {
|
||||
t.Fatalf("old bot request calls after reconnect tick = %d, want 1", got)
|
||||
if got := oldBot.SendChatActionCallCount(); got != 1 {
|
||||
t.Fatalf("old bot action calls after reconnect tick = %d, want 1", got)
|
||||
}
|
||||
if got := newBot.RequestCalls(); got != 1 {
|
||||
t.Fatalf("new bot request calls after reconnect tick = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformDownloadFileRejectsStaleBot(t *testing.T) {
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
oldBot := newStubTelegramBot("old")
|
||||
newBot := newStubTelegramBot("new")
|
||||
|
||||
if _, ok := p.publishConnectedBot(oldBot); !ok {
|
||||
t.Fatal("failed to publish old bot")
|
||||
}
|
||||
if _, ok := p.publishConnectedBot(newBot); !ok {
|
||||
t.Fatal("failed to publish new bot")
|
||||
}
|
||||
|
||||
_, err := p.downloadFile(oldBot, "file-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected stale bot error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not connected") {
|
||||
t.Fatalf("error = %q, want to contain %q", err.Error(), "not connected")
|
||||
}
|
||||
if got := oldBot.GetFileCalls(); got != 0 {
|
||||
t.Fatalf("old bot getFile calls = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformHandleCallbackQueryRejectsStaleBot(t *testing.T) {
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
oldBot := newStubTelegramBot("old")
|
||||
newBot := newStubTelegramBot("new")
|
||||
handled := make(chan *core.Message, 1)
|
||||
|
||||
p.handler = func(_ core.Platform, msg *core.Message) {
|
||||
handled <- msg
|
||||
}
|
||||
|
||||
if _, ok := p.publishConnectedBot(oldBot); !ok {
|
||||
t.Fatal("failed to publish old bot")
|
||||
}
|
||||
if _, ok := p.publishConnectedBot(newBot); !ok {
|
||||
t.Fatal("failed to publish new bot")
|
||||
}
|
||||
|
||||
callbackData := "perm:allow"
|
||||
p.handleCallbackQuery(oldBot, &tgbotapi.CallbackQuery{
|
||||
ID: "cb1",
|
||||
Data: callbackData,
|
||||
From: &tgbotapi.User{ID: 7, UserName: "alice"},
|
||||
Message: &tgbotapi.Message{
|
||||
MessageID: 10,
|
||||
Text: "perm?",
|
||||
Chat: &tgbotapi.Chat{ID: 1, Type: "private"},
|
||||
ReplyMarkup: &tgbotapi.InlineKeyboardMarkup{},
|
||||
},
|
||||
})
|
||||
|
||||
if got := oldBot.RequestCalls(); got != 0 {
|
||||
t.Fatalf("old bot request calls = %d, want 0", got)
|
||||
}
|
||||
if got := oldBot.SendCalls(); got != 0 {
|
||||
t.Fatalf("old bot send calls = %d, want 0", got)
|
||||
}
|
||||
|
||||
select {
|
||||
case msg := <-handled:
|
||||
t.Fatalf("unexpected handled callback message: %+v", msg)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformIsDirectedAtBotRejectsStaleBot(t *testing.T) {
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
oldBot := newStubTelegramBot("oldbot")
|
||||
newBot := newStubTelegramBot("newbot")
|
||||
|
||||
if _, ok := p.publishConnectedBot(oldBot); !ok {
|
||||
t.Fatal("failed to publish old bot")
|
||||
}
|
||||
if _, ok := p.publishConnectedBot(newBot); !ok {
|
||||
t.Fatal("failed to publish new bot")
|
||||
}
|
||||
|
||||
msg := &tgbotapi.Message{
|
||||
Text: "/help@oldbot",
|
||||
Chat: &tgbotapi.Chat{ID: 1, Type: "group"},
|
||||
Entities: []tgbotapi.MessageEntity{
|
||||
{Type: "bot_command", Offset: 0, Length: len("/help@oldbot")},
|
||||
},
|
||||
}
|
||||
|
||||
if got := p.isDirectedAtBot(oldBot, msg); got {
|
||||
t.Fatal("stale bot should not be treated as current bot target")
|
||||
if got := newBot.SendChatActionCallCount(); got != 1 {
|
||||
t.Fatalf("new bot action calls after reconnect tick = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,6 +541,76 @@ func TestExtractEntityText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAudioRejectsInvalidReplyContext(t *testing.T) {
|
||||
p := &Platform{}
|
||||
|
||||
err := p.SendAudio(context.Background(), "bad-context", []byte("data"), "mp3")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid reply context")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "telegram: SendAudio: invalid reply context type") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAudioReturnsConversionErrorForWAV(t *testing.T) {
|
||||
orig := telegramConvertAudioToOpus
|
||||
t.Cleanup(func() { telegramConvertAudioToOpus = orig })
|
||||
|
||||
telegramConvertAudioToOpus = func(_ context.Context, _ []byte, _ string) ([]byte, error) {
|
||||
return nil, errors.New("mock conversion failure")
|
||||
}
|
||||
|
||||
stubBot := newStubTelegramBot()
|
||||
p := &Platform{}
|
||||
p.bot = stubBot
|
||||
p.selfUser = &models.User{ID: 1, Username: "testbot"}
|
||||
|
||||
err := p.SendAudio(context.Background(), replyContext{chatID: 123}, []byte("wav-data"), "wav")
|
||||
if err == nil {
|
||||
t.Fatal("expected conversion error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "telegram: SendAudio: convert wav to opus") {
|
||||
t.Fatalf("unexpected error prefix: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mock conversion failure") {
|
||||
t.Fatalf("expected wrapped conversion error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateTelegramBotDescription_UTF8Safe(t *testing.T) {
|
||||
t.Parallel()
|
||||
cjk := strings.Repeat("你", 200)
|
||||
out := truncateTelegramBotDescription(cjk)
|
||||
if !utf8.ValidString(out) {
|
||||
t.Fatal("invalid UTF-8 from CJK description")
|
||||
}
|
||||
if got, max := utf8.RuneCountInString(out), 256; got > max {
|
||||
t.Fatalf("rune count %d > %d", got, max)
|
||||
}
|
||||
|
||||
long := strings.Repeat("b", 260)
|
||||
out2 := truncateTelegramBotDescription(long)
|
||||
if want := 256; utf8.RuneCountInString(out2) != want {
|
||||
t.Fatalf("ascii truncation: got %d runes want %d", utf8.RuneCountInString(out2), want)
|
||||
}
|
||||
if !utf8.ValidString(out2) {
|
||||
t.Fatal("invalid UTF-8 after ascii truncation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateForLog_UTF8Safe(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := strings.Repeat("世", 50) // 50 runes
|
||||
out := truncateForLog(s, 10)
|
||||
if !utf8.ValidString(out) {
|
||||
t.Fatal("invalid UTF-8")
|
||||
}
|
||||
if utf8.RuneCountInString(out) != 13 { // 10 + "..."
|
||||
t.Fatalf("got %d runes", utf8.RuneCountInString(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAudioMP3PrefersVoice(t *testing.T) {
|
||||
var paths []string
|
||||
p := newTelegramTestPlatform(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -728,69 +693,242 @@ func TestSendAudioFallsBackToSendAudioForMP3(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAudioRejectsInvalidReplyContext(t *testing.T) {
|
||||
p := &Platform{}
|
||||
|
||||
err := p.SendAudio(context.Background(), "bad-context", []byte("data"), "mp3")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid reply context")
|
||||
func TestBuildSessionKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
shared bool
|
||||
chatID int64
|
||||
thread int
|
||||
userID int64
|
||||
want string
|
||||
}{
|
||||
{name: "private no topic", shared: false, chatID: 100, thread: 0, userID: 7, want: "telegram:100:7"},
|
||||
{name: "private with topic", shared: false, chatID: 100, thread: 42, userID: 7, want: "telegram:100:42:7"},
|
||||
{name: "shared no topic", shared: true, chatID: 100, thread: 0, userID: 7, want: "telegram:100"},
|
||||
{name: "shared with topic", shared: true, chatID: 100, thread: 42, userID: 7, want: "telegram:100:42"},
|
||||
}
|
||||
if !strings.Contains(err.Error(), "telegram: SendAudio: invalid reply context type") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Platform{shareSessionInChannel: tt.shared}
|
||||
got := p.buildSessionKey(tt.chatID, tt.thread, tt.userID)
|
||||
if got != tt.want {
|
||||
t.Fatalf("buildSessionKey(%d, %d, %d) = %q, want %q", tt.chatID, tt.thread, tt.userID, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAudioReturnsConversionErrorForWAV(t *testing.T) {
|
||||
orig := telegramConvertAudioToOpus
|
||||
t.Cleanup(func() { telegramConvertAudioToOpus = orig })
|
||||
|
||||
telegramConvertAudioToOpus = func(_ context.Context, _ []byte, _ string) ([]byte, error) {
|
||||
return nil, errors.New("mock conversion failure")
|
||||
func TestReconstructReplyCtx(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
shared bool
|
||||
key string
|
||||
wantChat int64
|
||||
wantThr int
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "shared no topic", shared: true, key: "telegram:100", wantChat: 100, wantThr: 0},
|
||||
{name: "shared with topic", shared: true, key: "telegram:100:42", wantChat: 100, wantThr: 42},
|
||||
{name: "per-user no topic", shared: false, key: "telegram:100:7", wantChat: 100, wantThr: 0},
|
||||
{name: "per-user with topic", shared: false, key: "telegram:100:42:7", wantChat: 100, wantThr: 42},
|
||||
{name: "invalid prefix", shared: false, key: "slack:100:7", wantErr: true},
|
||||
{name: "too short", shared: false, key: "telegram", wantErr: true},
|
||||
}
|
||||
|
||||
p := &Platform{bot: &botAPIWrapper{BotAPI: &tgbotapi.BotAPI{}}}
|
||||
err := p.SendAudio(context.Background(), replyContext{chatID: 123}, []byte("wav-data"), "wav")
|
||||
if err == nil {
|
||||
t.Fatal("expected conversion error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "telegram: SendAudio: convert wav to opus") {
|
||||
t.Fatalf("unexpected error prefix: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mock conversion failure") {
|
||||
t.Fatalf("expected wrapped conversion error, got: %v", err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Platform{shareSessionInChannel: tt.shared}
|
||||
rctx, err := p.ReconstructReplyCtx(tt.key)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
rc := rctx.(replyContext)
|
||||
if rc.chatID != tt.wantChat {
|
||||
t.Fatalf("chatID = %d, want %d", rc.chatID, tt.wantChat)
|
||||
}
|
||||
if rc.threadID != tt.wantThr {
|
||||
t.Fatalf("threadID = %d, want %d", rc.threadID, tt.wantThr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateTelegramBotDescription_UTF8Safe(t *testing.T) {
|
||||
t.Parallel()
|
||||
cjk := strings.Repeat("你", 200)
|
||||
out := truncateTelegramBotDescription(cjk)
|
||||
if !utf8.ValidString(out) {
|
||||
t.Fatal("invalid UTF-8 from CJK description")
|
||||
}
|
||||
if got, max := utf8.RuneCountInString(out), 256; got > max {
|
||||
t.Fatalf("rune count %d > %d", got, max)
|
||||
func TestIsDirectedAtBot(t *testing.T) {
|
||||
p := &Platform{token: "token", httpClient: &http.Client{}}
|
||||
p.selfUser = &models.User{ID: 42, Username: "mybot"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
msg *models.Message
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "command without @suffix",
|
||||
msg: &models.Message{
|
||||
Text: "/help",
|
||||
Chat: models.Chat{ID: 1, Type: models.ChatTypeGroup},
|
||||
Entities: []models.MessageEntity{
|
||||
{Type: models.MessageEntityTypeBotCommand, Offset: 0, Length: 5},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "command @mybot",
|
||||
msg: &models.Message{
|
||||
Text: "/help@mybot",
|
||||
Chat: models.Chat{ID: 1, Type: models.ChatTypeGroup},
|
||||
Entities: []models.MessageEntity{
|
||||
{Type: models.MessageEntityTypeBotCommand, Offset: 0, Length: 11},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "command @otherbot",
|
||||
msg: &models.Message{
|
||||
Text: "/help@otherbot",
|
||||
Chat: models.Chat{ID: 1, Type: models.ChatTypeGroup},
|
||||
Entities: []models.MessageEntity{
|
||||
{Type: models.MessageEntityTypeBotCommand, Offset: 0, Length: 14},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "@mention in text",
|
||||
msg: &models.Message{
|
||||
Text: "hey @mybot do something",
|
||||
Chat: models.Chat{ID: 1, Type: models.ChatTypeGroup},
|
||||
Entities: []models.MessageEntity{
|
||||
{Type: models.MessageEntityTypeMention, Offset: 4, Length: 6},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "reply to bot message",
|
||||
msg: &models.Message{
|
||||
Text: "yes do it",
|
||||
Chat: models.Chat{ID: 1, Type: models.ChatTypeGroup},
|
||||
ReplyToMessage: &models.Message{
|
||||
From: &models.User{ID: 42},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "plain text not directed",
|
||||
msg: &models.Message{
|
||||
Text: "hello everyone",
|
||||
Chat: models.Chat{ID: 1, Type: models.ChatTypeGroup},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
long := strings.Repeat("b", 260)
|
||||
out2 := truncateTelegramBotDescription(long)
|
||||
if want := 256; utf8.RuneCountInString(out2) != want {
|
||||
t.Fatalf("ascii truncation: got %d runes want %d", utf8.RuneCountInString(out2), want)
|
||||
}
|
||||
if !utf8.ValidString(out2) {
|
||||
t.Fatal("invalid UTF-8 after ascii truncation")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.isDirectedAtBot(tt.msg)
|
||||
if got != tt.want {
|
||||
t.Fatalf("isDirectedAtBot() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateForLog_UTF8Safe(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := strings.Repeat("世", 50) // 50 runes
|
||||
out := truncateForLog(s, 10)
|
||||
if !utf8.ValidString(out) {
|
||||
t.Fatal("invalid UTF-8")
|
||||
func TestHandleMessageWithForumTopic(t *testing.T) {
|
||||
handled := make(chan *core.Message, 1)
|
||||
p := &Platform{
|
||||
token: "token",
|
||||
httpClient: &http.Client{},
|
||||
groupReplyAll: true,
|
||||
}
|
||||
if utf8.RuneCountInString(out) != 13 { // 10 + "..."
|
||||
t.Fatalf("got %d runes", utf8.RuneCountInString(out))
|
||||
p.handler = func(_ core.Platform, msg *core.Message) {
|
||||
handled <- msg
|
||||
}
|
||||
stubBot := newStubTelegramBot()
|
||||
p.bot = stubBot
|
||||
p.selfUser = &models.User{ID: 42, Username: "mybot"}
|
||||
|
||||
msg := &models.Message{
|
||||
ID: 10,
|
||||
MessageThreadID: 55,
|
||||
Text: "hello from topic",
|
||||
Date: int(time.Now().Unix()),
|
||||
From: &models.User{ID: 7, Username: "alice"},
|
||||
Chat: models.Chat{
|
||||
ID: 100,
|
||||
Type: models.ChatTypeSupergroup,
|
||||
Title: "Test Group",
|
||||
IsForum: true,
|
||||
},
|
||||
}
|
||||
|
||||
p.handleMessage(context.Background(), msg)
|
||||
|
||||
select {
|
||||
case got := <-handled:
|
||||
if got.SessionKey != "telegram:100:55:7" {
|
||||
t.Fatalf("SessionKey = %q, want %q", got.SessionKey, "telegram:100:55:7")
|
||||
}
|
||||
rc := got.ReplyCtx.(replyContext)
|
||||
if rc.threadID != 55 {
|
||||
t.Fatalf("threadID = %d, want 55", rc.threadID)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("message not handled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMessageNonForumIgnoresThreadID(t *testing.T) {
|
||||
handled := make(chan *core.Message, 1)
|
||||
p := &Platform{
|
||||
token: "token",
|
||||
httpClient: &http.Client{},
|
||||
groupReplyAll: true,
|
||||
}
|
||||
p.handler = func(_ core.Platform, msg *core.Message) {
|
||||
handled <- msg
|
||||
}
|
||||
stubBot := newStubTelegramBot()
|
||||
p.bot = stubBot
|
||||
p.selfUser = &models.User{ID: 42, Username: "mybot"}
|
||||
|
||||
msg := &models.Message{
|
||||
ID: 10,
|
||||
MessageThreadID: 55, // set but not a forum
|
||||
Text: "hello",
|
||||
Date: int(time.Now().Unix()),
|
||||
From: &models.User{ID: 7, Username: "alice"},
|
||||
Chat: models.Chat{
|
||||
ID: 100,
|
||||
Type: models.ChatTypeGroup,
|
||||
Title: "Test Group",
|
||||
IsForum: false,
|
||||
},
|
||||
}
|
||||
|
||||
p.handleMessage(context.Background(), msg)
|
||||
|
||||
select {
|
||||
case got := <-handled:
|
||||
if got.SessionKey != "telegram:100:7" {
|
||||
t.Fatalf("SessionKey = %q, want %q (no thread)", got.SessionKey, "telegram:100:7")
|
||||
}
|
||||
rc := got.ReplyCtx.(replyContext)
|
||||
if rc.threadID != 0 {
|
||||
t.Fatalf("threadID = %d, want 0", rc.threadID)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("message not handled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,10 +944,17 @@ func newTelegramTestPlatform(t *testing.T, handler func(http.ResponseWriter, *ht
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
bot, err := tgbotapi.NewBotAPIWithClient("TEST_TOKEN", server.URL+"/bot%s/%s", server.Client())
|
||||
b, err := tgbot.New("TEST_TOKEN",
|
||||
tgbot.WithServerURL(server.URL),
|
||||
tgbot.WithHTTPClient(5*time.Second, server.Client()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewBotAPIWithClient returned error: %v", err)
|
||||
t.Fatalf("tgbot.New returned error: %v", err)
|
||||
}
|
||||
|
||||
return &Platform{bot: &botAPIWrapper{BotAPI: bot}}
|
||||
return &Platform{
|
||||
bot: b,
|
||||
selfUser: &models.User{ID: 1, Username: "testbot"},
|
||||
httpClient: server.Client(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user