├── .npmrc ├── src ├── bot │ ├── controllers │ │ └── .gitkeep │ ├── types │ │ ├── sessionData.ts │ │ └── botContext.ts │ └── index.ts ├── repositories │ └── .gitkeep ├── web │ ├── controllers │ │ └── .gitkeep │ └── index.ts ├── redis.ts ├── prisma.ts ├── configs │ ├── getEnv.ts │ └── index.ts └── main.ts ├── .eslintrc.js ├── prisma └── schema.prisma ├── .env.example ├── .prettierrc.json ├── README.md ├── package.json ├── .gitignore └── tsconfig.json /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true -------------------------------------------------------------------------------- /src/bot/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/repositories/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/bot/types/sessionData.ts: -------------------------------------------------------------------------------- 1 | export interface SessionData {} 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@fullstacksjs'], 3 | }; 4 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | -------------------------------------------------------------------------------- /src/redis.ts: -------------------------------------------------------------------------------- 1 | import IORedis from 'ioredis'; 2 | 3 | import { getConfigs } from './configs'; 4 | 5 | const redis = new IORedis(getConfigs().redisUri); 6 | 7 | export default redis; 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV = development | production 2 | BOT_TOKEN = telegram bot token 3 | TELEGRAM_API_BASE_URL = https://api.telegram.org 4 | DATABASE_URL = 5 | REDIS_URI = redis://[user]:[password]@[host]:[port]/[db] -------------------------------------------------------------------------------- /src/web/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const app = express(); 4 | 5 | app.use(express.json({ limit: '100KB' })); 6 | app.use(express.urlencoded({ extended: true, limit: '100KB' })); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | async function connect() { 6 | await prisma.$connect(); 7 | } 8 | 9 | export { connect }; 10 | export default prisma; 11 | -------------------------------------------------------------------------------- /src/configs/getEnv.ts: -------------------------------------------------------------------------------- 1 | export default function getEnv(name: string, defaultValue?: string): string { 2 | const value = process.env[name]; 3 | if (value != null) return value; 4 | if (defaultValue != null) return defaultValue; 5 | throw new Error(`Environment ${name} is required.`); 6 | } 7 | -------------------------------------------------------------------------------- /src/bot/types/botContext.ts: -------------------------------------------------------------------------------- 1 | import { ConversationFlavor } from '@grammyjs/conversations'; 2 | import { Context, SessionFlavor } from 'grammy'; 3 | 4 | import { SessionData } from './sessionData'; 5 | 6 | export type BotContext = Context & 7 | SessionFlavor & 8 | ConversationFlavor; 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed", 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "endOfLine": "lf" 12 | } 13 | -------------------------------------------------------------------------------- /src/configs/index.ts: -------------------------------------------------------------------------------- 1 | import getEnv from './getEnv'; 2 | 3 | const getConfigs = () => ({ 4 | isDevelopment: getEnv('NODE_ENV', 'development') === 'development', 5 | port: getEnv('port', '3000'), 6 | applicationBaseUrL: getEnv('APPLICATION_BASE_URL', 'https://localhost'), 7 | botToken: getEnv('BOT_TOKEN'), 8 | telegramApiBaseUrl: getEnv('TELEGRAM_API_BASE_URL'), 9 | redisUri: getEnv('REDIS_URI'), 10 | }); 11 | 12 | export { getConfigs }; 13 | -------------------------------------------------------------------------------- /src/bot/index.ts: -------------------------------------------------------------------------------- 1 | import { conversations } from '@grammyjs/conversations'; 2 | import { RedisAdapter } from '@grammyjs/storage-redis'; 3 | import { Bot, session } from 'grammy'; 4 | 5 | import { getConfigs } from '../configs'; 6 | import redis from '../redis'; 7 | import { BotContext } from './types/botContext'; 8 | import { SessionData } from './types/sessionData'; 9 | 10 | const configs = getConfigs(); 11 | 12 | const bot = new Bot(configs.botToken, { 13 | client: { 14 | apiRoot: configs.telegramApiBaseUrl, 15 | }, 16 | }); 17 | 18 | const redisSessionStorage = new RedisAdapter({ 19 | instance: redis, 20 | }); 21 | bot.use( 22 | session({ 23 | initial: () => ({}), 24 | storage: redisSessionStorage, 25 | }), 26 | ); 27 | bot.use(conversations()); 28 | 29 | export default bot; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-telegram-bot-starter-pack 2 | 3 | ## How to use 4 | 5 | 1. clone the repo 6 | 2. run `npm install` 7 | 3. run `cp .env.example .env` and replace the variables with your data 8 | 4. define your database schema in prisma/schema.prisma 9 | 5. run `npm run prisma generate` to generate prisma client 10 | 6. run `npm run prisma db push` to sync database with schema 11 | 7. add your bot controllers in src/bot/controllers/ directory and register them inside src/bot/index.ts file 12 | 8. add your web controllers in src/web/controllers/ directory and register them in src/web/index.ts 13 | 9. run `npm run start:dev` to run the project and watch for changes to restart the server 14 | 15 | ## Questions may you have 🤔 16 | 17 | 1. why grammy? it's just like telegraf but with awesome documentation and support. 18 | 2. why prisma? awesome documentation, very easy to use, multi dialect, and great typing so why not? 19 | 20 | ## Deployment 21 | 22 | 1. make sure you set the APPLICATION_BASE_URL environment variable to your domain which webhook will be set on that 23 | 2. set NODE_ENV to production 24 | 3. start the project 25 | 26 | ## Contribution 27 | 28 | 1. fork the repo 29 | 2. create a branch with the kebab-case name which describes what you working on 30 | 3. apply your changes 31 | 4. commit with conventional commits rules 32 | 5. submit a PR 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import dotenv from 'dotenv'; 3 | import { IpFilter as ipFilter } from 'express-ipfilter'; 4 | import { webhookCallback } from 'grammy'; 5 | import path from 'path'; 6 | 7 | import { getConfigs } from './configs'; 8 | 9 | dotenv.config({ path: path.resolve(__dirname, '..', '.env') }); 10 | 11 | const configs = getConfigs(); 12 | 13 | import bot from './bot'; 14 | import { connect } from './prisma'; 15 | import app from './web'; 16 | 17 | async function main() { 18 | await connect(); 19 | console.log('database connected.'); 20 | 21 | if (configs.isDevelopment) { 22 | bot.start(); 23 | } else { 24 | const { data } = await axios.get( 25 | 'https://core.telegram.org/resources/cidr.txt', 26 | ); 27 | 28 | const telegramValidIps: string[] = data 29 | .split('\n') 30 | .map((ip: string) => ip.trim()) 31 | .filter((ip: string) => ip); 32 | 33 | app.use( 34 | '/bot', 35 | ipFilter(telegramValidIps, { mode: 'allow' }), 36 | webhookCallback(bot, 'express'), 37 | ); 38 | await bot.api.setWebhook(`${configs.applicationBaseUrL}/bot`); 39 | console.log(`webhook set to ${configs.applicationBaseUrL}/bot`); 40 | } 41 | 42 | app.listen(Number(configs.port), () => { 43 | console.log(`server is up and running on port ${configs.port}`); 44 | }); 45 | } 46 | 47 | main(); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-telegram-bot-starter-pack", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:dev": "nodemon src/main.ts", 8 | "build": "tsc", 9 | "start": "npm run build && node dist/main.js", 10 | "prisma": "prisma", 11 | "format": "prettier --ignore-path .gitignore --write .", 12 | "format:check": "prettier --ignore-path .gitignore --check .", 13 | "lint": "eslint --ignore-path .gitignore .", 14 | "lint:fix": "eslint --ignore-path .gitignore --fix ." 15 | }, 16 | "keywords": [], 17 | "author": "Mohammad MohammadAlian ", 18 | "license": "MIT", 19 | "volta": { 20 | "node": "20.15.0" 21 | }, 22 | "dependencies": { 23 | "@grammyjs/conversations": "2.1.0", 24 | "@grammyjs/storage-redis": "2.5.1", 25 | "@prisma/client": "6.12.0", 26 | "axios": "1.11.0", 27 | "dotenv": "17.2.1", 28 | "express": "5.1.0", 29 | "express-ipfilter": "1.3.2", 30 | "grammy": "1.37.0", 31 | "ioredis": "5.6.1" 32 | }, 33 | "devDependencies": { 34 | "@fullstacksjs/eslint-config": "13.2.2", 35 | "@types/express": "5.0.3", 36 | "@types/ioredis": "5.0.0", 37 | "@types/node": "24.1.0", 38 | "eslint": "9.32.0", 39 | "nodemon": "3.1.10", 40 | "prettier": "3.6.2", 41 | "prisma": "6.12.0", 42 | "ts-node": "10.9.2", 43 | "typescript": "5.8.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.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.* -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 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": "es2016" /* 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 TC39 stage 2 draft 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 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | "rootDir": "./src" /* Specify the root folder within your source files. */, 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "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. */ 50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | --------------------------------------------------------------------------------