Simple Serverless Telegram Bot with AWS Lambda

Previously, I created a simple reminder application 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

One of my main motivation to move away from SNS is due to the costs of publishing SMS messages. I wanted to bring that cost down even further.

Architecture

Serverless Telegram Bot (GitHub)

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 guide here: 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.

Creating a Telegram Bot

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 your Telegram Bot

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):

// Lambda function For Registration
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) })
            }
        })
    })
}
// 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 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 in AWS Console

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.

Leave a comment

Your email address will not be published. Required fields are marked *