diff --git a/PublishHelperBot/Handlers.fs b/PublishHelperBot/Handlers.fs deleted file mode 100644 index 3693555..0000000 --- a/PublishHelperBot/Handlers.fs +++ /dev/null @@ -1,103 +0,0 @@ -module PublishHelperBot.Handlers - -open System.Threading.Tasks -open Microsoft.FSharp.Core -open PublishHelperBot.Environment -open PublishHelperBot.YoutubeDl -open Telegram.Bot -open Telegram.Bot.Types -open Telegram.Bot.Types.Enums - -type BaseHandlerArgs = Update * BotConfig - -type HandlerArgs = Update * BotConfig * ITelegramBotClient - -type HandlerRequirements = BaseHandlerArgs -> bool - -type Handler = HandlerArgs -> Task - -type Handler<'deps> = 'deps * HandlerArgs -> Task - -// Utils -let UpdateIsAMessage (x: Update) = x.Type = UpdateType.Message - -let FromAdminChat (x: Message, c: BotConfig) = x.Chat.Id = c.adminChatId - -let HasReply (x: Message) = not(isNull x.ReplyToMessage) - -let ExtractPhotoFromMessage (x: Message) = Array.map (fun (p: PhotoSize) -> p.FileId) x.Photo - -let HasText (x: Message) = not(isNull x.Text) - -let UrlsAsAlbumInputMedia (urls: string[]): IAlbumInputMedia[] = - Array.map (fun (x: string) -> InputMediaPhoto(x)) urls - -// Post (Relay) command -type RelayCaptionMode = - | WithAuthor - | Anonymous - | Unknown - -let RelaySupportedContent (x: Message) = - match x.Type with - | MessageType.Text -> true - | MessageType.Photo -> true - | MessageType.Video -> true - | _ -> false - -let RelayCaptionType (command: string) = - match command with - | _ when command.StartsWith "\\post anon" -> Anonymous - | _ when command.StartsWith "\\post" -> WithAuthor - | _ -> Unknown - -let RelayCaption (name: string, url: string) = $"Прислал {name}" -let RelayParseMode = ParseMode.Html; - -let RelayResolveCaption (mode: RelayCaptionMode, username: string, linkUrl: string) = - match mode with - | WithAuthor -> RelayCaption(username, linkUrl) - | _ -> null - -let public RelayMatch: HandlerRequirements = fun (u, c) -> - UpdateIsAMessage u && - FromAdminChat <| (u.Message, c) && - HasReply u.Message && - HasText u.Message && - RelaySupportedContent u.Message.ReplyToMessage && - not (RelayCaptionType u.Message.Text = RelayCaptionMode.Unknown) - -let public RelayHandler: Handler = fun (update, config, tg) -> - let reply = update.Message.ReplyToMessage - let channelId = config.chanelId - let author = $"{reply.From.FirstName} {reply.From.LastName}" - let captionMode = RelayCaptionType update.Message.Text - - let photoMedia = lazy Array.get (ExtractPhotoFromMessage reply) 0 - let caption = lazy RelayResolveCaption(captionMode, author, config.relayUrl) - - match reply.Type with - | MessageType.Text -> tg.ForwardMessageAsync(channelId, reply.Chat.Id, reply.MessageId) - | MessageType.Photo -> tg.SendPhotoAsync(channelId, photoMedia.Value, caption = caption.Value, - parseMode = RelayParseMode) - | MessageType.Video -> tg.SendVideoAsync(channelId, reply.Video.FileId, caption = caption.Value, - parseMode = RelayParseMode) - | _ -> Task.CompletedTask - - -// YoutubeDL repost - -let YoutubeRepostMatchCmd = "\\ytdl" -let public YoutubeRepostMatch: HandlerRequirements = fun (u, c) -> - UpdateIsAMessage u && - FromAdminChat <| (u.Message, c) && - HasText <| u.Message && - u.Message.Text.StartsWith YoutubeRepostMatchCmd && - u.Message.Text.Split(' ').Length = 2 - -let public YoutubeRepostHandler: Handler = fun (yt, (u, c, tg)) -> - task { - let trim (x: string) = x.Trim() - let! id = YoutubeRepostMatchCmd |> u.Message.Text.Split |> Array.last |> trim |> yt.AddJob - do! tg.SendTextMessageAsync(c.adminChatId, id.ToString()) |> Async.AwaitTask |> Async.Ignore - } \ No newline at end of file diff --git a/PublishHelperBot/Program.fs b/PublishHelperBot/Program.fs index 0d55e66..137d894 100644 --- a/PublishHelperBot/Program.fs +++ b/PublishHelperBot/Program.fs @@ -1,71 +1,60 @@ open System open System.Net.Http -open System.Threading open System.Threading.Tasks -open PublishHelperBot.Handlers open PublishHelperBot.Environment open PublishHelperBot.YoutubeDl +open PublishHelperBot.Telegram open Telegram.Bot open Telegram.Bot.Polling open Telegram.Bot.Types open Telegram.Bot.Types.Enums -let createBot (config: BotConfig, http: HttpClient) = TelegramBotClient(config.token, http) - let config = createConfig "SBPB_CONFIG_PATH" -let botClient = createBot (config, new HttpClient()) +let botClient = TelegramBotClient (config.token, new HttpClient()) + +let youtubeDlClient = + YoutubeDlClient.createClient { + Client = new HttpClient() + BaseUrl = config.YoutubeDlUrl + } + +let tgService = + TgService.createService { + Client = botClient + ChannelId = config.chanelId + YoutubeDlClient = youtubeDlClient + AdminChatId = config.adminChatId + } let youtubeDlService = - let youtubeDlClient = - YoutubeDlClient.createClient { - Client = new HttpClient() - BaseUrl = config.YoutubeDlUrl - } - - let tgService = - TgService.createService { - Client = botClient - ChannelId = config.chanelId - YoutubeDlClient = youtubeDlClient - AdminChatId = config.adminChatId - } - YoutubeDlService.createService youtubeDlClient tgService -let startDate = DateTime.UtcNow +let isObsoleteUpdate = + let startDate = DateTime.UtcNow + fun (update: Update) -> + update.Type = UpdateType.Message + && update.Message.Date < startDate -let (|ObsoleteUpdate|RelayMatchUpdate|YoutubeRepostMatchUpdate|SkipUpdate|) (update: Update) = - let isObsoleteUpdate (update: Update) = - update.Type = UpdateType.Message && update.Message.Date < startDate - match update with - | _ when isObsoleteUpdate update -> ObsoleteUpdate - | _ when RelayMatch (update, config) -> RelayMatchUpdate - | _ when YoutubeRepostMatch (update, config) -> YoutubeRepostMatchUpdate - | _ -> SkipUpdate - -let updateHandle (bc: ITelegramBotClient) (update: Update) (ct: CancellationToken): Task = - let tgCtx = (update, config, bc) - match update with - | RelayMatchUpdate() -> - Logging.logger.Information("RelayMatchUpdate") - RelayHandler tgCtx - | YoutubeRepostMatchUpdate() -> - YoutubeRepostHandler <| (youtubeDlService, tgCtx) - | ObsoleteUpdate() -> - Logging.logger.Information("Skipping obsolete update") - Task.CompletedTask - | SkipUpdate() -> - Logging.logger.Information("Skipping update") - Task.CompletedTask - -let handlePollingError (_: ITelegramBotClient) (e: Exception) (_: CancellationToken) = +let handlePollingError _ (e: Exception) _ = Logging.logger.Error(e, "Polling error") Task.CompletedTask -let receiverOptions = ReceiverOptions(AllowedUpdates = Array.zeroCreate 0) - Logging.logger.Information("Starting bot") -botClient.StartReceiving(updateHandle, handlePollingError, receiverOptions) + +let botHandler = TgUpdateHandler.createHandler config tgService youtubeDlService +botClient.StartReceiving( + (fun client update _ -> + if not (isObsoleteUpdate update) then + botHandler.PostUpdate(update) + Task.CompletedTask + ), + handlePollingError, + ReceiverOptions( + AllowedUpdates = Array.zeroCreate 0 + ) +) + Logging.logger.Information("Я родился") + Console.ReadKey() |> ignore \ No newline at end of file diff --git a/PublishHelperBot/PublishHelperBot.fsproj b/PublishHelperBot/PublishHelperBot.fsproj index 091d7e5..2a5225a 100644 --- a/PublishHelperBot/PublishHelperBot.fsproj +++ b/PublishHelperBot/PublishHelperBot.fsproj @@ -12,8 +12,9 @@ + + - diff --git a/PublishHelperBot/Telegram.fs b/PublishHelperBot/Telegram.fs new file mode 100644 index 0000000..5685edc --- /dev/null +++ b/PublishHelperBot/Telegram.fs @@ -0,0 +1,297 @@ +module PublishHelperBot.Telegram + +open System +open System.IO +open Microsoft.FSharp.Control +open PublishHelperBot.Environment +open PublishHelperBot.Types +open Telegram.Bot +open Telegram.Bot.Types +open Telegram.Bot.Types.Enums +open Telegram.Bot.Types.InputFiles + +[] +module BotUpdateType = + let private getRelayCaptionType (command: string) = + match command with + | _ when command.StartsWith "\\post anon" -> Anonymous + | _ when command.StartsWith "\\post" -> WithAuthor + | _ -> Unknown + + let private updateIsAMessage (update: Update) = + update.Type = UpdateType.Message + + let private fromAdminChat (message: Message, adminChatId: ConfigChatId) = + message.Chat.Id = adminChatId + + let private hasReply (x: Message) = + not (isNull x.ReplyToMessage) + + let private hasText (x: Message) = + not (isNull x.Text) + + let private hasRelaySupportedContent (x: Message) = + match x.Type with + | MessageType.Text -> true + | MessageType.Photo -> true + | MessageType.Video -> true + | _ -> false + + let [] private YoutubeRepostMatchCmd = "\\ytdl" + let private isYoutubeRepost (update: Update, adminChatId: ConfigChatId) = + updateIsAMessage update && + fromAdminChat (update.Message, adminChatId) && + hasText update.Message && + update.Message.Text.StartsWith YoutubeRepostMatchCmd && + update.Message.Text.Split(' ').Length = 2 + + let private isRelay (update: Update, adminChatId: ConfigChatId) = + updateIsAMessage update && + fromAdminChat (update.Message, adminChatId) && + hasReply update.Message && + hasRelaySupportedContent update.Message.ReplyToMessage && + not (getRelayCaptionType update.Message.Text = RelayCaptionMode.Unknown) + + let private isPing (update: Update, adminChatId: ConfigChatId) = + updateIsAMessage update && + fromAdminChat (update.Message, adminChatId) && + hasText update.Message && + update.Message.Text.StartsWith("\ping") + + let getUpdateType (update: Update) (config: BotConfig) = + match update with + | _ when isPing (update, config.adminChatId) -> + BotUpdateType.Ping + + | _ when isYoutubeRepost(update, config.adminChatId) -> + let url = update.Message.Text.Split(' ').[1] + BotUpdateType.YoutubeRepost url + + | _ when isRelay(update, config.adminChatId) -> + let reply = update.Message.ReplyToMessage + let getCaption() = + let captionMode = getRelayCaptionType update.Message.Text + let author = $"{reply.From.FirstName} {reply.From.LastName}" + match captionMode with + | WithAuthor -> $"Прислал {author}" + | _ -> null + + match reply.Type with + | MessageType.Text -> + let args = { + ReplyChatId = reply.Chat.Id + ReplyMessageId = reply.MessageId + Relay = RelayType.Text + } + BotUpdateType.RelayUpdate args + + | MessageType.Photo -> + let caption = getCaption() + let media = + reply.Photo + |> Array.map (fun (p: PhotoSize) -> p.FileId) + |> Array.tryHead + + match media with + | Some media -> + let args = { + ReplyChatId = reply.Chat.Id + ReplyMessageId = reply.MessageId + Relay = RelayType.Photo (media, caption) + } + BotUpdateType.RelayUpdate args + + | None -> + BotUpdateType.Skip + + | MessageType.Video -> + let caption = getCaption() + let args = { + ReplyChatId = reply.Chat.Id + ReplyMessageId = reply.MessageId + Relay = RelayType.Video (reply.Video.FileId, caption) + } + BotUpdateType.RelayUpdate args + + | _ -> + BotUpdateType.Skip + + | _ -> + BotUpdateType.Skip + +[] +module TgService = + type private Msg = + | Ping + | PostVideo of url: string * savePath: string * externalId: Guid + | PostRelay of RelayArgs + | PostMessageToAdminChat of text: string + + let private createInbox (config: TgServiceConfig) = + MailboxProcessor.Start(fun inbox -> + let rec loop () = + async { + match! inbox.Receive() with + | Ping -> + Logging.logger.Information("Sending ГЫЧА)))0") + let sticker = InputOnlineFile( + value = "CAACAgIAAx0CQj8KlAACBPBj-ylrAcDqnwvpgEssCuN0aTilywACoxYAAvy_sEqzXsNGSWYfpS4E" + ) + do! + config.Client.SendStickerAsync( + config.AdminChatId, + sticker + ) + |> Async.AwaitTask + |> Async.Catch + |> Async.Ignore + return! loop () + + | PostRelay args -> + Logging.logger.Information("Posting relay, relay = {relay}", args) + match args.Relay with + | RelayType.Text -> + do! + config.Client.ForwardMessageAsync( + config.ChannelId, + args.ReplyChatId, + args.ReplyMessageId + ) + |> Async.AwaitTask + |> Async.Catch + |> Async.Ignore + + | RelayType.Photo(media, caption) -> + do! + config.Client.SendPhotoAsync( + config.ChannelId, + media, + caption, + parseMode = ParseMode.Html + ) + |> Async.AwaitTask + |> Async.Catch + |> Async.Ignore + + | RelayType.Video(media, caption) -> + do! + config.Client.SendVideoAsync( + config.ChannelId, + media, + caption = caption, + parseMode = ParseMode.Html + ) + |> Async.AwaitTask + |> Async.Catch + |> Async.Ignore + + return! loop () + + | PostMessageToAdminChat text -> + do! + config.Client.SendTextMessageAsync( + config.AdminChatId, text + ) + |> Async.AwaitTask + |> Async.Catch + |> Async.Ignore + + return! loop () + + | PostVideo (url, savePath, externalId) -> + try + Logging.logger.Information("Reading file path = {path}", savePath) + use file = File.OpenRead(savePath) + if (file.Length / 1024L / 1024L) < 50L then + let input = InputOnlineFile(file, Path.GetRandomFileName()) + let caption = $"Source: {url}" + Logging.logger.Information( + "Sending video to channel, channelId = {channelId}, caption = {caption}", + config.ChannelId, + caption) + do! config.Client.SendVideoAsync( + config.ChannelId, + input, + caption = caption + ) + |> Async.AwaitTask + |> Async.Catch + |> Async.Ignore + else + inbox.Post(PostMessageToAdminChat($"Да блять, видео вышло больше 50мб: {externalId}")) + finally + Logging.logger.Information("Deleting file path = {path}", savePath) + File.Delete(savePath) + + match! config.YoutubeDlClient.CleanJob(externalId) with + | Ok _ -> () + | Error _ -> () + return! loop () + } + loop () + ) + + let createService config = + let inbox = createInbox config + { new ITgService with + member this.PostRelay(args) = + inbox.Post(PostRelay(args)) + + member this.Ping() = + inbox.Post(Ping) + + member this.PostMessageToAdminChat(text) = + inbox.Post(PostMessageToAdminChat(text)) + + member this.PostVideo(url, savePath, externalId) = + inbox.Post(PostVideo(url, savePath, externalId)) } + +[] +module TgUpdateHandler = + type private Msg = + | NewUpdate of Update + + let private createInbox (config: BotConfig) (service: ITgService) (ytService: IYoutubeDlService) = + MailboxProcessor.Start(fun inbox -> + let rec loop() = + async { + match! inbox.Receive() with + | NewUpdate update -> + try + match BotUpdateType.getUpdateType update config with + | BotUpdateType.Skip -> + Logging.logger.Information("Skipping update") + + | BotUpdateType.Ping -> + Logging.logger.Information("Received ping") + service.Ping() + + | BotUpdateType.YoutubeRepost url -> + Logging.logger.Information("Received youtube repost update") + async { + let! id = ytService.AddJob(url) + service.PostMessageToAdminChat(id.ToString()) + } |> Async.Start + + | BotUpdateType.RelayUpdate relayArgs -> + Logging.logger.Information("Relay update") + service.PostRelay(relayArgs) + + with ex -> + Logging.logger.Error(ex, "Блядь") + try + service.PostMessageToAdminChat "паша сука" + with ex -> + Logging.logger.Error(ex, "Да блядь") + + return! loop () + + } + loop() + ) + + let createHandler config service ytService = + let inbox = createInbox config service ytService + { new ITgUpdateHandler with + member this.PostUpdate update = + inbox.Post(NewUpdate(update)) } \ No newline at end of file diff --git a/PublishHelperBot/Types.fs b/PublishHelperBot/Types.fs new file mode 100644 index 0000000..eef7373 --- /dev/null +++ b/PublishHelperBot/Types.fs @@ -0,0 +1,77 @@ +module PublishHelperBot.Types + +open System +open Telegram.Bot +open Telegram.Bot.Types + +type ConfigChatId = int64 + +type CreateYoutubeDlJob = { + url: string + savePath: string +} + +type CreateYoutubeDlJobSuccess = { + task: Guid +} + +type YoutubeDlStateResponse = { + state: string +} + +type YoutubeDlError = { + message: string +} + +type CreateJobResult = Result + +type CheckJobResult = Result + +type CleanJobResult = Result + +type IYoutubeDlClient = + abstract member CreateJob: CreateYoutubeDlJob -> Async + abstract member CheckJob: externalId: Guid -> Async + abstract member CleanJob: externalId: Guid -> Async + +type IYoutubeDlService = + abstract member AddJob: url: string -> Async + +type TgServiceConfig = { + Client: ITelegramBotClient + ChannelId: ConfigChatId + AdminChatId: ConfigChatId + YoutubeDlClient: IYoutubeDlClient +} + +type RelayCaptionMode = + | WithAuthor + | Anonymous + | Unknown + +type RelayType = + | Text + | Photo of media: string * caption: string + | Video of video: string * caption: string + +type RelayArgs = { + ReplyChatId: int64 + ReplyMessageId: int + Relay: RelayType +} + +[] +type BotUpdateType = + | RelayUpdate of RelayArgs + | YoutubeRepost of url: string + | Ping + | Skip + +type ITgUpdateHandler = + abstract member PostUpdate: Update -> unit + +type ITgService = + abstract member PostRelay: args: RelayArgs -> unit + abstract member PostMessageToAdminChat: text: string -> unit + abstract member Ping: unit -> unit + abstract member PostVideo: url: string * savePath: string * externalId: Guid -> unit \ No newline at end of file diff --git a/PublishHelperBot/YoutubeDl.fs b/PublishHelperBot/YoutubeDl.fs index f47aa4f..695d483 100644 --- a/PublishHelperBot/YoutubeDl.fs +++ b/PublishHelperBot/YoutubeDl.fs @@ -9,38 +9,7 @@ open System.Threading.Tasks open Microsoft.FSharp.Core open Newtonsoft.Json open PublishHelperBot.Environment -open Telegram.Bot -open Telegram.Bot.Types.InputFiles - -type ChatId = int64 - -type CreateYoutubeDlJob = { - url: string - savePath: string -} - -type CreateYoutubeDlJobSuccess = { - task: Guid -} - -type YoutubeDlStateResponse = { - state: string -} - -type YoutubeDlError = { - message: string -} - -type CreateJobResult = Result - -type CheckJobResult = Result - -type CleanJobResult = Result - -type IYoutubeDlClient = - abstract member CreateJob: CreateYoutubeDlJob -> Async - abstract member CheckJob: externalId: Guid -> Async - abstract member CleanJob: externalId: Guid -> Async +open PublishHelperBot.Types type YoutubeDlClientConfig = { BaseUrl: string @@ -49,7 +18,6 @@ type YoutubeDlClientConfig = { [] module YoutubeDlClient = - type private YoutubeDlClientActions = Create | Check | Delete type private HttpMethods = GET | POST | DELETE @@ -192,69 +160,6 @@ module YoutubeDlClient = inbox.Post(CleanJob(externalId, tcs)) tcs.Task |> Async.AwaitTask } -type TgServiceConfig = { - Client: ITelegramBotClient - ChannelId: ChatId - AdminChatId: ChatId - YoutubeDlClient: IYoutubeDlClient -} - -type ITgService = - abstract member PostVideo: url: string * savePath: string * externalId: Guid -> unit - -[] -module TgService = - type private Msg = - | PostVideo of url: string * savePath: string * externalId: Guid - - let private createInbox (config: TgServiceConfig) = - MailboxProcessor.Start(fun inbox -> - let rec loop () = - async { - match! inbox.Receive() with - | PostVideo (url, savePath, externalId) -> - try - try - Logging.logger.Information("Reading file path = {path}", savePath) - use file = File.OpenRead(savePath) - if (file.Length / 1024L / 1024L) < 50L then - let input = InputOnlineFile(file, Path.GetRandomFileName()) - let caption = $"Source: {url}" - Logging.logger.Information( - "Sending video to channel, channelId = {channelId}, caption = {caption}", - config.ChannelId, - caption) - do! config.Client.SendVideoAsync( - config.ChannelId, - input, - caption = caption - ) - |> Async.AwaitTask |> Async.Ignore - else - do! config.Client.SendTextMessageAsync(config.AdminChatId, $"Да блять, видео вышло больше 50мб: {externalId}") |> Async.AwaitTask |> Async.Ignore - with ex -> - Logging.logger.Error(ex, "Failed to send video") - finally - Logging.logger.Information("Deleting file path = {path}", savePath) - File.Delete(savePath) - - match! config.YoutubeDlClient.CleanJob(externalId) with - | Ok _ -> () - | Error _ -> () - return! loop () - } - loop () - ) - - let createService config = - let inbox = createInbox config - { new ITgService with - member this.PostVideo(url, savePath, externalId) = - inbox.Post(PostVideo(url, savePath, externalId)) } - -type IYoutubeDlService = - abstract member AddJob: url: string -> Async - [] module YoutubeDlService = type private Msg = @@ -383,6 +288,7 @@ module YoutubeDlService = "Failed to receive video from youtube client, job = {job}, message = {message}", externalId, e.message) + Logging.logger.Information("Deleting file path = {path}", job.SavePath) File.Delete(job.SavePath) return! loop jobQueue None diff --git a/youtube-dl-api/youtube_dl_api/__init__.py b/youtube-dl-api/youtube_dl_api/__init__.py index f7a4649..90c16fa 100644 --- a/youtube-dl-api/youtube_dl_api/__init__.py +++ b/youtube-dl-api/youtube_dl_api/__init__.py @@ -20,14 +20,17 @@ def report_state(id: str): def load_video(url: str, file_path: str, id: str): - opts = { - "format": 'best[height<=480][ext=mp4]', - "quiet": True, - "outtmpl": file_path, - "progress_hooks": [report_state(id)] - } - with YoutubeDL(opts) as ydl: - ydl.download([url]) + try: + opts = { + "format": 'best[height<=480][ext=mp4]/bestvideo+bestaudio[ext=mp4]', + "quiet": True, + "outtmpl": file_path, + "progress_hooks": [report_state(id)] + } + with YoutubeDL(opts) as ydl: + ydl.download([url]) + except Exception: + del backgroundJobs[id] @app.get("/api/info")