From 32bd1646c9b5dd02c0de15465cc68ed450ce9b66 Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Tue, 3 Oct 2023 18:00:44 +0530 Subject: [PATCH 1/7] First port into worker-style bot --- FSharp.Examples.Polling/Configuration.fs | 10 - FSharp.Examples.Polling/Extensions.fs | 22 ++ .../FSharp.Examples.Polling.fsproj | 43 ++- FSharp.Examples.Polling/Program.fs | 293 +++--------------- .../Properties/launchSettings.json | 11 + .../Services/Internal/ReceiverFuncs.fs | 45 +++ .../Services/Internal/UpdateHandlerFuncs.fs | 269 ++++++++++++++++ .../Services/PollingService.fs | 40 +++ .../Services/ReceiverService.fs | 20 ++ .../Services/UpdateHandler.fs | 28 ++ FSharp.Examples.Polling/Util.fs | 16 + .../appsettings.Development.json | 11 + FSharp.Examples.Polling/appsettings.json | 11 + 13 files changed, 533 insertions(+), 286 deletions(-) delete mode 100644 FSharp.Examples.Polling/Configuration.fs create mode 100644 FSharp.Examples.Polling/Extensions.fs create mode 100644 FSharp.Examples.Polling/Properties/launchSettings.json create mode 100644 FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs create mode 100644 FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs create mode 100644 FSharp.Examples.Polling/Services/PollingService.fs create mode 100644 FSharp.Examples.Polling/Services/ReceiverService.fs create mode 100644 FSharp.Examples.Polling/Services/UpdateHandler.fs create mode 100644 FSharp.Examples.Polling/Util.fs create mode 100644 FSharp.Examples.Polling/appsettings.Development.json create mode 100644 FSharp.Examples.Polling/appsettings.json diff --git a/FSharp.Examples.Polling/Configuration.fs b/FSharp.Examples.Polling/Configuration.fs deleted file mode 100644 index d9abdef8..00000000 --- a/FSharp.Examples.Polling/Configuration.fs +++ /dev/null @@ -1,10 +0,0 @@ -// Configuration for Telegram Bot (Fsharp) -// -// Copyright (c) 2021 Arvind Devarajan -// Licensed to you under the MIT License. -// See the LICENSE file in the project root for more information. - -namespace FSharp.Examples.Polling - -module TelegramBotCfg = - let token = "{BOT_TOKEN}" diff --git a/FSharp.Examples.Polling/Extensions.fs b/FSharp.Examples.Polling/Extensions.fs new file mode 100644 index 00000000..1738eee8 --- /dev/null +++ b/FSharp.Examples.Polling/Extensions.fs @@ -0,0 +1,22 @@ +// Extension functions needed to extract bot configuration +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.DependencyInjection + +open System +open Microsoft.Extensions.Options +open System.Runtime.CompilerServices + +[] +type PollingExtensions() = + [] + static member GetConfiguration<'T when 'T: not struct>(sp: IServiceProvider) = + let o = sp.GetService>() + + if isNull o then + raise <| ArgumentNullException nameof<'T> + + o.Value diff --git a/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj b/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj index fbaf8432..3c92a08e 100644 --- a/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj +++ b/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj @@ -1,23 +1,20 @@ - - - - Exe - net6.0 - enable - 3390;$(WarnOn) - - - - - Always - - - - - - - - - - - + + + net7.0 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FSharp.Examples.Polling/Program.fs b/FSharp.Examples.Polling/Program.fs index c2b76a40..c832351e 100644 --- a/FSharp.Examples.Polling/Program.fs +++ b/FSharp.Examples.Polling/Program.fs @@ -1,261 +1,48 @@ -// Example Telegram bot in F# +// Bot entrypoint // -// Copyright (c) 2021 Arvind Devarajan +// Copyright (c) 2023 Arvind Devarajan // Licensed to you under the MIT License. // See the LICENSE file in the project root for more information. namespace FSharp.Examples.Polling -open System -open System.IO -open System.Threading -open Telegram.Bot -open Telegram.Bot.Types -open Telegram.Bot.Exceptions -open Telegram.Bot.Types.Enums -open Telegram.Bot.Types.InlineQueryResults -open Telegram.Bot.Types.ReplyMarkups -open Telegram.Bot.Polling -open System.Threading.Tasks - -module Handlers = - let handleErrorAsync (botClient:ITelegramBotClient) (err:Exception) (cts:CancellationToken) = async { - let errormsg = - match err with - | :? ApiRequestException as apiex -> $"Telegram API Error:\n[{apiex.ErrorCode}]\n{apiex.Message}" - | _ -> err.ToString() - - Console.WriteLine(errormsg) - } - - let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (query:CallbackQuery) = async { - do! - botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") - |> Async.AwaitTask - - do! - botClient.SendTextMessageAsync(ChatId(query.Message.Chat.Id), $"Received {query.Data}") - |> Async.AwaitTask - |> Async.Ignore - } - - let botOnInlineQueryReceived (botClient:ITelegramBotClient) (inlinequery:InlineQuery) = async { - Console.WriteLine($"Received inline query from: {inlinequery.From.Id}"); - - // displayed result - let results = seq { - InlineQueryResultArticle( - id = "3", - title = "TgBots", - inputMessageContent = InputTextMessageContent("hello")) - } - - do! - botClient.AnswerInlineQueryAsync(inlinequery.Id, - results |> Seq.cast, - isPersonal = true, - cacheTime = 0) - |> Async.AwaitTask - |> Async.Ignore - } - - let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (chosenInlineResult:ChosenInlineResult) = async { - Console.WriteLine($"Received inline result: {chosenInlineResult.ResultId}") - } - - let botOnMessageReceived (botClient:ITelegramBotClient) (message:Message) = - Console.WriteLine($"Receive message type: {message.Type}"); - - let sendInlineKeyboard = async { - do! - botClient.SendChatActionAsync(ChatId(message.Chat.Id), ChatAction.Typing) - |> Async.AwaitTask - |> Async.Ignore - - let inlineKeyboard = seq { - // first row - seq { - InlineKeyboardButton.WithCallbackData("1.1", "11"); - InlineKeyboardButton.WithCallbackData("1.2", "12"); - }; - - // second row - seq { - InlineKeyboardButton.WithCallbackData("2.1", "21"); - InlineKeyboardButton.WithCallbackData("2.2", "22"); - }; - } - - do! - botClient.SendTextMessageAsync( - chatId = ChatId(message.Chat.Id), - text = "Choose", - replyMarkup = InlineKeyboardMarkup(inlineKeyboard)) - |> Async.AwaitTask - |> Async.Ignore - } - - let sendReplyKeyboard = async { - let replyKeyboardMarkup = - ReplyKeyboardMarkup( seq { - - // first row - seq { KeyboardButton("1.1"); KeyboardButton("1.2") }; - - // second row - seq { KeyboardButton("1.1"); KeyboardButton("1.2") }; - }, - ResizeKeyboard = true) - - do! - botClient.SendTextMessageAsync( - chatId = ChatId(message.Chat.Id), - text = "Choose", - replyMarkup = replyKeyboardMarkup) - |> Async.AwaitTask - |> Async.Ignore - } +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Hosting - let removeKeyboard = async { - do! - botClient.SendTextMessageAsync( - chatId = ChatId(message.Chat.Id), - text = "Removing keyboard", - replyMarkup = ReplyKeyboardRemove()) - |> Async.AwaitTask - |> Async.Ignore - } - - let sendFile = async { - do! - botClient.SendChatActionAsync(ChatId(message.Chat.Id), ChatAction.UploadPhoto) - |> Async.AwaitTask - |> Async.Ignore - - let filePath = @"Files/tux.png" - use fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read) - - let fileName = - filePath.Split(Path.DirectorySeparatorChar) - |> Array.last - - do! - botClient.SendPhotoAsync( - chatId = ChatId(message.Chat.Id), - photo = InputFileStream(fileStream, fileName), - caption = "Nice Picture") - |> Async.AwaitTask - |> Async.Ignore - } - - let requestContactAndLocation = async { - let requestReplyKeyboard = - ReplyKeyboardMarkup(seq { - KeyboardButton.WithRequestLocation("Location"); - KeyboardButton.WithRequestContact("Contact"); - }, - ResizeKeyboard = true) - - do! - botClient.SendTextMessageAsync( - chatId = ChatId(message.Chat.Id), - text = "Who or Where are you?", - replyMarkup = requestReplyKeyboard) - |> Async.AwaitTask - |> Async.Ignore - } - - let usage = async { - let usage = - "Usage:\n" + - "/inline - send inline keyboard\n" + - "/keyboard - send custom keyboard\n" + - "/remove - remove custom keyboard\n" + - "/photo - send a photo\n" + - "/request - request location or contact" - - do! - botClient.SendTextMessageAsync( - chatId = ChatId(message.Chat.Id), - text = usage, - replyMarkup = ReplyKeyboardRemove()) - |> Async.AwaitTask - |> Async.Ignore - } - - async { - if message.Type <> MessageType.Text then - () - else - let fn = - // We use tryHead here just in case we get an empty - // response from the user - match message.Text.Split(' ') |> Array.tryHead with - | Some "/inline" -> sendInlineKeyboard - | Some "/keyboard" -> sendReplyKeyboard - | Some "/remove" -> removeKeyboard - | Some "/photo" -> sendFile - | Some "/request" -> requestContactAndLocation - | _ -> usage - - do! fn - } - - let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (update:Update) = async { - Console.WriteLine($"Unknown update type: {update.Type}"); - } - - let handleUpdateAsync botClient (update:Update) cts = async { - try - let fn = - match update.Type with - | UpdateType.Message -> botOnMessageReceived botClient update.Message - | UpdateType.EditedMessage -> botOnMessageReceived botClient update.EditedMessage - | UpdateType.CallbackQuery -> botOnCallbackQueryReceived botClient update.CallbackQuery - | UpdateType.InlineQuery -> botOnInlineQueryReceived botClient update.InlineQuery - | UpdateType.ChosenInlineResult -> botOnChosenInlineResultReceived botClient update.ChosenInlineResult - | _ -> unknownUpdateHandlerAsync botClient update - return fn |> Async.Start - with - | ex -> do! handleErrorAsync botClient ex cts - } - -type FuncConvert = - // There is quite some bit of jugglery here, so requires some explanation: - // DefaultUpdateHandler() requires two arguments, both of type Func<_,_,_,_>, and both - // of which return a Task. Now, in order to be in the F# domain, we would like to - // have this Func<> defined as an F# function: and so we need to explicitely construct - // Func<>s by passing them to an inner lambda function. - // Now, the inner lambda function needs to return a Task, but we want to use F# async. - // We therefore use a Async.StartAsTask to start an async computation and get back a Task. - // The last ":>" is for upcasting Task (returned by the async computations) to their - // base class Task to avoid and error that says "the expression expects a Task but we have a - // Task here.". - // The good thing about doing all this is that handleUpdateAsync and handleErrorAsync are both - // in the F# domain - so now can take all advantages of F#! - static member inline ToCSharpDelegate (f) = - Func<_,_,_,_>(fun a b c -> Async.StartAsTask (f a b c) :> Task) - -module TelegramBot = - [] - let main argv = - let botClient - = TelegramBotClient(TelegramBotCfg.token) - - async { - let! me = botClient.GetMeAsync() |> Async.AwaitTask - printfn $"Hello, World! I am user {me.Id} and my name is {me.FirstName}." - printfn $"Start listening for {me.Username}..." - - use cts = new CancellationTokenSource(); - - botClient.StartReceiving( - Handlers.handleUpdateAsync |> FuncConvert.ToCSharpDelegate, - Handlers.handleErrorAsync |> FuncConvert.ToCSharpDelegate, - ReceiverOptions( AllowedUpdates = [||] ), - cts.Token) - } |> Async.RunSynchronously +open Telegram.Bot - printfn "Press to exit" - Console.Read() |> ignore - 0 +open FSharp.Examples.Polling.Services + +type BotConfiguration = { + BotToken: string +} + +module Program = + let createHostBuilder args = + Host.CreateDefaultBuilder(args) + .ConfigureServices(fun context services -> + + context.Configuration.GetSection(nameof(BotConfiguration)) |> ignore + + // Register named HttpClient to benefits from IHttpClientFactory + // and consume it with ITelegramBotClient typed client. + // More read: + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#typed-clients + // https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests + services.AddHttpClient("telegram_bot_client") + .AddTypedClient( + fun httpClient sp -> + let botConfig = sp.GetConfiguration() + let options = TelegramBotClientOptions(botConfig.BotToken) + TelegramBotClient(options, httpClient) :> ITelegramBotClient + ) |> ignore + + services.AddScoped() |> ignore + services.AddScoped>() |> ignore + services.AddHostedService() |> ignore) + + [] + let main args = + createHostBuilder(args).Build().Run() + + 0 // exit code diff --git a/FSharp.Examples.Polling/Properties/launchSettings.json b/FSharp.Examples.Polling/Properties/launchSettings.json new file mode 100644 index 00000000..1181dfad --- /dev/null +++ b/FSharp.Examples.Polling/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "FSharp.Examples.Polling": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs b/FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs new file mode 100644 index 00000000..7d35f936 --- /dev/null +++ b/FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs @@ -0,0 +1,45 @@ +// Receiver to receive messages from user +// +// The received messages are also displatched to +// the update handler to get responses to be sent +// back to the user +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling.Services.Internal + +open System.Threading.Tasks +open System.Threading + +open Microsoft.Extensions.Logging + +open Telegram.Bot.Polling +open Telegram.Bot.Types.Enums +open Telegram.Bot + +// Type of Receiver Async function +type ReceiverAsyncFunc = CancellationToken -> Task + +module ReceiverFuncs = + let receiveAsync (botClient: ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (uh: IUpdateHandler) = task { + let options = ReceiverOptions( + AllowedUpdates = Array.empty, + ThrowPendingUpdates = true + ) + + let! me = botClient.GetMeAsync(cts) + let username = + match me.Username with + | null -> "My Awesome Bot" + | v -> v + + logger.LogInformation $"Start receiving updates for {username}" + + botClient.ReceiveAsync( + updateHandler = uh, + receiverOptions = options, + cancellationToken = cts + ) |> ignore + } diff --git a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs new file mode 100644 index 00000000..3b53e4cb --- /dev/null +++ b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs @@ -0,0 +1,269 @@ +// Implementation of all the Update Handlers +// +// This file contains tha actual F# implementations of all +// handlers, using the idiomatic patterns in F#. +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling.Services.Internal + +open System +open System.IO +open System.Threading + +open Microsoft.Extensions.Logging + +open Telegram.Bot +open Telegram.Bot.Exceptions +open Telegram.Bot.Types +open Telegram.Bot.Types.Enums +open Telegram.Bot.Types.InlineQueryResults +open Telegram.Bot.Types.ReplyMarkups + +open FSharp.Examples.Polling.Util + +module UpdateHandlerFuncs = + let handlePollingErrorAsync _ (logger: ILogger) _ (err:Exception) = task { + let errormsg = + match err with + | :? ApiRequestException as apiex -> $"Telegram API Error:\n[{apiex.ErrorCode}]\n{apiex.Message}" + | _ -> err.ToString() + + logInfo logger $"{errormsg}" + } + + let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (query:CallbackQuery) = async { + do! + botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") + |> Async.AwaitTask + + do! + botClient.SendTextMessageAsync( + chatId = query.Message.Chat.Id, + text = $"Received {query.Data}", + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let botOnInlineQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (inlinequery:InlineQuery) = async { + logInfo logger $"Received inline query from: {inlinequery.From.Id}" + + do! + // displayed result + let results = seq { + InlineQueryResultArticle( + id = "3", + title = "TgBots", + inputMessageContent = InputTextMessageContent("hello")) + } + + botClient.AnswerInlineQueryAsync(inlinequery.Id, + results |> Seq.cast, + isPersonal = true, + cacheTime = 0, + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (chosenInlineResult:ChosenInlineResult) = async { + logInfo logger $"Received inline result: {chosenInlineResult.ResultId}" + } + + let botOnMessageReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = + logInfo logger $"Receive message type: {message.Type}" + + let sendInlineKeyboard = async { + do! + botClient.SendChatActionAsync( + chatId = message.Chat.Id, + chatAction = ChatAction.Typing, + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + + // Simulate a long running task + async { do! Async.Sleep 500 } |> ignore + + let inlineKeyboard = seq { + // first row + seq { + InlineKeyboardButton.WithCallbackData("1.1", "11"); + InlineKeyboardButton.WithCallbackData("1.2", "12"); + }; + + // second row + seq { + InlineKeyboardButton.WithCallbackData("2.1", "21"); + InlineKeyboardButton.WithCallbackData("2.2", "22"); + }; + } + + do! + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Choose", + replyMarkup = InlineKeyboardMarkup(inlineKeyboard), + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let sendReplyKeyboard = async { + let replyKeyboardMarkup = + ReplyKeyboardMarkup( seq { + + // first row + seq { KeyboardButton("1.1"); KeyboardButton("1.2") }; + + // second row + seq { KeyboardButton("1.1"); KeyboardButton("1.2") }; + }, + ResizeKeyboard = true) + + do! + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Choose", + replyMarkup = replyKeyboardMarkup, + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let removeKeyboard = async { + do! + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Removing keyboard", + replyMarkup = ReplyKeyboardRemove(), + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let sendFile = async { + do! + botClient.SendChatActionAsync( + message.Chat.Id, + ChatAction.UploadPhoto, + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + + let filePath = @"Files/tux.png" + use fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read) + + let fileName = + filePath.Split(Path.DirectorySeparatorChar) + |> Array.last + + do! + botClient.SendPhotoAsync( + chatId = message.Chat.Id, + photo = InputFileStream(fileStream, fileName), + caption = "Nice Picture", + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let requestContactAndLocation = async { + let requestReplyKeyboard = + ReplyKeyboardMarkup(seq { + KeyboardButton.WithRequestLocation("Location"); + KeyboardButton.WithRequestContact("Contact"); + }, + ResizeKeyboard = true) + + do! + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Who or Where are you?", + replyMarkup = requestReplyKeyboard, + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let startInlineQuery = async { + let inlineKeyboard = + InlineKeyboardMarkup (InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("Inline Mode")) + + do! + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Press the button to start Inline Query", + replyMarkup = inlineKeyboard, + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + let failingHandler = + raise <| IndexOutOfRangeException() + + let usage = async { + let usage = + "Usage:\n" + + "/inline_keyboard - send inline keyboard\n" + + "/keyboard - send custom keyboard\n" + + "/remove - remove custom keyboard\n" + + "/photo - send a photo\n" + + "/request - request location or contact" + + "/inline_mode - send keyboard with inline query" + + "/throw - Simulate a failed bot message handler" + + do! + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = usage, + replyMarkup = ReplyKeyboardRemove(), + cancellationToken = cts) + |> Async.AwaitTask + |> Async.Ignore + } + + async { + if message.Type <> MessageType.Text then + () + else + let fn = + // We use tryHead here just in case we get an empty + // response from the user + match message.Text.Split(' ') |> Array.tryHead with + | Some "/inline_keyboard" -> sendInlineKeyboard + | Some "/keyboard" -> sendReplyKeyboard + | Some "/remove" -> removeKeyboard + | Some "/photo" -> sendFile + | Some "/request" -> requestContactAndLocation + | Some "/inline_mode" -> startInlineQuery + | Some "/throw" -> failingHandler + | _ -> usage + + do! fn + } + + let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (update:Update) = async { + logInfo logger $"Unknown update type: {update.Type}" + } + + let handleUpdateAsync botClient logger cts (update:Update) = task { + let handleUpdate f = f botClient logger cts + + try + let fn = + match update.Type with + | UpdateType.Message -> handleUpdate botOnMessageReceived update.Message + | UpdateType.EditedMessage -> handleUpdate botOnMessageReceived update.EditedMessage + | UpdateType.CallbackQuery -> handleUpdate botOnCallbackQueryReceived update.CallbackQuery + | UpdateType.InlineQuery -> handleUpdate botOnInlineQueryReceived update.InlineQuery + | UpdateType.ChosenInlineResult -> handleUpdate botOnChosenInlineResultReceived update.ChosenInlineResult + | _ -> handleUpdate unknownUpdateHandlerAsync update + return fn |> Async.Start + with + | ex -> do! handleUpdate handlePollingErrorAsync ex + } diff --git a/FSharp.Examples.Polling/Services/PollingService.fs b/FSharp.Examples.Polling/Services/PollingService.fs new file mode 100644 index 00000000..d76d96cd --- /dev/null +++ b/FSharp.Examples.Polling/Services/PollingService.fs @@ -0,0 +1,40 @@ +// The worker entry-point +// +// Background service consuming a scoped service. +// See more: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services#consuming-a-scoped-service-in-a-background-task +// +// This file simply forms that "link" between the .NET +// class to the actual service functions implemented in +// FSharp.Examples.Polling.Services.Internal.PollingServiceFuncs +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling.Services + +open System +open System.Threading +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging +open Microsoft.Extensions.Hosting + +open FSharp.Examples.Polling.Util + +type PollingService(sp: IServiceProvider, logger: ILogger) = + inherit BackgroundService() + override __.ExecuteAsync(cts: CancellationToken) = task { + logInfo logger "Starting polling service" + let receive _ = + use scope = sp.CreateScope() + let receiver = scope.ServiceProvider.GetRequiredService>() + receiver.ReceiveAsync cts + + let isCancellationRequested _ = cts.IsCancellationRequested + + Seq.initInfinite receive + |> Seq.takeWhile isCancellationRequested + |> ignore + + return 0 + } diff --git a/FSharp.Examples.Polling/Services/ReceiverService.fs b/FSharp.Examples.Polling/Services/ReceiverService.fs new file mode 100644 index 00000000..14239398 --- /dev/null +++ b/FSharp.Examples.Polling/Services/ReceiverService.fs @@ -0,0 +1,20 @@ +// Receiver Service - receives messages from the user +// +// This file simply forms that "link" between the .NET +// class to the actual receiver functions implemented in +// FSharp.Examples.Polling.Services.Internal.ReceiverFuncs +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling.Services + +open System.Threading +open Microsoft.Extensions.Logging +open Telegram.Bot + +open FSharp.Examples.Polling.Services.Internal + +type ReceiverService<'T>(botClient: ITelegramBotClient, updateHandler: UpdateHandler, logger: ILogger<'T>) = + member __.ReceiveAsync(cts: CancellationToken) = ReceiverFuncs.receiveAsync botClient logger cts updateHandler diff --git a/FSharp.Examples.Polling/Services/UpdateHandler.fs b/FSharp.Examples.Polling/Services/UpdateHandler.fs new file mode 100644 index 00000000..4db009e7 --- /dev/null +++ b/FSharp.Examples.Polling/Services/UpdateHandler.fs @@ -0,0 +1,28 @@ +// Update Handler class inherited from IUpdateHandler +// +// This file simply forms that "link" between the .NET +// class to the actual implementation of the handlers +// as seen in the module +// FSharp.Examples.Polling.Services.Internal.UpdateHandlerFuncs +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling.Services + +open System +open System.Threading +open Microsoft.Extensions.Logging +open Telegram.Bot +open Telegram.Bot.Polling + +open FSharp.Examples.Polling.Services.Internal + +type UpdateHandler(botClient: ITelegramBotClient, logger: ILogger) = + interface IUpdateHandler with + member __.HandleUpdateAsync( _ , update, cancellation) = + UpdateHandlerFuncs.handleUpdateAsync botClient logger cancellation update + + member __.HandlePollingErrorAsync( _ , ex: Exception, cancellationToken: CancellationToken) = + UpdateHandlerFuncs.handlePollingErrorAsync botClient logger cancellationToken ex diff --git a/FSharp.Examples.Polling/Util.fs b/FSharp.Examples.Polling/Util.fs new file mode 100644 index 00000000..0f749125 --- /dev/null +++ b/FSharp.Examples.Polling/Util.fs @@ -0,0 +1,16 @@ +// Utility functions +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling + +open Microsoft.Extensions.Logging + +module Util = + // Placeholder for unimplemented functions + let undefined<'T> : 'T = failwith "Not implemented yet" + + // Log information using the passed-in logger + let logInfo (logger: ILogger) (msg: string) = logger.LogInformation msg diff --git a/FSharp.Examples.Polling/appsettings.Development.json b/FSharp.Examples.Polling/appsettings.Development.json new file mode 100644 index 00000000..6203dc3d --- /dev/null +++ b/FSharp.Examples.Polling/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "BotConfiguration": { + "BotToken": "{BOT_TOKEN}" + } +} diff --git a/FSharp.Examples.Polling/appsettings.json b/FSharp.Examples.Polling/appsettings.json new file mode 100644 index 00000000..6203dc3d --- /dev/null +++ b/FSharp.Examples.Polling/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "BotConfiguration": { + "BotToken": "{BOT_TOKEN}" + } +} From 2ff5d5f680b64df955e36f3542370e86ffbcc628 Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Wed, 11 Oct 2023 04:13:38 +0530 Subject: [PATCH 2/7] Refactored. --- FSharp.Examples.Polling/.editorconfig | 33 +++ .../FSharp.Examples.Polling.fsproj | 4 +- FSharp.Examples.Polling/Program.fs | 17 +- .../Services/Internal/IReceiverService.fs | 15 ++ .../Services/Internal/ReceiverFuncs.fs | 45 ---- .../Services/Internal/UpdateHandlerFuncs.fs | 243 ++++++++---------- .../Services/PollingService.fs | 31 ++- .../Services/ReceiverService.fs | 44 +++- FSharp.Examples.Polling/Util.fs | 5 + 9 files changed, 223 insertions(+), 214 deletions(-) create mode 100644 FSharp.Examples.Polling/.editorconfig create mode 100644 FSharp.Examples.Polling/Services/Internal/IReceiverService.fs delete mode 100644 FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs diff --git a/FSharp.Examples.Polling/.editorconfig b/FSharp.Examples.Polling/.editorconfig new file mode 100644 index 00000000..3504404e --- /dev/null +++ b/FSharp.Examples.Polling/.editorconfig @@ -0,0 +1,33 @@ +# http://EditorConfig.org + +# This file is the top-most EditorConfig file +root = true + +# All Files +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# Solution Files +[*.sln] +indent_style = tab + +# XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Configuration Files +[*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +[*.cs] +# RCS1090: Call 'ConfigureAwait(false)'. +dotnet_diagnostic.RCS1090.severity = none + +[*.md] +trim_trailing_whitespace = false +indent_size = 2 diff --git a/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj b/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj index 3c92a08e..247a2bb8 100644 --- a/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj +++ b/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj @@ -4,8 +4,8 @@ + - @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/FSharp.Examples.Polling/Program.fs b/FSharp.Examples.Polling/Program.fs index c832351e..2a99c69a 100644 --- a/FSharp.Examples.Polling/Program.fs +++ b/FSharp.Examples.Polling/Program.fs @@ -13,33 +13,32 @@ open Telegram.Bot open FSharp.Examples.Polling.Services -type BotConfiguration = { - BotToken: string -} +type BotConfiguration() = + member val BotToken: string module Program = let createHostBuilder args = Host.CreateDefaultBuilder(args) .ConfigureServices(fun context services -> - context.Configuration.GetSection(nameof(BotConfiguration)) |> ignore + let cfg = context.Configuration.GetSection(nameof(BotConfiguration)) + let token = cfg.["BotToken"] // Register named HttpClient to benefits from IHttpClientFactory // and consume it with ITelegramBotClient typed client. // More read: // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#typed-clients // https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests - services.AddHttpClient("telegram_bot_client") - .AddTypedClient( + services.AddHttpClient("telegram_bot_client").AddTypedClient( fun httpClient sp -> - let botConfig = sp.GetConfiguration() - let options = TelegramBotClientOptions(botConfig.BotToken) +// let botConfig = sp.GetConfiguration() + let options = TelegramBotClientOptions(token) TelegramBotClient(options, httpClient) :> ITelegramBotClient ) |> ignore services.AddScoped() |> ignore services.AddScoped>() |> ignore - services.AddHostedService() |> ignore) + services.AddHostedService>>() |> ignore) [] let main args = diff --git a/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs b/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs new file mode 100644 index 00000000..9b9b0020 --- /dev/null +++ b/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs @@ -0,0 +1,15 @@ +// Receiver Service Interface +// +// All receivers must implement this interface +// +// Copyright (c) 2023 Arvind Devarajan +// Licensed to you under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace FSharp.Examples.Polling.Services.Internal + +open System.Threading +open System.Threading.Tasks + +type IReceiverService = + abstract member ReceiveAsync: CancellationToken -> Task diff --git a/FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs b/FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs deleted file mode 100644 index 7d35f936..00000000 --- a/FSharp.Examples.Polling/Services/Internal/ReceiverFuncs.fs +++ /dev/null @@ -1,45 +0,0 @@ -// Receiver to receive messages from user -// -// The received messages are also displatched to -// the update handler to get responses to be sent -// back to the user -// -// Copyright (c) 2023 Arvind Devarajan -// Licensed to you under the MIT License. -// See the LICENSE file in the project root for more information. - -namespace FSharp.Examples.Polling.Services.Internal - -open System.Threading.Tasks -open System.Threading - -open Microsoft.Extensions.Logging - -open Telegram.Bot.Polling -open Telegram.Bot.Types.Enums -open Telegram.Bot - -// Type of Receiver Async function -type ReceiverAsyncFunc = CancellationToken -> Task - -module ReceiverFuncs = - let receiveAsync (botClient: ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (uh: IUpdateHandler) = task { - let options = ReceiverOptions( - AllowedUpdates = Array.empty, - ThrowPendingUpdates = true - ) - - let! me = botClient.GetMeAsync(cts) - let username = - match me.Username with - | null -> "My Awesome Bot" - | v -> v - - logger.LogInformation $"Start receiving updates for {username}" - - botClient.ReceiveAsync( - updateHandler = uh, - receiverOptions = options, - cancellationToken = cts - ) |> ignore - } diff --git a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs index 3b53e4cb..9448d81e 100644 --- a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs +++ b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs @@ -12,6 +12,7 @@ namespace FSharp.Examples.Polling.Services.Internal open System open System.IO open System.Threading +open System.Threading.Tasks open Microsoft.Extensions.Logging @@ -25,6 +26,7 @@ open Telegram.Bot.Types.ReplyMarkups open FSharp.Examples.Polling.Util module UpdateHandlerFuncs = + let handlePollingErrorAsync _ (logger: ILogger) _ (err:Exception) = task { let errormsg = match err with @@ -34,88 +36,73 @@ module UpdateHandlerFuncs = logInfo logger $"{errormsg}" } - let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (query:CallbackQuery) = async { - do! - botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") - |> Async.AwaitTask + let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (query:CallbackQuery) = + botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") |> Async.AwaitTask |> ignore - do! - botClient.SendTextMessageAsync( + botClient.SendTextMessageAsync( chatId = query.Message.Chat.Id, text = $"Received {query.Data}", - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + cancellationToken = cts) |> Async.AwaitTask |> ignore + - let botOnInlineQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (inlinequery:InlineQuery) = async { + let botOnInlineQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (inlinequery:InlineQuery) = logInfo logger $"Received inline query from: {inlinequery.From.Id}" - do! - // displayed result - let results = seq { - InlineQueryResultArticle( - id = "3", - title = "TgBots", - inputMessageContent = InputTextMessageContent("hello")) - } + // displayed result + let results = seq { + InlineQueryResultArticle( + id = "3", + title = "TgBots", + inputMessageContent = InputTextMessageContent("hello")) + } - botClient.AnswerInlineQueryAsync(inlinequery.Id, - results |> Seq.cast, - isPersonal = true, - cacheTime = 0, - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + botClient.AnswerInlineQueryAsync(inlinequery.Id, + results |> Seq.cast, + isPersonal = true, + cacheTime = 0, + cancellationToken = cts) + |> Async.AwaitTask |> ignore - let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (chosenInlineResult:ChosenInlineResult) = async { + let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (chosenInlineResult:ChosenInlineResult) = logInfo logger $"Received inline result: {chosenInlineResult.ResultId}" - } let botOnMessageReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = logInfo logger $"Receive message type: {message.Type}" - let sendInlineKeyboard = async { - do! - botClient.SendChatActionAsync( - chatId = message.Chat.Id, - chatAction = ChatAction.Typing, - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore + let sendInlineKeyboard = + botClient.SendChatActionAsync( + chatId = message.Chat.Id, + chatAction = ChatAction.Typing, + cancellationToken = cts) + |> Async.AwaitTask |> ignore // Simulate a long running task - async { do! Async.Sleep 500 } |> ignore + delay 500 cts let inlineKeyboard = seq { // first row seq { InlineKeyboardButton.WithCallbackData("1.1", "11"); InlineKeyboardButton.WithCallbackData("1.2", "12"); - }; + } // second row seq { InlineKeyboardButton.WithCallbackData("2.1", "21"); InlineKeyboardButton.WithCallbackData("2.2", "22"); - }; + } } - do! - botClient.SendTextMessageAsync( - chatId = message.Chat.Id, - text = "Choose", - replyMarkup = InlineKeyboardMarkup(inlineKeyboard), - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Choose", + replyMarkup = InlineKeyboardMarkup(inlineKeyboard), + cancellationToken = cts) + |> Async.AwaitTask |> ignore - let sendReplyKeyboard = async { + let sendReplyKeyboard = let replyKeyboardMarkup = ReplyKeyboardMarkup( seq { - // first row seq { KeyboardButton("1.1"); KeyboardButton("1.2") }; @@ -124,35 +111,27 @@ module UpdateHandlerFuncs = }, ResizeKeyboard = true) - do! - botClient.SendTextMessageAsync( + botClient.SendTextMessageAsync( chatId = message.Chat.Id, text = "Choose", replyMarkup = replyKeyboardMarkup, cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + |> Async.AwaitTask |> ignore - let removeKeyboard = async { - do! - botClient.SendTextMessageAsync( - chatId = message.Chat.Id, - text = "Removing keyboard", - replyMarkup = ReplyKeyboardRemove(), - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } - - let sendFile = async { - do! - botClient.SendChatActionAsync( - message.Chat.Id, - ChatAction.UploadPhoto, - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore + let removeKeyboard = + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Removing keyboard", + replyMarkup = ReplyKeyboardRemove(), + cancellationToken = cts) + |> Async.AwaitTask |> ignore + + let sendFile = + botClient.SendChatActionAsync( + message.Chat.Id, + ChatAction.UploadPhoto, + cancellationToken = cts) + |> Async.AwaitTask |> ignore let filePath = @"Files/tux.png" use fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read) @@ -161,17 +140,14 @@ module UpdateHandlerFuncs = filePath.Split(Path.DirectorySeparatorChar) |> Array.last - do! - botClient.SendPhotoAsync( - chatId = message.Chat.Id, - photo = InputFileStream(fileStream, fileName), - caption = "Nice Picture", - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + botClient.SendPhotoAsync( + chatId = message.Chat.Id, + photo = InputFileStream(fileStream, fileName), + caption = "Nice Picture", + cancellationToken = cts) + |> Async.AwaitTask |> ignore - let requestContactAndLocation = async { + let requestContactAndLocation = let requestReplyKeyboard = ReplyKeyboardMarkup(seq { KeyboardButton.WithRequestLocation("Location"); @@ -179,34 +155,27 @@ module UpdateHandlerFuncs = }, ResizeKeyboard = true) - do! - botClient.SendTextMessageAsync( - chatId = message.Chat.Id, - text = "Who or Where are you?", - replyMarkup = requestReplyKeyboard, - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Who or Where are you?", + replyMarkup = requestReplyKeyboard, + cancellationToken = cts) + |> Async.AwaitTask |> ignore - let startInlineQuery = async { + let startInlineQuery = let inlineKeyboard = InlineKeyboardMarkup (InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("Inline Mode")) - do! - botClient.SendTextMessageAsync( - chatId = message.Chat.Id, - text = "Press the button to start Inline Query", - replyMarkup = inlineKeyboard, - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = "Press the button to start Inline Query", + replyMarkup = inlineKeyboard, + cancellationToken = cts) + |> Async.AwaitTask |> ignore - let failingHandler = - raise <| IndexOutOfRangeException() + let failingHandler = raise <| IndexOutOfRangeException() - let usage = async { + let usage = let usage = "Usage:\n" + "/inline_keyboard - send inline keyboard\n" + @@ -217,39 +186,32 @@ module UpdateHandlerFuncs = "/inline_mode - send keyboard with inline query" + "/throw - Simulate a failed bot message handler" - do! - botClient.SendTextMessageAsync( - chatId = message.Chat.Id, - text = usage, - replyMarkup = ReplyKeyboardRemove(), - cancellationToken = cts) - |> Async.AwaitTask - |> Async.Ignore - } - - async { - if message.Type <> MessageType.Text then - () - else - let fn = - // We use tryHead here just in case we get an empty - // response from the user - match message.Text.Split(' ') |> Array.tryHead with - | Some "/inline_keyboard" -> sendInlineKeyboard - | Some "/keyboard" -> sendReplyKeyboard - | Some "/remove" -> removeKeyboard - | Some "/photo" -> sendFile - | Some "/request" -> requestContactAndLocation - | Some "/inline_mode" -> startInlineQuery - | Some "/throw" -> failingHandler - | _ -> usage - - do! fn - } - - let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (update:Update) = async { + botClient.SendTextMessageAsync( + chatId = message.Chat.Id, + text = usage, + replyMarkup = ReplyKeyboardRemove(), + cancellationToken = cts) + |> Async.AwaitTask |> ignore + + if message.Type <> MessageType.Text then + () + else + let fn = + // We use tryHead here just in case we get an empty + // response from the user + match message.Text.Split(' ') |> Array.tryHead with + | Some "/inline_keyboard" -> sendInlineKeyboard + | Some "/keyboard" -> sendReplyKeyboard + | Some "/remove" -> removeKeyboard + | Some "/photo" -> sendFile + | Some "/request" -> requestContactAndLocation + | Some "/inline_mode" -> startInlineQuery + | Some "/throw" -> failingHandler + | _ -> usage + fn + + let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (update:Update) = logInfo logger $"Unknown update type: {update.Type}" - } let handleUpdateAsync botClient logger cts (update:Update) = task { let handleUpdate f = f botClient logger cts @@ -263,7 +225,8 @@ module UpdateHandlerFuncs = | UpdateType.InlineQuery -> handleUpdate botOnInlineQueryReceived update.InlineQuery | UpdateType.ChosenInlineResult -> handleUpdate botOnChosenInlineResultReceived update.ChosenInlineResult | _ -> handleUpdate unknownUpdateHandlerAsync update - return fn |> Async.Start + + fn with | ex -> do! handleUpdate handlePollingErrorAsync ex } diff --git a/FSharp.Examples.Polling/Services/PollingService.fs b/FSharp.Examples.Polling/Services/PollingService.fs index d76d96cd..f08d9ee3 100644 --- a/FSharp.Examples.Polling/Services/PollingService.fs +++ b/FSharp.Examples.Polling/Services/PollingService.fs @@ -15,26 +15,35 @@ namespace FSharp.Examples.Polling.Services open System open System.Threading +open System.Threading.Tasks + open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging open Microsoft.Extensions.Hosting +open FSharp.Examples.Polling.Services.Internal open FSharp.Examples.Polling.Util -type PollingService(sp: IServiceProvider, logger: ILogger) = +type PollingService<'T when 'T:> IReceiverService>(sp: IServiceProvider, logger: ILogger>) = inherit BackgroundService() - override __.ExecuteAsync(cts: CancellationToken) = task { - logInfo logger "Starting polling service" - let receive _ = + + member __.doWork (cts: CancellationToken) = + let getReceiverService _ = use scope = sp.CreateScope() - let receiver = scope.ServiceProvider.GetRequiredService>() - receiver.ReceiveAsync cts + let service: 'T = scope.ServiceProvider.GetRequiredService<'T>() + service - let isCancellationRequested _ = cts.IsCancellationRequested + let cancellationNotRequested _ = (not cts.IsCancellationRequested) - Seq.initInfinite receive - |> Seq.takeWhile isCancellationRequested - |> ignore + try + Seq.initInfinite getReceiverService + |> Seq.takeWhile cancellationNotRequested + |> Seq.iter (fun r -> (r.ReceiveAsync cts |> Async.AwaitTask |> ignore)) + with + | e -> + logger.LogError($"Polling failed with exception: {e.ToString}: {e.Message}"); - return 0 + override __.ExecuteAsync(cts: CancellationToken) = task { + logInfo logger "Starting polling service" + __.doWork cts } diff --git a/FSharp.Examples.Polling/Services/ReceiverService.fs b/FSharp.Examples.Polling/Services/ReceiverService.fs index 14239398..2813b15b 100644 --- a/FSharp.Examples.Polling/Services/ReceiverService.fs +++ b/FSharp.Examples.Polling/Services/ReceiverService.fs @@ -1,20 +1,50 @@ -// Receiver Service - receives messages from the user +// Receiver to receive messages from user // -// This file simply forms that "link" between the .NET -// class to the actual receiver functions implemented in -// FSharp.Examples.Polling.Services.Internal.ReceiverFuncs +// The received messages are also displatched to +// the update handler to get responses to be sent +// back to the user // // Copyright (c) 2023 Arvind Devarajan // Licensed to you under the MIT License. // See the LICENSE file in the project root for more information. - namespace FSharp.Examples.Polling.Services open System.Threading open Microsoft.Extensions.Logging +open Telegram.Bot.Polling +open Telegram.Bot.Types.Enums open Telegram.Bot open FSharp.Examples.Polling.Services.Internal -type ReceiverService<'T>(botClient: ITelegramBotClient, updateHandler: UpdateHandler, logger: ILogger<'T>) = - member __.ReceiveAsync(cts: CancellationToken) = ReceiverFuncs.receiveAsync botClient logger cts updateHandler +open FSharp.Examples.Polling.Util + +type ReceiverService<'T when 'T :> IUpdateHandler>(botClient: ITelegramBotClient, updateHandler: UpdateHandler, logger: ILogger<'T>) = + interface IReceiverService with + member __.ReceiveAsync(cts: CancellationToken) = task { + let options = ReceiverOptions( + AllowedUpdates = Array.empty, + ThrowPendingUpdates = true + ) + + let! me = botClient.GetMeAsync(cts) |> Async.AwaitTask + let username = + match me.Username with + | null -> "My Awesome Bot" + | v -> v + + logger.LogInformation $"Start receiving updates for {username}" + + botClient.ReceiveAsync( + updateHandler = updateHandler, + receiverOptions = options, + cancellationToken = cts + ) |> Async.AwaitTask |> ignore + + // Delay here before the next call is done to Receive + delay 500 cts + } + + + + //ReceiverFuncs.receiveAsync botClient logger cts updateHandler diff --git a/FSharp.Examples.Polling/Util.fs b/FSharp.Examples.Polling/Util.fs index 0f749125..80fa03a1 100644 --- a/FSharp.Examples.Polling/Util.fs +++ b/FSharp.Examples.Polling/Util.fs @@ -6,6 +6,8 @@ namespace FSharp.Examples.Polling +open System.Threading +open System.Threading.Tasks open Microsoft.Extensions.Logging module Util = @@ -14,3 +16,6 @@ module Util = // Log information using the passed-in logger let logInfo (logger: ILogger) (msg: string) = logger.LogInformation msg + + let delay (t: int) (cts: CancellationToken)= + Task.Delay(t, cts) |> Async.AwaitTask |> ignore From 4cd0804e6cbb301010627b97a6fb85af1ef5ca35 Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Mon, 16 Oct 2023 07:53:54 +0530 Subject: [PATCH 3/7] Intermediate bug fix --- .../Services/Internal/UpdateHandlerFuncs.fs | 20 +++++++++---------- .../Services/PollingService.fs | 8 ++++---- .../Services/ReceiverService.fs | 17 ++++++---------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs index 9448d81e..8089e9f6 100644 --- a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs +++ b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs @@ -27,15 +27,6 @@ open FSharp.Examples.Polling.Util module UpdateHandlerFuncs = - let handlePollingErrorAsync _ (logger: ILogger) _ (err:Exception) = task { - let errormsg = - match err with - | :? ApiRequestException as apiex -> $"Telegram API Error:\n[{apiex.ErrorCode}]\n{apiex.Message}" - | _ -> err.ToString() - - logInfo logger $"{errormsg}" - } - let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (query:CallbackQuery) = botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") |> Async.AwaitTask |> ignore @@ -213,7 +204,16 @@ module UpdateHandlerFuncs = let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (update:Update) = logInfo logger $"Unknown update type: {update.Type}" - let handleUpdateAsync botClient logger cts (update:Update) = task { + let handlePollingErrorAsync _ (logger: ILogger) _ (err:Exception) = async { + let errormsg = + match err with + | :? ApiRequestException as apiex -> $"Telegram API Error:\n[{apiex.ErrorCode}]\n{apiex.Message}" + | _ -> err.ToString() + + logInfo logger $"{errormsg}" + } + + let handleUpdateAsync botClient logger cts (update:Update) = async { let handleUpdate f = f botClient logger cts try diff --git a/FSharp.Examples.Polling/Services/PollingService.fs b/FSharp.Examples.Polling/Services/PollingService.fs index f08d9ee3..6ec7b666 100644 --- a/FSharp.Examples.Polling/Services/PollingService.fs +++ b/FSharp.Examples.Polling/Services/PollingService.fs @@ -27,7 +27,7 @@ open FSharp.Examples.Polling.Util type PollingService<'T when 'T:> IReceiverService>(sp: IServiceProvider, logger: ILogger>) = inherit BackgroundService() - member __.doWork (cts: CancellationToken) = + member __.doWork (cts: CancellationToken) = async { let getReceiverService _ = use scope = sp.CreateScope() let service: 'T = scope.ServiceProvider.GetRequiredService<'T>() @@ -38,12 +38,12 @@ type PollingService<'T when 'T:> IReceiverService>(sp: IServiceProvider, logger: try Seq.initInfinite getReceiverService |> Seq.takeWhile cancellationNotRequested - |> Seq.iter (fun r -> (r.ReceiveAsync cts |> Async.AwaitTask |> ignore)) + |> Seq.fold with | e -> logger.LogError($"Polling failed with exception: {e.ToString}: {e.Message}"); + } - override __.ExecuteAsync(cts: CancellationToken) = task { + override __.ExecuteAsync(cts: CancellationToken) = logInfo logger "Starting polling service" __.doWork cts - } diff --git a/FSharp.Examples.Polling/Services/ReceiverService.fs b/FSharp.Examples.Polling/Services/ReceiverService.fs index 2813b15b..e7d454b4 100644 --- a/FSharp.Examples.Polling/Services/ReceiverService.fs +++ b/FSharp.Examples.Polling/Services/ReceiverService.fs @@ -22,8 +22,11 @@ open FSharp.Examples.Polling.Util type ReceiverService<'T when 'T :> IUpdateHandler>(botClient: ITelegramBotClient, updateHandler: UpdateHandler, logger: ILogger<'T>) = interface IReceiverService with member __.ReceiveAsync(cts: CancellationToken) = task { + + logInfo logger "ReceiveAsync called" + let options = ReceiverOptions( - AllowedUpdates = Array.empty, + AllowedUpdates = [||], ThrowPendingUpdates = true ) @@ -33,18 +36,10 @@ type ReceiverService<'T when 'T :> IUpdateHandler>(botClient: ITelegramBotClient | null -> "My Awesome Bot" | v -> v - logger.LogInformation $"Start receiving updates for {username}" + logInfo logger $"Start receiving updates for {username}" botClient.ReceiveAsync( updateHandler = updateHandler, receiverOptions = options, - cancellationToken = cts - ) |> Async.AwaitTask |> ignore - - // Delay here before the next call is done to Receive - delay 500 cts + cancellationToken = cts) |> Async.AwaitTask |> ignore } - - - - //ReceiverFuncs.receiveAsync botClient logger cts updateHandler From 6fb74ba2efa29fe265d0317cbb75466a1e92d4ae Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Fri, 27 Oct 2023 04:12:47 +0530 Subject: [PATCH 4/7] Async handling. --- FSharp.Examples.Polling/Program.fs | 7 ++-- .../Services/Internal/IReceiverService.fs | 2 +- .../Services/PollingService.fs | 7 ++-- .../Services/ReceiverService.fs | 32 +++++++++++-------- .../Services/UpdateHandler.fs | 4 +-- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/FSharp.Examples.Polling/Program.fs b/FSharp.Examples.Polling/Program.fs index 2a99c69a..3c1b52a7 100644 --- a/FSharp.Examples.Polling/Program.fs +++ b/FSharp.Examples.Polling/Program.fs @@ -13,6 +13,8 @@ open Telegram.Bot open FSharp.Examples.Polling.Services +type ReceiverSvc = ReceiverService + type BotConfiguration() = member val BotToken: string @@ -31,14 +33,13 @@ module Program = // https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests services.AddHttpClient("telegram_bot_client").AddTypedClient( fun httpClient sp -> -// let botConfig = sp.GetConfiguration() let options = TelegramBotClientOptions(token) TelegramBotClient(options, httpClient) :> ITelegramBotClient ) |> ignore services.AddScoped() |> ignore - services.AddScoped>() |> ignore - services.AddHostedService>>() |> ignore) + services.AddScoped() |> ignore + services.AddHostedService>() |> ignore) [] let main args = diff --git a/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs b/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs index 9b9b0020..fe1b3483 100644 --- a/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs +++ b/FSharp.Examples.Polling/Services/Internal/IReceiverService.fs @@ -12,4 +12,4 @@ open System.Threading open System.Threading.Tasks type IReceiverService = - abstract member ReceiveAsync: CancellationToken -> Task + abstract member ReceiveAsync: CancellationToken -> Async diff --git a/FSharp.Examples.Polling/Services/PollingService.fs b/FSharp.Examples.Polling/Services/PollingService.fs index 6ec7b666..7ee04c59 100644 --- a/FSharp.Examples.Polling/Services/PollingService.fs +++ b/FSharp.Examples.Polling/Services/PollingService.fs @@ -15,7 +15,6 @@ namespace FSharp.Examples.Polling.Services open System open System.Threading -open System.Threading.Tasks open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging @@ -27,7 +26,7 @@ open FSharp.Examples.Polling.Util type PollingService<'T when 'T:> IReceiverService>(sp: IServiceProvider, logger: ILogger>) = inherit BackgroundService() - member __.doWork (cts: CancellationToken) = async { + member __.doWork (cts: CancellationToken) = task { let getReceiverService _ = use scope = sp.CreateScope() let service: 'T = scope.ServiceProvider.GetRequiredService<'T>() @@ -36,9 +35,9 @@ type PollingService<'T when 'T:> IReceiverService>(sp: IServiceProvider, logger: let cancellationNotRequested _ = (not cts.IsCancellationRequested) try - Seq.initInfinite getReceiverService + return Seq.initInfinite getReceiverService |> Seq.takeWhile cancellationNotRequested - |> Seq.fold + |> Seq.iter (fun r -> (r.ReceiveAsync cts |> ignore)) with | e -> logger.LogError($"Polling failed with exception: {e.ToString}: {e.Message}"); diff --git a/FSharp.Examples.Polling/Services/ReceiverService.fs b/FSharp.Examples.Polling/Services/ReceiverService.fs index e7d454b4..47167293 100644 --- a/FSharp.Examples.Polling/Services/ReceiverService.fs +++ b/FSharp.Examples.Polling/Services/ReceiverService.fs @@ -10,9 +10,9 @@ namespace FSharp.Examples.Polling.Services open System.Threading +open System.Threading.Tasks open Microsoft.Extensions.Logging open Telegram.Bot.Polling -open Telegram.Bot.Types.Enums open Telegram.Bot open FSharp.Examples.Polling.Services.Internal @@ -21,7 +21,7 @@ open FSharp.Examples.Polling.Util type ReceiverService<'T when 'T :> IUpdateHandler>(botClient: ITelegramBotClient, updateHandler: UpdateHandler, logger: ILogger<'T>) = interface IReceiverService with - member __.ReceiveAsync(cts: CancellationToken) = task { + member __.ReceiveAsync(cts: CancellationToken) = async { logInfo logger "ReceiveAsync called" @@ -30,16 +30,20 @@ type ReceiverService<'T when 'T :> IUpdateHandler>(botClient: ITelegramBotClient ThrowPendingUpdates = true ) - let! me = botClient.GetMeAsync(cts) |> Async.AwaitTask - let username = - match me.Username with - | null -> "My Awesome Bot" - | v -> v - - logInfo logger $"Start receiving updates for {username}" - - botClient.ReceiveAsync( - updateHandler = updateHandler, - receiverOptions = options, - cancellationToken = cts) |> Async.AwaitTask |> ignore + try + let! me = botClient.GetMeAsync(cts) |> Async.AwaitTask + let username = + match me.Username with + | null -> "My Awesome Bot" + | v -> v + + logInfo logger $"Start receiving updates for {username}" + + botClient.ReceiveAsync( + updateHandler = updateHandler, + receiverOptions = options, + cancellationToken = cts) |> ignore + with + | :? TaskCanceledException -> logInfo logger "INFO: Receive cancelled." + | e -> logInfo logger $"ERROR: {e.Message}" } diff --git a/FSharp.Examples.Polling/Services/UpdateHandler.fs b/FSharp.Examples.Polling/Services/UpdateHandler.fs index 4db009e7..9eac9212 100644 --- a/FSharp.Examples.Polling/Services/UpdateHandler.fs +++ b/FSharp.Examples.Polling/Services/UpdateHandler.fs @@ -22,7 +22,7 @@ open FSharp.Examples.Polling.Services.Internal type UpdateHandler(botClient: ITelegramBotClient, logger: ILogger) = interface IUpdateHandler with member __.HandleUpdateAsync( _ , update, cancellation) = - UpdateHandlerFuncs.handleUpdateAsync botClient logger cancellation update + UpdateHandlerFuncs.handleUpdateAsync botClient logger cancellation update |> Async.StartAsTask :> Tasks.Task member __.HandlePollingErrorAsync( _ , ex: Exception, cancellationToken: CancellationToken) = - UpdateHandlerFuncs.handlePollingErrorAsync botClient logger cancellationToken ex + UpdateHandlerFuncs.handlePollingErrorAsync botClient logger cancellationToken ex |> Async.StartAsTask :> Tasks.Task From 7a353338dddff951fc8aea54d98090f078d90062 Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Sun, 5 Nov 2023 09:15:42 +0530 Subject: [PATCH 5/7] Bug fix: Avoided parallel execution --- .../Services/Internal/UpdateHandlerFuncs.fs | 44 +++++++++---------- .../Services/PollingService.fs | 2 +- .../Services/ReceiverService.fs | 5 ++- FSharp.Examples.Polling/Util.fs | 3 -- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs index 8089e9f6..ebae07ce 100644 --- a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs +++ b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs @@ -33,7 +33,7 @@ module UpdateHandlerFuncs = botClient.SendTextMessageAsync( chatId = query.Message.Chat.Id, text = $"Received {query.Data}", - cancellationToken = cts) |> Async.AwaitTask |> ignore + cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore let botOnInlineQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (inlinequery:InlineQuery) = @@ -52,10 +52,12 @@ module UpdateHandlerFuncs = isPersonal = true, cacheTime = 0, cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (chosenInlineResult:ChosenInlineResult) = - logInfo logger $"Received inline result: {chosenInlineResult.ResultId}" + async { + logInfo logger $"Received inline result: {chosenInlineResult.ResultId}" + } let botOnMessageReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = logInfo logger $"Receive message type: {message.Type}" @@ -64,11 +66,7 @@ module UpdateHandlerFuncs = botClient.SendChatActionAsync( chatId = message.Chat.Id, chatAction = ChatAction.Typing, - cancellationToken = cts) - |> Async.AwaitTask |> ignore - - // Simulate a long running task - delay 500 cts + cancellationToken = cts) |> Async.AwaitTask |> ignore let inlineKeyboard = seq { // first row @@ -89,7 +87,7 @@ module UpdateHandlerFuncs = text = "Choose", replyMarkup = InlineKeyboardMarkup(inlineKeyboard), cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore let sendReplyKeyboard = let replyKeyboardMarkup = @@ -107,7 +105,7 @@ module UpdateHandlerFuncs = text = "Choose", replyMarkup = replyKeyboardMarkup, cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore let removeKeyboard = botClient.SendTextMessageAsync( @@ -115,7 +113,7 @@ module UpdateHandlerFuncs = text = "Removing keyboard", replyMarkup = ReplyKeyboardRemove(), cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore let sendFile = botClient.SendChatActionAsync( @@ -136,7 +134,7 @@ module UpdateHandlerFuncs = photo = InputFileStream(fileStream, fileName), caption = "Nice Picture", cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore let requestContactAndLocation = let requestReplyKeyboard = @@ -151,7 +149,7 @@ module UpdateHandlerFuncs = text = "Who or Where are you?", replyMarkup = requestReplyKeyboard, cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore let startInlineQuery = let inlineKeyboard = @@ -162,7 +160,7 @@ module UpdateHandlerFuncs = text = "Press the button to start Inline Query", replyMarkup = inlineKeyboard, cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore let failingHandler = raise <| IndexOutOfRangeException() @@ -182,15 +180,13 @@ module UpdateHandlerFuncs = text = usage, replyMarkup = ReplyKeyboardRemove(), cancellationToken = cts) - |> Async.AwaitTask |> ignore + |> Async.AwaitTask |> Async.Ignore - if message.Type <> MessageType.Text then - () - else - let fn = + match message.Text with + | text when message.Text <> "" -> // We use tryHead here just in case we get an empty // response from the user - match message.Text.Split(' ') |> Array.tryHead with + match text.Split(' ') |> Array.tryHead with | Some "/inline_keyboard" -> sendInlineKeyboard | Some "/keyboard" -> sendReplyKeyboard | Some "/remove" -> removeKeyboard @@ -199,10 +195,12 @@ module UpdateHandlerFuncs = | Some "/inline_mode" -> startInlineQuery | Some "/throw" -> failingHandler | _ -> usage - fn + | _ -> async { return () } let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (update:Update) = - logInfo logger $"Unknown update type: {update.Type}" + async { + logInfo logger $"Unknown update type: {update.Type}" + } let handlePollingErrorAsync _ (logger: ILogger) _ (err:Exception) = async { let errormsg = @@ -226,7 +224,7 @@ module UpdateHandlerFuncs = | UpdateType.ChosenInlineResult -> handleUpdate botOnChosenInlineResultReceived update.ChosenInlineResult | _ -> handleUpdate unknownUpdateHandlerAsync update - fn + return! fn with | ex -> do! handleUpdate handlePollingErrorAsync ex } diff --git a/FSharp.Examples.Polling/Services/PollingService.fs b/FSharp.Examples.Polling/Services/PollingService.fs index 7ee04c59..415b4b8a 100644 --- a/FSharp.Examples.Polling/Services/PollingService.fs +++ b/FSharp.Examples.Polling/Services/PollingService.fs @@ -37,7 +37,7 @@ type PollingService<'T when 'T:> IReceiverService>(sp: IServiceProvider, logger: try return Seq.initInfinite getReceiverService |> Seq.takeWhile cancellationNotRequested - |> Seq.iter (fun r -> (r.ReceiveAsync cts |> ignore)) + |> Seq.iter (fun r -> (r.ReceiveAsync cts |> Async.RunSynchronously |> ignore)) with | e -> logger.LogError($"Polling failed with exception: {e.ToString}: {e.Message}"); diff --git a/FSharp.Examples.Polling/Services/ReceiverService.fs b/FSharp.Examples.Polling/Services/ReceiverService.fs index 47167293..3bb7620d 100644 --- a/FSharp.Examples.Polling/Services/ReceiverService.fs +++ b/FSharp.Examples.Polling/Services/ReceiverService.fs @@ -13,6 +13,7 @@ open System.Threading open System.Threading.Tasks open Microsoft.Extensions.Logging open Telegram.Bot.Polling +open Telegram.Bot.Types open Telegram.Bot open FSharp.Examples.Polling.Services.Internal @@ -39,10 +40,10 @@ type ReceiverService<'T when 'T :> IUpdateHandler>(botClient: ITelegramBotClient logInfo logger $"Start receiving updates for {username}" - botClient.ReceiveAsync( + return! botClient.ReceiveAsync( updateHandler = updateHandler, receiverOptions = options, - cancellationToken = cts) |> ignore + cancellationToken = cts) |> Async.AwaitTask with | :? TaskCanceledException -> logInfo logger "INFO: Receive cancelled." | e -> logInfo logger $"ERROR: {e.Message}" diff --git a/FSharp.Examples.Polling/Util.fs b/FSharp.Examples.Polling/Util.fs index 80fa03a1..e78d6eb8 100644 --- a/FSharp.Examples.Polling/Util.fs +++ b/FSharp.Examples.Polling/Util.fs @@ -16,6 +16,3 @@ module Util = // Log information using the passed-in logger let logInfo (logger: ILogger) (msg: string) = logger.LogInformation msg - - let delay (t: int) (cts: CancellationToken)= - Task.Delay(t, cts) |> Async.AwaitTask |> ignore From 64a99fb9103c96282b3dd63a79f3042e3ead7622 Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Sun, 5 Nov 2023 16:47:35 +0530 Subject: [PATCH 6/7] Complete F# bot working and tested. --- .../Services/Internal/UpdateHandlerFuncs.fs | 114 +++++++++--------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs index ebae07ce..db0e0678 100644 --- a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs +++ b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs @@ -12,7 +12,6 @@ namespace FSharp.Examples.Polling.Services.Internal open System open System.IO open System.Threading -open System.Threading.Tasks open Microsoft.Extensions.Logging @@ -26,43 +25,8 @@ open Telegram.Bot.Types.ReplyMarkups open FSharp.Examples.Polling.Util module UpdateHandlerFuncs = - - let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (query:CallbackQuery) = - botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") |> Async.AwaitTask |> ignore - - botClient.SendTextMessageAsync( - chatId = query.Message.Chat.Id, - text = $"Received {query.Data}", - cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - - - let botOnInlineQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (inlinequery:InlineQuery) = - logInfo logger $"Received inline query from: {inlinequery.From.Id}" - - // displayed result - let results = seq { - InlineQueryResultArticle( - id = "3", - title = "TgBots", - inputMessageContent = InputTextMessageContent("hello")) - } - - botClient.AnswerInlineQueryAsync(inlinequery.Id, - results |> Seq.cast, - isPersonal = true, - cacheTime = 0, - cancellationToken = cts) - |> Async.AwaitTask - - let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (chosenInlineResult:ChosenInlineResult) = - async { - logInfo logger $"Received inline result: {chosenInlineResult.ResultId}" - } - - let botOnMessageReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = - logInfo logger $"Receive message type: {message.Type}" - - let sendInlineKeyboard = + module private BotTextMessages = + let sendInlineKeyboard (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = botClient.SendChatActionAsync( chatId = message.Chat.Id, chatAction = ChatAction.Typing, @@ -89,7 +53,7 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - let sendReplyKeyboard = + let sendReplyKeyboard (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = let replyKeyboardMarkup = ReplyKeyboardMarkup( seq { // first row @@ -107,7 +71,7 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - let removeKeyboard = + let removeKeyboard (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = botClient.SendTextMessageAsync( chatId = message.Chat.Id, text = "Removing keyboard", @@ -115,7 +79,7 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - let sendFile = + let sendFile (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = botClient.SendChatActionAsync( message.Chat.Id, ChatAction.UploadPhoto, @@ -136,7 +100,7 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - let requestContactAndLocation = + let requestContactAndLocation (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = let requestReplyKeyboard = ReplyKeyboardMarkup(seq { KeyboardButton.WithRequestLocation("Location"); @@ -151,7 +115,7 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - let startInlineQuery = + let startInlineQuery (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = let inlineKeyboard = InlineKeyboardMarkup (InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("Inline Mode")) @@ -162,18 +126,19 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore - let failingHandler = raise <| IndexOutOfRangeException() + let failingHandler (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = + raise <| IndexOutOfRangeException() - let usage = + let usage (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = let usage = "Usage:\n" + "/inline_keyboard - send inline keyboard\n" + "/keyboard - send custom keyboard\n" + "/remove - remove custom keyboard\n" + "/photo - send a photo\n" + - "/request - request location or contact" + - "/inline_mode - send keyboard with inline query" + - "/throw - Simulate a failed bot message handler" + "/request - request location or contact\n" + + "/inline_mode - send keyboard with inline query\n" + + "/throw - Simulate a failed bot message handler\n" botClient.SendTextMessageAsync( chatId = message.Chat.Id, @@ -182,21 +147,58 @@ module UpdateHandlerFuncs = cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore + let botOnMessageReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (message:Message) = + let handleBotMessage f = f botClient logger cts message + + logInfo logger $"Receive message type: {message.Type}" + match message.Text with | text when message.Text <> "" -> // We use tryHead here just in case we get an empty // response from the user match text.Split(' ') |> Array.tryHead with - | Some "/inline_keyboard" -> sendInlineKeyboard - | Some "/keyboard" -> sendReplyKeyboard - | Some "/remove" -> removeKeyboard - | Some "/photo" -> sendFile - | Some "/request" -> requestContactAndLocation - | Some "/inline_mode" -> startInlineQuery - | Some "/throw" -> failingHandler - | _ -> usage + | Some "/inline_keyboard" -> handleBotMessage BotTextMessages.sendInlineKeyboard + | Some "/keyboard" -> handleBotMessage BotTextMessages.sendReplyKeyboard + | Some "/remove" -> handleBotMessage BotTextMessages.removeKeyboard + | Some "/photo" -> handleBotMessage BotTextMessages.sendFile + | Some "/request" -> handleBotMessage BotTextMessages.requestContactAndLocation + | Some "/inline_mode" -> handleBotMessage BotTextMessages.startInlineQuery + | Some "/throw" -> handleBotMessage BotTextMessages.failingHandler + | _ -> handleBotMessage BotTextMessages.usage | _ -> async { return () } + let botOnCallbackQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (query:CallbackQuery) = + botClient.AnswerCallbackQueryAsync(query.Id, $"Received {query.Data}") |> Async.AwaitTask |> ignore + + botClient.SendTextMessageAsync( + chatId = query.Message.Chat.Id, + text = $"Received {query.Data}", + cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore + + + let botOnInlineQueryReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (inlinequery:InlineQuery) = + logInfo logger $"Received inline query from: {inlinequery.From.Id}" + + // displayed result + let results = seq { + InlineQueryResultArticle( + id = "3", + title = "TgBots", + inputMessageContent = InputTextMessageContent("hello")) + } + + botClient.AnswerInlineQueryAsync(inlinequery.Id, + results |> Seq.cast, + isPersonal = true, + cacheTime = 0, + cancellationToken = cts) + |> Async.AwaitTask + + let botOnChosenInlineResultReceived (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (chosenInlineResult:ChosenInlineResult) = + async { + logInfo logger $"Received inline result: {chosenInlineResult.ResultId}" + } + let unknownUpdateHandlerAsync (botClient:ITelegramBotClient) (logger: ILogger) (cts: CancellationToken) (update:Update) = async { logInfo logger $"Unknown update type: {update.Type}" From 69cf93d38e5881899d48e1ae940521d79ff9c980 Mon Sep 17 00:00:00 2001 From: arvindd <1006084+arvindd@users.noreply.github.com> Date: Sun, 5 Nov 2023 19:50:29 +0530 Subject: [PATCH 7/7] Targeted .NET 6.0, and fixed photo bug. --- FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj | 7 ++++++- .../Services/Internal/UpdateHandlerFuncs.fs | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj b/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj index 247a2bb8..75d5381f 100644 --- a/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj +++ b/FSharp.Examples.Polling/FSharp.Examples.Polling.fsproj @@ -1,6 +1,6 @@ - net7.0 + net6.0 @@ -12,6 +12,11 @@ + + + Always + + diff --git a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs index db0e0678..7395dd0d 100644 --- a/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs +++ b/FSharp.Examples.Polling/Services/Internal/UpdateHandlerFuncs.fs @@ -87,15 +87,16 @@ module UpdateHandlerFuncs = |> Async.AwaitTask |> ignore let filePath = @"Files/tux.png" - use fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read) - let fileName = filePath.Split(Path.DirectorySeparatorChar) |> Array.last + let inputStream = + InputFileStream(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), + fileName) botClient.SendPhotoAsync( chatId = message.Chat.Id, - photo = InputFileStream(fileStream, fileName), + photo = inputStream, caption = "Nice Picture", cancellationToken = cts) |> Async.AwaitTask |> Async.Ignore