Simple Serverless Telegram Bot with AWS Lambda
Tags: lambda, aws, serverless
Published: 2021-10-04 13:29:27

Previously, I created a [simple reminder application](https://blog.zhenkai.xyz/serverless-sms-reminder-part-1/) with AWS Lambda and Simple Notification Service (SNS) SMS messages. With so many instant messenger applications these days, it is about time I move away from using SMS messages, specifically to Telegram.

## Requirements
There are no changes to the previous requirements. To recap:
* To send a text message to people once a month
* Cheap
* Serverless on AWS

## Architecture
![Serverless Telegram Bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8lv2w7yh9gzkpe68yl6n.png)
Similar to the previous application, I will still be using CloudWatch Events to trigger a Lambda function in a monthly interval to send the reminder message.

Previously, I had to manually subscribe mobile numbers to the SNS topic. The improvement this time for the Telegram bot comes from user self-service on-boarding to subscribe to the reminder notification, which is actually just a simple `/start` to the bot.

## How the Bot works
Every day, a Lambda function (named Register) will be triggered to call the Telegram Bot API to retrieve any outstanding `/start` initiation. Upon detecting an outstanding message, the chat ID and username will be extracted and appended to the DynamoDB table.

Every month, a Lambda function (named sendMessage) will be triggered to scan the above-mentioned DynamoDB table, and recursively send out a custom Telegram message via the bot.

## How to Configure the Bot
### Step 1: Create a Telegram Bot with Botfather
Following the [Telegram bot guide](https://core.telegram.org/bots), you start by looking for @Botfather in Telegram and following the instructions to create a new bot. Give your bot a meaningful name, description and profile picture.

![BotFather](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4aayk9j9fm0vzebm1gx.jpeg)

Take note of the token that Botfather assigns to you; it will be required for your Lambda functions to call your bot API.

### Step 2: Create a DynamoDB Table
Telegram Chat IDs will be unique for every user that initiated a chat with my Telegram Bot. This made chat IDs a great candidate as the primary partition key. I set the primary partition key as chat_id (Number).

I also configured on-demand Read/Write Capacity modes, because I knew my DynamoDB Table is not going to be accessed frequently.

![DynamoDB Table for Telegram Bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0cz7ohrichvixj5tlxee.png)

Remember to take note of the DynamoDB Table name, you will need it for the next step.

### Step 3: Create Lambda functions (register and sendMessage)
Go to the Lambda console page and create 2 Lambda functions, 1 for the registration workflow and 1 for sending messages. The codes for the Lambda functions are as follows (I used the Node.js runtime):

```javascript
var AWS = require("aws-sdk")
var docClient = new AWS.DynamoDB.DocumentClient({region: "ap-southeast-1"})
const telegram = require('telegram-bot-api')

const tableName = process.env.DDB_TABLE_NAME

var bot = new telegram({
    token: process.env.TG_API_KEY,
    updates: {
        enabled: true
    }
})

const mp = new telegram.GetUpdateMessageProvider()
bot.setMessageProvider(mp)

bot.start()
.then(() => {
    console.log('API is started')
})
.catch(console.err)

exports.handler = () => {
    bot.on('update', update => {
        
        console.log(update)
        var params = {
            TableName: tableName,
            Item: { 
                chat_id: update.message.chat.id
            }
        }
        console.log("Adding a new item...")
        docClient.put(params, (err) => {
            if (err) {
                console.log('error cant read.', JSON.stringify(err, null, 2))
                bot.sendMessage({
                    chat_id: update.message.chat.id,
                    text: `Registration to Mr Gentle Reminderer is unsuccessful. Please let Zhen Kai know of the error. ${JSON.stringify(err, null, 2)}`
                }).catch(error => { console.log(error) })
            } else {
                console.log('succeed')
                bot.sendMessage({
                    chat_id: update.message.chat.id,
                    text: 'Registration to Mr Gentle Reminderer is successful! The gentle reminder will be sent on the first day of every month. Thanks!'
                }).catch(error => { console.log(error) })
            }
        })
    })
}
```

```javascript
// Lambda function for sending messages
var AWS = require("aws-sdk")
var docClient = new AWS.DynamoDB.DocumentClient({region: "ap-southeast-1"})
const telegram = require('telegram-bot-api')

const tableName = process.env.DDB_TABLE_NAME

var bot = new telegram({
    token: process.env.TG_API_KEY,
    updates: {
        enabled: true
    }
})

var params = {
    TableName: tableName,
    ProjectionExpression: "chat_id"
}

exports.handler = () => {

    docClient.scan(params, (err, data) => {
        if (err) {
            console.error("Unable to scan the table. Error JSON:", JSON.stringify(err, null, 2))
        } else {
            console.log("Scan succeeded")
            data.Items.forEach(chatIdObj => {
                console.log('Chat ID is:', chatIdObj.chat_id)
                bot.sendMessage({
                    chat_id: chatIdObj.chat_id,
                    text: 'Gentle reminder to transfer the monthly subscription fee to the subscriber. Thanks!'
                }).catch(error => { console.log(error) })
            })
            
        }
    })
}
```
A few things to note:
* I used the [Telegram Bot API NPM package](https://www.npmjs.com/package/telegram-bot-api) for calling the Bot APIs.
* I zipped the npm package together with my Lambda code before I uploaded it.
* I used the DDB_TABLE_NAME environment variable for my Lambda function to know which table to call. Assign your DynamoDB Table to this variable.
* I used the TG_API_KEY environment variable for my Lambda function to know which Bot API to call. Assign your Bot token to this variable.
* Remember to give your Lambda functions the right IAM permissions to call DynamoDB.

### Step 4: Configure CloudWatch Events
At the CloudWatch console page, under Events, create a new Event with the cron expression you want to trigger your respective Lambda functions.

I wanted to consolidate registration once a day and send messages once a month, so I used the following cron expressions:
* Daily register: cron(0 10 * * ? *)
* Monthly sendMessage: cron(0 11 1 * ? *)

Head over to the Lambda console page and choose “+ Add trigger”, and choose EventBridge (CloudWatch Events) and select the CloudWatch events you have created.

![Lambda function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4fov3zjqhlig6v9czoi.png)

## Summary
There are still a lot of room for improvement for this simple Telegram bot I hacked together over the weekend. A quicker feedback during bot registration using webhooks is a future improvement I would want to make.

I hope you had fun with this little project. Reach out to me if you have any feedback.