feat(channels/telegram): Telegram 渠道插件(Bot API 长轮询/Webhook + file_id 媒体 + 主动推送强)
This commit is contained in:
1 parent
0b036645bb
commit
f9f5a78a02
12 files changed
+1983
No files matched your search
@@ -0,0 +1,171 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
)
|
||||
|
||||
// fakeUpdate builds a minimal telegramUpdate JSON object.
|
||||
func fakeUpdate(id int, text string) map[string]any {
|
||||
return map[string]any{
|
||||
"update_id": id,
|
||||
"message": map[string]any{
|
||||
"message_id": id,
|
||||
"from": map[string]any{"id": 1, "first_name": "Tester"},
|
||||
"chat": map[string]any{"id": 10, "type": "private"},
|
||||
"date": 1700000000,
|
||||
"text": text,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMessages_LongPollOffset(t *testing.T) {
|
||||
t.Parallel()
|
||||
var mu sync.Mutex
|
||||
first := true
|
||||
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if methodOf(r) != "getUpdates" {
|
||||
http.Error(w, "unexpected method", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
firstCall := first
|
||||
first = false
|
||||
mu.Unlock()
|
||||
if firstCall {
|
||||
okResponse(w, []map[string]any{fakeUpdate(1, "one"), fakeUpdate(2, "two")})
|
||||
return
|
||||
}
|
||||
okResponse(w, []map[string]any{})
|
||||
}))
|
||||
if err := a.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
emitted := make(chan adapter.UnifiedMessage, 4)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
a.ReadMessages(ctx, func(m adapter.UnifiedMessage) { emitted <- m })
|
||||
close(done)
|
||||
}()
|
||||
|
||||
got := make([]adapter.UnifiedMessage, 0, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case m := <-emitted:
|
||||
got = append(got, m)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timeout waiting for emitted message")
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("ReadMessages did not return after ctx cancel")
|
||||
}
|
||||
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("emitted %d messages, want 2", len(got))
|
||||
}
|
||||
if got[0].Content != "one" || got[1].Content != "two" {
|
||||
t.Errorf("contents = %q/%q, want one/two", got[0].Content, got[1].Content)
|
||||
}
|
||||
a.mu.RLock()
|
||||
offset := a.offset
|
||||
a.mu.RUnlock()
|
||||
if offset != 3 {
|
||||
t.Errorf("offset = %d, want 3", offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMessages_CtxPreCanceled(t *testing.T) {
|
||||
t.Parallel()
|
||||
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "no api call expected", http.StatusNotFound)
|
||||
}))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
a.ReadMessages(ctx, func(m adapter.UnifiedMessage) {})
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("ReadMessages should return immediately on canceled ctx")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMessages_WebhookWaits(t *testing.T) {
|
||||
t.Parallel()
|
||||
var calls int
|
||||
var mu sync.Mutex
|
||||
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if methodOf(r) == "setWebhook" {
|
||||
okResponse(w, map[string]any{"webhook_url": "https://example.com/hook"})
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
calls++
|
||||
mu.Unlock()
|
||||
http.Error(w, "no getUpdates expected in webhook mode", http.StatusNotFound)
|
||||
}))
|
||||
a.cfg.WebhookURL = "https://example.com/hook"
|
||||
if err := a.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
a.ReadMessages(ctx, func(m adapter.UnifiedMessage) {})
|
||||
close(done)
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("ReadMessages should return on cancel in webhook mode")
|
||||
}
|
||||
mu.Lock()
|
||||
if calls != 0 {
|
||||
t.Errorf("getUpdates called %d times in webhook mode", calls)
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
func TestReadMessages_ErrorRetriesThenCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if methodOf(r) != "getUpdates" {
|
||||
http.Error(w, "unexpected method", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
failResponse(w, "Conflict", 409)
|
||||
}))
|
||||
if err := a.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
a.ReadMessages(ctx, func(m adapter.UnifiedMessage) {})
|
||||
close(done)
|
||||
}()
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
cancel()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("ReadMessages should return on cancel despite getUpdates errors")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user