Init
This commit is contained in:
parent
264c97183e
commit
3faede7b1c
74 changed files with 6228 additions and 1 deletions
134
bot/core/default.go
Normal file
134
bot/core/default.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"govd/database"
|
||||
"govd/models"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
func HandleDefaultFormatDownload(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
dlCtx *models.DownloadContext,
|
||||
) error {
|
||||
storedMedias, err := database.GetDefaultMedias(
|
||||
dlCtx.Extractor.CodeName,
|
||||
dlCtx.MatchedContentID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get default medias: %w", err)
|
||||
}
|
||||
|
||||
if len(storedMedias) > 0 {
|
||||
return HandleDefaultStoredFormatDownload(
|
||||
bot, ctx, dlCtx, storedMedias,
|
||||
)
|
||||
}
|
||||
|
||||
response, err := dlCtx.Extractor.Run(dlCtx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("extractor fetch run failed: %w", err)
|
||||
}
|
||||
|
||||
mediaList := response.MediaList
|
||||
if len(mediaList) == 0 {
|
||||
return fmt.Errorf("no media found for content ID: %s", dlCtx.MatchedContentID)
|
||||
}
|
||||
|
||||
for i := range mediaList {
|
||||
defaultFormat := mediaList[i].GetDefaultFormat()
|
||||
if defaultFormat == nil {
|
||||
return fmt.Errorf("no default format found for media at index %d", i)
|
||||
}
|
||||
if len(defaultFormat.URL) == 0 {
|
||||
return fmt.Errorf("media format at index %d has no URL", i)
|
||||
}
|
||||
// ensure we can merge video and audio formats
|
||||
ensureMergeFormats(mediaList[i], defaultFormat)
|
||||
mediaList[i].Format = defaultFormat
|
||||
}
|
||||
|
||||
medias, err := DownloadMedias(mediaList, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download media list: %w", err)
|
||||
}
|
||||
|
||||
if len(medias) == 0 {
|
||||
return fmt.Errorf("no formats downloaded")
|
||||
}
|
||||
|
||||
isCaptionEnabled := true
|
||||
if dlCtx.GroupSettings != nil && !*dlCtx.GroupSettings.Captions {
|
||||
isCaptionEnabled = false
|
||||
}
|
||||
messageCaption := FormatCaption(
|
||||
mediaList[0],
|
||||
isCaptionEnabled,
|
||||
)
|
||||
|
||||
// plugins act as post-processing for the media.
|
||||
// they are run after the media is downloaded
|
||||
// and before it is sent to the user
|
||||
// this allows for things like merging audio and video, etc.
|
||||
for _, media := range medias {
|
||||
for _, plugin := range media.Media.Format.Plugins {
|
||||
err = plugin(media)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run plugin: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = SendMedias(
|
||||
bot, ctx, dlCtx,
|
||||
medias,
|
||||
&models.SendMediaFormatsOptions{
|
||||
Caption: messageCaption,
|
||||
IsStored: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send formats: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleDefaultStoredFormatDownload(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
dlCtx *models.DownloadContext,
|
||||
storedMedias []*models.Media,
|
||||
) error {
|
||||
isCaptionEnabled := true
|
||||
if dlCtx.GroupSettings != nil && !*dlCtx.GroupSettings.Captions {
|
||||
isCaptionEnabled = false
|
||||
}
|
||||
messageCaption := FormatCaption(
|
||||
storedMedias[0],
|
||||
isCaptionEnabled,
|
||||
)
|
||||
var formats []*models.DownloadedMedia
|
||||
for _, media := range storedMedias {
|
||||
formats = append(formats, &models.DownloadedMedia{
|
||||
FilePath: "",
|
||||
ThumbnailFilePath: "",
|
||||
Media: media,
|
||||
})
|
||||
}
|
||||
_, err := SendMedias(
|
||||
bot, ctx, dlCtx,
|
||||
formats,
|
||||
&models.SendMediaFormatsOptions{
|
||||
Caption: messageCaption,
|
||||
IsStored: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send media: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
187
bot/core/download.go
Normal file
187
bot/core/download.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"govd/enums"
|
||||
"govd/models"
|
||||
"govd/util"
|
||||
)
|
||||
|
||||
func downloadMediaItem(
|
||||
ctx context.Context,
|
||||
media *models.Media,
|
||||
config *models.DownloadConfig,
|
||||
idx int,
|
||||
) (*models.DownloadedMedia, error) {
|
||||
if config == nil {
|
||||
config = util.DefaultConfig()
|
||||
}
|
||||
|
||||
format := media.Format
|
||||
if format == nil {
|
||||
return nil, fmt.Errorf("media format is nil")
|
||||
}
|
||||
|
||||
fileName := format.GetFileName()
|
||||
var filePath string
|
||||
var thumbnailFilePath string
|
||||
|
||||
if format.Type != enums.MediaTypePhoto {
|
||||
if len(format.Segments) == 0 {
|
||||
path, err := util.DownloadFile(
|
||||
ctx, format.URL,
|
||||
fileName, config,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download file: %w", err)
|
||||
}
|
||||
filePath = path
|
||||
} else {
|
||||
path, err := util.DownloadFileWithSegments(
|
||||
ctx, format.Segments,
|
||||
fileName, config,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download segments: %w", err)
|
||||
}
|
||||
filePath = path
|
||||
}
|
||||
|
||||
if format.Type == enums.MediaTypeVideo || format.Type == enums.MediaTypeAudio {
|
||||
path, err := getFileThumbnail(format, filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get thumbnail: %w", err)
|
||||
}
|
||||
thumbnailFilePath = path
|
||||
}
|
||||
|
||||
if format.Type == enums.MediaTypeVideo {
|
||||
if format.Width == 0 || format.Height == 0 || format.Duration == 0 {
|
||||
insertVideoInfo(format, filePath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
file, err := util.DownloadFileInMemory(ctx, format.URL, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download image: %w", err)
|
||||
}
|
||||
path := filepath.Join(config.DownloadDir, fileName)
|
||||
if err := util.ImgToJPEG(file, path); err != nil {
|
||||
return nil, fmt.Errorf("failed to convert image: %w", err)
|
||||
}
|
||||
filePath = path
|
||||
}
|
||||
|
||||
return &models.DownloadedMedia{
|
||||
FilePath: filePath,
|
||||
ThumbnailFilePath: thumbnailFilePath,
|
||||
Media: media,
|
||||
Index: idx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func StartDownloadTask(
|
||||
media *models.Media,
|
||||
idx int,
|
||||
config *models.DownloadConfig,
|
||||
) (*models.DownloadedMedia, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
return downloadMediaItem(ctx, media, config, idx)
|
||||
}
|
||||
|
||||
func StartConcurrentDownload(
|
||||
media *models.Media,
|
||||
resultsChan chan<- models.DownloadedMedia,
|
||||
config *models.DownloadConfig,
|
||||
errChan chan<- error,
|
||||
wg *sync.WaitGroup,
|
||||
idx int,
|
||||
) {
|
||||
defer wg.Done()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
result, err := downloadMediaItem(ctx, media, config, idx)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
resultsChan <- *result
|
||||
}
|
||||
|
||||
func DownloadMedia(
|
||||
media *models.Media,
|
||||
config *models.DownloadConfig,
|
||||
) (*models.DownloadedMedia, error) {
|
||||
return StartDownloadTask(media, 0, config)
|
||||
}
|
||||
|
||||
func DownloadMedias(
|
||||
medias []*models.Media,
|
||||
config *models.DownloadConfig,
|
||||
) ([]*models.DownloadedMedia, error) {
|
||||
if len(medias) == 0 {
|
||||
return []*models.DownloadedMedia{}, nil
|
||||
}
|
||||
|
||||
if len(medias) == 1 {
|
||||
result, err := DownloadMedia(medias[0], config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*models.DownloadedMedia{result}, nil
|
||||
}
|
||||
|
||||
resultsChan := make(chan models.DownloadedMedia, len(medias))
|
||||
errChan := make(chan error, len(medias))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for idx, media := range medias {
|
||||
wg.Add(1)
|
||||
go StartConcurrentDownload(media, resultsChan, config, errChan, &wg, idx)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
var results []*models.DownloadedMedia
|
||||
var firstError error
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
firstError = err
|
||||
}
|
||||
default:
|
||||
// no errors (yet)
|
||||
}
|
||||
|
||||
for result := range resultsChan {
|
||||
resultCopy := result // create a copy to avoid pointer issues
|
||||
results = append(results, &resultCopy)
|
||||
}
|
||||
|
||||
if firstError != nil {
|
||||
return results, firstError
|
||||
}
|
||||
|
||||
if len(results) > 1 {
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
return results[i].Index < results[j].Index
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
251
bot/core/inline.go
Normal file
251
bot/core/inline.go
Normal file
|
@ -0,0 +1,251 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var InlineTasks = make(map[string]*models.DownloadContext)
|
||||
|
||||
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 {
|
||||
log.Println("failed to answer inline query:", err)
|
||||
}
|
||||
if !ok {
|
||||
log.Println("failed to answer inline query")
|
||||
return nil
|
||||
}
|
||||
InlineTasks[taskID] = dlCtx
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetInlineFormat(
|
||||
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)
|
||||
}
|
||||
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(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)
|
||||
err = StoreMedias(
|
||||
dlCtx, msgs,
|
||||
medias,
|
||||
)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to store media: %w", err)
|
||||
return
|
||||
}
|
||||
mediaChan <- medias[0].Media
|
||||
}
|
147
bot/core/main.go
Normal file
147
bot/core/main.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"govd/enums"
|
||||
"govd/models"
|
||||
"govd/util"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
func HandleDownloadRequest(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
dlCtx *models.DownloadContext,
|
||||
) error {
|
||||
chatID := ctx.EffectiveMessage.Chat.Id
|
||||
if dlCtx.Extractor.Type == enums.ExtractorTypeSingle {
|
||||
TypingEffect(bot, ctx, chatID)
|
||||
err := HandleDefaultFormatDownload(bot, ctx, dlCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return util.ErrUnsupportedExtractorType
|
||||
}
|
||||
|
||||
func SendMedias(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
dlCtx *models.DownloadContext,
|
||||
medias []*models.DownloadedMedia,
|
||||
options *models.SendMediaFormatsOptions,
|
||||
) ([]gotgbot.Message, error) {
|
||||
var chatID int64
|
||||
var messageOptions *gotgbot.SendMediaGroupOpts
|
||||
|
||||
if dlCtx.GroupSettings != nil {
|
||||
if len(medias) > dlCtx.GroupSettings.MediaGroupLimit {
|
||||
return nil, util.ErrMediaGroupLimitExceeded
|
||||
}
|
||||
if !*dlCtx.GroupSettings.NSFW {
|
||||
for _, media := range medias {
|
||||
if media.Media.NSFW {
|
||||
return nil, util.ErrNSFWNotAllowed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.Message != nil:
|
||||
chatID = ctx.EffectiveMessage.Chat.Id
|
||||
messageOptions = &gotgbot.SendMediaGroupOpts{
|
||||
ReplyParameters: &gotgbot.ReplyParameters{
|
||||
MessageId: ctx.EffectiveMessage.MessageId,
|
||||
},
|
||||
}
|
||||
case ctx.CallbackQuery != nil:
|
||||
chatID = ctx.CallbackQuery.Message.GetChat().Id
|
||||
messageOptions = nil
|
||||
case ctx.InlineQuery != nil:
|
||||
chatID = ctx.InlineQuery.From.Id
|
||||
messageOptions = nil
|
||||
case ctx.ChosenInlineResult != nil:
|
||||
chatID = ctx.ChosenInlineResult.From.Id
|
||||
messageOptions = &gotgbot.SendMediaGroupOpts{
|
||||
DisableNotification: true,
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("failed to get chat id")
|
||||
}
|
||||
|
||||
var sentMessages []gotgbot.Message
|
||||
|
||||
mediaGroupChunks := slices.Collect(
|
||||
slices.Chunk(medias, 10),
|
||||
)
|
||||
|
||||
for _, chunk := range mediaGroupChunks {
|
||||
var inputMediaList []gotgbot.InputMedia
|
||||
for idx, media := range chunk {
|
||||
var caption string
|
||||
|
||||
if idx == 0 {
|
||||
caption = options.Caption
|
||||
}
|
||||
inputMedia, err := media.Media.Format.GetInputMedia(
|
||||
media.FilePath,
|
||||
media.ThumbnailFilePath,
|
||||
caption,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get input media: %w", err)
|
||||
}
|
||||
inputMediaList = append(inputMediaList, inputMedia)
|
||||
}
|
||||
mediaType := chunk[0].Media.Format.Type
|
||||
SendingEffect(bot, ctx, chatID, mediaType)
|
||||
msgs, err := bot.SendMediaGroup(
|
||||
chatID,
|
||||
inputMediaList,
|
||||
messageOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, media := range chunk {
|
||||
if media.FilePath != "" {
|
||||
os.Remove(media.FilePath)
|
||||
}
|
||||
if media.ThumbnailFilePath != "" {
|
||||
os.Remove(media.ThumbnailFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
sentMessages = append(sentMessages, msgs...)
|
||||
if sentMessages[0].Chat.Type != "private" {
|
||||
if len(mediaGroupChunks) > 1 {
|
||||
time.Sleep(3 * time.Second)
|
||||
} // avoid floodwait?
|
||||
}
|
||||
}
|
||||
if len(sentMessages) == 0 {
|
||||
return nil, errors.New("no messages sent")
|
||||
}
|
||||
if !options.IsStored {
|
||||
err := StoreMedias(
|
||||
dlCtx,
|
||||
sentMessages,
|
||||
medias,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to cache formats: %w", err)
|
||||
}
|
||||
}
|
||||
return sentMessages, nil
|
||||
}
|
280
bot/core/util.go
Normal file
280
bot/core/util.go
Normal file
|
@ -0,0 +1,280 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"govd/database"
|
||||
"govd/enums"
|
||||
"govd/models"
|
||||
"govd/plugins"
|
||||
"govd/util"
|
||||
"govd/util/av"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
func getFileThumbnail(
|
||||
format *models.MediaFormat,
|
||||
filePath string,
|
||||
) (string, error) {
|
||||
fileDir := filepath.Dir(filePath)
|
||||
fileName := filepath.Base(filePath)
|
||||
fileExt := filepath.Ext(fileName)
|
||||
fileBaseName := fileName[:len(fileName)-len(fileExt)]
|
||||
thumbnailFilePath := filepath.Join(fileDir, fileBaseName+".thumb.jpeg")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if len(format.Thumbnail) > 0 {
|
||||
file, err := util.DownloadFileInMemory(ctx, format.Thumbnail, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download file in memory: %w", err)
|
||||
}
|
||||
err = util.ImgToJPEG(file, thumbnailFilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to convert to JPEG: %w", err)
|
||||
}
|
||||
return thumbnailFilePath, nil
|
||||
}
|
||||
if format.Type == enums.MediaTypeVideo {
|
||||
err := av.ExtractVideoThumbnail(filePath, thumbnailFilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to extract video thumbnail: %w", err)
|
||||
}
|
||||
return thumbnailFilePath, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func insertVideoInfo(
|
||||
format *models.MediaFormat,
|
||||
filePath string,
|
||||
) {
|
||||
width, height, duration := av.GetVideoInfo(filePath)
|
||||
format.Width = width
|
||||
format.Height = height
|
||||
format.Duration = duration
|
||||
}
|
||||
|
||||
func GetMessageFileID(msg *gotgbot.Message) string {
|
||||
switch {
|
||||
case msg.Video != nil:
|
||||
return msg.Video.FileId
|
||||
case msg.Animation != nil:
|
||||
return msg.Animation.FileId
|
||||
case msg.Photo != nil:
|
||||
return msg.Photo[len(msg.Photo)-1].FileId
|
||||
case msg.Document != nil:
|
||||
return msg.Document.FileId
|
||||
case msg.Audio != nil:
|
||||
return msg.Audio.FileId
|
||||
case msg.Voice != nil:
|
||||
return msg.Voice.FileId
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func GetMessageFileSize(msg *gotgbot.Message) int64 {
|
||||
switch {
|
||||
case msg.Video != nil:
|
||||
return msg.Video.FileSize
|
||||
case msg.Animation != nil:
|
||||
return msg.Animation.FileSize
|
||||
case msg.Photo != nil:
|
||||
return msg.Photo[len(msg.Photo)-1].FileSize
|
||||
case msg.Document != nil:
|
||||
return msg.Document.FileSize
|
||||
case msg.Audio != nil:
|
||||
return msg.Audio.FileSize
|
||||
case msg.Voice != nil:
|
||||
return msg.Voice.FileSize
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func StoreMedias(
|
||||
dlCtx *models.DownloadContext,
|
||||
msgs []gotgbot.Message,
|
||||
medias []*models.DownloadedMedia,
|
||||
) error {
|
||||
var storedMedias []*models.Media
|
||||
if len(medias) == 0 {
|
||||
return fmt.Errorf("no media to store")
|
||||
}
|
||||
for idx, msg := range msgs {
|
||||
fileID := GetMessageFileID(&msg)
|
||||
if len(fileID) == 0 {
|
||||
return fmt.Errorf("no file ID found for media at index %d", idx)
|
||||
}
|
||||
fileSize := GetMessageFileSize(&msg)
|
||||
medias[idx].Media.Format.FileID = fileID
|
||||
medias[idx].Media.Format.FileSize = fileSize
|
||||
storedMedias = append(
|
||||
storedMedias,
|
||||
medias[idx].Media,
|
||||
)
|
||||
}
|
||||
for _, media := range storedMedias {
|
||||
err := database.StoreMedia(
|
||||
dlCtx.Extractor.CodeName,
|
||||
media.ContentID,
|
||||
media,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store media: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FormatCaption(
|
||||
media *models.Media,
|
||||
isEnabled bool,
|
||||
) string {
|
||||
newCaption := fmt.Sprintf(
|
||||
"<a href='%s'>source</a> - @govd_bot\n",
|
||||
media.ContentURL,
|
||||
)
|
||||
if isEnabled && media.Caption.Valid {
|
||||
text := media.Caption.String
|
||||
if len(text) > 600 {
|
||||
text = text[:600] + "..."
|
||||
}
|
||||
newCaption += fmt.Sprintf(
|
||||
"<blockquote expandable>%s</blockquote>\n",
|
||||
util.EscapeCaption(text),
|
||||
)
|
||||
}
|
||||
return newCaption
|
||||
}
|
||||
|
||||
func TypingEffect(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
chatID int64,
|
||||
) {
|
||||
bot.SendChatAction(
|
||||
chatID,
|
||||
"typing",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func SendingEffect(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
chatID int64,
|
||||
mediaType enums.MediaType,
|
||||
) {
|
||||
action := "upload_document"
|
||||
if mediaType == enums.MediaTypeVideo {
|
||||
action = "upload_video"
|
||||
}
|
||||
if mediaType == enums.MediaTypeAudio {
|
||||
action = "upload_audio"
|
||||
}
|
||||
if mediaType == enums.MediaTypePhoto {
|
||||
action = "upload_photo"
|
||||
}
|
||||
bot.SendChatAction(
|
||||
chatID,
|
||||
action,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func HandleErrorMessage(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
err error,
|
||||
) {
|
||||
currentError := err
|
||||
for currentError != nil {
|
||||
var botError *util.Error
|
||||
if errors.As(currentError, &botError) {
|
||||
SendErrorMessage(bot, ctx, fmt.Sprintf(
|
||||
"error occurred when downloading: %s",
|
||||
currentError.Error(),
|
||||
))
|
||||
return
|
||||
}
|
||||
currentError = errors.Unwrap(currentError)
|
||||
}
|
||||
|
||||
lastError := util.GetLastError(err)
|
||||
errorMessage := fmt.Sprintf(
|
||||
"error occurred when downloading: %s",
|
||||
lastError.Error(),
|
||||
)
|
||||
|
||||
if strings.Contains(errorMessage, bot.Token) {
|
||||
errorMessage = "telegram related error, probably connection issue"
|
||||
}
|
||||
|
||||
SendErrorMessage(bot, ctx, errorMessage)
|
||||
|
||||
}
|
||||
|
||||
func SendErrorMessage(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
errorMessage string,
|
||||
) {
|
||||
log.Println(errorMessage)
|
||||
|
||||
switch {
|
||||
case ctx.Update.Message != nil:
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
errorMessage,
|
||||
nil,
|
||||
)
|
||||
case ctx.Update.InlineQuery != nil:
|
||||
ctx.InlineQuery.Answer(
|
||||
bot,
|
||||
nil,
|
||||
&gotgbot.AnswerInlineQueryOpts{
|
||||
CacheTime: 1,
|
||||
Button: &gotgbot.InlineQueryResultsButton{
|
||||
Text: errorMessage,
|
||||
StartParameter: "start",
|
||||
},
|
||||
},
|
||||
)
|
||||
case ctx.ChosenInlineResult != nil:
|
||||
bot.EditMessageText(
|
||||
errorMessage,
|
||||
&gotgbot.EditMessageTextOpts{
|
||||
InlineMessageId: ctx.ChosenInlineResult.InlineMessageId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ensureMergeFormats(
|
||||
media *models.Media,
|
||||
videoFormat *models.MediaFormat,
|
||||
) {
|
||||
if videoFormat.Type != enums.MediaTypeVideo {
|
||||
return
|
||||
}
|
||||
if videoFormat.AudioCodec != "" {
|
||||
return
|
||||
}
|
||||
// video with no audio
|
||||
audioFormat := media.GetDefaultAudioFormat()
|
||||
if audioFormat == nil {
|
||||
return
|
||||
}
|
||||
videoFormat.AudioCodec = audioFormat.AudioCodec
|
||||
videoFormat.Plugins = append(videoFormat.Plugins, plugins.MergeAudio)
|
||||
}
|
44
bot/handlers/ext.go
Normal file
44
bot/handlers/ext.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
extractors "govd/ext"
|
||||
"strings"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
func ExtractorsHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
ctx.CallbackQuery.Answer(bot, nil)
|
||||
|
||||
messageText := "available extractors:\n"
|
||||
extractorNames := make([]string, 0, len(extractors.List))
|
||||
for _, extractor := range extractors.List {
|
||||
if extractor.IsRedirect {
|
||||
continue
|
||||
}
|
||||
extractorNames = append(extractorNames, extractor.Name)
|
||||
}
|
||||
messageText += strings.Join(extractorNames, ", ")
|
||||
|
||||
ctx.EffectiveMessage.EditText(
|
||||
bot,
|
||||
messageText,
|
||||
&gotgbot.EditMessageTextOpts{
|
||||
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{
|
||||
IsDisabled: true,
|
||||
},
|
||||
ReplyMarkup: gotgbot.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "back",
|
||||
CallbackData: "start",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
45
bot/handlers/help.go
Normal file
45
bot/handlers/help.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
var helpMessage = "usage:\n" +
|
||||
"- you can add the bot to a group " +
|
||||
"to start catching sent links\n" +
|
||||
"- you can send a link to the bot privately " +
|
||||
"to download the media too\n\n" +
|
||||
"group commands:\n" +
|
||||
"- /settings = show current settings\n" +
|
||||
"- /captions (true|false) = enable/disable descriptions\n" +
|
||||
"- /nsfw (true|false) = enable/disable nsfw content\n" +
|
||||
"- /limit (int) = set max items in media groups\n\n" +
|
||||
"note: the bot is still in beta, " +
|
||||
"so expect some bugs and missing features.\n"
|
||||
|
||||
var helpKeyboard = gotgbot.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "back",
|
||||
CallbackData: "start",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func HelpHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
ctx.CallbackQuery.Answer(bot, nil)
|
||||
ctx.EffectiveMessage.EditText(
|
||||
bot,
|
||||
helpMessage,
|
||||
&gotgbot.EditMessageTextOpts{
|
||||
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{
|
||||
IsDisabled: true,
|
||||
},
|
||||
ReplyMarkup: helpKeyboard,
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
91
bot/handlers/inline.go
Normal file
91
bot/handlers/inline.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"govd/bot/core"
|
||||
"govd/models"
|
||||
"govd/util"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
extractors "govd/ext"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
func InlineDownloadHandler(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
) error {
|
||||
url := strings.TrimSpace(ctx.InlineQuery.Query)
|
||||
if url == "" {
|
||||
ctx.InlineQuery.Answer(bot, []gotgbot.InlineQueryResult{}, &gotgbot.AnswerInlineQueryOpts{
|
||||
CacheTime: 1,
|
||||
IsPersonal: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
dlCtx, err := extractors.CtxByURL(url)
|
||||
if err != nil || dlCtx == nil || dlCtx.Extractor == nil {
|
||||
ctx.InlineQuery.Answer(bot, []gotgbot.InlineQueryResult{}, &gotgbot.AnswerInlineQueryOpts{
|
||||
CacheTime: 1,
|
||||
IsPersonal: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return core.HandleInline(bot, ctx, dlCtx)
|
||||
}
|
||||
|
||||
func InlineDownloadResultHandler(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
) error {
|
||||
dlCtx, ok := core.InlineTasks[ctx.ChosenInlineResult.ResultId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defer delete(core.InlineTasks, ctx.ChosenInlineResult.ResultId)
|
||||
|
||||
mediaChan := make(chan *models.Media, 1)
|
||||
errChan := make(chan error, 1)
|
||||
timeout, cancel := context.WithTimeout(
|
||||
context.Background(),
|
||||
5*time.Minute,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
go core.GetInlineFormat(
|
||||
bot, ctx, dlCtx,
|
||||
mediaChan, errChan,
|
||||
)
|
||||
select {
|
||||
case media := <-mediaChan:
|
||||
err := core.HandleInlineCachedResult(
|
||||
bot, ctx,
|
||||
dlCtx, media,
|
||||
)
|
||||
if err != nil {
|
||||
core.HandleErrorMessage(bot, ctx, err)
|
||||
return nil
|
||||
}
|
||||
case err := <-errChan:
|
||||
core.HandleErrorMessage(bot, ctx, err)
|
||||
return nil
|
||||
case <-timeout.Done():
|
||||
core.HandleErrorMessage(bot, ctx, util.ErrTimeout)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func InlineLoadingHandler(
|
||||
bot *gotgbot.Bot,
|
||||
ctx *ext.Context,
|
||||
) error {
|
||||
ctx.CallbackQuery.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{
|
||||
Text: "wait !",
|
||||
ShowAlert: true,
|
||||
})
|
||||
return nil
|
||||
}
|
72
bot/handlers/instances.go
Normal file
72
bot/handlers/instances.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
var buildHash = "unknown"
|
||||
var branchName = "unknown"
|
||||
|
||||
func getInstanceMessage() string {
|
||||
return "current instance\n" +
|
||||
"go version: %s\n" +
|
||||
"build: <a href='%s'>%s</a>\n" +
|
||||
"branch: <a href='%s'>%s</a>\n\n" +
|
||||
"public instances\n" +
|
||||
"- @govd_bot | main official instance\n" +
|
||||
"\nwant to add your own instance? reach us on @govdsupport"
|
||||
}
|
||||
|
||||
func InstancesHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
var commitURL string
|
||||
var branchURL string
|
||||
|
||||
repoURL := os.Getenv("REPO_URL")
|
||||
if repoURL != "" {
|
||||
commitURL = fmt.Sprintf(
|
||||
"%s/tree/%s",
|
||||
repoURL,
|
||||
buildHash,
|
||||
)
|
||||
branchURL = fmt.Sprintf(
|
||||
"%s/tree/%s",
|
||||
repoURL,
|
||||
branchName,
|
||||
)
|
||||
}
|
||||
messageText := fmt.Sprintf(
|
||||
getInstanceMessage(),
|
||||
strings.TrimPrefix(runtime.Version(), "go"),
|
||||
commitURL,
|
||||
buildHash,
|
||||
branchURL,
|
||||
branchName,
|
||||
)
|
||||
ctx.CallbackQuery.Answer(bot, nil)
|
||||
ctx.EffectiveMessage.EditText(
|
||||
bot,
|
||||
messageText,
|
||||
&gotgbot.EditMessageTextOpts{
|
||||
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{
|
||||
IsDisabled: true,
|
||||
},
|
||||
ReplyMarkup: gotgbot.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "back",
|
||||
CallbackData: "start",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
213
bot/handlers/settings.go
Normal file
213
bot/handlers/settings.go
Normal file
|
@ -0,0 +1,213 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"govd/database"
|
||||
"govd/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
func SettingsHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
if ctx.EffectiveMessage.Chat.Type == "private" {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"use this command in group chats only",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
settings, err := database.GetGroupSettings(ctx.EffectiveMessage.Chat.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
fmt.Sprintf(
|
||||
"settings for this group\n\ncaptions: %s\nnsfw: %s\nmedia group limit: %d",
|
||||
strconv.FormatBool(*settings.Captions),
|
||||
strconv.FormatBool(*settings.NSFW),
|
||||
settings.MediaGroupLimit,
|
||||
),
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CaptionsHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
if ctx.EffectiveMessage.Chat.Type == "private" {
|
||||
return nil
|
||||
}
|
||||
|
||||
chatID := ctx.EffectiveMessage.Chat.Id
|
||||
userID := ctx.EffectiveMessage.From.Id
|
||||
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"usage: /captions (true|false)",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !util.IsUserAdmin(bot, chatID, userID) {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"you don't have permission to change settings",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
userInput := strings.ToLower(args[1])
|
||||
value, err := strconv.ParseBool(userInput)
|
||||
if err != nil {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
fmt.Sprintf("invalid value (%s), use true or false", userInput),
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
settings, err := database.GetGroupSettings(chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.Captions = &value
|
||||
err = database.UpdateGroupSettings(chatID, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var message string
|
||||
if value {
|
||||
message = "captions enabled"
|
||||
} else {
|
||||
message = "captions disabled"
|
||||
}
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
message,
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NSFWHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
if ctx.EffectiveMessage.Chat.Type == "private" {
|
||||
return nil
|
||||
}
|
||||
|
||||
chatID := ctx.EffectiveMessage.Chat.Id
|
||||
userID := ctx.EffectiveMessage.From.Id
|
||||
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"usage: /nsfw (true|false)",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !util.IsUserAdmin(bot, chatID, userID) {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"you don't have permission to change settings",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
userInput := strings.ToLower(args[1])
|
||||
value, err := strconv.ParseBool(userInput)
|
||||
if err != nil {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
fmt.Sprintf("invalid value (%s), use true or false", userInput),
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
settings, err := database.GetGroupSettings(chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.NSFW = &value
|
||||
err = database.UpdateGroupSettings(chatID, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var message string
|
||||
if value {
|
||||
message = "nsfw enabled"
|
||||
} else {
|
||||
message = "nsfw disabled"
|
||||
}
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
message,
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func MediaGroupLimitHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
if ctx.EffectiveMessage.Chat.Type == "private" {
|
||||
return nil
|
||||
}
|
||||
|
||||
chatID := ctx.EffectiveMessage.Chat.Id
|
||||
userID := ctx.EffectiveMessage.From.Id
|
||||
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"usage: /limit (int)",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !util.IsUserAdmin(bot, chatID, userID) {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"you don't have permission to change settings",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
value, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
fmt.Sprintf("invalid value (%s), use a number", args[1]),
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if value < 1 || value > 20 {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
"media group limit must be between 1 and 20",
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
settings, err := database.GetGroupSettings(chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.MediaGroupLimit = value
|
||||
err = database.UpdateGroupSettings(chatID, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
fmt.Sprintf("media group limit set to %d", value),
|
||||
nil,
|
||||
)
|
||||
return nil
|
||||
}
|
88
bot/handlers/start.go
Normal file
88
bot/handlers/start.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
var startMessage = "govd is an open-source telegram bot " +
|
||||
"that allows you to download medias from " +
|
||||
"various platforms. the project born after " +
|
||||
"the discontinuation of an " +
|
||||
"highly popular bot, known as UVD."
|
||||
|
||||
func getStartKeyboard(bot *gotgbot.Bot) gotgbot.InlineKeyboardMarkup {
|
||||
return gotgbot.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "add to group",
|
||||
Url: fmt.Sprintf(
|
||||
"https://t.me/%s?startgroup=true",
|
||||
bot.Username,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "usage",
|
||||
CallbackData: "help",
|
||||
},
|
||||
{
|
||||
Text: "stats",
|
||||
CallbackData: "stats",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "extractors",
|
||||
CallbackData: "extractors",
|
||||
},
|
||||
{
|
||||
Text: "support",
|
||||
Url: "https://t.me/govdsupport",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "instances",
|
||||
CallbackData: "instances",
|
||||
},
|
||||
{
|
||||
Text: "github",
|
||||
Url: os.Getenv("REPO_URL"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func StartHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
if ctx.EffectiveMessage.Chat.Type != "private" {
|
||||
return nil
|
||||
}
|
||||
keyboard := getStartKeyboard(bot)
|
||||
if ctx.Update.Message != nil {
|
||||
ctx.EffectiveMessage.Reply(
|
||||
bot,
|
||||
startMessage,
|
||||
&gotgbot.SendMessageOpts{
|
||||
ReplyMarkup: &keyboard,
|
||||
},
|
||||
)
|
||||
} else if ctx.Update.CallbackQuery != nil {
|
||||
ctx.CallbackQuery.Answer(bot, nil)
|
||||
ctx.EffectiveMessage.EditText(
|
||||
bot,
|
||||
startMessage,
|
||||
&gotgbot.EditMessageTextOpts{
|
||||
ReplyMarkup: keyboard,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
89
bot/handlers/stats.go
Normal file
89
bot/handlers/stats.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"govd/database"
|
||||
"time"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
TotalUsers int64
|
||||
TotalGroups int64
|
||||
TotalDailyUsers int64
|
||||
TotalMedia int64
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var lastSavedStats *Stats
|
||||
|
||||
var statsMessage = "users: %d\nusers today: %d\ngroups: %d\ndownloads: %d\n\nupdates every 10 minutes"
|
||||
|
||||
func StatsHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
if ctx.EffectiveMessage.Chat.Type != "private" {
|
||||
return nil
|
||||
}
|
||||
ctx.CallbackQuery.Answer(bot, nil)
|
||||
stats := GetStats()
|
||||
ctx.EffectiveMessage.EditText(
|
||||
bot,
|
||||
fmt.Sprintf(
|
||||
statsMessage,
|
||||
stats.TotalUsers,
|
||||
stats.TotalDailyUsers,
|
||||
stats.TotalGroups,
|
||||
stats.TotalMedia,
|
||||
),
|
||||
&gotgbot.EditMessageTextOpts{
|
||||
ReplyMarkup: gotgbot.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "back",
|
||||
CallbackData: "start",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateStats() {
|
||||
totalUsers, err := database.GetUsersCount()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
totalGroups, err := database.GetGroupsCount()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
totalDailyUsers, err := database.GetDailyUserCount()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
totalMedia, err := database.GetMediaCount()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lastSavedStats = &Stats{
|
||||
TotalUsers: totalUsers,
|
||||
TotalGroups: totalGroups,
|
||||
TotalDailyUsers: totalDailyUsers,
|
||||
TotalMedia: totalMedia,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func GetStats() *Stats {
|
||||
if lastSavedStats == nil {
|
||||
UpdateStats()
|
||||
}
|
||||
if lastSavedStats.UpdatedAt.Add(10 * time.Minute).Before(time.Now()) {
|
||||
UpdateStats()
|
||||
}
|
||||
return lastSavedStats
|
||||
}
|
65
bot/handlers/url.go
Normal file
65
bot/handlers/url.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"govd/bot/core"
|
||||
"govd/database"
|
||||
extractors "govd/ext"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/message"
|
||||
)
|
||||
|
||||
func URLHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
|
||||
messageURL := getMessageURL(ctx.EffectiveMessage)
|
||||
if messageURL == "" {
|
||||
return nil
|
||||
}
|
||||
dlCtx, err := extractors.CtxByURL(messageURL)
|
||||
if err != nil {
|
||||
core.HandleErrorMessage(
|
||||
bot, ctx, err)
|
||||
return nil
|
||||
}
|
||||
if dlCtx == nil || dlCtx.Extractor == nil {
|
||||
return nil
|
||||
}
|
||||
userID := ctx.EffectiveMessage.From.Id
|
||||
if ctx.EffectiveMessage.Chat.Type != "private" {
|
||||
settings, err := database.GetGroupSettings(ctx.EffectiveMessage.Chat.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dlCtx.GroupSettings = settings
|
||||
}
|
||||
if userID != 1087968824 {
|
||||
// groupAnonymousBot
|
||||
_, err = database.GetUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = core.HandleDownloadRequest(bot, ctx, dlCtx)
|
||||
if err != nil {
|
||||
core.HandleErrorMessage(
|
||||
bot, ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func URLFilter(msg *gotgbot.Message) bool {
|
||||
return message.Text(msg) && !message.Command(msg) && containsURL(msg)
|
||||
}
|
||||
|
||||
func containsURL(msg *gotgbot.Message) bool {
|
||||
return message.Entity("url")(msg)
|
||||
}
|
||||
|
||||
func getMessageURL(msg *gotgbot.Message) string {
|
||||
for _, entity := range msg.Entities {
|
||||
if entity.Type == "url" {
|
||||
return msg.Text[entity.Offset : entity.Offset+entity.Length]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
118
bot/main.go
Normal file
118
bot/main.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
botHandlers "govd/bot/handlers"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/callbackquery"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/choseninlineresult"
|
||||
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/inlinequery"
|
||||
)
|
||||
|
||||
var AllowedUpdates = []string{
|
||||
"message",
|
||||
"callback_query",
|
||||
"inline_query",
|
||||
"chosen_inline_result",
|
||||
}
|
||||
|
||||
func Start() {
|
||||
token := os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalf("BOT_TOKEN is not provided")
|
||||
}
|
||||
b, err := gotgbot.NewBot(token, &gotgbot.BotOpts{
|
||||
BotClient: NewBotClient(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create bot: %v", err)
|
||||
}
|
||||
dispatcher := ext.NewDispatcher(&ext.DispatcherOpts{
|
||||
Error: func(b *gotgbot.Bot, ctx *ext.Context, err error) ext.DispatcherAction {
|
||||
log.Println("an error occurred while handling update:", err.Error())
|
||||
return ext.DispatcherActionNoop
|
||||
},
|
||||
MaxRoutines: ext.DefaultMaxRoutines,
|
||||
})
|
||||
updater := ext.NewUpdater(dispatcher, nil)
|
||||
registerHandlers(dispatcher)
|
||||
err = updater.StartPolling(b, &ext.PollingOpts{
|
||||
DropPendingUpdates: true,
|
||||
GetUpdatesOpts: &gotgbot.GetUpdatesOpts{
|
||||
Timeout: 9 * 60,
|
||||
RequestOpts: &gotgbot.RequestOpts{
|
||||
Timeout: time.Minute * 10,
|
||||
},
|
||||
AllowedUpdates: AllowedUpdates,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start polling: %v", err)
|
||||
}
|
||||
log.Printf("bot started on: %s\n", b.User.Username)
|
||||
}
|
||||
|
||||
func registerHandlers(dispatcher *ext.Dispatcher) {
|
||||
dispatcher.AddHandler(handlers.NewMessage(
|
||||
botHandlers.URLFilter,
|
||||
botHandlers.URLHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCommand(
|
||||
"start",
|
||||
botHandlers.StartHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCallback(
|
||||
callbackquery.Equal("start"),
|
||||
botHandlers.StartHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCallback(
|
||||
callbackquery.Equal("help"),
|
||||
botHandlers.HelpHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCommand(
|
||||
"settings",
|
||||
botHandlers.SettingsHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCommand(
|
||||
"captions",
|
||||
botHandlers.CaptionsHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCommand(
|
||||
"nsfw",
|
||||
botHandlers.NSFWHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCommand(
|
||||
"limit",
|
||||
botHandlers.MediaGroupLimitHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCallback(
|
||||
callbackquery.Equal("stats"),
|
||||
botHandlers.StatsHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCallback(
|
||||
callbackquery.Equal("extractors"),
|
||||
botHandlers.ExtractorsHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCallback(
|
||||
callbackquery.Equal("instances"),
|
||||
botHandlers.InstancesHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewInlineQuery(
|
||||
inlinequery.All,
|
||||
botHandlers.InlineDownloadHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewChosenInlineResult(
|
||||
choseninlineresult.All,
|
||||
botHandlers.InlineDownloadResultHandler,
|
||||
))
|
||||
dispatcher.AddHandler(handlers.NewCallback(
|
||||
callbackquery.Equal("inline:loading"),
|
||||
botHandlers.InlineLoadingHandler,
|
||||
))
|
||||
}
|
56
bot/middleware.go
Normal file
56
bot/middleware.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||
)
|
||||
|
||||
type BotClient struct {
|
||||
gotgbot.BotClient
|
||||
}
|
||||
|
||||
func (b BotClient) RequestWithContext(
|
||||
ctx context.Context,
|
||||
token string,
|
||||
method string,
|
||||
params map[string]string,
|
||||
data map[string]gotgbot.FileReader,
|
||||
opts *gotgbot.RequestOpts,
|
||||
) (json.RawMessage, error) {
|
||||
if strings.HasPrefix(method, "send") || method == "copyMessage" {
|
||||
params["allow_sending_without_reply"] = "true"
|
||||
}
|
||||
if strings.HasPrefix(method, "send") || strings.HasPrefix(method, "edit") {
|
||||
params["parse_mode"] = "HTML"
|
||||
}
|
||||
val, err := b.BotClient.RequestWithContext(ctx, token, method, params, data, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
func NewBotClient() BotClient {
|
||||
botAPIURL := os.Getenv("BOT_API_URL")
|
||||
if botAPIURL == "" {
|
||||
log.Println("BOT_API_URL is not provided, using default")
|
||||
botAPIURL = gotgbot.DefaultAPIURL
|
||||
}
|
||||
return BotClient{
|
||||
BotClient: &gotgbot.BaseBotClient{
|
||||
Client: http.Client{},
|
||||
UseTestEnvironment: false,
|
||||
DefaultRequestOpts: &gotgbot.RequestOpts{
|
||||
Timeout: 10 * time.Minute,
|
||||
APIURL: botAPIURL,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue