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
open System.Net.Http open System.Net.Http
open System.Threading
open System.Threading.Tasks open System.Threading.Tasks
open PublishHelperBot.Handlers
open PublishHelperBot.Environment open PublishHelperBot.Environment
open PublishHelperBot.YoutubeDl open PublishHelperBot.YoutubeDl
open PublishHelperBot.Telegram
open Telegram.Bot open Telegram.Bot
open Telegram.Bot.Polling open Telegram.Bot.Polling
open Telegram.Bot.Types open Telegram.Bot.Types
open Telegram.Bot.Types.Enums open Telegram.Bot.Types.Enums
let createBot (config: BotConfig, http: HttpClient) = TelegramBotClient(config.token, http)
let config = createConfig "SBPB_CONFIG_PATH" 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 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 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 handlePollingError _ (e: Exception) _ =
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) =
Logging.logger.Error(e, "Polling error") Logging.logger.Error(e, "Polling error")
Task.CompletedTask Task.CompletedTask
let receiverOptions = ReceiverOptions(AllowedUpdates = Array.zeroCreate<UpdateType> 0)
Logging.logger.Information("Starting bot") 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("Я родился") Logging.logger.Information("Я родился")
Console.ReadKey() |> ignore Console.ReadKey() |> ignore

View File

@ -12,8 +12,9 @@
<ItemGroup> <ItemGroup>
<Compile Include="Environment.fs" /> <Compile Include="Environment.fs" />
<Compile Include="Types.fs" />
<Compile Include="Telegram.fs" />
<Compile Include="YoutubeDl.fs" /> <Compile Include="YoutubeDl.fs" />
<Compile Include="Handlers.fs" />
<Compile Include="Program.fs" /> <Compile Include="Program.fs" />
<Content Include="config.example.json" /> <Content Include="config.example.json" />
</ItemGroup> </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 Microsoft.FSharp.Core
open Newtonsoft.Json open Newtonsoft.Json
open PublishHelperBot.Environment open PublishHelperBot.Environment
open Telegram.Bot open PublishHelperBot.Types
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>
type YoutubeDlClientConfig = { type YoutubeDlClientConfig = {
BaseUrl: string BaseUrl: string
@ -49,7 +18,6 @@ type YoutubeDlClientConfig = {
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module YoutubeDlClient = module YoutubeDlClient =
type private YoutubeDlClientActions = Create | Check | Delete type private YoutubeDlClientActions = Create | Check | Delete
type private HttpMethods = GET | POST | DELETE type private HttpMethods = GET | POST | DELETE
@ -192,69 +160,6 @@ module YoutubeDlClient =
inbox.Post(CleanJob(externalId, tcs)) inbox.Post(CleanJob(externalId, tcs))
tcs.Task |> Async.AwaitTask } 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>] [<RequireQualifiedAccess>]
module YoutubeDlService = module YoutubeDlService =
type private Msg = type private Msg =
@ -383,6 +288,7 @@ module YoutubeDlService =
"Failed to receive video from youtube client, job = {job}, message = {message}", "Failed to receive video from youtube client, job = {job}, message = {message}",
externalId, externalId,
e.message) e.message)
Logging.logger.Information("Deleting file path = {path}", job.SavePath)
File.Delete(job.SavePath) File.Delete(job.SavePath)
return! loop jobQueue None 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): def load_video(url: str, file_path: str, id: str):
opts = { try:
"format": 'best[height<=480][ext=mp4]', opts = {
"quiet": True, "format": 'best[height<=480][ext=mp4]/bestvideo+bestaudio[ext=mp4]',
"outtmpl": file_path, "quiet": True,
"progress_hooks": [report_state(id)] "outtmpl": file_path,
} "progress_hooks": [report_state(id)]
with YoutubeDL(opts) as ydl: }
ydl.download([url]) with YoutubeDL(opts) as ydl:
ydl.download([url])
except Exception:
del backgroundJobs[id]
@app.get("/api/info") @app.get("/api/info")