- 新增 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 超量用例)
95 lines
2.1 KiB
Go
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
|
|
}
|
|
}
|