├── package.json
├── LICENSE
├── README.md
├── serverless.yml
├── .gitignore
├── index.ts
└── tsconfig.json
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "telegram-serverless-ts-bot-tutorial",
3 | "version": "0.0.1",
4 | "description": "Telegram typescript serverless Bot",
5 | "main": "index.ts",
6 | "author": "Roy Shilkrot",
7 | "license": "MIT",
8 | "dependencies": {
9 | "aws-lambda": "^1.0.7",
10 | "aws-sdk": "^2.1378.0",
11 | "node-telegram-bot-api": "^0.61.0",
12 | "reflect-metadata": "^0.1.13"
13 | },
14 | "devDependencies": {
15 | "@types/aws-lambda": "^8.10.115",
16 | "@types/aws-sdk": "^2.7.0",
17 | "@types/node": "^20.1.3",
18 | "@types/node-telegram-bot-api": "^0.61.6",
19 | "@types/validator": "^13.7.17",
20 | "serverless": "^3.30.1",
21 | "serverless-plugin-typescript": "^2.1.2",
22 | "ts-node-dev": "^2.0.0",
23 | "typescript": "^5.0.4"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Roy Shilkrot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Telegram Typescript Serverless Bot Tutorial
2 |
3 |
4 | [](https://img.shields.io/github/license/royshil/telegram-serverless-ts-bot-tutorial)
5 |
6 | [**Blogpost**](https://www.morethantechnical.com/2023/05/18/aws-lambda-nodejs-telegram-bot-with-typescript-serverless-and-dynamodb/)
7 | | [**YouTube**](http://www.youtube.com/watch?v=MtWUKFcljKg)
8 |
9 |
10 |
11 | In this tutorial, we will explore how to build a simple Telegram bot using serverless with TypeScript and AWS Lambda. We’ll leverage the power of AWS services such as API Gateway and DynamoDB to create a highly scalable and efficient bot. While there are various tutorials available online, this guide aims to provide a more comprehensive and detailed approach.
12 |
13 | Run npm
14 | ```sh
15 | $ npm i
16 | ```
17 |
18 | Deploy the bot CF stack:
19 | ```sh
20 | $ ./node_modules/.bin/serverless deploy
21 | ```
22 |
23 | CURL command for setting the webhook for the bot: (URL given by the `serverless deploy` output)
24 | ```sh
25 | $ curl --request POST --url "https://api.telegram.org/bot/setWebhook" --header 'content-type: application/json' --data '{"url": ""}'
26 | ```
27 |
28 | ## Code Walkthrough Video
29 | [](http://www.youtube.com/watch?v=MtWUKFcljKg)
30 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | service: telegram-serverless-ts-bot-tutorial
2 |
3 | frameworkVersion: "3"
4 |
5 | provider:
6 | name: aws
7 | runtime: nodejs18.x
8 | environment:
9 | DYNAMODB_TABLE: ${self:service}-${sls:stage}
10 | iam:
11 | role:
12 | statements:
13 | - Effect: Allow
14 | Action:
15 | - dynamodb:Query
16 | - dynamodb:Scan
17 | - dynamodb:GetItem
18 | - dynamodb:PutItem
19 | - dynamodb:UpdateItem
20 | - dynamodb:DeleteItem
21 | Resource: "arn:aws:dynamodb:${aws:region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
22 |
23 | functions:
24 | webhook:
25 | handler: index.webhook
26 | events:
27 | - http:
28 | path: webhook
29 | method: post
30 |
31 | plugins:
32 | - serverless-plugin-typescript
33 |
34 | resources:
35 | Resources:
36 | ExampleDynamoDbTable:
37 | Type: 'AWS::DynamoDB::Table'
38 | DeletionPolicy: Retain
39 | Properties:
40 | AttributeDefinitions:
41 | -
42 | AttributeName: id
43 | AttributeType: S
44 | -
45 | AttributeName: chatId
46 | AttributeType: S
47 | KeySchema:
48 | -
49 | AttributeName: chatId
50 | KeyType: HASH
51 | -
52 | AttributeName: id
53 | KeyType: RANGE
54 | GlobalSecondaryIndexes:
55 | -
56 | IndexName: chatId-index
57 | KeySchema:
58 | -
59 | AttributeName: chatId
60 | KeyType: HASH
61 | Projection:
62 | ProjectionType: ALL
63 | BillingMode: PAY_PER_REQUEST
64 | TableName: ${self:provider.environment.DYNAMODB_TABLE}
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import TelegramBot from "node-telegram-bot-api";
2 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
3 | import { DynamoDB } from "aws-sdk";
4 |
5 | import { randomUUID } from "crypto";
6 |
7 | const dynamoDb = new DynamoDB.DocumentClient();
8 | const params = {
9 | TableName: process.env.DYNAMODB_TABLE!,
10 | };
11 |
12 | // replace the value below with the Telegram token you receive from @BotFather
13 | const token = " ";
14 |
15 | const bot = new TelegramBot(token);
16 |
17 | let globalResolve: (value: any) => void = () => {};
18 |
19 | export const webhook = async (
20 | event: APIGatewayProxyEvent
21 | ): Promise => {
22 | const bodyParsed = JSON.parse(event.body!);
23 | console.log("bodyParsed", bodyParsed);
24 | await new Promise((resolve, reject) => {
25 | globalResolve = resolve;
26 | bot.processUpdate(bodyParsed);
27 | // set timeout to 3 seconds to resolve the promise in case the bot doesn't respond
28 | setTimeout(() => {
29 | // make sure to resolve the promise in case of timeout as well
30 | // do not reject the promise, otherwise the lambda will be marked as failed
31 | resolve("global timeout");
32 | }, 3000);
33 | });
34 |
35 | // respond to Telegram that the webhook has been received.
36 | // if this is not sent, telegram will try to resend the webhook over and over again.
37 | return {
38 | statusCode: 200,
39 | body: JSON.stringify({ message: "function executed successfully" }),
40 | };
41 | };
42 |
43 | bot.onText(
44 | /\/todos/,
45 | async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
46 | const chatId = msg.chat.id;
47 | try {
48 | // Find all todos for this chatId
49 | const r = await dynamoDb
50 | .query({
51 | ...params,
52 | KeyConditionExpression: "chatId = :chatId",
53 | ExpressionAttributeValues: {
54 | ":chatId": chatId.toString(),
55 | },
56 | })
57 | .promise();
58 | if (r.Items == undefined || r.Items!.length == 0) {
59 | await bot.sendMessage(chatId, `0️⃣ No TODOs found`);
60 | globalResolve("ok");
61 | return;
62 | }
63 | let message = "";
64 | for (const todo of r.Items!) {
65 | message += `➖ ${todo.what}\n`;
66 | }
67 | await bot.sendMessage(chatId, `📝 Current TODOs:\n${message}`);
68 | } catch (error) {
69 | console.error(error);
70 | await bot.sendMessage(chatId, `❌ Error getting TODOs: ${error}`);
71 | }
72 | globalResolve("ok");
73 | }
74 | );
75 |
76 | bot.onText(
77 | /\/add (.+)/,
78 | async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
79 | const chatId = msg.chat.id;
80 | const what = match![1];
81 | const id = randomUUID();
82 | try {
83 | await dynamoDb
84 | .put({
85 | ...params,
86 | Item: {
87 | id,
88 | chatId: chatId.toString(),
89 | what,
90 | },
91 | })
92 | .promise();
93 | await bot.sendMessage(chatId, `✅ Added TODO: ${what}`);
94 | } catch (error) {
95 | console.error(error);
96 | await bot.sendMessage(chatId, `❌ Error adding TODO: ${what} (${error})`);
97 | }
98 | globalResolve("ok");
99 | }
100 | );
101 |
102 | bot.onText(
103 | /\/remove (.+)/,
104 | async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
105 | const chatId = msg.chat.id;
106 | const what = match![1];
107 | try {
108 | const r = await dynamoDb
109 | .query({
110 | ...params,
111 | KeyConditionExpression: "chatId = :chatId",
112 | FilterExpression: "what = :what",
113 | ExpressionAttributeValues: {
114 | ":chatId": chatId.toString(),
115 | ":what": what,
116 | },
117 | })
118 | .promise();
119 | if (r.Items == undefined || r.Items!.length == 0) {
120 | await bot.sendMessage(chatId, `❌ No TODO found`);
121 | globalResolve("ok");
122 | return;
123 | }
124 | await dynamoDb
125 | .delete({
126 | ...params,
127 | Key: {
128 | id: r.Items![0].id,
129 | chatId: chatId.toString(),
130 | },
131 | })
132 | .promise();
133 | await bot.sendMessage(chatId, `✅ Removed TODO: ${what}`);
134 | } catch (error) {
135 | console.error(error);
136 | await bot.sendMessage(
137 | chatId,
138 | `❌ Error removing TODO: ${what} (${error})`
139 | );
140 | }
141 | globalResolve("ok");
142 | }
143 | );
144 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | // "resolveJsonModule": true, /* Enable importing .json files. */
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 |
46 | /* JavaScript Support */
47 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
50 |
51 | /* Emit */
52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
58 | // "outDir": "./", /* Specify an output folder for all emitted files. */
59 | // "removeComments": true, /* Disable emitting comments. */
60 | // "noEmit": true, /* Disable emitting files from a compilation. */
61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
68 | // "newLine": "crlf", /* Set the newline character for emitting files. */
69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
75 |
76 | /* Interop Constraints */
77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
83 |
84 | /* Type Checking */
85 | "strict": true, /* Enable all strict type-checking options. */
86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
104 |
105 | /* Completeness */
106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
108 | }
109 | }
110 |
--------------------------------------------------------------------------------