add ping move to boxes etc

This commit is contained in:
Vladislav Khapin 2023-02-26 20:08:42 +04:00
parent f0e6aaa7e5
commit 979120383a
7 changed files with 426 additions and 256 deletions

View File

@ -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) = $"<a href=\"{url}\">Прислал</a> {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<IYoutubeDlService> = 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
}

View File

@ -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<UpdateType> 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<UpdateType> 0
)
)
Logging.logger.Information("Я родился")
Console.ReadKey() |> ignore

View File

@ -12,8 +12,9 @@
<ItemGroup>
<Compile Include="Environment.fs" />
<Compile Include="Types.fs" />
<Compile Include="Telegram.fs" />
<Compile Include="YoutubeDl.fs" />
<Compile Include="Handlers.fs" />
<Compile Include="Program.fs" />
<Content Include="config.example.json" />
</ItemGroup>

View File

@ -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
[<RequireQualifiedAccess>]
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 [<Literal>] 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 -> $"<a href=\"{config.relayUrl}\">Прислал</a> {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
[<RequireQualifiedAccess>]
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)) }
[<RequireQualifiedAccess>]
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)) }

77
PublishHelperBot/Types.fs Normal file
View File

@ -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<CreateYoutubeDlJobSuccess, YoutubeDlError>
type CheckJobResult = Result<YoutubeDlStateResponse, YoutubeDlError>
type CleanJobResult = Result<YoutubeDlStateResponse, YoutubeDlError>
type IYoutubeDlClient =
abstract member CreateJob: CreateYoutubeDlJob -> Async<CreateJobResult>
abstract member CheckJob: externalId: Guid -> Async<CheckJobResult>
abstract member CleanJob: externalId: Guid -> Async<CleanJobResult>
type IYoutubeDlService =
abstract member AddJob: url: string -> Async<Guid>
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
}
[<RequireQualifiedAccess>]
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

View File

@ -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<CreateYoutubeDlJobSuccess, YoutubeDlError>
type CheckJobResult = Result<YoutubeDlStateResponse, YoutubeDlError>
type CleanJobResult = Result<YoutubeDlStateResponse, YoutubeDlError>
type IYoutubeDlClient =
abstract member CreateJob: CreateYoutubeDlJob -> Async<CreateJobResult>
abstract member CheckJob: externalId: Guid -> Async<CheckJobResult>
abstract member CleanJob: externalId: Guid -> Async<CleanJobResult>
open PublishHelperBot.Types
type YoutubeDlClientConfig = {
BaseUrl: string
@ -49,7 +18,6 @@ type YoutubeDlClientConfig = {
[<RequireQualifiedAccess>]
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
[<RequireQualifiedAccess>]
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<Guid>
[<RequireQualifiedAccess>]
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

View File

@ -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")