govd/bot/core/inline.go
stefanodvx 93e964a28b instagram: new extraction method
new extraction method first tries to fetch content directly from instagram graphql API, fallback to html embed page. in case every method fail, rely on 3rd party
2025-04-23 18:52:05 +02:00

302 lines
6.2 KiB
Go

package core
import (
"context"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"govd/database"
"govd/enums"
"govd/models"
"govd/util"
"github.com/PaulSonOfLars/gotgbot/v2"
"github.com/PaulSonOfLars/gotgbot/v2/ext"
)
type TaskEntry struct {
Task *models.DownloadContext
CreatedAt time.Time
}
var InlineTasks sync.Map
var cleanupActive sync.Once
const taskTimeout = 5 * time.Minute
func GetTask(id string) (*models.DownloadContext, bool) {
value, ok := InlineTasks.Load(id)
if !ok {
return nil, false
}
entry, ok := value.(TaskEntry)
if !ok {
return nil, false
}
return entry.Task, true
}
func SetTask(id string, task *models.DownloadContext) {
InlineTasks.Store(id, TaskEntry{
Task: task,
CreatedAt: time.Now(),
})
cleanupActive.Do(startTasksCleanup)
}
func DeleteTask(id string) {
InlineTasks.Delete(id)
}
func startTasksCleanup() {
go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
cleanupStaleTasks()
}
}()
}
func cleanupStaleTasks() {
now := time.Now()
InlineTasks.Range(func(key, value interface{}) bool {
entry, ok := value.(TaskEntry)
if !ok {
InlineTasks.Delete(key)
return true
}
if now.Sub(entry.CreatedAt) > taskTimeout {
InlineTasks.Delete(key)
}
return true
})
}
func HandleInline(
bot *gotgbot.Bot,
ctx *ext.Context,
dlCtx *models.DownloadContext,
) error {
if dlCtx.Extractor.Type != enums.ExtractorTypeSingle {
return util.ErrNotImplemented
}
contentID := dlCtx.MatchedContentID
cached, err := database.GetDefaultMedias(
dlCtx.Extractor.CodeName,
contentID,
)
if err != nil {
return err
}
if len(cached) > 0 {
if len(cached) > 1 {
return util.ErrInlineMediaGroup
}
err = HandleInlineCached(
bot, ctx,
dlCtx, cached[0],
)
if err != nil {
return err
}
return nil
}
err = StartInlineTask(bot, ctx, dlCtx)
if err != nil {
return err
}
return nil
}
func HandleInlineCached(
bot *gotgbot.Bot,
ctx *ext.Context,
dlCtx *models.DownloadContext,
media *models.Media,
) error {
var result gotgbot.InlineQueryResult
format := media.Format
resultID := fmt.Sprintf("%d:%s", ctx.EffectiveUser.Id, format.FormatID)
resultTitle := "share"
mediaCaption := FormatCaption(media, true)
_, inputFileType := format.GetFormatInfo()
switch inputFileType {
case "photo":
result = &gotgbot.InlineQueryResultCachedPhoto{
Id: resultID,
PhotoFileId: format.FileID,
Title: resultTitle,
Caption: mediaCaption,
ParseMode: "HTML",
}
case "video":
result = &gotgbot.InlineQueryResultCachedVideo{
Id: resultID,
VideoFileId: format.FileID,
Title: resultTitle,
Caption: mediaCaption,
ParseMode: "HTML",
}
case "audio":
result = &gotgbot.InlineQueryResultCachedAudio{
Id: resultID,
AudioFileId: format.FileID,
Caption: mediaCaption,
ParseMode: "HTML",
}
case "document":
result = &gotgbot.InlineQueryResultCachedDocument{
Id: resultID,
DocumentFileId: format.FileID,
Title: resultTitle,
Caption: mediaCaption,
ParseMode: "HTML",
}
default:
return errors.New("unsupported input file type")
}
ctx.InlineQuery.Answer(
bot, []gotgbot.InlineQueryResult{result},
&gotgbot.AnswerInlineQueryOpts{
CacheTime: 1,
IsPersonal: true,
},
)
return nil
}
func HandleInlineCachedResult(
bot *gotgbot.Bot,
ctx *ext.Context,
dlCtx *models.DownloadContext,
media *models.Media,
) error {
format := media.Format
messageCaption := FormatCaption(media, true)
inputMedia, err := format.GetInputMediaWithFileID(messageCaption)
if err != nil {
return err
}
_, _, err = bot.EditMessageMedia(
inputMedia,
&gotgbot.EditMessageMediaOpts{
InlineMessageId: ctx.ChosenInlineResult.InlineMessageId,
},
)
if err != nil {
return err
}
return nil
}
func StartInlineTask(
bot *gotgbot.Bot,
ctx *ext.Context,
dlCtx *models.DownloadContext,
) error {
randomID, err := uuid.NewUUID()
if err != nil {
return errors.New("could not generate task ID")
}
taskID := randomID.String()
inlineResult := &gotgbot.InlineQueryResultArticle{
Id: taskID,
Title: "share",
InputMessageContent: &gotgbot.InputTextMessageContent{
MessageText: "loading media plese wait...",
ParseMode: "HTML",
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{
IsDisabled: true,
},
},
ReplyMarkup: &gotgbot.InlineKeyboardMarkup{
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
{
{
Text: "...",
CallbackData: "inline:loading",
},
},
},
},
}
ok, err := ctx.InlineQuery.Answer(
bot, []gotgbot.InlineQueryResult{inlineResult},
&gotgbot.AnswerInlineQueryOpts{
CacheTime: 1,
IsPersonal: true,
},
)
if err != nil || !ok {
return nil
}
SetTask(taskID, dlCtx)
return nil
}
func GetInlineFormat(
taskCtx context.Context,
bot *gotgbot.Bot,
ctx *ext.Context,
dlCtx *models.DownloadContext,
mediaChan chan<- *models.Media,
errChan chan<- error,
) {
response, err := dlCtx.Extractor.Run(dlCtx)
if err != nil {
errChan <- fmt.Errorf("failed to get media: %w", err)
return
}
mediaList := response.MediaList
if len(mediaList) == 0 {
errChan <- fmt.Errorf("no media found for content ID: %s", dlCtx.MatchedContentID)
return
}
if len(mediaList) > 1 {
errChan <- util.ErrInlineMediaGroup
return
}
for i := range mediaList {
defaultFormat := mediaList[i].GetDefaultFormat()
if defaultFormat == nil {
errChan <- fmt.Errorf("no default format found for media at index %d", i)
return
}
if len(defaultFormat.URL) == 0 {
errChan <- fmt.Errorf("media format at index %d has no URL", i)
return
}
// ensure we can merge video and audio formats
ensureMergeFormats(mediaList[i], defaultFormat)
mediaList[i].Format = defaultFormat
}
messageCaption := FormatCaption(mediaList[0], true)
medias, err := DownloadMedias(taskCtx, mediaList, nil)
if err != nil {
errChan <- fmt.Errorf("failed to download medias: %w", err)
return
}
msgs, err := SendMedias(
bot, ctx, dlCtx,
medias, &models.SendMediaFormatsOptions{
Caption: messageCaption,
},
)
if err != nil {
errChan <- fmt.Errorf("failed to send media: %w", err)
return
}
msg := &msgs[0]
msg.Delete(bot, nil)
mediaChan <- medias[0].Media
}