Home

Awesome

.NET Core NuGet NuGet NuGet

<img src="https://github.com/Dolfik1/Funogram/raw/master/docs/files/img/logo.png" alt="Funogram Logo" width="200" align="right" />

Introduction

This library provides F# interface to Telegram Bot API. The library has two parts. The first one is Funogram. It contains basic functions to interact with Telegram-like servers (REST interaction, serialization, etc). And the second part is Funogram.Telegram. It contains all Telegram methods and types for specified Telegram Bot API version.

In addition to the pure API implementation, this library provides high-level functions to make the development of bots easy. These functions are located in Funogram.Telegram.Bot module.

Breaking changes

Funogram.Telegram 6.0.0.x has breaking changes.

Installation

You need to install latest Funogram and Funogram.Telegram packages from nuget:

dotnet add package Funogram
dotnet add package Funogram.Telegram

Installation for .NET Framework users:

Install-Package Funogram
Install-Package Funogram.Telegram

Examples

You may start learning Funogram from example bots.

Hello, world!

First of all you need to get an API key. See Telegram Bot API documentation.

When Bot API token is registered you may start to write your first bot. Let's write simple bot that sends "Hello, world!" in reply to any message. You can find source code of the bot here.

Let's open the necessary namespaces:

open Funogram.Api
open Funogram.Telegram
open Funogram.Telegram.Bot

There are two mutually exclusive ways of receiving updates for your bot — the getUpdates method on one hand and webhooks on the other. Incoming updates are stored on the server until the bot receives them either way, but they will not be kept longer than 24 hours.

The Funogram library automatically receive updates and passes them to your updates handler. Let's write updates handle:

let updateArrived (ctx: UpdateContext) =
  match ctx.Update.Message with
  | Some { MessageId = messageId; Chat = chat } ->
    Api.sendMessageReply chat.Id "Hello, world!" messageId 
    |> api ctx.Config
    |> Async.Ignore
    |> Async.Start
  | _ -> ()

This simple handler is checking if update contains Message. If a Message found in the Update then bot will send answer:

Api.sendMessageReply chat.Id "Hello, world!" messageId 
|> api ctx.Config
|> Async.Ignore
|> Async.Start

This code also might be written like this:

Req.SendMessage.Make(chat.Id, text = "Hello, world!", replyToMessageId = messageId) 
|> api ctx.Config
|> Async.Ignore
|> Async.Start

Now we need to set bot token, pass update handler and start the bot. It might be done with few lines of code:

[<EntryPoint>]
let main _ =
  async {
    let config = Config.defaultConfig |> Config.withReadTokenFromFile
    let! _ = Api.deleteWebhookBase () |> api config
    return! startBot config updateArrived None
  } |> Async.RunSynchronously
  0

Let's take a look at each line in detail. Firstly we need to setup bot token

let config = Config.defaultConfig |> Config.withReadTokenFromFile

The default config looks like:

let defaultConfig =
  { Token = ""
    Offset = Some 0L
    Limit = Some 100
    Timeout = Some 60000
    AllowedUpdates = None
    Client = new HttpClient()
    ApiEndpointUrl = Uri("https://api.telegram.org/bot")
    WebHook = None
    OnError = (fun e -> printfn "%A" e) }

as you can notice there is no set token by default. There are two built-in ways to setup token:

  1. Config.withReadTokenFromFile function will read token from file named token (file should be located in run directory). If file does not exist the app will wait for token input in stdin and then save it to token file.
  2. Config.withReadTokenFromEnv "mytokenenv" function will read value from mytokenenv environment variable.

You also can specify token manually:

let config = { Config.defaultConfig with Token = "mysecrettoken" }

When token is set we are ready to make requests. Let's invoke deleteWebhook function:

let! _ = Api.deleteWebhookBase () |> api config

This function is not necessary but I always invoking it to force reset websocket configuration. If you have no plans to use websocket connection you can skip this step. Otherwise it may result to missing updates.

Finally we are ready to start the bot loop:

return! startBot config updateArrived None

This function will start main bot loop. For now on all updates will be collected and redirected to our update hook.

You may have noticed that getUpdates method returns Array of Update but in our handler we have only single update. This is done for the convenience of the end developer. In most of cases you need to process updates sequentially. But if you want to process whole received updates you may pass hook function as last parameter of startBot. For example:

return! startBot config (fun _ -> ()) ((fun updates -> printfn "Got updates array: %A" updates) |> Some)

Processing commands

Funogram can automatically parse specified commands with arguments:


let updateArrived (ctx: UpdateContext) =
  processCommands ctx [|
    cmd "/start" (fun _ -> printfn "User invoked /start command!")
    cmdScan "/say %s" (fun text _ -> printfn "User invoked say command with text %s" text)
    cmdScan "/sum %i %i" (fun a b _ -> printfn "Sum of %i and %i is %i" a b (a + b))
  |] |> ignore

processCommands function returns false if at least one of commands is matched.

There are two main functions: cmd and cmdScan. There are only one difference between both commands. cmdScan is used for command with arguments when the cmd is for command without arguments. Both functions has two parameters. First parameter is command itself (it may contain arguments in case of cmdScan). The second parameter is callback function.

cmdScan works mostly like sprintf function but it's logic is reversed. The values will be extracted and passed as first parameter of callback function.

Commands processor also correctly process commands with bot username. For example the are two valid commands for bot:

when @MyExampleFSharpBot is bot's username.

You also may write your own command function with custom logic (if required) which may check text for keywords instead of command (see Bot.fs for reference)

Configure WebSockets

If you want to use webhooks, you should start application with admin privileges. The sample app is located here.

To get updates via webhooks you need send your endpoint address to Telegram server:

let! hook = setWebhookBase webSocketEndpoint None None None |> api config

You can use ngrok service to test webhooks on your local machine that no have public address.

Then you should set WebHook field in BotConfig. WebHook field have BotWebHook type that contains two fields: Listener and ValidateRequest:

let apiPath = sprintf "/%s" config.Token
let webSocketEndpoint = sprintf "https://1c0860ec2320.ngrok.io/%s" webSocketEndpoint apiPath
let! hook = setWebhookBase webSocketEndpoint None None None |> api config
match hook with
| Ok ->
  use listener = new HttpListener()
  listener.Prefixes.Add("http://*:4444/")
  listener.Start()

  let webhook = { Listener = listener; ValidateRequest = (fun req -> req.Url.LocalPath = apiPath) }
  return! startBot { config with WebHook = Some webhook } updateArrived None

| Error e -> 
  printf "Can't set webhook: %A" e
  return ()

Telegram recommends using a secret path in URL with your bot's token. You can validate telegram request in ValidateRequest function.

Note: if you want to rollback to getUpdates method you need to clear webhook. This might be done via deleteWebhook function.

Codegen

Funogram types are generated automatically from Telegram Bot API reference. The code generation tool is located in src/Funogram.Generator folder.

To start generator run Funogram.Generator project. You should specify project's directory as working directory to get correct result.

Telegram Bot API reference will parsed and output will copied to src/Funogram.Generator/out folder. The generated files are:

The generated code will be copied to src/Funogram.Telegram folder:

If you want to generate types and methods for old Telegram server version you may specify link to web archive.

You also can patch code to load reference data from *.json files. This will allow you to make changes if needed.

The generator supports remapping. This make it possible to replace some values in generated json to your own (see RemapTypes.json and RemapMethods.json)

Advanced

The library is built around types. Any API request or response is F# record type:

type GetUpdates =
  {
    Offset: int64 option
    Limit: int64 option
    Timeout: int64 option
    AllowedUpdates: string[] option
  }
  static member Make(?offset: int64, ?limit: int64, ?timeout: int64, ?allowedUpdates: string[]) = 
    {
      Offset = offset
      Limit = limit
      Timeout = timeout
      AllowedUpdates = allowedUpdates
    }
  interface IRequestBase<Update[]> with
    member _.MethodName = "getUpdates"

The request types should implement IRequestBase<T> interface when T is return type and MethodName is Telegram's Bot API method name. You can create your own request type and send it to the server without patching Funogram.Telegram library. This might be useful if Funogram is not updated to latest Bot API version but you want to use some new features without waiting for new release.

Response types are also record types:


/// This object contains information about one answer option in a poll.
and [<CLIMutable>] PollOption =
  {
    /// Option text, 1-100 characters
    [<DataMember(Name = "text")>]
    Text: string
    /// Number of users that voted for this option
    [<DataMember(Name = "voter_count")>]
    VoterCount: int64
  }
  static member Create(text: string, voterCount: int64) = 
    {
      Text = text
      VoterCount = voterCount
    }

All fields in type should be marked with DataMember attribute to avoid problems with parameters naming.

Telegram Bot API have four ways of passing parameters in Bot API requests:

The library uses URL query string method if possible and multipart/form-data method for all requests with files. The preferred method will be choosen by request data. If you want to use application/json method for some reasons you can use Funogram.Tools.Api.makeJsonBodyRequestAsync function (this function will not work with files). You also can implement your own method of Bot API request (see Funogram/Tools.fs for reference)

Articles

DEPRECATED: Funogram: Writing Telegram Bots In F#

DEPRECATED: Funogram.Keyboard: How to reserve seats on an airplane using F # and telegram

Plugins and Extensions

DEPRECATED: Funogram.Keyboard