AWS CDK - Building Telegram bot with AWS Lambda and API Gateway Proxy Integration - Part 2
Tags: javascript, aws, awcdk, telegram
Published: 2022-02-20 13:18:30

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](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

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

## 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/

Step 2: After successful login, search for "**BotFather**" bot in Telegram search bar.
![Telegram Web UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/igtpio94j2mza7512bxt.JPG)

Step 3: Type `/help` and then bot would reply with its menu. Click on `/newbot`to 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rhaic4m5cvxglmnlpole.JPG)

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/webhook`added 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.

```javascript
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 };
```

## 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
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ur661jfgj10u0uyzlis2.png)
 
 File Path: `\aws-cdk-telegram-bot-example\assets\lambda\telegram-bot\index.js`
 
 Lets add few libraries in our lambda code. e.g axios
```javascript
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);
  });
}
```
## 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
![CDK Deploy commandline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdx31zxszkljpl0uqa3c.JPG)

End ouput:
![CDK Deploy output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30dh32qb6pu0hy1hxut5.png)

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

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

- API Gateway Console UI
![API Gateway Console UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7e46tkthxsgzsw5noes9.png)

- Cloudformation Console UI
![Cloudformation Console UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jbsnlq9bsw7r1mt7xuoh.png)

- Lambda Console UI
![Lambda Console UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2fyy60xwy7zeakyqd5v.png)

## 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.

- Set Webhook
https://api.telegram.org/bot{bot_token}/setWebhook?url={url_to_send_updates_to}
- Get Webhook Info which will contain info on pending replies count and last response.
https://api.telegram.org/bot{bot_token}/getWebhookInfo?url={url_to_send_updates_to}
- Get some more info.
https://api.telegram.org/bot{bot_token}/WebhookInfo?url={url_to_send_updates_to}
- Delete webhook and stop pushing message to API Gateway URL.
https://api.telegram.org/bot{bot_token}/deleteWebhook?url={url_to_send_updates_to}


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iktuz9jeur5eyntqcxbp.png)

and testing the normal `/bot` path from browser.
![URL Check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rjh5kw00n2uyuik1p46.png)
 

## 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](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

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

{% user arki7n %}