Files
Cyrene_v2/plugins/channels/telegram/poll.go
T
AskaEth 3be70f8232
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(channels): 适配器入站内联附件字节(telegram/kook/qq_obv11/weixin_ilink)
- 新增 internal/channel/mediafetch:来源白名单(deny-by-default,支持 *.suffix)
  + 逐跳重定向校验 + 边读边限 + 超时;只回来源主机,绝不回完整 URL
- 四渠道各自 media.go 接通入站内联:只从平台载荷给出的引用取回字节
  (telegram file_id→getFile→file_path 形态校验;kook CDN;qq 文件段 url;
   weixin cdnURL + AES-ECB 解密)
- 常量全部复用既有来源:documents.ExtAllowed / DefaultMaxBytes / MaxPerMessage
- channelbridge 诚实提示文案改为对「渠道只给远程引用」与「代取失败」两种成因都成立
- 18 个测试函数(含四渠道「正文里的 URL ⇒ 0 次命中」红线反证、端到端进模型可见视图)
- 变异验证:去掉尺寸上限 ⇒ 四渠道红(含修正后不再是假绿的 weixin 超量用例)
2026-10-04 22:13:17 +08:00

95 lines
2.1 KiB
Go

package telegram
import (
"context"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// ReadMessages runs a blocking getUpdates long-poll loop, emitting each
// normalized inbound message to emit until ctx is canceled or the adapter is
// disconnected. When running in webhook mode it does not poll; it simply waits
// for ctx to be canceled, since messages are delivered through the message
// handler via ProcessUpdate instead.
//
// The update offset is advanced only after a batch has been fully processed, so
// a canceled read does not lose the batch; acknowledgment is batched so the
// getUpdates call does not re-deliver the same update_id flood.
func (a *Adapter) ReadMessages(ctx context.Context, emit func(adapter.UnifiedMessage)) {
if emit == nil {
return
}
if a.isWebhookMode() {
<-ctx.Done()
return
}
if err := a.requireConnected(); err != nil {
a.log.Warn("read not started", "err", err)
return
}
pollCtx, cancel := context.WithCancel(ctx)
a.mu.Lock()
a.runID++
a.pollCancel = cancel
runID := a.runID
a.mu.Unlock()
defer func() {
a.mu.Lock()
if a.runID == runID {
a.pollCancel = nil
}
a.mu.Unlock()
cancel()
}()
for {
if err := pollCtx.Err(); err != nil {
return
}
updates, err := a.getUpdates(pollCtx)
if err != nil {
a.log.Warn("long poll failed", "err", err)
if !sleepWait(pollCtx, pollInterval) {
return
}
continue
}
if len(updates) == 0 {
if !sleepWait(pollCtx, pollInterval) {
return
}
continue
}
for _, u := range updates {
msg, ok := a.toUnifiedMessage(u)
if !ok {
continue
}
if err := msg.Validate(); err != nil {
a.log.Warn("skip invalid message", "err", err)
continue
}
a.inlineInboundMedia(pollCtx, &msg)
emit(msg)
}
a.mu.Lock()
a.offset = updates[len(updates)-1].UpdateID + 1
a.mu.Unlock()
}
}
// sleepWait blocks for d or until ctx is canceled, returning false when the
// context fires first.
func sleepWait(ctx context.Context, d time.Duration) bool {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
return false
case <-t.C:
return true
}
}