Azure Functions: Building a Telegram Bot with PowerShell 2

#powershell, #cloud edit this page

Welcome back to the second part of our little fun experiment with Telegram and Azure Functions. In the first part we created the function app and the bot, now let’s hook them up.

Connecting the bot with the Azure Function

We can get updates from Telegram bots in two ways, there is a getUpdate method that can be polled periodically, or we can use Webhooks. Our HTTP trigger function in Azure will run whenever it receives a http request, so it is just perfect for the latter.

To configure our Azure Function as a destination for the bot’s webhook, we use the setWebhook method and pass a hashtable with an URL parameter:

Invoke-RestMethod -Uri https://api.telegram.org/bot528059907:AAxxVs/setWebhook -Body @{
    "url"="https://ntsystemsbot.azurewebsites.net/api/Listener?code=WaaxxyA=="
} -Method Post

To verify that the URL has been set correctly, we can call getWebhookInfo:

[PS] C:\> Invoke-RestMethod https://api.telegram.org/bot528059907:AAxxVs/getWebhookInfo

  ok result
  -- ------
True @{url=https://ntsystemsbot.azurewebsites.net/api/Listener?code=Waa...

At this point our function will get a http request every time someone writes to the Bot account. So now it’s time to do something with it.

Overview

First of all: I am sure there are a hundred ways this can be done better… What I am trying to accomplish with this simple example is a demonstration of Azure Functions and the different triggers, inputs, and outputs it provides. That said, here’s what I came up with:

Our first function, the Listener, uses a HTTP trigger as input and Azure Queue Storage as output. This function will run, whenever a user sends a message to our bot and it will take the message and write it to a Storage Queue.

Another function, I called it UpdateData uses a timer trigger to get information from the blog and write it to Azure Blob Storage.

The third function, our Worker, uses Azure Queue Storage as trigger and Azure Blob Storage as input. The trigger is the same Queue that the Listener writes to, so a request coming from Telegram will be written to a Queue by the Listener function and trigger this Worker function.

A last function, the Responder, finally sends a POST request to the Telegram bot. This function is triggered by the Worker’s output queue and uses no other inputs or outputs.

To summarize, the Storage Queues are connected like this:

  • Listener > Worker > Responder

The Functions

There are different ways to create or modify functions. The Azure console provides an easy-to-use GUI to configure triggers, inputs and outputs. It even has an editor for the functions’s code :) For the more advanced stuff, we can connect to the functions Kudu Services and upload files directly or use the debug consoles. Each function basically consists of a folder, the folder contains a function.json file which describes the function (type, trigger, input, output) and a run.ps1 file which contains the PowerShell code.

The Listener

As mentioned before, the first function just listens for a http request by the Telegram Bot and writes it to a storage queue.

The function.json file defines the trigger httpTrigger and the input/output variables. The following is the actual configuration file for my Listener function:

{
  "bindings": [
    {
      "name": "req",
      "type": "httpTrigger",
      "direction": "in",
      "webHookType": "genericJson"
    },
    {
      "name": "res",
      "type": "http",
      "direction": "out"
    },
    {
      "type": "queue",
      "name": "outputQueueItem",
      "queueName": "listenerqueue",
      "connection": "AzureWebJobsDashboard",
      "direction": "out"
    }
  ],
  "disabled": false
}

The name defined in each object is made available as variable in the function’s code and contains a file path. That allows us to use Get-Content $req to get the HTTP request’s body. As well as Out-File $outputQueueItem and Out-File $res to write to the Storage Queue or respond to the HTTP request, respectively. How cool is that?!

The functions code, in the run.ps1 file is as simple as that:

# POST method: $req
$requestBody = Get-Content $req -Raw 

# Wirte input to Queue
Out-File -FilePath $outputQueueItem -Encoding Ascii -inputObject $requestBody

# Respond to the incoming web request
Out-File -Encoding Ascii -FilePath $res -inputObject $true

The request body is made available through the $req variable, we read the variable and write it to the $outputQueueItem which represents the Storage Queue listenerqueue\outputQueueItem in the AzureWebJobsDashboard storage account.

We do also respond to the incoming web request by writing to the $res variable.

The Worker

The next function is triggered by the same listenerqueue\outputQueueItem we write to in the first function, so it will always run, after the first function finished. The content of the storage queue is made available through the $triggerInput variable:

{
  "bindings": [
    {
      "name": "triggerInput",
      "type": "queueTrigger",
      "direction": "in",
      "queueName": "listenerqueue",
      "connection": "AzureWebJobsDashboard"
    },
    {
      "type": "blob",
      "name": "inPosts",
      "path": "outcontainer/posts",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    },
    {
      "type": "queue",
      "name": "outputQueueItem",
      "queueName": "workerqueue",
      "connection": "AzureWebJobsDashboard",
      "direction": "out"
    }
  ],
  "disabled": false
}

This function does not send HTTP responses, so there is no need to define an HTTP output. The only output for the Worker is another storage queue.

As you’ve noted, we define another input for this function, namely outcontainer/posts, we’ll get to that later.

The PowerShell code in run.ps1 does the actual work, so it evaluates the input and decides what to do with it.


# Read input from StorageQueue
$requestBody = Get-Content $triggerInput -Raw | ConvertFrom-Json
$posts = Get-Content $inPosts -Raw | ConvertFrom-Json | Select-Object -Expand items
...
Out-File -Encoding Ascii -FilePath $outputQueueItem -inputObject ($outObj | ConvertTo-Json)

Again, we simply read the input using Get-Content and write the output to the defined variable. I’ve omitted the actual code, to make this readable. You can find the code here: ntsystemsit/ntsystemsbot

The Responder

The responder is finally triggered by the workerqueue\outputQueueItem and sends an http request to the Telegram Bot API, thereby responding to the user. The configuration is basically the same as above, and you can find it in the GitHub repo.

To actually send a message to the Telegram Bot, I’ve created the following helper function. It uses Invoke-RestMethod to send a POST request to the /sendMessage API endpoint.

function New-TelegramMessage {
    [cmdletbinding()]
    param(
        $ChatId,
        $Text,
        $Mode = "Markdown",
        $ReplyId,
        $ReplyMarkup
    )
    $body = @{
        "parse_mode" = $mode;
        "chat_id"= $ChatId;
        "text" = $Text;
    }
    if($ReplyId) {
        $body.Add("reply_to_message_id",$ReplyId)
    }
    if($ReplyMarkup) {
        $body.Add("reply_markup",(ConvertTo-Json $ReplyMarkup -Depth 5))
    }
    Invoke-RestMethod -Uri https://api.telegram.org/bot$env:TG_Token/sendMessage -Body $body -Method Post
}

Note: The URL must contain the Bot’s API key. As I wanted to publish the code, I’ve stored the key in the function’s application settings. These settings are available as environment variables in the code, so I can access the key thorough: $env:TG_Token.

Update Data

The last piece we need for our little example is the UpdateData function. This one uses a timer trigger and just gets all posts of our blog and stores them in an Azure Storage Blob.

The function definition contains the schedule and the Storage Blob we want to write to:

{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 1 * * *"
    },
    {
      "type": "blob",
      "name": "outPosts",
      "path": "outcontainer/posts",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

The schedule is in a cron-type format, so this function will run at 01:00 hours every day.

Again, the PowerShell code is simple enough:

(Invoke-WebRequest https://ntsystems.it/api/v1/posts/ -UseBasicParsing).content | Out-File -Encoding ascii -FilePath $outPosts

The Bot in action

Ok, so with all of our code in place, we should now be able to communicate with our Bot using the Telegram messenger. Just search for the ntsystemsbot account and try it out :)

the result

This turned out to be a long story and there is so much left to explore. I hope the first two posts of this little series helped you understand the basics of Azure Functions, if someone finds a more practical use case a PowerShell-based Chatbot, I’d like to hear about it :)

Thanks for reading!

Tom

More Information: