├── .env.example ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── api └── server.ts ├── locales ├── en.ftl └── uk.ftl ├── package-lock.json ├── package.json ├── scripts └── start.ts ├── src ├── bot │ ├── callback-data │ │ ├── change-language.ts │ │ └── index.ts │ ├── context.ts │ ├── features │ │ ├── bot-admin.ts │ │ ├── index.ts │ │ ├── inline-mode.ts │ │ ├── language.ts │ │ ├── unhandled.ts │ │ └── welcome.ts │ ├── filters │ │ ├── index.ts │ │ └── is-bot-admin.ts │ ├── handlers │ │ ├── commands │ │ │ └── setcommands.ts │ │ ├── error.ts │ │ └── index.ts │ ├── helpers │ │ ├── keyboard.ts │ │ └── logging.ts │ ├── i18n.ts │ ├── index.ts │ ├── keyboards │ │ ├── change-language.ts │ │ └── index.ts │ └── middlewares │ │ ├── index.ts │ │ └── update-logger.ts ├── config.ts ├── logger.ts └── server │ └── index.ts ├── tsconfig.json ├── vercel.json └── webapp ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── link.svg │ └── telegram.svg ├── components │ ├── QrScanner.vue │ └── ScanResult.vue ├── composable │ └── useI18n.ts ├── helpers │ └── url.ts ├── locales │ ├── en.ts │ └── uk.ts ├── main.ts ├── style.css └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | BOT_TOKEN=123:ABCABCD 3 | BOT_WEBHOOK=https://www.example.com/ 4 | LOG_LEVEL=debug 5 | BOT_SERVER_HOST=0.0.0.0 6 | BOT_SERVER_PORT=80 7 | BOT_ALLOWED_UPDATES=[] 8 | BOT_ADMIN_USER_ID=1 9 | WEBAPP_URL= 10 | API_URL= 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:unicorn/recommended", 10 | "plugin:prettier/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "project": [ 15 | "tsconfig.json" 16 | ] 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint", 20 | "prettier", 21 | "unicorn", 22 | "import" 23 | ], 24 | "rules": { 25 | "@typescript-eslint/no-unused-vars": [ 26 | "error", 27 | { 28 | "varsIgnorePattern": "^_", 29 | "argsIgnorePattern": "^_" 30 | } 31 | ], 32 | "arrow-body-style": "off", 33 | "prefer-arrow-callback": "off", 34 | "consistent-return": "off", 35 | "import/prefer-default-export": "off", 36 | "import/extensions": "off", 37 | "no-underscore-dangle": "off", 38 | "prettier/prettier": [ 39 | "error", 40 | { 41 | "trailingComma": "all" 42 | } 43 | ], 44 | "unicorn/no-array-method-this-argument": "off", 45 | "unicorn/no-array-callback-reference": "off", 46 | "unicorn/prevent-abbreviations": [ 47 | "error", 48 | { 49 | "replacements": { 50 | "ctx": false 51 | } 52 | } 53 | ] 54 | }, 55 | "settings": { 56 | "import/parsers": { 57 | "@typescript-eslint/parser": [".ts"] 58 | }, 59 | "import/resolver": { 60 | "typescript": { 61 | "extensions": [".js", ".ts"] 62 | } 63 | } 64 | }, 65 | "ignorePatterns": [ 66 | "webapp/" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | groups: 8 | dependencies: 9 | patterns: 10 | - "*" -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run lint 25 | - run: npm run typecheck -------------------------------------------------------------------------------- /.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 build 49 | build 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 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 | 132 | # Data 133 | data/ 134 | 135 | *.env 136 | 137 | # Jetbrains IDE 138 | .idea/ 139 | 140 | # Ignore SQLite database 141 | *.db 142 | *.db-journal -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "macabeus.vscode-fluent", 5 | "rvest.vs-code-prettier-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "eslint.codeActionsOnSave.mode": "problems", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🤖 Scan Tool Bot

2 | 3 | 4 | 5 |

6 | Open in Telegram 7 |
or 8 |
9 | just type @ScanToolBot in message input field 10 | 11 |

12 | 13 | ## Features 14 | 15 | - Scan QR codes with a camera 16 | - Generate QR codes 17 | - Works in any chat via inline mode 18 | 19 | ## Launch 20 | 21 | 1. Close repository: 22 | 23 | ```sh 24 | git clone git@github.com:bot-base/scan-tool-bot.git 25 | ``` 26 | 27 | 2. Create an environment variables file: 28 | 29 | ```bash 30 | cp .env.example .env 31 | ``` 32 | 33 | 3. Launch web app following the instructions in [webapp/README.md](webapp/README.md). 34 | 35 | 4. Set BOT_TOKEN, WEBAPP_URL, API_URL [environment variables](#environment-variables) in `.env` file. 36 | 37 | 5. Launch bot 38 | 39 | Development mode: 40 | 41 | ```bash 42 | # 1. Install dependencies 43 | npm i 44 | 45 | # 2. Set BOT_SERVER_HOST to localhost 46 | # Set BOT_SERVER_PORT to any available port 47 | 48 | # 2. Run bot (in watch mode) 49 | npm run dev 50 | ``` 51 | 52 | Production mode: 53 | 54 | ```bash 55 | # 1. Install dependencies 56 | npm i --only=prod 57 | 58 | # 2. Set NODE_ENV to production 59 | # Change BOT_WEBHOOK to the actual URL to receive updates 60 | 61 | # 3. Run bot 62 | npm start 63 | # or 64 | npm run start:force # if you want to skip type checking 65 | ``` 66 | 67 | ### List of available commands 68 | - `npm run lint` — Lint source code. 69 | - `npm run format` — Format source code. 70 | - `npm run typecheck` — Runs type checking. 71 | - `npm run dev` — Starts the bot in development mode. 72 | - `npm run start` — Starts the bot. 73 | - `npm run start:force` — Starts the bot without type checking. 74 | 75 | ## Environment Variables 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 99 | 100 | 101 | 102 | 105 | 108 | 109 | 110 | 111 | 114 | 117 | 118 | 119 | 120 | 123 | 126 | 127 | 128 | 129 | 132 | 138 | 139 | 140 | 141 | 144 | 148 | 149 | 150 | 151 | 154 | 158 | 159 | 160 | 161 | 164 | 168 | 169 | 170 | 171 | 174 | 178 | 179 | 180 |
VariableTypeDescription
NODE_ENVStringApplication environment (development or production)
BOT_TOKEN 94 | String 95 | 97 | Token, get it from @BotFather. 98 |
BOT_WEBHOOK 103 | String 104 | 106 | Webhook endpoint, used to configure webhook in production environment. 107 |
WEBAPP_URL 112 | String 113 | 115 | HTTPS link to Web App. 116 |
API_URL 121 | String 122 | 124 | `/api` endpoint (must be public and available to Telegram) 125 |
LOG_LEVEL 130 | String 131 | 133 | Optional. 134 | Application log level. 135 | See Pino docs for a complete list of available log levels.
136 | Defaults to info. 137 |
BOT_SERVER_HOST 142 | String 143 | 145 | Optional. Server address.
146 | Defaults to 0.0.0.0. 147 |
BOT_SERVER_PORT 152 | Number 153 | 155 | Optional. Server port.
156 | Defaults to 80. 157 |
BOT_ALLOWED_UPDATES 162 | Array of String 163 | 165 | Optional. A JSON-serialized list of the update types you want your bot to receive. See Update for a complete list of available update types.
166 | Defaults to an empty array (all update types except chat_member). 167 |
BOT_ADMIN_USER_ID 172 | Number or
Array of Number 173 |
175 | Optional. Administrator user ID. Commands such as /setcommands will only be available to a user with this ID.
176 | Defaults to an empty array. 177 |
181 | -------------------------------------------------------------------------------- /api/server.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | import { createBot } from "#root/bot/index.js"; 3 | import { config } from "#root/config.js"; 4 | import { createServer } from "#root/server/index.js"; 5 | 6 | const bot = createBot(config.BOT_TOKEN); 7 | const server = await createServer(bot); 8 | 9 | export default async (request: IncomingMessage, response: ServerResponse) => { 10 | await server.ready(); 11 | server.server.emit("request", request, response); 12 | }; 13 | -------------------------------------------------------------------------------- /locales/en.ftl: -------------------------------------------------------------------------------- 1 | start_command = 2 | .description = Start the bot 3 | language_command = 4 | .description = Change language 5 | setcommands_command = 6 | .description = Set bot commands 7 | 8 | welcome = 9 | .message = Welcome! 10 | .scan_qr_button = Scan QR 11 | .generate_qr_button = Generate QR 12 | inline_mode = 13 | .scan_qr_button = Open QR Scanner 14 | .result_label = Send result 15 | language = 16 | .select = Please, select your language 17 | .changed = Language successfully changed! 18 | admin = 19 | .commands-updated = Commands updated. 20 | unhandled = Unrecognized command. Try /start -------------------------------------------------------------------------------- /locales/uk.ftl: -------------------------------------------------------------------------------- 1 | start_command = 2 | .description = Запустити бота 3 | language_command = 4 | .description = Змінити мову 5 | setcommands_command = 6 | .description = Налаштувати команди бота 7 | 8 | welcome = 9 | .message = Вітаю! 10 | .scan_qr_button = Сканувати QR 11 | .generate_qr_button = Створити QR 12 | inline_mode = 13 | .scan_qr_button = Відкрити QR-сканер 14 | .result_label = Надіслати результат 15 | language = 16 | .select = Будь ласка, оберіть вашу мову 17 | .changed = Мова успішно змінена! 18 | admin = 19 | .commands-updated = Команди оновлено. 20 | unhandled = Невідома команда. Спробуйте /start -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegram-bot-template", 3 | "version": "0.1.0", 4 | "description": "Telegram bot starter template", 5 | "type": "module", 6 | "imports": { 7 | "#root/*": "./build/src/*" 8 | }, 9 | "scripts": { 10 | "lint": "eslint .", 11 | "format": "eslint . --fix", 12 | "typecheck": "tsc", 13 | "build": "tsc --noEmit false", 14 | "dev": "tsc-watch --onSuccess \"tsx ./scripts/start.ts\"", 15 | "start": "tsc && tsx ./scripts/start.ts", 16 | "start:force": "tsx ./scripts/start.ts", 17 | "prepare": "npx husky install" 18 | }, 19 | "author": "deptyped ", 20 | "license": "MIT", 21 | "private": true, 22 | "dependencies": { 23 | "@grammyjs/auto-chat-action": "0.1.1", 24 | "@grammyjs/hydrate": "1.3.1", 25 | "@grammyjs/i18n": "1.0.1", 26 | "@grammyjs/parse-mode": "1.7.1", 27 | "@grammyjs/types": "3.1.3", 28 | "callback-data": "1.0.2", 29 | "dotenv": "16.3.1", 30 | "fastify": "4.21.0", 31 | "grammy": "1.17.2", 32 | "grammy-guard": "0.5.0", 33 | "iso-639-1": "2.1.15", 34 | "node-graceful-shutdown": "1.1.5", 35 | "pino": "8.15.0", 36 | "pino-pretty": "10.2.0", 37 | "qrcode": "1.5.3", 38 | "tsx": "3.12.7", 39 | "zod": "3.22.1" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "20.5.0", 43 | "@types/qrcode": "^1.5.1", 44 | "@typescript-eslint/eslint-plugin": "6.4.0", 45 | "@typescript-eslint/parser": "6.4.0", 46 | "eslint": "8.47.0", 47 | "eslint-config-airbnb-base": "15.0.0", 48 | "eslint-config-prettier": "9.0.0", 49 | "eslint-import-resolver-typescript": "3.6.0", 50 | "eslint-plugin-import": "2.28.0", 51 | "eslint-plugin-prettier": "5.0.0", 52 | "eslint-plugin-unicorn": "48.0.1", 53 | "husky": "8.0.3", 54 | "lint-staged": "14.0.0", 55 | "prettier": "3.0.2", 56 | "prettier-plugin-organize-imports": "3.2.3", 57 | "tsc-watch": "6.0.4", 58 | "typescript": "5.1.6" 59 | }, 60 | "engines": { 61 | "node": ">=18.0.0", 62 | "npm": ">=8.0.0" 63 | }, 64 | "lint-staged": { 65 | "*.ts": "npm run lint" 66 | } 67 | } -------------------------------------------------------------------------------- /scripts/start.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import { onShutdown } from "node-graceful-shutdown"; 4 | import { createBot } from "#root/bot/index.js"; 5 | import { config } from "#root/config.js"; 6 | import { logger } from "#root/logger.js"; 7 | import { createServer } from "#root/server/index.js"; 8 | 9 | try { 10 | const bot = createBot(config.BOT_TOKEN); 11 | const server = await createServer(bot); 12 | 13 | // Graceful shutdown 14 | onShutdown(async () => { 15 | logger.info("shutdown"); 16 | 17 | await server.close(); 18 | await bot.stop(); 19 | }); 20 | 21 | await server.listen({ 22 | host: config.BOT_SERVER_HOST, 23 | port: config.BOT_SERVER_PORT, 24 | }); 25 | 26 | if (config.isProd) { 27 | // to prevent receiving updates before the bot is ready 28 | await bot.init(); 29 | 30 | await bot.api.setWebhook(config.BOT_WEBHOOK, { 31 | allowed_updates: config.BOT_ALLOWED_UPDATES, 32 | }); 33 | } else if (config.isDev) { 34 | await bot.start({ 35 | allowed_updates: config.BOT_ALLOWED_UPDATES, 36 | onStart: ({ username }) => 37 | logger.info({ 38 | msg: "bot running...", 39 | username, 40 | }), 41 | }); 42 | } 43 | } catch (error) { 44 | logger.error(error); 45 | process.exit(1); 46 | } 47 | -------------------------------------------------------------------------------- /src/bot/callback-data/change-language.ts: -------------------------------------------------------------------------------- 1 | import { createCallbackData } from "callback-data"; 2 | 3 | export const changeLanguageData = createCallbackData("language", { 4 | code: String, 5 | }); 6 | -------------------------------------------------------------------------------- /src/bot/callback-data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./change-language.js"; 2 | -------------------------------------------------------------------------------- /src/bot/context.ts: -------------------------------------------------------------------------------- 1 | import { Update, UserFromGetMe } from "@grammyjs/types"; 2 | import { Context as DefaultContext, SessionFlavor, type Api } from "grammy"; 3 | import type { AutoChatActionFlavor } from "@grammyjs/auto-chat-action"; 4 | import type { HydrateFlavor } from "@grammyjs/hydrate"; 5 | import type { I18nFlavor } from "@grammyjs/i18n"; 6 | import type { ParseModeFlavor } from "@grammyjs/parse-mode"; 7 | import type { Logger } from "#root/logger.js"; 8 | 9 | export type SessionData = { 10 | // field?: string; 11 | }; 12 | 13 | type ExtendedContextFlavor = { 14 | logger: Logger; 15 | }; 16 | 17 | export type Context = ParseModeFlavor< 18 | HydrateFlavor< 19 | DefaultContext & 20 | ExtendedContextFlavor & 21 | SessionFlavor & 22 | I18nFlavor & 23 | AutoChatActionFlavor 24 | > 25 | >; 26 | 27 | interface Dependencies { 28 | logger: Logger; 29 | } 30 | 31 | export function createContextConstructor({ logger }: Dependencies) { 32 | return class extends DefaultContext implements ExtendedContextFlavor { 33 | logger: Logger; 34 | 35 | constructor(update: Update, api: Api, me: UserFromGetMe) { 36 | super(update, api, me); 37 | 38 | this.logger = logger.child({ 39 | update_id: this.update.update_id, 40 | }); 41 | } 42 | } as unknown as new (update: Update, api: Api, me: UserFromGetMe) => Context; 43 | } 44 | -------------------------------------------------------------------------------- /src/bot/features/bot-admin.ts: -------------------------------------------------------------------------------- 1 | import { chatAction } from "@grammyjs/auto-chat-action"; 2 | import { Composer } from "grammy"; 3 | import type { Context } from "#root/bot/context.js"; 4 | import { isBotAdmin } from "#root/bot/filters/index.js"; 5 | import { setCommandsHandler } from "#root/bot/handlers/index.js"; 6 | import { logHandle } from "#root/bot/helpers/logging.js"; 7 | 8 | const composer = new Composer(); 9 | 10 | const feature = composer.chatType("private").filter(isBotAdmin); 11 | 12 | feature.command( 13 | "setcommands", 14 | logHandle("command-setcommands"), 15 | chatAction("typing"), 16 | setCommandsHandler, 17 | ); 18 | 19 | export { composer as botAdminFeature }; 20 | -------------------------------------------------------------------------------- /src/bot/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bot-admin.js"; 2 | export * from "./inline-mode.js"; 3 | export * from "./language.js"; 4 | export * from "./unhandled.js"; 5 | export * from "./welcome.js"; 6 | -------------------------------------------------------------------------------- /src/bot/features/inline-mode.ts: -------------------------------------------------------------------------------- 1 | import { Composer } from "grammy"; 2 | import type { InlineQueryResultsButton } from "@grammyjs/types"; 3 | import type { Context } from "#root/bot/context.js"; 4 | import { logHandle } from "#root/bot/helpers/logging.js"; 5 | import { config } from "#root/config.js"; 6 | 7 | const composer = new Composer(); 8 | 9 | const feature = composer; 10 | 11 | const CACHE_TIME = config.isDev ? { cache_time: 0 } : {}; 12 | 13 | function getOpenScannerButton(ctx: Context): InlineQueryResultsButton { 14 | return { 15 | text: ctx.t("inline_mode.scan_qr_button"), 16 | web_app: { 17 | url: config.WEBAPP_URL, 18 | }, 19 | }; 20 | } 21 | 22 | // handle as a scan result if starts with "# " 23 | feature.inlineQuery(/^#\s/, logHandle("inline-mode-scan-result"), (ctx) => { 24 | const [_prefix, result] = ctx.inlineQuery.query.split("# "); 25 | 26 | return ctx.answerInlineQuery( 27 | result.length > 0 28 | ? [ 29 | { 30 | id: "result", 31 | type: "article", 32 | title: ctx.t("inline_mode.result_label"), 33 | description: result, 34 | input_message_content: { 35 | message_text: result, 36 | }, 37 | }, 38 | ] 39 | : [], 40 | { 41 | button: getOpenScannerButton(ctx), 42 | ...CACHE_TIME, 43 | }, 44 | ); 45 | }); 46 | 47 | feature.on( 48 | "inline_query", 49 | logHandle("inline-mode-generate-qr"), 50 | async (ctx) => { 51 | const { query } = ctx.inlineQuery; 52 | const qrCodeUrl = `${config.API_URL}/qr-code?data=${encodeURIComponent( 53 | query, 54 | )}`; 55 | 56 | return ctx.answerInlineQuery( 57 | query.length > 0 58 | ? [ 59 | { 60 | id: "result", 61 | type: "photo", 62 | photo_url: qrCodeUrl, 63 | thumbnail_url: qrCodeUrl, 64 | photo_width: 256, 65 | photo_height: 256, 66 | }, 67 | ] 68 | : [], 69 | { 70 | button: getOpenScannerButton(ctx), 71 | ...CACHE_TIME, 72 | }, 73 | ); 74 | }, 75 | ); 76 | 77 | export { composer as inlineModeFeature }; 78 | -------------------------------------------------------------------------------- /src/bot/features/language.ts: -------------------------------------------------------------------------------- 1 | import { Composer } from "grammy"; 2 | import { changeLanguageData } from "#root/bot/callback-data/index.js"; 3 | import type { Context } from "#root/bot/context.js"; 4 | import { logHandle } from "#root/bot/helpers/logging.js"; 5 | import { i18n } from "#root/bot/i18n.js"; 6 | import { createChangeLanguageKeyboard } from "#root/bot/keyboards/index.js"; 7 | 8 | const composer = new Composer(); 9 | 10 | const feature = composer.chatType("private"); 11 | 12 | feature.command("language", logHandle("command-language"), async (ctx) => { 13 | return ctx.reply(ctx.t("language.select"), { 14 | reply_markup: await createChangeLanguageKeyboard(ctx), 15 | }); 16 | }); 17 | 18 | feature.callbackQuery( 19 | changeLanguageData.filter(), 20 | logHandle("keyboard-language-select"), 21 | async (ctx) => { 22 | const { code: languageCode } = changeLanguageData.unpack( 23 | ctx.callbackQuery.data, 24 | ); 25 | 26 | if (i18n.locales.includes(languageCode)) { 27 | await ctx.i18n.setLocale(languageCode); 28 | 29 | return ctx.editMessageText(ctx.t("language.changed"), { 30 | reply_markup: await createChangeLanguageKeyboard(ctx), 31 | }); 32 | } 33 | }, 34 | ); 35 | 36 | export { composer as languageFeature }; 37 | -------------------------------------------------------------------------------- /src/bot/features/unhandled.ts: -------------------------------------------------------------------------------- 1 | import { Composer } from "grammy"; 2 | import type { Context } from "#root/bot/context.js"; 3 | import { logHandle } from "#root/bot/helpers/logging.js"; 4 | 5 | const composer = new Composer(); 6 | 7 | const feature = composer.chatType("private"); 8 | 9 | // ignore message when user generates QR code in chat with bot 10 | feature.on("message").filter( 11 | (ctx) => ctx.message?.via_bot?.id === ctx.me.id, 12 | logHandle("via-bot-ignore"), 13 | () => {}, 14 | ); 15 | 16 | feature.on("message", logHandle("unhandled-message"), (ctx) => { 17 | return ctx.reply(ctx.t("unhandled")); 18 | }); 19 | 20 | feature.on("callback_query", logHandle("unhandled-callback-query"), (ctx) => { 21 | return ctx.answerCallbackQuery(); 22 | }); 23 | 24 | export { composer as unhandledFeature }; 25 | -------------------------------------------------------------------------------- /src/bot/features/welcome.ts: -------------------------------------------------------------------------------- 1 | import { Composer, InlineKeyboard } from "grammy"; 2 | import type { Context } from "#root/bot/context.js"; 3 | import { logHandle } from "#root/bot/helpers/logging.js"; 4 | import { config } from "#root/config.js"; 5 | 6 | const composer = new Composer(); 7 | 8 | const feature = composer.chatType("private"); 9 | 10 | feature.command("start", logHandle("command-start"), (ctx) => { 11 | return ctx.reply(ctx.t("welcome.message"), { 12 | reply_markup: new InlineKeyboard() 13 | .webApp(ctx.t("welcome.scan_qr_button"), config.WEBAPP_URL) 14 | .switchInlineCurrent(ctx.t("welcome.generate_qr_button")), 15 | }); 16 | }); 17 | 18 | export { composer as welcomeFeature }; 19 | -------------------------------------------------------------------------------- /src/bot/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./is-bot-admin.js"; 2 | -------------------------------------------------------------------------------- /src/bot/filters/is-bot-admin.ts: -------------------------------------------------------------------------------- 1 | import { isUserHasId } from "grammy-guard"; 2 | import { config } from "#root/config.js"; 3 | 4 | export const isBotAdmin = isUserHasId(...config.BOT_ADMIN_USER_ID); 5 | -------------------------------------------------------------------------------- /src/bot/handlers/commands/setcommands.ts: -------------------------------------------------------------------------------- 1 | import { BotCommand } from "@grammyjs/types"; 2 | import { CommandContext } from "grammy"; 3 | import { i18n, isMultipleLocales } from "#root/bot/i18n.js"; 4 | import { config } from "#root/config.js"; 5 | import type { Context } from "#root/bot/context.js"; 6 | 7 | // function getLanguageCommand(localeCode: string): BotCommand { 8 | // return { 9 | // command: "language", 10 | // description: i18n.t(localeCode, "language_command.description"), 11 | // }; 12 | // } 13 | 14 | function getPrivateChatCommands(localeCode: string): BotCommand[] { 15 | return [ 16 | { 17 | command: "start", 18 | description: i18n.t(localeCode, "start_command.description"), 19 | }, 20 | ]; 21 | } 22 | 23 | function getPrivateChatAdminCommands(localeCode: string): BotCommand[] { 24 | return [ 25 | { 26 | command: "setcommands", 27 | description: i18n.t(localeCode, "setcommands_command.description"), 28 | }, 29 | ]; 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | function getGroupChatCommands(localeCode: string): BotCommand[] { 34 | return []; 35 | } 36 | 37 | export async function setCommandsHandler(ctx: CommandContext) { 38 | const DEFAULT_LANGUAGE_CODE = "en"; 39 | 40 | // set private chat commands 41 | await ctx.api.setMyCommands( 42 | [ 43 | ...getPrivateChatCommands(DEFAULT_LANGUAGE_CODE), 44 | // ...(isMultipleLocales ? [getLanguageCommand(DEFAULT_LANGUAGE_CODE)] : []), 45 | ], 46 | { 47 | scope: { 48 | type: "all_private_chats", 49 | }, 50 | }, 51 | ); 52 | 53 | if (isMultipleLocales) { 54 | const requests = i18n.locales.map((code) => 55 | ctx.api.setMyCommands( 56 | [ 57 | ...getPrivateChatCommands(code), 58 | // ...(isMultipleLocales 59 | // ? [getLanguageCommand(DEFAULT_LANGUAGE_CODE)] 60 | // : []), 61 | ], 62 | { 63 | language_code: code, 64 | scope: { 65 | type: "all_private_chats", 66 | }, 67 | }, 68 | ), 69 | ); 70 | 71 | await Promise.all(requests); 72 | } 73 | 74 | // set group chat commands 75 | await ctx.api.setMyCommands(getGroupChatCommands(DEFAULT_LANGUAGE_CODE), { 76 | scope: { 77 | type: "all_group_chats", 78 | }, 79 | }); 80 | 81 | if (isMultipleLocales) { 82 | const requests = i18n.locales.map((code) => 83 | ctx.api.setMyCommands(getGroupChatCommands(code), { 84 | language_code: code, 85 | scope: { 86 | type: "all_group_chats", 87 | }, 88 | }), 89 | ); 90 | 91 | await Promise.all(requests); 92 | } 93 | 94 | // set private chat commands for owner 95 | await ctx.api.setMyCommands( 96 | [ 97 | ...getPrivateChatCommands(DEFAULT_LANGUAGE_CODE), 98 | ...getPrivateChatAdminCommands(DEFAULT_LANGUAGE_CODE), 99 | // ...(isMultipleLocales ? [getLanguageCommand(DEFAULT_LANGUAGE_CODE)] : []), 100 | ], 101 | { 102 | scope: { 103 | type: "chat", 104 | chat_id: Number(config.BOT_ADMIN_USER_ID), 105 | }, 106 | }, 107 | ); 108 | 109 | return ctx.reply(ctx.t("admin.commands-updated")); 110 | } 111 | -------------------------------------------------------------------------------- /src/bot/handlers/error.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from "grammy"; 2 | import type { Context } from "#root/bot/context.js"; 3 | import { getUpdateInfo } from "#root/bot/helpers/logging.js"; 4 | 5 | export const errorHandler: ErrorHandler = (error) => { 6 | const { ctx } = error; 7 | 8 | ctx.logger.error({ 9 | err: error.error, 10 | update: getUpdateInfo(ctx), 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/bot/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error.js"; 2 | export * from "./commands/setcommands.js"; 3 | -------------------------------------------------------------------------------- /src/bot/helpers/keyboard.ts: -------------------------------------------------------------------------------- 1 | export function chunk(array: T[], size: number) { 2 | const result = []; 3 | for (let index = 0; index < array.length; index += size) { 4 | result.push(array.slice(index, index + size)); 5 | } 6 | return result; 7 | } 8 | -------------------------------------------------------------------------------- /src/bot/helpers/logging.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | import type { Update } from "@grammyjs/types"; 3 | import type { Context } from "#root/bot/context.js"; 4 | 5 | export function getUpdateInfo(ctx: Context): Omit { 6 | // eslint-disable-next-line camelcase, @typescript-eslint/no-unused-vars 7 | const { update_id, ...update } = ctx.update; 8 | 9 | return update; 10 | } 11 | 12 | export function logHandle(id: string): Middleware { 13 | return (ctx, next) => { 14 | ctx.logger.info({ 15 | msg: `handle ${id}`, 16 | ...(id.startsWith("unhandled") ? { update: getUpdateInfo(ctx) } : {}), 17 | }); 18 | 19 | return next(); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/bot/i18n.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { I18n } from "@grammyjs/i18n"; 3 | import type { Context } from "#root/bot/context.js"; 4 | 5 | export const i18n = new I18n({ 6 | defaultLocale: "en", 7 | directory: path.resolve(process.cwd(), "locales"), 8 | fluentBundleOptions: { 9 | useIsolating: false, 10 | }, 11 | }); 12 | 13 | export const isMultipleLocales = i18n.locales.length > 1; 14 | -------------------------------------------------------------------------------- /src/bot/index.ts: -------------------------------------------------------------------------------- 1 | import { autoChatAction } from "@grammyjs/auto-chat-action"; 2 | import { hydrate } from "@grammyjs/hydrate"; 3 | import { hydrateReply, parseMode } from "@grammyjs/parse-mode"; 4 | import { BotConfig, Bot as TelegramBot } from "grammy"; 5 | import { Context, createContextConstructor } from "#root/bot/context.js"; 6 | import { 7 | botAdminFeature, 8 | inlineModeFeature, 9 | unhandledFeature, 10 | welcomeFeature, 11 | } from "#root/bot/features/index.js"; 12 | import { errorHandler } from "#root/bot/handlers/index.js"; 13 | import { i18n } from "#root/bot/i18n.js"; 14 | import { updateLogger } from "#root/bot/middlewares/index.js"; 15 | import { config } from "#root/config.js"; 16 | import { logger } from "#root/logger.js"; 17 | 18 | type Options = { 19 | config?: Omit, "ContextConstructor">; 20 | }; 21 | 22 | export function createBot(token: string, options: Options = {}) { 23 | const bot = new TelegramBot(token, { 24 | ...options.config, 25 | ContextConstructor: createContextConstructor({ logger }), 26 | }); 27 | 28 | // Middlewares 29 | bot.api.config.use(parseMode("HTML")); 30 | 31 | if (config.isDev) { 32 | bot.use(updateLogger()); 33 | } 34 | 35 | bot.use(autoChatAction(bot.api)); 36 | bot.use(hydrateReply); 37 | bot.use(hydrate()); 38 | bot.use(i18n); 39 | 40 | // Handlers 41 | bot.use(welcomeFeature); 42 | bot.use(botAdminFeature); 43 | bot.use(inlineModeFeature); 44 | 45 | // session is not supported 46 | // if (isMultipleLocales) { 47 | // bot.use(languageFeature); 48 | // } 49 | 50 | // must be the last handler 51 | bot.use(unhandledFeature); 52 | 53 | if (config.isDev) { 54 | bot.catch(errorHandler); 55 | } 56 | 57 | return bot; 58 | } 59 | 60 | export type Bot = ReturnType; 61 | -------------------------------------------------------------------------------- /src/bot/keyboards/change-language.ts: -------------------------------------------------------------------------------- 1 | import { InlineKeyboard } from "grammy"; 2 | import ISO6391 from "iso-639-1"; 3 | import { changeLanguageData } from "#root/bot/callback-data/index.js"; 4 | import type { Context } from "#root/bot/context.js"; 5 | import { i18n } from "#root/bot/i18n.js"; 6 | import { chunk } from "#root/bot/helpers/keyboard.js"; 7 | 8 | export const createChangeLanguageKeyboard = async (ctx: Context) => { 9 | const currentLocaleCode = await ctx.i18n.getLocale(); 10 | 11 | const getLabel = (code: string) => { 12 | const isActive = code === currentLocaleCode; 13 | 14 | return `${isActive ? "✅ " : ""}${ISO6391.getNativeName(code)}`; 15 | }; 16 | 17 | return InlineKeyboard.from( 18 | chunk( 19 | i18n.locales.map((localeCode) => ({ 20 | text: getLabel(localeCode), 21 | callback_data: changeLanguageData.pack({ 22 | code: localeCode, 23 | }), 24 | })), 25 | 2, 26 | ), 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/bot/keyboards/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./change-language.js"; 2 | -------------------------------------------------------------------------------- /src/bot/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./update-logger.js"; 2 | -------------------------------------------------------------------------------- /src/bot/middlewares/update-logger.ts: -------------------------------------------------------------------------------- 1 | import { performance } from "node:perf_hooks"; 2 | import { Middleware } from "grammy"; 3 | import type { Context } from "#root/bot/context.js"; 4 | import { getUpdateInfo } from "#root/bot/helpers/logging.js"; 5 | 6 | export function updateLogger(): Middleware { 7 | return async (ctx, next) => { 8 | ctx.api.config.use((previous, method, payload, signal) => { 9 | ctx.logger.debug({ 10 | msg: "bot api call", 11 | method, 12 | payload, 13 | }); 14 | 15 | return previous(method, payload, signal); 16 | }); 17 | 18 | ctx.logger.debug({ 19 | msg: "update received", 20 | update: getUpdateInfo(ctx), 21 | }); 22 | 23 | const startTime = performance.now(); 24 | try { 25 | await next(); 26 | } finally { 27 | const endTime = performance.now(); 28 | ctx.logger.debug({ 29 | msg: "update processed", 30 | duration: endTime - startTime, 31 | }); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import z, { ZodError, ZodIssueCode } from "zod"; 3 | import { API_CONSTANTS } from "grammy"; 4 | 5 | function parseJsonSafe(path: string) { 6 | return (value: unknown) => { 7 | try { 8 | return JSON.parse(String(value)); 9 | } catch { 10 | throw new ZodError([ 11 | { 12 | code: ZodIssueCode.custom, 13 | path: [path], 14 | fatal: true, 15 | message: "Invalid JSON", 16 | }, 17 | ]); 18 | } 19 | }; 20 | } 21 | 22 | const configSchema = z.object({ 23 | NODE_ENV: z.enum(["development", "production"]), 24 | LOG_LEVEL: z 25 | .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]) 26 | .default("info"), 27 | BOT_SERVER_HOST: z.string().default("0.0.0.0"), 28 | BOT_SERVER_PORT: z.coerce.number().positive().default(80), 29 | BOT_ALLOWED_UPDATES: z 30 | .preprocess( 31 | parseJsonSafe("BOT_ALLOWED_UPDATES"), 32 | z.array(z.enum(API_CONSTANTS.ALL_UPDATE_TYPES)), 33 | ) 34 | .default([]), 35 | BOT_TOKEN: z.string(), 36 | BOT_WEBHOOK: z.string().url(), 37 | BOT_ADMIN_USER_ID: z 38 | .preprocess( 39 | parseJsonSafe("BOT_ADMIN_USER_ID"), 40 | z.array(z.coerce.number().safe()).or(z.coerce.number().safe()), 41 | ) 42 | .transform((v) => (Array.isArray(v) ? v : [v])) 43 | .default([]), 44 | WEBAPP_URL: z.string().url(), 45 | API_URL: z.string().url(), 46 | }); 47 | 48 | const parseConfig = (environment: NodeJS.ProcessEnv) => { 49 | const config = configSchema.parse(environment); 50 | 51 | return { 52 | ...config, 53 | isDev: process.env.NODE_ENV === "development", 54 | isProd: process.env.NODE_ENV === "production", 55 | }; 56 | }; 57 | 58 | export type Config = ReturnType; 59 | 60 | export const config = parseConfig(process.env); 61 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { pino } from "pino"; 2 | import { config } from "#root/config.js"; 3 | 4 | export const logger = pino({ 5 | level: config.LOG_LEVEL, 6 | transport: { 7 | targets: [ 8 | ...(config.isDev 9 | ? [ 10 | { 11 | target: "pino-pretty", 12 | level: config.LOG_LEVEL, 13 | options: { 14 | ignore: "pid,hostname", 15 | colorize: true, 16 | translateTime: true, 17 | }, 18 | }, 19 | ] 20 | : [ 21 | { 22 | target: "pino/file", 23 | level: config.LOG_LEVEL, 24 | options: {}, 25 | }, 26 | ]), 27 | ], 28 | }, 29 | }); 30 | 31 | export type Logger = typeof logger; 32 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import fastify from "fastify"; 2 | import { BotError, webhookCallback } from "grammy"; 3 | import QRCode from "qrcode"; 4 | import type { Bot } from "#root/bot/index.js"; 5 | import { errorHandler } from "#root/bot/handlers/index.js"; 6 | import { logger } from "#root/logger.js"; 7 | import { config } from "#root/config.js"; 8 | 9 | export const createServer = async (bot: Bot) => { 10 | const server = fastify({ 11 | logger, 12 | }); 13 | 14 | server.setErrorHandler(async (error, request, response) => { 15 | if (error instanceof BotError) { 16 | errorHandler(error); 17 | 18 | await response.code(200).send({}); 19 | } else { 20 | logger.error(error); 21 | 22 | await response.status(500).send({ error: "Oops! Something went wrong." }); 23 | } 24 | }); 25 | 26 | server.get("/", () => ({ status: true })); 27 | 28 | server.get(`/${bot.token}`, async (request, response) => { 29 | const hostname = request.headers["x-forwarded-host"]; 30 | if (typeof hostname === "string") { 31 | const webhookUrl = new URL(bot.token, `https://${hostname}`).href; 32 | await bot.api.setWebhook(webhookUrl, { 33 | allowed_updates: config.BOT_ALLOWED_UPDATES, 34 | }); 35 | await response.send({ 36 | status: true, 37 | }); 38 | } else { 39 | await response.status(500).send({ 40 | status: false, 41 | }); 42 | } 43 | }); 44 | 45 | server.get("/api/qr-code", async (request, response) => { 46 | const { data } = request.query as { data: string | undefined }; 47 | 48 | if (typeof data === "string") { 49 | const qrCodeDataUrl = await QRCode.toDataURL(data, { 50 | type: "image/jpeg", 51 | width: 256, 52 | maskPattern: 2, 53 | margin: 2, 54 | }); 55 | const qrCode = Buffer.from(qrCodeDataUrl.split(",")[1], "base64"); 56 | 57 | await response 58 | .type("image/jpeg") 59 | .header("Content-Disposition", "inline") 60 | .send(qrCode); 61 | } else { 62 | await response.status(400).send({ 63 | error: "Missing data", 64 | }); 65 | } 66 | }); 67 | 68 | server.post(`/${bot.token}`, webhookCallback(bot, "fastify")); 69 | 70 | return server; 71 | }; 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "skipLibCheck": true, 5 | "esModuleInterop": true, 6 | "preserveWatchOutput": true, 7 | "noEmit": true, 8 | "module": "NodeNext", 9 | "target": "ES2021", 10 | "moduleResolution": "NodeNext", 11 | "sourceMap": true, 12 | "outDir": "build", 13 | "rootDir": ".", 14 | "paths": { 15 | "#root/*": [ 16 | "./src/*" 17 | ] 18 | } 19 | }, 20 | "include": [ 21 | "api/**/*", 22 | "src/**/*", 23 | "scripts/**/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "outputDirectory": "build", 3 | "routes": [ 4 | { 5 | "src": "/.*", 6 | "dest": "/api/server.ts" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /webapp/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /webapp/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ### Development mode 4 | 5 | 1. Install dependencies: 6 | 7 | ```sh 8 | npm i 9 | ``` 10 | 11 | 2. Run the app in the development mode: 12 | 13 | ```sh 14 | npm run dev 15 | ``` 16 | 17 | The page will reload if you make edits. 18 | 19 | 3. Update the `WEBAPP_URL` [environment variable](../README.md#environment-variables) 20 | 21 | Since Telegram only accepts HTTPS URLs for Web Apps, you'll need to use a tunneling software like [serveo](https://serveo.net) or [ngrok](https://ngrok.com) ([list of tunnelling software and services](https://github.com/anderspitman/awesome-tunneling#readme)): 22 | 23 | ```sh 24 | # serveo usage example (no client required) 25 | ssh -R 80:localhost:5173 serveo.net 26 | 27 | # ngrok usage example 28 | ngrok http 5173 29 | ``` 30 | 31 | > Note: `5173` is default port. If the port differs from `5173`, change it accordingly. 32 | 33 | Set the [environment variable](../README.md#environment-variables) `WEBAPP_URL` to obtained link. 34 | 35 | ### Production mode 36 | 37 | 1. Install dependencies: 38 | 39 | ```sh 40 | npm i 41 | ``` 42 | 43 | 2. Build the app for production to the `dist` folder: 44 | 45 | ```sh 46 | npm run build 47 | ``` 48 | 49 | 3. Deploy the `dist` folder to any static hosting provider. 50 | 51 | 4. Set the [environment variable](../README.md#environment-variables) `WEBAPP_URL` to your link. 52 | 53 | # Vue 3 + TypeScript + Vite 54 | 55 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /webapp/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "webapp", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "vue": "^3.3.4" 12 | }, 13 | "devDependencies": { 14 | "@types/telegram-web-app": "^6.7.0", 15 | "@vitejs/plugin-vue": "^4.2.3", 16 | "typescript": "^5.1.6", 17 | "vite": "^4.4.9", 18 | "vite-svg-loader": "^4.0.0", 19 | "vue-tg": "^0.0.1", 20 | "vue-tsc": "^1.8.8" 21 | } 22 | }, 23 | "node_modules/@babel/parser": { 24 | "version": "7.22.10", 25 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", 26 | "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", 27 | "bin": { 28 | "parser": "bin/babel-parser.js" 29 | }, 30 | "engines": { 31 | "node": ">=6.0.0" 32 | } 33 | }, 34 | "node_modules/@esbuild/android-arm": { 35 | "version": "0.18.20", 36 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", 37 | "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", 38 | "cpu": [ 39 | "arm" 40 | ], 41 | "dev": true, 42 | "optional": true, 43 | "os": [ 44 | "android" 45 | ], 46 | "engines": { 47 | "node": ">=12" 48 | } 49 | }, 50 | "node_modules/@esbuild/android-arm64": { 51 | "version": "0.18.20", 52 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", 53 | "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", 54 | "cpu": [ 55 | "arm64" 56 | ], 57 | "dev": true, 58 | "optional": true, 59 | "os": [ 60 | "android" 61 | ], 62 | "engines": { 63 | "node": ">=12" 64 | } 65 | }, 66 | "node_modules/@esbuild/android-x64": { 67 | "version": "0.18.20", 68 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", 69 | "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", 70 | "cpu": [ 71 | "x64" 72 | ], 73 | "dev": true, 74 | "optional": true, 75 | "os": [ 76 | "android" 77 | ], 78 | "engines": { 79 | "node": ">=12" 80 | } 81 | }, 82 | "node_modules/@esbuild/darwin-arm64": { 83 | "version": "0.18.20", 84 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", 85 | "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", 86 | "cpu": [ 87 | "arm64" 88 | ], 89 | "dev": true, 90 | "optional": true, 91 | "os": [ 92 | "darwin" 93 | ], 94 | "engines": { 95 | "node": ">=12" 96 | } 97 | }, 98 | "node_modules/@esbuild/darwin-x64": { 99 | "version": "0.18.20", 100 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", 101 | "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", 102 | "cpu": [ 103 | "x64" 104 | ], 105 | "dev": true, 106 | "optional": true, 107 | "os": [ 108 | "darwin" 109 | ], 110 | "engines": { 111 | "node": ">=12" 112 | } 113 | }, 114 | "node_modules/@esbuild/freebsd-arm64": { 115 | "version": "0.18.20", 116 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", 117 | "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", 118 | "cpu": [ 119 | "arm64" 120 | ], 121 | "dev": true, 122 | "optional": true, 123 | "os": [ 124 | "freebsd" 125 | ], 126 | "engines": { 127 | "node": ">=12" 128 | } 129 | }, 130 | "node_modules/@esbuild/freebsd-x64": { 131 | "version": "0.18.20", 132 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", 133 | "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", 134 | "cpu": [ 135 | "x64" 136 | ], 137 | "dev": true, 138 | "optional": true, 139 | "os": [ 140 | "freebsd" 141 | ], 142 | "engines": { 143 | "node": ">=12" 144 | } 145 | }, 146 | "node_modules/@esbuild/linux-arm": { 147 | "version": "0.18.20", 148 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", 149 | "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", 150 | "cpu": [ 151 | "arm" 152 | ], 153 | "dev": true, 154 | "optional": true, 155 | "os": [ 156 | "linux" 157 | ], 158 | "engines": { 159 | "node": ">=12" 160 | } 161 | }, 162 | "node_modules/@esbuild/linux-arm64": { 163 | "version": "0.18.20", 164 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", 165 | "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", 166 | "cpu": [ 167 | "arm64" 168 | ], 169 | "dev": true, 170 | "optional": true, 171 | "os": [ 172 | "linux" 173 | ], 174 | "engines": { 175 | "node": ">=12" 176 | } 177 | }, 178 | "node_modules/@esbuild/linux-ia32": { 179 | "version": "0.18.20", 180 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", 181 | "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", 182 | "cpu": [ 183 | "ia32" 184 | ], 185 | "dev": true, 186 | "optional": true, 187 | "os": [ 188 | "linux" 189 | ], 190 | "engines": { 191 | "node": ">=12" 192 | } 193 | }, 194 | "node_modules/@esbuild/linux-loong64": { 195 | "version": "0.18.20", 196 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", 197 | "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", 198 | "cpu": [ 199 | "loong64" 200 | ], 201 | "dev": true, 202 | "optional": true, 203 | "os": [ 204 | "linux" 205 | ], 206 | "engines": { 207 | "node": ">=12" 208 | } 209 | }, 210 | "node_modules/@esbuild/linux-mips64el": { 211 | "version": "0.18.20", 212 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", 213 | "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", 214 | "cpu": [ 215 | "mips64el" 216 | ], 217 | "dev": true, 218 | "optional": true, 219 | "os": [ 220 | "linux" 221 | ], 222 | "engines": { 223 | "node": ">=12" 224 | } 225 | }, 226 | "node_modules/@esbuild/linux-ppc64": { 227 | "version": "0.18.20", 228 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", 229 | "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", 230 | "cpu": [ 231 | "ppc64" 232 | ], 233 | "dev": true, 234 | "optional": true, 235 | "os": [ 236 | "linux" 237 | ], 238 | "engines": { 239 | "node": ">=12" 240 | } 241 | }, 242 | "node_modules/@esbuild/linux-riscv64": { 243 | "version": "0.18.20", 244 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", 245 | "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", 246 | "cpu": [ 247 | "riscv64" 248 | ], 249 | "dev": true, 250 | "optional": true, 251 | "os": [ 252 | "linux" 253 | ], 254 | "engines": { 255 | "node": ">=12" 256 | } 257 | }, 258 | "node_modules/@esbuild/linux-s390x": { 259 | "version": "0.18.20", 260 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", 261 | "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", 262 | "cpu": [ 263 | "s390x" 264 | ], 265 | "dev": true, 266 | "optional": true, 267 | "os": [ 268 | "linux" 269 | ], 270 | "engines": { 271 | "node": ">=12" 272 | } 273 | }, 274 | "node_modules/@esbuild/linux-x64": { 275 | "version": "0.18.20", 276 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", 277 | "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", 278 | "cpu": [ 279 | "x64" 280 | ], 281 | "dev": true, 282 | "optional": true, 283 | "os": [ 284 | "linux" 285 | ], 286 | "engines": { 287 | "node": ">=12" 288 | } 289 | }, 290 | "node_modules/@esbuild/netbsd-x64": { 291 | "version": "0.18.20", 292 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", 293 | "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", 294 | "cpu": [ 295 | "x64" 296 | ], 297 | "dev": true, 298 | "optional": true, 299 | "os": [ 300 | "netbsd" 301 | ], 302 | "engines": { 303 | "node": ">=12" 304 | } 305 | }, 306 | "node_modules/@esbuild/openbsd-x64": { 307 | "version": "0.18.20", 308 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", 309 | "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", 310 | "cpu": [ 311 | "x64" 312 | ], 313 | "dev": true, 314 | "optional": true, 315 | "os": [ 316 | "openbsd" 317 | ], 318 | "engines": { 319 | "node": ">=12" 320 | } 321 | }, 322 | "node_modules/@esbuild/sunos-x64": { 323 | "version": "0.18.20", 324 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", 325 | "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", 326 | "cpu": [ 327 | "x64" 328 | ], 329 | "dev": true, 330 | "optional": true, 331 | "os": [ 332 | "sunos" 333 | ], 334 | "engines": { 335 | "node": ">=12" 336 | } 337 | }, 338 | "node_modules/@esbuild/win32-arm64": { 339 | "version": "0.18.20", 340 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", 341 | "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", 342 | "cpu": [ 343 | "arm64" 344 | ], 345 | "dev": true, 346 | "optional": true, 347 | "os": [ 348 | "win32" 349 | ], 350 | "engines": { 351 | "node": ">=12" 352 | } 353 | }, 354 | "node_modules/@esbuild/win32-ia32": { 355 | "version": "0.18.20", 356 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", 357 | "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", 358 | "cpu": [ 359 | "ia32" 360 | ], 361 | "dev": true, 362 | "optional": true, 363 | "os": [ 364 | "win32" 365 | ], 366 | "engines": { 367 | "node": ">=12" 368 | } 369 | }, 370 | "node_modules/@esbuild/win32-x64": { 371 | "version": "0.18.20", 372 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", 373 | "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", 374 | "cpu": [ 375 | "x64" 376 | ], 377 | "dev": true, 378 | "optional": true, 379 | "os": [ 380 | "win32" 381 | ], 382 | "engines": { 383 | "node": ">=12" 384 | } 385 | }, 386 | "node_modules/@jridgewell/sourcemap-codec": { 387 | "version": "1.4.15", 388 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 389 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" 390 | }, 391 | "node_modules/@trysound/sax": { 392 | "version": "0.2.0", 393 | "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", 394 | "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", 395 | "dev": true, 396 | "engines": { 397 | "node": ">=10.13.0" 398 | } 399 | }, 400 | "node_modules/@types/telegram-web-app": { 401 | "version": "6.7.0", 402 | "resolved": "https://registry.npmjs.org/@types/telegram-web-app/-/telegram-web-app-6.7.0.tgz", 403 | "integrity": "sha512-ill31jBCQW6bo4k0GjsUVNKP0MY336p7ZUxmMi+WofAbO7Reb366jsnqNIrqNtxcIB4kKEpMUHVniArcXSML6g==", 404 | "dev": true 405 | }, 406 | "node_modules/@vitejs/plugin-vue": { 407 | "version": "4.2.3", 408 | "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", 409 | "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", 410 | "dev": true, 411 | "engines": { 412 | "node": "^14.18.0 || >=16.0.0" 413 | }, 414 | "peerDependencies": { 415 | "vite": "^4.0.0", 416 | "vue": "^3.2.25" 417 | } 418 | }, 419 | "node_modules/@volar/language-core": { 420 | "version": "1.10.0", 421 | "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.0.tgz", 422 | "integrity": "sha512-ddyWwSYqcbEZNFHm+Z3NZd6M7Ihjcwl/9B5cZd8kECdimVXUFdFi60XHWD27nrWtUQIsUYIG7Ca1WBwV2u2LSQ==", 423 | "dev": true, 424 | "dependencies": { 425 | "@volar/source-map": "1.10.0" 426 | } 427 | }, 428 | "node_modules/@volar/source-map": { 429 | "version": "1.10.0", 430 | "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.10.0.tgz", 431 | "integrity": "sha512-/ibWdcOzDGiq/GM1JU2eX8fH1bvAhl66hfe8yEgLEzg9txgr6qb5sQ/DEz5PcDL75tF5H5sCRRwn8Eu8ezi9mw==", 432 | "dev": true, 433 | "dependencies": { 434 | "muggle-string": "^0.3.1" 435 | } 436 | }, 437 | "node_modules/@volar/typescript": { 438 | "version": "1.10.0", 439 | "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.10.0.tgz", 440 | "integrity": "sha512-OtqGtFbUKYC0pLNIk3mHQp5xWnvL1CJIUc9VE39VdZ/oqpoBh5jKfb9uJ45Y4/oP/WYTrif/Uxl1k8VTPz66Gg==", 441 | "dev": true, 442 | "dependencies": { 443 | "@volar/language-core": "1.10.0" 444 | } 445 | }, 446 | "node_modules/@vue/compiler-core": { 447 | "version": "3.3.4", 448 | "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", 449 | "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", 450 | "dependencies": { 451 | "@babel/parser": "^7.21.3", 452 | "@vue/shared": "3.3.4", 453 | "estree-walker": "^2.0.2", 454 | "source-map-js": "^1.0.2" 455 | } 456 | }, 457 | "node_modules/@vue/compiler-dom": { 458 | "version": "3.3.4", 459 | "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", 460 | "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", 461 | "dependencies": { 462 | "@vue/compiler-core": "3.3.4", 463 | "@vue/shared": "3.3.4" 464 | } 465 | }, 466 | "node_modules/@vue/compiler-sfc": { 467 | "version": "3.3.4", 468 | "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", 469 | "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", 470 | "dependencies": { 471 | "@babel/parser": "^7.20.15", 472 | "@vue/compiler-core": "3.3.4", 473 | "@vue/compiler-dom": "3.3.4", 474 | "@vue/compiler-ssr": "3.3.4", 475 | "@vue/reactivity-transform": "3.3.4", 476 | "@vue/shared": "3.3.4", 477 | "estree-walker": "^2.0.2", 478 | "magic-string": "^0.30.0", 479 | "postcss": "^8.1.10", 480 | "source-map-js": "^1.0.2" 481 | } 482 | }, 483 | "node_modules/@vue/compiler-ssr": { 484 | "version": "3.3.4", 485 | "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", 486 | "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", 487 | "dependencies": { 488 | "@vue/compiler-dom": "3.3.4", 489 | "@vue/shared": "3.3.4" 490 | } 491 | }, 492 | "node_modules/@vue/language-core": { 493 | "version": "1.8.8", 494 | "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.8.tgz", 495 | "integrity": "sha512-i4KMTuPazf48yMdYoebTkgSOJdFraE4pQf0B+FTOFkbB+6hAfjrSou/UmYWRsWyZV6r4Rc6DDZdI39CJwL0rWw==", 496 | "dev": true, 497 | "dependencies": { 498 | "@volar/language-core": "~1.10.0", 499 | "@volar/source-map": "~1.10.0", 500 | "@vue/compiler-dom": "^3.3.0", 501 | "@vue/reactivity": "^3.3.0", 502 | "@vue/shared": "^3.3.0", 503 | "minimatch": "^9.0.0", 504 | "muggle-string": "^0.3.1", 505 | "vue-template-compiler": "^2.7.14" 506 | }, 507 | "peerDependencies": { 508 | "typescript": "*" 509 | }, 510 | "peerDependenciesMeta": { 511 | "typescript": { 512 | "optional": true 513 | } 514 | } 515 | }, 516 | "node_modules/@vue/reactivity": { 517 | "version": "3.3.4", 518 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", 519 | "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", 520 | "dependencies": { 521 | "@vue/shared": "3.3.4" 522 | } 523 | }, 524 | "node_modules/@vue/reactivity-transform": { 525 | "version": "3.3.4", 526 | "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", 527 | "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", 528 | "dependencies": { 529 | "@babel/parser": "^7.20.15", 530 | "@vue/compiler-core": "3.3.4", 531 | "@vue/shared": "3.3.4", 532 | "estree-walker": "^2.0.2", 533 | "magic-string": "^0.30.0" 534 | } 535 | }, 536 | "node_modules/@vue/runtime-core": { 537 | "version": "3.3.4", 538 | "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", 539 | "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", 540 | "dependencies": { 541 | "@vue/reactivity": "3.3.4", 542 | "@vue/shared": "3.3.4" 543 | } 544 | }, 545 | "node_modules/@vue/runtime-dom": { 546 | "version": "3.3.4", 547 | "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", 548 | "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", 549 | "dependencies": { 550 | "@vue/runtime-core": "3.3.4", 551 | "@vue/shared": "3.3.4", 552 | "csstype": "^3.1.1" 553 | } 554 | }, 555 | "node_modules/@vue/server-renderer": { 556 | "version": "3.3.4", 557 | "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", 558 | "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", 559 | "dependencies": { 560 | "@vue/compiler-ssr": "3.3.4", 561 | "@vue/shared": "3.3.4" 562 | }, 563 | "peerDependencies": { 564 | "vue": "3.3.4" 565 | } 566 | }, 567 | "node_modules/@vue/shared": { 568 | "version": "3.3.4", 569 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", 570 | "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" 571 | }, 572 | "node_modules/@vue/typescript": { 573 | "version": "1.8.8", 574 | "resolved": "https://registry.npmjs.org/@vue/typescript/-/typescript-1.8.8.tgz", 575 | "integrity": "sha512-jUnmMB6egu5wl342eaUH236v8tdcEPXXkPgj+eI/F6JwW/lb+yAU6U07ZbQ3MVabZRlupIlPESB7ajgAGixhow==", 576 | "dev": true, 577 | "dependencies": { 578 | "@volar/typescript": "~1.10.0", 579 | "@vue/language-core": "1.8.8" 580 | } 581 | }, 582 | "node_modules/balanced-match": { 583 | "version": "1.0.2", 584 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 585 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 586 | "dev": true 587 | }, 588 | "node_modules/boolbase": { 589 | "version": "1.0.0", 590 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 591 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 592 | "dev": true 593 | }, 594 | "node_modules/brace-expansion": { 595 | "version": "2.0.1", 596 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 597 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 598 | "dev": true, 599 | "dependencies": { 600 | "balanced-match": "^1.0.0" 601 | } 602 | }, 603 | "node_modules/commander": { 604 | "version": "7.2.0", 605 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 606 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 607 | "dev": true, 608 | "engines": { 609 | "node": ">= 10" 610 | } 611 | }, 612 | "node_modules/css-select": { 613 | "version": "5.1.0", 614 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 615 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 616 | "dev": true, 617 | "dependencies": { 618 | "boolbase": "^1.0.0", 619 | "css-what": "^6.1.0", 620 | "domhandler": "^5.0.2", 621 | "domutils": "^3.0.1", 622 | "nth-check": "^2.0.1" 623 | }, 624 | "funding": { 625 | "url": "https://github.com/sponsors/fb55" 626 | } 627 | }, 628 | "node_modules/css-tree": { 629 | "version": "2.3.1", 630 | "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", 631 | "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", 632 | "dev": true, 633 | "dependencies": { 634 | "mdn-data": "2.0.30", 635 | "source-map-js": "^1.0.1" 636 | }, 637 | "engines": { 638 | "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" 639 | } 640 | }, 641 | "node_modules/css-what": { 642 | "version": "6.1.0", 643 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 644 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 645 | "dev": true, 646 | "engines": { 647 | "node": ">= 6" 648 | }, 649 | "funding": { 650 | "url": "https://github.com/sponsors/fb55" 651 | } 652 | }, 653 | "node_modules/csso": { 654 | "version": "5.0.5", 655 | "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", 656 | "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", 657 | "dev": true, 658 | "dependencies": { 659 | "css-tree": "~2.2.0" 660 | }, 661 | "engines": { 662 | "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", 663 | "npm": ">=7.0.0" 664 | } 665 | }, 666 | "node_modules/csso/node_modules/css-tree": { 667 | "version": "2.2.1", 668 | "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", 669 | "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", 670 | "dev": true, 671 | "dependencies": { 672 | "mdn-data": "2.0.28", 673 | "source-map-js": "^1.0.1" 674 | }, 675 | "engines": { 676 | "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", 677 | "npm": ">=7.0.0" 678 | } 679 | }, 680 | "node_modules/csso/node_modules/mdn-data": { 681 | "version": "2.0.28", 682 | "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", 683 | "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", 684 | "dev": true 685 | }, 686 | "node_modules/csstype": { 687 | "version": "3.1.2", 688 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", 689 | "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" 690 | }, 691 | "node_modules/de-indent": { 692 | "version": "1.0.2", 693 | "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", 694 | "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", 695 | "dev": true 696 | }, 697 | "node_modules/dom-serializer": { 698 | "version": "2.0.0", 699 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 700 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 701 | "dev": true, 702 | "dependencies": { 703 | "domelementtype": "^2.3.0", 704 | "domhandler": "^5.0.2", 705 | "entities": "^4.2.0" 706 | }, 707 | "funding": { 708 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 709 | } 710 | }, 711 | "node_modules/domelementtype": { 712 | "version": "2.3.0", 713 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 714 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 715 | "dev": true, 716 | "funding": [ 717 | { 718 | "type": "github", 719 | "url": "https://github.com/sponsors/fb55" 720 | } 721 | ] 722 | }, 723 | "node_modules/domhandler": { 724 | "version": "5.0.3", 725 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 726 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 727 | "dev": true, 728 | "dependencies": { 729 | "domelementtype": "^2.3.0" 730 | }, 731 | "engines": { 732 | "node": ">= 4" 733 | }, 734 | "funding": { 735 | "url": "https://github.com/fb55/domhandler?sponsor=1" 736 | } 737 | }, 738 | "node_modules/domutils": { 739 | "version": "3.1.0", 740 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 741 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 742 | "dev": true, 743 | "dependencies": { 744 | "dom-serializer": "^2.0.0", 745 | "domelementtype": "^2.3.0", 746 | "domhandler": "^5.0.3" 747 | }, 748 | "funding": { 749 | "url": "https://github.com/fb55/domutils?sponsor=1" 750 | } 751 | }, 752 | "node_modules/entities": { 753 | "version": "4.5.0", 754 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 755 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 756 | "dev": true, 757 | "engines": { 758 | "node": ">=0.12" 759 | }, 760 | "funding": { 761 | "url": "https://github.com/fb55/entities?sponsor=1" 762 | } 763 | }, 764 | "node_modules/esbuild": { 765 | "version": "0.18.20", 766 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", 767 | "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", 768 | "dev": true, 769 | "hasInstallScript": true, 770 | "bin": { 771 | "esbuild": "bin/esbuild" 772 | }, 773 | "engines": { 774 | "node": ">=12" 775 | }, 776 | "optionalDependencies": { 777 | "@esbuild/android-arm": "0.18.20", 778 | "@esbuild/android-arm64": "0.18.20", 779 | "@esbuild/android-x64": "0.18.20", 780 | "@esbuild/darwin-arm64": "0.18.20", 781 | "@esbuild/darwin-x64": "0.18.20", 782 | "@esbuild/freebsd-arm64": "0.18.20", 783 | "@esbuild/freebsd-x64": "0.18.20", 784 | "@esbuild/linux-arm": "0.18.20", 785 | "@esbuild/linux-arm64": "0.18.20", 786 | "@esbuild/linux-ia32": "0.18.20", 787 | "@esbuild/linux-loong64": "0.18.20", 788 | "@esbuild/linux-mips64el": "0.18.20", 789 | "@esbuild/linux-ppc64": "0.18.20", 790 | "@esbuild/linux-riscv64": "0.18.20", 791 | "@esbuild/linux-s390x": "0.18.20", 792 | "@esbuild/linux-x64": "0.18.20", 793 | "@esbuild/netbsd-x64": "0.18.20", 794 | "@esbuild/openbsd-x64": "0.18.20", 795 | "@esbuild/sunos-x64": "0.18.20", 796 | "@esbuild/win32-arm64": "0.18.20", 797 | "@esbuild/win32-ia32": "0.18.20", 798 | "@esbuild/win32-x64": "0.18.20" 799 | } 800 | }, 801 | "node_modules/estree-walker": { 802 | "version": "2.0.2", 803 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 804 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" 805 | }, 806 | "node_modules/fsevents": { 807 | "version": "2.3.2", 808 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 809 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 810 | "dev": true, 811 | "hasInstallScript": true, 812 | "optional": true, 813 | "os": [ 814 | "darwin" 815 | ], 816 | "engines": { 817 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 818 | } 819 | }, 820 | "node_modules/he": { 821 | "version": "1.2.0", 822 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 823 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 824 | "dev": true, 825 | "bin": { 826 | "he": "bin/he" 827 | } 828 | }, 829 | "node_modules/lru-cache": { 830 | "version": "6.0.0", 831 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 832 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 833 | "dev": true, 834 | "dependencies": { 835 | "yallist": "^4.0.0" 836 | }, 837 | "engines": { 838 | "node": ">=10" 839 | } 840 | }, 841 | "node_modules/magic-string": { 842 | "version": "0.30.2", 843 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.2.tgz", 844 | "integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==", 845 | "dependencies": { 846 | "@jridgewell/sourcemap-codec": "^1.4.15" 847 | }, 848 | "engines": { 849 | "node": ">=12" 850 | } 851 | }, 852 | "node_modules/mdn-data": { 853 | "version": "2.0.30", 854 | "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", 855 | "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", 856 | "dev": true 857 | }, 858 | "node_modules/minimatch": { 859 | "version": "9.0.3", 860 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", 861 | "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", 862 | "dev": true, 863 | "dependencies": { 864 | "brace-expansion": "^2.0.1" 865 | }, 866 | "engines": { 867 | "node": ">=16 || 14 >=14.17" 868 | }, 869 | "funding": { 870 | "url": "https://github.com/sponsors/isaacs" 871 | } 872 | }, 873 | "node_modules/muggle-string": { 874 | "version": "0.3.1", 875 | "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", 876 | "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", 877 | "dev": true 878 | }, 879 | "node_modules/nanoid": { 880 | "version": "3.3.6", 881 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", 882 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", 883 | "funding": [ 884 | { 885 | "type": "github", 886 | "url": "https://github.com/sponsors/ai" 887 | } 888 | ], 889 | "bin": { 890 | "nanoid": "bin/nanoid.cjs" 891 | }, 892 | "engines": { 893 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 894 | } 895 | }, 896 | "node_modules/nth-check": { 897 | "version": "2.1.1", 898 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 899 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 900 | "dev": true, 901 | "dependencies": { 902 | "boolbase": "^1.0.0" 903 | }, 904 | "funding": { 905 | "url": "https://github.com/fb55/nth-check?sponsor=1" 906 | } 907 | }, 908 | "node_modules/picocolors": { 909 | "version": "1.0.0", 910 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 911 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 912 | }, 913 | "node_modules/postcss": { 914 | "version": "8.4.27", 915 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", 916 | "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", 917 | "funding": [ 918 | { 919 | "type": "opencollective", 920 | "url": "https://opencollective.com/postcss/" 921 | }, 922 | { 923 | "type": "tidelift", 924 | "url": "https://tidelift.com/funding/github/npm/postcss" 925 | }, 926 | { 927 | "type": "github", 928 | "url": "https://github.com/sponsors/ai" 929 | } 930 | ], 931 | "dependencies": { 932 | "nanoid": "^3.3.6", 933 | "picocolors": "^1.0.0", 934 | "source-map-js": "^1.0.2" 935 | }, 936 | "engines": { 937 | "node": "^10 || ^12 || >=14" 938 | } 939 | }, 940 | "node_modules/rollup": { 941 | "version": "3.28.0", 942 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.0.tgz", 943 | "integrity": "sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==", 944 | "dev": true, 945 | "bin": { 946 | "rollup": "dist/bin/rollup" 947 | }, 948 | "engines": { 949 | "node": ">=14.18.0", 950 | "npm": ">=8.0.0" 951 | }, 952 | "optionalDependencies": { 953 | "fsevents": "~2.3.2" 954 | } 955 | }, 956 | "node_modules/semver": { 957 | "version": "7.5.4", 958 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 959 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 960 | "dev": true, 961 | "dependencies": { 962 | "lru-cache": "^6.0.0" 963 | }, 964 | "bin": { 965 | "semver": "bin/semver.js" 966 | }, 967 | "engines": { 968 | "node": ">=10" 969 | } 970 | }, 971 | "node_modules/source-map-js": { 972 | "version": "1.0.2", 973 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 974 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 975 | "engines": { 976 | "node": ">=0.10.0" 977 | } 978 | }, 979 | "node_modules/svgo": { 980 | "version": "3.0.2", 981 | "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz", 982 | "integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==", 983 | "dev": true, 984 | "dependencies": { 985 | "@trysound/sax": "0.2.0", 986 | "commander": "^7.2.0", 987 | "css-select": "^5.1.0", 988 | "css-tree": "^2.2.1", 989 | "csso": "^5.0.5", 990 | "picocolors": "^1.0.0" 991 | }, 992 | "bin": { 993 | "svgo": "bin/svgo" 994 | }, 995 | "engines": { 996 | "node": ">=14.0.0" 997 | }, 998 | "funding": { 999 | "type": "opencollective", 1000 | "url": "https://opencollective.com/svgo" 1001 | } 1002 | }, 1003 | "node_modules/typescript": { 1004 | "version": "5.1.6", 1005 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 1006 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 1007 | "dev": true, 1008 | "bin": { 1009 | "tsc": "bin/tsc", 1010 | "tsserver": "bin/tsserver" 1011 | }, 1012 | "engines": { 1013 | "node": ">=14.17" 1014 | } 1015 | }, 1016 | "node_modules/vite": { 1017 | "version": "4.4.9", 1018 | "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", 1019 | "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", 1020 | "dev": true, 1021 | "dependencies": { 1022 | "esbuild": "^0.18.10", 1023 | "postcss": "^8.4.27", 1024 | "rollup": "^3.27.1" 1025 | }, 1026 | "bin": { 1027 | "vite": "bin/vite.js" 1028 | }, 1029 | "engines": { 1030 | "node": "^14.18.0 || >=16.0.0" 1031 | }, 1032 | "funding": { 1033 | "url": "https://github.com/vitejs/vite?sponsor=1" 1034 | }, 1035 | "optionalDependencies": { 1036 | "fsevents": "~2.3.2" 1037 | }, 1038 | "peerDependencies": { 1039 | "@types/node": ">= 14", 1040 | "less": "*", 1041 | "lightningcss": "^1.21.0", 1042 | "sass": "*", 1043 | "stylus": "*", 1044 | "sugarss": "*", 1045 | "terser": "^5.4.0" 1046 | }, 1047 | "peerDependenciesMeta": { 1048 | "@types/node": { 1049 | "optional": true 1050 | }, 1051 | "less": { 1052 | "optional": true 1053 | }, 1054 | "lightningcss": { 1055 | "optional": true 1056 | }, 1057 | "sass": { 1058 | "optional": true 1059 | }, 1060 | "stylus": { 1061 | "optional": true 1062 | }, 1063 | "sugarss": { 1064 | "optional": true 1065 | }, 1066 | "terser": { 1067 | "optional": true 1068 | } 1069 | } 1070 | }, 1071 | "node_modules/vite-svg-loader": { 1072 | "version": "4.0.0", 1073 | "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-4.0.0.tgz", 1074 | "integrity": "sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==", 1075 | "dev": true, 1076 | "dependencies": { 1077 | "@vue/compiler-sfc": "^3.2.20", 1078 | "svgo": "^3.0.2" 1079 | } 1080 | }, 1081 | "node_modules/vue": { 1082 | "version": "3.3.4", 1083 | "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", 1084 | "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", 1085 | "dependencies": { 1086 | "@vue/compiler-dom": "3.3.4", 1087 | "@vue/compiler-sfc": "3.3.4", 1088 | "@vue/runtime-dom": "3.3.4", 1089 | "@vue/server-renderer": "3.3.4", 1090 | "@vue/shared": "3.3.4" 1091 | } 1092 | }, 1093 | "node_modules/vue-template-compiler": { 1094 | "version": "2.7.14", 1095 | "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", 1096 | "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", 1097 | "dev": true, 1098 | "dependencies": { 1099 | "de-indent": "^1.0.2", 1100 | "he": "^1.2.0" 1101 | } 1102 | }, 1103 | "node_modules/vue-tg": { 1104 | "version": "0.0.1", 1105 | "resolved": "https://registry.npmjs.org/vue-tg/-/vue-tg-0.0.1.tgz", 1106 | "integrity": "sha512-HMK+mMtkl5Gqzshr9fh/sU8H6wGN5NEHC9UleXLHe5uGGh1w02a9QDu2ClfuYK3c16I6JIgW1Ck20a6S4uOwsw==", 1107 | "dev": true, 1108 | "dependencies": { 1109 | "vue": "^3" 1110 | } 1111 | }, 1112 | "node_modules/vue-tsc": { 1113 | "version": "1.8.8", 1114 | "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.8.tgz", 1115 | "integrity": "sha512-bSydNFQsF7AMvwWsRXD7cBIXaNs/KSjvzWLymq/UtKE36697sboX4EccSHFVxvgdBlI1frYPc/VMKJNB7DFeDQ==", 1116 | "dev": true, 1117 | "dependencies": { 1118 | "@vue/language-core": "1.8.8", 1119 | "@vue/typescript": "1.8.8", 1120 | "semver": "^7.3.8" 1121 | }, 1122 | "bin": { 1123 | "vue-tsc": "bin/vue-tsc.js" 1124 | }, 1125 | "peerDependencies": { 1126 | "typescript": "*" 1127 | } 1128 | }, 1129 | "node_modules/yallist": { 1130 | "version": "4.0.0", 1131 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1132 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 1133 | "dev": true 1134 | } 1135 | } 1136 | } 1137 | -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.3.4" 13 | }, 14 | "devDependencies": { 15 | "@types/telegram-web-app": "^6.7.0", 16 | "@vitejs/plugin-vue": "^4.2.3", 17 | "typescript": "^5.1.6", 18 | "vite": "^4.4.9", 19 | "vite-svg-loader": "^4.0.0", 20 | "vue-tg": "^0.0.1", 21 | "vue-tsc": "^1.8.8" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webapp/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /webapp/src/assets/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /webapp/src/assets/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webapp/src/components/QrScanner.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 47 | -------------------------------------------------------------------------------- /webapp/src/components/ScanResult.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 44 | 45 | -------------------------------------------------------------------------------- /webapp/src/composable/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue" 2 | import en from "../locales/en" 3 | 4 | let messages = reactive(en) 5 | 6 | export function useI18n () { 7 | return { 8 | messages, 9 | changeLocale (key: string) { 10 | import(`../locales/${key}.ts`) 11 | .then((data) => Object.assign(messages, data.default)) 12 | .catch(console.error) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /webapp/src/helpers/url.ts: -------------------------------------------------------------------------------- 1 | export function isValidUrl(value: string) { 2 | let url; 3 | 4 | try { 5 | url = new URL(value); 6 | } catch { 7 | return false; 8 | } 9 | 10 | return url.protocol === "http:" || url.protocol === "https:"; 11 | } -------------------------------------------------------------------------------- /webapp/src/locales/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | qrScanner: { 3 | openScanner: 'Open Scanner', 4 | }, 5 | scanResult: { 6 | openLink: 'Open link', 7 | send: 'Send', 8 | } 9 | } -------------------------------------------------------------------------------- /webapp/src/locales/uk.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | qrScanner: { 3 | openScanner: 'Відкрити сканер', 4 | }, 5 | scanResult: { 6 | openLink: 'Перейти за посиланням', 7 | send: 'Надіслати', 8 | } 9 | } -------------------------------------------------------------------------------- /webapp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /webapp/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: var(--tg-theme-link-color); 3 | --primary-hover: var(--tg-theme-button-color); 4 | } 5 | 6 | body { 7 | margin: 0; 8 | display: flex; 9 | place-items: start; 10 | min-width: 320px; 11 | min-height: 100vh; 12 | } 13 | -------------------------------------------------------------------------------- /webapp/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /webapp/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /webapp/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import svgLoader from 'vite-svg-loader' 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | vue({ 8 | script: { 9 | defineModel: true 10 | } 11 | }), 12 | svgLoader({ 13 | defaultImport: 'component', 14 | }), 15 | ], 16 | }) 17 | --------------------------------------------------------------------------------