Files
Cyrene_v2/plugins/channels/telegram/media.go
T

110 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package telegram
// 本文件承载 Telegram 的**入站附件字节内联**(切片 IM-1 / IM-IMG):Bot API 上报的
// **文件类与图片类**媒体都只给 `file_id`,字节要分两步取回 —— 先 `getFile` 把平台文件 id
// 换成一个**平台自己返回**的相对路径,再把该路径拼到已配置的 API 根之后下载(两步同一条
// 路、同一份来源白名单;图片多出来的只是「字节必须是白名单光栅图」与 5 MiB 的图片上限)。
//
// 三条边界:
// - 地址**完全由配置与平台返回值构造**:调用方(发送方)无法让请求打到别的主机;
// 平台返回的路径还要过 `sanitizeFilePath`(拒绝对协议前缀、`..`、查询串);
// - 来源白名单 = 已配置的 API 根(默认 `https://api.telegram.org`):白名单之外的
// 主机**一个字节都不发出去**;
// - 尺寸/超时/预筛/日志口径全在 `internal/channel/mediafetch`(四个适配器共用),
// 这里只负责「把平台引用换成一个平台资产地址」。
import (
"context"
"encoding/json"
"fmt"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/mediafetch"
)
// getFileMethod 是 Bot API 里「用 file_id 换文件路径」的方法名。
const getFileMethod = "getFile"
// inlineInboundMedia 在消息投递给上层之前,把受支持格式的附件(**文件类与图片类**)取回并
// 内联。失败绝不抛出:该附件保持无字节,由 channelbridge 的入站摄取照既有口径给模型
// 一行诚实提示(design/24 §4.4)。
func (a *Adapter) inlineInboundMedia(ctx context.Context, msg *adapter.UnifiedMessage) {
mediafetch.Inline(ctx, a.log, msg, telegramFetcher{a: a})
}
// telegramFetcher 实现 mediafetch.Fetcher:Telegram 给的是 `file_id`,因此它必须
// 先问平台「这个 id 对应哪个路径」,再下载。
type telegramFetcher struct{ a *Adapter }
// Fetch 取回一份 Telegram 附件的字节。体积上限与超时都不在这里写数:前者由
// mediafetch 的默认值(`documents.DefaultMaxBytes`)兜底,后者是具名常量
// `mediafetch.DefaultTimeout`。
func (f telegramFetcher) Fetch(ctx context.Context, m adapter.Media) ([]byte, error) {
filePath, err := f.a.filePath(ctx, m.URL)
if err != nil {
return nil, err
}
return mediafetch.Fetch(ctx, mediafetch.Request{
URL: f.a.fileURL(filePath),
Allow: f.a.assetAllowlist(),
Client: f.a.client,
Timeout: mediafetch.DefaultTimeout,
})
}
// assetAllowlist 给出文件与**图片**下载的来源白名单:**只有已配置的 Bot API 根**(图片不
// 另立名单 —— 两者落的是同一台主机,另立一份只会多一处漂移)。文件地址
// 由「已配置的根 + 平台返回的相对路径」拼成,因此它不可能指向别的主机;白名单再把
// 这条性质钉死在代码里(配置读不出根时给空名单 ⇒ 谁都不许,deny-by-default)。
func (a *Adapter) assetAllowlist() mediafetch.Allowlist {
origin, err := mediafetch.OriginOf(a.baseURL)
if err != nil {
return mediafetch.NewAllowlist()
}
return mediafetch.NewAllowlist(origin)
}
// fileURL 把平台给出的相对路径拼成下载地址。token 在路径里(与 endpoint 同口径),
// 绝不进查询串;这个地址**只用于请求**,绝不进日志(mediafetch 的错误只带来源)。
func (a *Adapter) fileURL(filePath string) string {
return a.baseURL + "/file/bot" + a.cfg.BotToken + "/" + strings.TrimLeft(filePath, "/")
}
// filePath 调 getFile 把平台文件 id 换成平台自己返回的相对路径,并做形态校验。
func (a *Adapter) filePath(ctx context.Context, fileID string) (string, error) {
if strings.TrimSpace(fileID) == "" {
return "", ErrNoFilePath
}
out, err := a.doJSON(ctx, getFileMethod, map[string]any{"file_id": fileID})
if err != nil {
return "", err
}
var res struct {
FilePath string `json:"file_path"`
}
if err := json.Unmarshal(out.Result, &res); err != nil {
return "", fmt.Errorf("telegram: getFile result: %w", err)
}
return sanitizeFilePath(res.FilePath)
}
// sanitizeFilePath 校验平台返回的文件路径:非空、不含协议前缀 / `..` / 查询串 /
// 控制字符。校验失败时返回 ErrNoFilePath —— 绝不把可疑路径拼进下载地址。
func sanitizeFilePath(filePath string) (string, error) {
path := strings.TrimSpace(filePath)
if path == "" {
return "", ErrNoFilePath
}
for _, r := range path {
if r < ' ' || r == 0x7f {
return "", fmt.Errorf("%w: 平台返回的路径含控制字符", ErrNoFilePath)
}
}
if strings.Contains(path, "://") || strings.Contains(path, "..") ||
strings.ContainsAny(path, "\\?#") {
return "", fmt.Errorf("%w: 平台返回的路径形态非法", ErrNoFilePath)
}
return path, nil
}