- Added Handlers struct and methods for task operations - Implemented task progress tracking and storage - Created server setup with middleware for logging and recovery - Added support for Telegram file extraction and Telegraph image extraction - Introduced webhook functionality for task status updates - Defined request and response types for API interactions
273 lines
7.6 KiB
Go
273 lines
7.6 KiB
Go
package api
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/celestix/gotgproto/ext"
|
||
"github.com/charmbracelet/log"
|
||
"github.com/gotd/td/tg"
|
||
"github.com/krau/SaveAny-Bot/client/bot"
|
||
userclient "github.com/krau/SaveAny-Bot/client/user"
|
||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||
)
|
||
|
||
// MessageContext 保存消息和获取它所用的 context
|
||
type MessageContext struct {
|
||
Message *tg.Message
|
||
Client *ext.Context
|
||
}
|
||
|
||
// getClientContext 获取可用的客户端上下文
|
||
// 优先使用 Bot,失败后回退到 Userbot
|
||
func getClientContext() (*ext.Context, error) {
|
||
// 首先尝试获取 Bot context
|
||
if botCtx := bot.ExtContext(); botCtx != nil {
|
||
return botCtx, nil
|
||
}
|
||
|
||
// 回退到 Userbot
|
||
if uc := userclient.GetCtx(); uc != nil {
|
||
return uc, nil
|
||
}
|
||
|
||
return nil, fmt.Errorf("no client available (bot and userbot are not initialized)")
|
||
}
|
||
|
||
// resolveChatID 解析聊天 ID
|
||
func resolveChatID(_ context.Context, idOrUsername string) (int64, error) {
|
||
// 如果是数字 ID
|
||
if id, err := strconv.ParseInt(idOrUsername, 10, 64); err == nil {
|
||
// 私有频道 ID 需要加上 -100 前缀
|
||
if id > 0 {
|
||
return -1000000000000 - id, nil
|
||
}
|
||
return id, nil
|
||
}
|
||
|
||
// 获取可用的客户端上下文
|
||
clientCtx, err := getClientContext()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
// 使用 tgutil 的 ParseChatID
|
||
return tgutil.ParseChatID(clientCtx, idOrUsername)
|
||
}
|
||
|
||
// ParseMessageLink 解析 Telegram 消息链接
|
||
// 支持格式:
|
||
// - https://t.me/username/123
|
||
// - https://t.me/c/123456789/123
|
||
// - https://t.me/c/123456789/111/456 (topic id)
|
||
// - https://t.me/username/123?comment=2 (评论)
|
||
func ParseMessageLink(ctx context.Context, link string) (int64, int, error) {
|
||
u, err := url.Parse(link)
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("invalid URL: %w", err)
|
||
}
|
||
paths := strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
|
||
|
||
if cmt := u.Query().Get("comment"); cmt != "" {
|
||
// 频道评论的消息链接
|
||
if len(paths) < 1 {
|
||
return 0, 0, fmt.Errorf("invalid message link format: %s", link)
|
||
}
|
||
// 简化处理:返回错误,提示不支持评论链接
|
||
return 0, 0, fmt.Errorf("comment links are not supported")
|
||
}
|
||
|
||
switch len(paths) {
|
||
case 2: // https://t.me/username/123
|
||
chatID, err := resolveChatID(ctx, paths[0])
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("failed to resolve chat ID: %w", err)
|
||
}
|
||
msgID, err := strconv.Atoi(paths[1])
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("failed to parse message ID: %w", err)
|
||
}
|
||
return chatID, msgID, nil
|
||
case 3:
|
||
// https://t.me/c/123456789/123
|
||
// https://t.me/username/123/456 , 123: topic id
|
||
chatPart, msgPart := paths[1], paths[2]
|
||
if paths[0] != "c" {
|
||
chatPart = paths[0]
|
||
}
|
||
chatID, err := resolveChatID(ctx, chatPart)
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("failed to resolve chat ID: %w", err)
|
||
}
|
||
msgID, err := strconv.Atoi(msgPart)
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("failed to parse message ID: %w", err)
|
||
}
|
||
return chatID, msgID, nil
|
||
case 4:
|
||
// https://t.me/c/123456789/111/456 111: topic id
|
||
if paths[0] != "c" {
|
||
return 0, 0, fmt.Errorf("invalid message link format: %s", link)
|
||
}
|
||
chatID, err := resolveChatID(ctx, paths[1])
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("failed to resolve chat ID: %w", err)
|
||
}
|
||
msgID, err := strconv.Atoi(paths[3])
|
||
if err != nil {
|
||
return 0, 0, fmt.Errorf("failed to parse message ID: %w", err)
|
||
}
|
||
return chatID, msgID, nil
|
||
}
|
||
return 0, 0, fmt.Errorf("invalid message link format: %s", link)
|
||
}
|
||
|
||
// getMessageWithContext 通过 ID 获取消息,返回消息和使用的 context
|
||
// 确保消息获取和后续文件创建使用同一个 context
|
||
func getMessageWithContext(_ context.Context, chatID int64, msgID int) (*MessageContext, error) {
|
||
// 首先尝试使用 Bot
|
||
if botCtx := bot.ExtContext(); botCtx != nil {
|
||
msg, err := tgutil.GetMessageByID(botCtx, chatID, msgID)
|
||
if err == nil {
|
||
return &MessageContext{Message: msg, Client: botCtx}, nil
|
||
}
|
||
}
|
||
|
||
// 回退到 Userbot
|
||
uc := userclient.GetCtx()
|
||
if uc == nil {
|
||
return nil, fmt.Errorf("userbot not initialized and bot cannot access this message")
|
||
}
|
||
|
||
msg, err := tgutil.GetMessageByID(uc, chatID, msgID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &MessageContext{Message: msg, Client: uc}, nil
|
||
}
|
||
|
||
// getGroupedMessagesWithContext 获取媒体组消息,返回消息列表和使用的 context
|
||
// 确保消息获取和后续文件创建使用同一个 context
|
||
func getGroupedMessagesWithContext(ctx *MessageContext, chatID int64) ([]*tg.Message, error) {
|
||
msg := ctx.Message
|
||
clientCtx := ctx.Client
|
||
|
||
groupID, ok := msg.GetGroupedID()
|
||
if !ok || groupID == 0 {
|
||
return []*tg.Message{msg}, nil
|
||
}
|
||
|
||
// 使用获取原始消息的同一个 client 获取媒体组
|
||
msgs, err := tgutil.GetGroupedMessages(clientCtx, chatID, msg)
|
||
if err != nil || len(msgs) == 0 {
|
||
// 如果获取失败,至少返回原始消息
|
||
return []*tg.Message{msg}, nil
|
||
}
|
||
|
||
return msgs, nil
|
||
}
|
||
|
||
// ExtractFilesFromLinks 从消息链接中提取文件
|
||
// 每个文件的处理流程:解析链接 -> 获取消息 -> 获取媒体组 -> 创建文件对象
|
||
// 对于单个文件,全程使用同一个 client context,不会交叉
|
||
func ExtractFilesFromLinks(ctx context.Context, links []string) ([]tfile.TGFileMessage, error) {
|
||
logger := log.FromContext(ctx)
|
||
var files []tfile.TGFileMessage
|
||
|
||
for _, link := range links {
|
||
link = strings.TrimSpace(link)
|
||
if link == "" {
|
||
continue
|
||
}
|
||
|
||
// 验证链接格式
|
||
if !isValidMessageLink(link) {
|
||
logger.Errorf("Invalid message link format: %s", link)
|
||
continue
|
||
}
|
||
|
||
chatID, msgID, err := ParseMessageLink(ctx, link)
|
||
if err != nil {
|
||
logger.Errorf("Failed to parse message link %s: %v", link, err)
|
||
continue
|
||
}
|
||
|
||
// 解析链接 URL 检查是否有 single 参数
|
||
u, _ := url.Parse(link)
|
||
single := u != nil && u.Query().Has("single")
|
||
|
||
// 获取消息和使用的 context(Bot 优先,失败回退 Userbot)
|
||
msgCtx, err := getMessageWithContext(ctx, chatID, msgID)
|
||
if err != nil {
|
||
logger.Errorf("Failed to get message %d from chat %d: %v", msgID, chatID, err)
|
||
continue
|
||
}
|
||
|
||
msg := msgCtx.Message
|
||
clientCtx := msgCtx.Client
|
||
|
||
if msg.Media == nil {
|
||
logger.Warnf("Message %d has no media", msgID)
|
||
continue
|
||
}
|
||
|
||
media, ok := msg.GetMedia()
|
||
if !ok {
|
||
logger.Warnf("Failed to get media from message %d", msgID)
|
||
continue
|
||
}
|
||
|
||
// 检查是否是媒体组
|
||
groupID, isGroup := msg.GetGroupedID()
|
||
if isGroup && groupID != 0 && !single {
|
||
// 使用同一个 client context 获取媒体组
|
||
groupMsgs, err := getGroupedMessagesWithContext(msgCtx, chatID)
|
||
if err != nil {
|
||
logger.Errorf("Failed to get grouped messages: %v", err)
|
||
} else {
|
||
for _, gmsg := range groupMsgs {
|
||
if gmsg.Media == nil {
|
||
continue
|
||
}
|
||
gmedia, ok := gmsg.GetMedia()
|
||
if !ok {
|
||
continue
|
||
}
|
||
// 使用获取消息时使用的同一个 client context 创建文件
|
||
file, err := tfile.FromMediaMessage(gmedia, clientCtx.Raw, gmsg)
|
||
if err != nil {
|
||
logger.Errorf("Failed to create file from media: %v", err)
|
||
continue
|
||
}
|
||
files = append(files, file)
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 单个文件 - 使用获取消息时使用的同一个 client context 创建文件
|
||
file, err := tfile.FromMediaMessage(media, clientCtx.Raw, msg)
|
||
if err != nil {
|
||
logger.Errorf("Failed to create file from media: %v", err)
|
||
continue
|
||
}
|
||
files = append(files, file)
|
||
}
|
||
|
||
if len(files) == 0 {
|
||
return nil, fmt.Errorf("no files found in provided links")
|
||
}
|
||
|
||
return files, nil
|
||
}
|
||
|
||
// isValidMessageLink 检查是否是有效的 Telegram 消息链接
|
||
func isValidMessageLink(link string) bool {
|
||
return strings.HasPrefix(link, "https://t.me/") || strings.HasPrefix(link, "http://t.me/")
|
||
}
|