AWS CDK - Building Telegram bot with AWS Lambda and API Gateway Proxy Integration - Part 2

arki7n

Akhilesh Yadav

Posted on February 20, 2022

AWS CDK - Building Telegram bot with AWS Lambda and API Gateway Proxy Integration - Part 2

Welcome to part 2 of this series. This would be the final series of AWS CDK - Building Telegram bot with AWS Lambda and API Gateway Proxy Integration . Regret for posting this lately.

You can find the sourcecode on Git repository by following below link. Checkout branch part2
https://github.com/arki7n/aws-cdk-telegram-bot-example.git

Commands for local usage:

git clone https://github.com/arki7n/aws-cdk-telegram-bot-example.git
git checkout part2
Enter fullscreen mode Exit fullscreen mode

So let me start about all the update. Incase you have the main branch of the repository, you can follow changes and execute one by one.

Let's update few packages to make sure existing code doesn't break with AWS CDK feature updates.

npm install @aws-cdk/core@1.137
npm install -g aws-cdk
Enter fullscreen mode Exit fullscreen mode

Setting up a new Telegram Bot

Head to your Web browser.
Step 1: Open below link and signin in with your telegram account.

https://web.telegram.org/k/
Enter fullscreen mode Exit fullscreen mode

Step 2: After successful login, search for "BotFather" bot in Telegram search bar.
Telegram Web UI

Step 3: Type /help and then bot would reply with its menu. Click on /newbotto begin setting up new bot.

Step 4: I will be creating up a bot with the intention of saving bookmark links or texts to some database. And then be able to view the database.

Telegram BotFather

Step 5: Save the API KEY token which would be required to access telegram APIs.

Example Telegram Bot: https://t.me/BookmarkmeBot

Telegram API Resources

There are 2 ways to setup bot with Telegram and hook our custom lambda function with it.

  1. Using HTTP long-polling on Telegram API: Active Server driven and could be expensive to keep the server running and poll for new user messages to our bot.
  2. Webhook: As soon as the bot receives new message, Telegram Server sends the message to our custom HTTP URL using POST method. We would use API Gateway URL and lambda function would do the rest of the work of processing data and sending response back to the telegram user.

Setting up new API Gateway URL path for Webhook in AWS CDK code.

There are some new addition in the earlier code of Part 1.

  1. Used npm 'path' package to get relevant directory of lambda function.
  2. Added dynamic description for Lambda to always upload code on new deployments irrespective of any changes in code.
  3. Added Lambda Versioning to track changes in AWS Lambda UI Console.
  4. Enabling CORS at API Gateway side to let telegram server push Webhook message without getting blocked (403 Forbidden Error).
  5. New Resource path /bot/webhookadded with POST method integration with lambda. Keeping /bot path for manual health check and see new lambda version information is available.
  6. Output URL of API URL and cloudwatch log UI.

Find the code below for aws-cdk-telegram-bot-example\cdk-tool\lib\cdk-tool-stack.js file. Make sure to replace the BOT_TOKEN with yours.

const cdk = require("@aws-cdk/core");
const lambda = require("@aws-cdk/aws-lambda");
const apigw = require("@aws-cdk/aws-apigateway");
const path = require('path');
const BOT_TOKEN = '5118686429:AAHtgBvYLyrTSIUJ-iNRmV5MiuTYcSfAXIYeysdf'; // PASTE Telegram API BOT TOKEN here

class CdkToolStack extends cdk.Stack {
  /**
   *
   * @param {cdk.Construct} scope
   * @param {string} id
   * @param {cdk.StackProps=} props
   */
  constructor(scope, id, props) {
    super(scope, id, props);

    // All constructs take these same three arguments : scope, id/name, props
    const lambdaTelegram = new lambda.Function(this, "telegramBotHandler", {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: "index.handler",
      code: lambda.Code.fromAsset(path.join(__dirname, '../../assets/lambda/telegram-bot')), // Get relevant path to lambda directory.
      architecture: lambda.Architecture.ARM_64,
      environment: {
        'CURRENT_ENV': 'dev',
        'BOT_TOKEN': BOT_TOKEN
      },
      description: `Generated on: ${new Date().toISOString()}`  // added to keep pushing latest code on AWS lambda on each deployment.
    });

    /*Versioning every new changes and keeping track of it. Check AWS Lambda UI Console*/
    const version = new lambda.Version(this, 'Ver'+new Date().toISOString(), {
      lambda: lambdaTelegram,
    });

    // All constructs take these same three arguments : scope, id/name, props
    // defines an API Gateway REST API resource backed by our "telegrambot-api" function.
    const restApi = new apigw.RestApi(this, "telegrambot-api", { 
        deploy: false,
        defaultCorsPreflightOptions: { // Enable CORS policy to allow from any origin. Customize as needed.
          allowHeaders: [
            'Content-Type',
            'X-Amz-Date',
            'Authorization',
            'X-Api-Key',
          ],
          allowMethods: ['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
          allowCredentials: false,
          allowOrigins: apigw.Cors.ALL_ORIGINS,
        }
    });

    // Let's keep this as it as and use it for normal 'Hello World' Response with GET method integration with lamhda.
    restApi.root
      .addResource("bot")
      .addMethod("GET", new apigw.LambdaIntegration(lambdaTelegram, { proxy: true }));

    // Lets add nested resource under /bot resource path and attach a POST method with same Lambda integration.
    restApi.root
      .getResource("bot")
      .addResource("webhook")
      .addMethod("POST", new apigw.LambdaIntegration(lambdaTelegram, { proxy: true }));

    // All constructs take these same three arguments : scope, id/name, props
    const devDeploy = new apigw.Deployment(this, "dev-deployment", { api: restApi });

    // All constructs take these same three arguments : scope, id/name, props
    const devStage = new apigw.Stage(this, "devStage", {
      deployment: devDeploy,
      stageName: 'dev' // If not passed, by default it will be 'prod'
    });

    // All constructs take these same three arguments : scope, id/name, props
    new cdk.CfnOutput(this, "BotURL", {
      value: `https://${restApi.restApiId}.execute-api.${this.region}.amazonaws.com/dev/bot`,
    });

    new cdk.CfnOutput(this, "BotWebhookUrl", {
      value: `https://${restApi.restApiId}.execute-api.${this.region}.amazonaws.com/dev/bot/webhook`,
    });

    new cdk.CfnOutput(this, "Lambda Cloudwatch Log URL", {
      value: `https://console.aws.amazon.com/cloudwatch/home?region=${this.region}#logsV2:log-groups/log-group/$252Faws$252Flambda$252F${lambdaTelegram.functionName}`
    });
  }
}

module.exports = { CdkToolStack };
Enter fullscreen mode Exit fullscreen mode

Update Lambda code

As we have gone with Webhook Approach, Telegram Server would push new user messages to our set Webhook URL. (Will show how to set webhook URL).

A normal lambda event object would like below. You can find resource path info, used method and stringified JSON object of telegram bot user message within body field.

{
    "resource": "/bot/webhook",
    "path": "/bot/webhook",
    "httpMethod": "POST",
    "headers": {
        "Accept-Encoding": "gzip, deflate",
        ....
    },
    "multiValueHeaders": {
        "Accept-Encoding": [
            "gzip, deflate"
        ],
        .....
    },
    "queryStringParameters": null,
    "multiValueQueryStringParameters": null,
    "pathParameters": null,
    "stageVariables": null,
    "requestContext": {
        "resourceId": "93ctxg",
        "resourcePath": "/bot/webhook",
        "httpMethod": "POST",
        "extendedRequestId": "N1EZWE8FIAMFimA=",
        "requestTime": "20/Feb/2022:07:02:06 +0000",
        "path": "/dev/bot/webhook",
        "accountId": "285535506992",
        "protocol": "HTTP/1.1",
        "stage": "dev",
        .......
        "domainName": "tq9rr56bhc.execute-api.us-east-1.amazonaws.com",
        "apiId": "tq9rr56bhc"
    },
    "body": "{\"update_id\":192810399,\n\"message\":{\"message_id\":15,\"from\":{\"id\":198940317,\"is_bot\":false,\"first_name\":\"Vikit\",\"username\":\"redblueshine\",\"language_code\":\"en\"},\"chat\":{\"id\":198940317,\"first_name\":\"Vikit\",\"username\":\"redblueshine\",\"type\":\"private\"},\"date\":1645340526,\"text\":\"hi\"}}",
    "isBase64Encoded": false
}
Enter fullscreen mode Exit fullscreen mode

Let's parse the stringified JSON object using JSON.parse(PASTE_STRINGIFIED_DATA) method. You will find from field containing id (Telegram UserID) and text field containing message. We will be requiring this 2 field information for replying towards the message sent by bot user.

Telegram webhook received data

File Path: \aws-cdk-telegram-bot-example\assets\lambda\telegram-bot\index.js

Lets add few libraries in our lambda code. e.g axios

const axios = require('axios');
const telegramLink = `https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`;

exports.handler = async function(event) {
    console.log("request:", JSON.stringify(event, undefined, 2));

    if(event.path==="/bot" || event.path==="/bot/"){
      return {
        statusCode: 200,
        headers: { "Content-Type": "text/plain" },
        body: `Hello, CDK! You've hit ${process.env.AWS_LAMBDA_FUNCTION_NAME} with ${process.env.AWS_LAMBDA_FUNCTION_VERSION}\n`
      };
    }

    try {
      if(event.body){
        const jsonData = JSON.parse(event.body).message;
        await sendReply(jsonData.from.id, 'Processing data:'+jsonData.text);
      }
    } catch(e){
      console.log('Error occured:',e);
    }
    return {
      statusCode: 200,
      headers: { "Content-Type": "text/plain" },
      body: `Success`
    };
  };

function sendReply(chatId, textReply){
  var data = JSON.stringify({
    "chat_id": chatId,
    "text": textReply,
    "disable_notification": true
  });

  const config = {
    method: 'post',
    url: telegramLink,
    headers: { 
      'Content-Type': 'application/json'
    },
    data : data
  };

  return axios(config)
  .then(function (response) {
    console.log(JSON.stringify(response.data));
  })
  .catch(function (error) {
    console.log(error);
  });
}
Enter fullscreen mode Exit fullscreen mode

Deploy CDK changes

Save all the changes and hit below command from directory path aws-cdk-telegram-bot-example/cdk-tool

cdk deploy --require-approval never
Enter fullscreen mode Exit fullscreen mode

CDK Deploy commandline

End ouput:
CDK Deploy output

Save above 3 links(BotURL, BotWebhookURL, LambdaCloudwatchLogURL) in notepad as we would need it later on.

If incase there is error, you can destroy and recreate by executing below commands.

cdk destroy
cdk deploy --require-approval never
Enter fullscreen mode Exit fullscreen mode

You can confirm changes by moving to API Gateway UI, Lambda UI and associated Cloudformation template.

  • API Gateway Console UI
    API Gateway Console UI

  • Cloudformation Console UI
    Cloudformation Console UI

  • Lambda Console UI
    Lambda Console UI

Setting up Telegram Webhook

Telegram API Documentation can be found at: https://core.telegram.org/bots/api

Set webhook url for the given Telegram Bot.

bot_token=Collected while creating new telegram bot.
url_to_send_updates_to = BotWebhookURL from last step.

You can simply paste below links in web browser.

So make sure to set webhook url and check the webhook info.

Final Testing

Head over to your created Telegram bot and send some text.
Example: https://t.me/BookmarkmeBot
Telegram Bot Demo

and testing the normal /bot path from browser.
URL Check

What's Next

I am not going to make this post lengthy. So you can write additional javascript code to save all your received data to AWS dynamoDB table or you can use Airtable API and see data with Airtable Site UI. Or build some jokes api, dictionary or anything basd on your requirements.

You can find the sourcecode on Git repository at below link. Checkout branch part2
https://github.com/arki7n/aws-cdk-telegram-bot-example.git

Commands for local usage:

git clone https://github.com/arki7n/aws-cdk-telegram-bot-example.git
git checkout part2
Enter fullscreen mode Exit fullscreen mode

Do not forget to destory the cloudformation stack after testing.
Run cdk destroy to wipe out all the created resources.

If you need some more help related to Airtable or building some additional logic, you can follow me at Twitter and I will help you to give some solution.

Follow Twitter: https://twitter.com/arki7n

💖 💪 🙅 🚩
arki7n
Akhilesh Yadav

Posted on February 20, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related