mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 21:37:33 +08:00
## Summary - Add Telegram long-polling channel support. - Add WeCom webhook and WebSocket channel support with tests. Related to #17520
255 lines
7.9 KiB
Go
255 lines
7.9 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package channels
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"ragflow/internal/channels/core"
|
|
)
|
|
|
|
type telegramRoundTripperFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f telegramRoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return f(req)
|
|
}
|
|
|
|
func TestNewTelegramChannelFromConfigRequiresToken(t *testing.T) {
|
|
if _, err := newTelegramChannelFromConfig("account-1", map[string]any{}); err == nil {
|
|
t.Fatal("newTelegramChannelFromConfig accepted a missing token")
|
|
}
|
|
}
|
|
|
|
func TestTelegramChatType(t *testing.T) {
|
|
tests := map[string]string{
|
|
"private": "p2p",
|
|
"group": "group",
|
|
"supergroup": "group",
|
|
"channel": "channel",
|
|
"": "unknown",
|
|
"custom": "custom",
|
|
}
|
|
for input, want := range tests {
|
|
if got := telegramChatType(input); got != want {
|
|
t.Fatalf("telegramChatType(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTelegramHandleUpdateMapsCaptionAndIgnoresBots(t *testing.T) {
|
|
ch := newTelegramChannel(telegramAccount{AccountID: "account-1", Token: "token"})
|
|
messages := make(chan core.IncomingMessage, 1)
|
|
ch.SetMessageHandler(func(_ context.Context, msg core.IncomingMessage) error {
|
|
messages <- msg
|
|
return nil
|
|
})
|
|
|
|
ch.handleUpdate(context.Background(), telegramUpdate{
|
|
UpdateID: 1,
|
|
Message: &telegramMessage{
|
|
MessageID: 42,
|
|
From: &telegramUser{ID: 7},
|
|
Chat: telegramChat{ID: -10, Type: "supergroup"},
|
|
Caption: "caption text",
|
|
},
|
|
})
|
|
ch.handleUpdate(context.Background(), telegramUpdate{
|
|
UpdateID: 2,
|
|
Message: &telegramMessage{
|
|
MessageID: 43,
|
|
From: &telegramUser{ID: 8, IsBot: true},
|
|
},
|
|
})
|
|
|
|
select {
|
|
case msg := <-messages:
|
|
if msg.ChatID != "-10" || msg.ChatType != "group" || msg.MessageID != "42" || msg.SenderID != "7" || msg.Text != "caption text" {
|
|
t.Fatalf("unexpected incoming message: %+v", msg)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for incoming message")
|
|
}
|
|
|
|
select {
|
|
case msg := <-messages:
|
|
t.Fatalf("bot message was dispatched: %+v", msg)
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
func TestTelegramHandleUpdateMapsChannelPostWithoutSender(t *testing.T) {
|
|
ch := newTelegramChannel(telegramAccount{AccountID: "account-1", Token: "token"})
|
|
messages := make(chan core.IncomingMessage, 1)
|
|
ch.SetMessageHandler(func(_ context.Context, msg core.IncomingMessage) error {
|
|
messages <- msg
|
|
return nil
|
|
})
|
|
|
|
ch.handleUpdate(context.Background(), telegramUpdate{
|
|
ChannelPost: &telegramMessage{
|
|
MessageID: 42,
|
|
Chat: telegramChat{ID: -100123, Type: "channel"},
|
|
Text: "channel post",
|
|
},
|
|
})
|
|
|
|
select {
|
|
case msg := <-messages:
|
|
if msg.ChatID != "-100123" || msg.ChatType != "channel" || msg.MessageID != "42" || msg.SenderID != "-100123" || msg.Text != "channel post" {
|
|
t.Fatalf("unexpected incoming message: %+v", msg)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for channel post")
|
|
}
|
|
}
|
|
|
|
func TestTelegramCallAPISanitizesTransportError(t *testing.T) {
|
|
const token = "sensitive-bot-token"
|
|
transportErr := &url.Error{
|
|
Op: "Post",
|
|
URL: "https://api.telegram.org/bot" + token + "/sendMessage",
|
|
Err: errors.New("connection refused"),
|
|
}
|
|
ch := newTelegramChannel(telegramAccount{AccountID: "account-1", Token: token})
|
|
ch.client = &http.Client{Transport: telegramRoundTripperFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, transportErr
|
|
})}
|
|
|
|
err := ch.callAPI(context.Background(), "sendMessage", map[string]any{}, nil)
|
|
if err == nil {
|
|
t.Fatal("callAPI() error = nil")
|
|
}
|
|
if strings.Contains(err.Error(), token) {
|
|
t.Fatalf("callAPI() exposed token in error: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "sendMessage") {
|
|
t.Fatalf("callAPI() error = %q, want method context", err)
|
|
}
|
|
if !errors.Is(err, transportErr) {
|
|
t.Fatalf("callAPI() error does not unwrap transport error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTelegramSendUsesReplyParameters(t *testing.T) {
|
|
request := make(chan map[string]any, 1)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.HasSuffix(r.URL.Path, "/sendMessage") {
|
|
t.Errorf("unexpected Telegram API path: %s", r.URL.Path)
|
|
http.Error(w, "unexpected path", http.StatusNotFound)
|
|
return
|
|
}
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
t.Errorf("decode request: %v", err)
|
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
request <- payload
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":1}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
ch := newTelegramChannel(telegramAccount{
|
|
AccountID: "account-1",
|
|
Token: "token",
|
|
APIBaseURL: server.URL,
|
|
})
|
|
if err := ch.Send(context.Background(), core.OutgoingMessage{
|
|
ChatID: "-100",
|
|
Text: "answer",
|
|
ReplyToMessageID: "12",
|
|
}); err != nil {
|
|
t.Fatalf("Send() error = %v", err)
|
|
}
|
|
|
|
payload := <-request
|
|
if payload["chat_id"] != float64(-100) || payload["text"] != "answer" {
|
|
t.Fatalf("unexpected send payload: %+v", payload)
|
|
}
|
|
reply, ok := payload["reply_parameters"].(map[string]any)
|
|
if !ok || reply["message_id"] != float64(12) || reply["allow_sending_without_reply"] != true {
|
|
t.Fatalf("unexpected reply parameters: %+v", payload["reply_parameters"])
|
|
}
|
|
}
|
|
|
|
func TestTelegramSendIgnoresInvalidChatID(t *testing.T) {
|
|
ch := newTelegramChannel(telegramAccount{AccountID: "account-1", Token: "token"})
|
|
if err := ch.Send(context.Background(), core.OutgoingMessage{ChatID: "invalid", Text: "answer"}); err != nil {
|
|
t.Fatalf("Send() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestTelegramStartAndStopPollsUpdates(t *testing.T) {
|
|
updateReturned := false
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/deleteWebhook"):
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":true}`))
|
|
case strings.HasSuffix(r.URL.Path, "/getUpdates"):
|
|
if !updateReturned {
|
|
updateReturned = true
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":[{"update_id":9,"message":{"message_id":3,"from":{"id":4,"is_bot":false},"chat":{"id":5,"type":"private"},"text":"hello"}}]}`))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":[]}`))
|
|
case strings.HasSuffix(r.URL.Path, "/sendMessage"):
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":1}}`))
|
|
default:
|
|
t.Errorf("unexpected Telegram API path: %s", r.URL.Path)
|
|
http.Error(w, "unexpected path", http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
ch := newTelegramChannel(telegramAccount{
|
|
AccountID: "account-1",
|
|
Token: "token",
|
|
APIBaseURL: server.URL,
|
|
})
|
|
message := make(chan core.IncomingMessage, 1)
|
|
ch.SetMessageHandler(func(_ context.Context, msg core.IncomingMessage) error {
|
|
message <- msg
|
|
return nil
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
if err := ch.Start(ctx); err != nil {
|
|
t.Fatalf("Start() error = %v", err)
|
|
}
|
|
select {
|
|
case msg := <-message:
|
|
if msg.Text != "hello" || msg.MessageID != "3" || ch.offset != 10 {
|
|
t.Fatalf("unexpected polled message: %+v, offset=%d", msg, ch.offset)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for polled message")
|
|
}
|
|
|
|
if err := ch.Stop(context.Background()); err != nil {
|
|
t.Fatalf("Stop() error = %v", err)
|
|
}
|
|
}
|