first commit

This commit is contained in:
commit 93d13b613d
19 changed files with 7894 additions and 0 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
.git
.gitignore
.env
*.log
.DS_Store
README.md

9
.env.example Normal file
View File

@ -0,0 +1,9 @@
TELEGRAM_BOT_TOKEN=bot_token
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=karta_soup
NODE_ENV=production

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
dist
node_modules
.env
*.log
.DS_Store

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main"]

135
README.md Normal file
View File

@ -0,0 +1,135 @@
# Карта Суп Telegram Bot
Telegram бот для проверки баланса и истории транзакций карты Карта Суп.
## Возможности
- 💳 Сохранение кода карты для каждого пользователя
- ✍️ Ввод кода карты (13 цифр, начинается с 2001)
- 💰 Проверка баланса карты
- 📊 Просмотр последних транзакций
- 🔄 Изменение кода карты
- 📱 Удобное меню с кнопками
- 🛡️ Защита от спама (rate limiting)
- ✅ Валидация кода карты
## Технологии
- NestJS
- TypeScript
- Telegraf (Telegram Bot API)
- TypeORM
- PostgreSQL
- Docker
## Установка и запуск
### Локальная разработка
1. Установите зависимости:
```bash
npm install
```
2. Создайте файл `.env` на основе `.env.example`:
```bash
cp .env.example .env
```
3. Заполните переменные окружения в `.env`:
```env
TELEGRAM_BOT_TOKEN=your_bot_token_here
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=karta_soup
NODE_ENV=development
```
4. Запустите PostgreSQL (или используйте Docker):
```bash
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=karta_soup \
-p 5432:5432 \
postgres:15-alpine
```
5. Запустите приложение:
```bash
npm run start:dev
```
### Запуск с Docker
1. Создайте файл `.env`:
```bash
cp .env.example .env
```
2. Укажите токен бота в `.env`:
```env
TELEGRAM_BOT_TOKEN=your_bot_token_here
```
3. Запустите контейнеры:
```bash
docker-compose up -d
```
4. Проверьте логи:
```bash
docker-compose logs -f app
```
### Деплой на сервер
1. Скопируйте проект на сервер:
```bash
scp -r . user@your-server:/path/to/app
```
2. На сервере создайте `.env` файл с вашими настройками
3. Запустите:
```bash
docker-compose up -d
```
## Команды бота
- `/start` - Начать работу с ботом
- `/balance` - Проверить баланс
- `/changecode` - Изменить код карты
- `/help` - Показать справку
## Структура проекта
```
src/
├── bot/
│ ├── bot.module.ts # Модуль бота
│ └── bot.update.ts # Обработчики команд и сообщений
├── entities/
│ └── user.entity.ts # Сущность пользователя
├── interfaces/
│ └── balance.interface.ts # Интерфейсы для API
├── services/
│ ├── karta-soup.service.ts # Сервис для работы с API Карта Суп
│ └── user.service.ts # Сервис для работы с пользователями
├── app.module.ts # Главный модуль приложения
└── main.ts # Точка входа
```
## API Карта Суп
Бот использует публичный API для получения информации о балансе:
```
GET https://meal.gift-cards.ru/api/1/cards/{code}?limit=100
```
## Лицензия
MIT

43
docker-compose.yml Normal file
View File

@ -0,0 +1,43 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: karta-soup-db
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: karta_soup
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- karta-soup-network
app:
build:
context: .
dockerfile: Dockerfile
container_name: karta-soup-bot
restart: unless-stopped
environment:
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_USER: postgres
DATABASE_PASSWORD: postgres
DATABASE_NAME: karta_soup
NODE_ENV: production
depends_on:
- postgres
networks:
- karta-soup-network
ports:
- "3000:3000"
volumes:
postgres_data:
networks:
karta-soup-network:
driver: bridge

8
nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

6970
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "karta-soup-bot",
"version": "1.0.0",
"description": "Telegram bot for Карта Суп balance checking",
"author": "",
"private": true,
"license": "MIT",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"broadcast": "ts-node src/scripts/broadcast.ts"
},
"dependencies": {
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.0",
"@nestjs/platform-express": "^10.3.0",
"@nestjs/typeorm": "^10.0.1",
"axios": "^1.6.5",
"dotenv": "^16.3.1",
"nestjs-telegraf": "^2.7.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"telegraf": "^4.15.0",
"typeorm": "^0.3.19"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
"@nestjs/schematics": "^10.1.0",
"@types/express": "^4.17.21",
"@types/node": "^20.11.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.56.0",
"prettier": "^3.2.4",
"source-map-support": "^0.5.21",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
}
}

29
src/app.module.ts Normal file
View File

@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BotModule } from './bot/bot.module';
import { User } from './entities/user.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DATABASE_HOST'),
port: configService.get('DATABASE_PORT'),
username: configService.get('DATABASE_USER'),
password: configService.get('DATABASE_PASSWORD'),
database: configService.get('DATABASE_NAME'),
entities: [User],
synchronize: true,
}),
inject: [ConfigService],
}),
BotModule,
],
})
export class AppModule {}

23
src/bot/bot.module.ts Normal file
View File

@ -0,0 +1,23 @@
import { Module } from "@nestjs/common";
import { TelegrafModule } from "nestjs-telegraf";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { BotUpdate } from "./bot.update";
import { UserService } from "../services/user.service";
import { KartaSoupService } from "../services/karta-soup.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "../entities/user.entity";
@Module({
imports: [
TelegrafModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
token: configService.get<string>("TELEGRAM_BOT_TOKEN"),
}),
inject: [ConfigService],
}),
TypeOrmModule.forFeature([User]),
],
providers: [BotUpdate, UserService, KartaSoupService],
})
export class BotModule {}

262
src/bot/bot.update.ts Normal file
View File

@ -0,0 +1,262 @@
import { Update, Ctx, Start, Help, Command, On, Action } from "nestjs-telegraf";
import { Context, Markup } from "telegraf";
import { UserService } from "../services/user.service";
import { KartaSoupService } from "../services/karta-soup.service";
import { Logger } from "@nestjs/common";
interface SessionContext extends Context {
session?: {
awaitingCode?: boolean;
};
}
@Update()
export class BotUpdate {
private readonly logger = new Logger(BotUpdate.name);
private userSessions: Map<string, { awaitingCode: boolean }> = new Map();
private lastBalanceCheck: Map<
string,
{ timestamp: number; success: boolean }
> = new Map();
constructor(
private readonly userService: UserService,
private readonly kartaSoupService: KartaSoupService
) {}
@Start()
async start(@Ctx() ctx: SessionContext) {
const telegramId = ctx.from.id.toString();
let user = await this.userService.findByTelegramId(telegramId);
if (!user) {
user = await this.userService.createUser(
telegramId,
ctx.from.username,
ctx.from.first_name,
ctx.from.last_name
);
}
const welcomeMessage = `Добро пожаловать в бот Карта Суп! 🍲
Я помогу вам проверить баланс и историю транзакций вашей карты.
Используйте кнопки ниже для управления:`;
if (user.kartaSoupCode) {
await ctx.reply(welcomeMessage, this.getMainMenu());
} else {
await ctx.reply(welcomeMessage);
await ctx.reply(
"Пожалуйста, отправьте код вашей карты Карта Суп (13 цифр, начинается с 2001):"
);
this.userSessions.set(telegramId, { awaitingCode: true });
}
}
@Help()
async help(@Ctx() ctx: Context) {
await ctx.reply(
`Доступные команды:
/start - Начать работу с ботом
/balance - Проверить баланс
/changecode - Изменить код карты
/help - Показать эту справку`,
this.getMainMenu()
);
}
@Command("balance")
async checkBalance(@Ctx() ctx: Context) {
const telegramId = ctx.from.id.toString();
const rateLimitMessage = this.checkRateLimit(telegramId);
if (rateLimitMessage) {
await ctx.reply(rateLimitMessage);
return;
}
const code = await this.userService.getKartaSoupCode(telegramId);
if (!code) {
await ctx.reply(
"У вас не сохранен код карты. Пожалуйста, отправьте код вашей карты:"
);
this.userSessions.set(telegramId, { awaitingCode: true });
return;
}
await this.fetchAndDisplayBalance(ctx, code, telegramId);
}
@Command("changecode")
async changeCode(@Ctx() ctx: Context) {
const telegramId = ctx.from.id.toString();
await ctx.reply(
"Отправьте новый код вашей карты Карта Суп (13 цифр, начинается с 2001):"
);
this.userSessions.set(telegramId, { awaitingCode: true });
}
@Action("check_balance")
async onCheckBalance(@Ctx() ctx: any) {
await ctx.answerCbQuery();
const telegramId = ctx.from.id.toString();
const rateLimitMessage = this.checkRateLimit(telegramId);
if (rateLimitMessage) {
await ctx.reply(rateLimitMessage);
return;
}
const code = await this.userService.getKartaSoupCode(telegramId);
if (!code) {
await ctx.reply(
"У вас не сохранен код карты. Пожалуйста, отправьте код вашей карты:"
);
this.userSessions.set(telegramId, { awaitingCode: true });
return;
}
await this.fetchAndDisplayBalance(ctx, code, telegramId);
}
@Action("change_code")
async onChangeCode(@Ctx() ctx: any) {
await ctx.answerCbQuery();
const telegramId = ctx.from.id.toString();
await ctx.reply(
"Отправьте новый код вашей карты Карта Суп (13 цифр, начинается с 2001):"
);
this.userSessions.set(telegramId, { awaitingCode: true });
}
@On("text")
async onText(@Ctx() ctx: Context & { message: { text: string } }) {
const telegramId = ctx.from.id.toString();
const session = this.userSessions.get(telegramId);
if (session?.awaitingCode) {
const code = ctx.message.text.replace(/\s/g, "");
const validationError = this.validateCardCode(code);
if (validationError) {
await ctx.reply(validationError);
return;
}
try {
await ctx.reply("Проверяю код... ⏳");
const balanceData = await this.kartaSoupService.getBalance(code);
await this.userService.updateKartaSoupCode(telegramId, code);
this.userSessions.delete(telegramId);
await ctx.reply(`✅ Код успешно сохранен!`);
await this.displayBalance(ctx, balanceData);
} catch (error) {
this.logger.error(`Error saving code for user ${telegramId}:`, error);
await ctx.reply(
"❌ Не удалось проверить код. Убедитесь, что код введен правильно и попробуйте снова."
);
}
}
}
private validateCardCode(code: string): string | null {
if (!/^\d{13}$/.test(code)) {
return "❌ Код карты должен содержать ровно 13 цифр.";
}
if (!code.startsWith("2001")) {
return "❌ Код карты должен начинаться с 2001.";
}
return null;
}
private checkRateLimit(telegramId: string): string | null {
const lastCheck = this.lastBalanceCheck.get(telegramId);
if (!lastCheck) {
return null;
}
const now = Date.now();
const timePassed = now - lastCheck.timestamp;
const requiredDelay = lastCheck.success ? 60000 : 10000;
const remainingTime = requiredDelay - timePassed;
if (remainingTime > 0) {
const seconds = Math.ceil(remainingTime / 1000);
return `⏳ Пожалуйста, подождите ${seconds} секунд перед следующей проверкой баланса.`;
}
return null;
}
private async fetchAndDisplayBalance(
ctx: Context,
code: string,
telegramId: string
) {
try {
await ctx.reply("Получаю данные... ⏳");
const balanceData = await this.kartaSoupService.getBalance(code);
await this.displayBalance(ctx, balanceData);
this.lastBalanceCheck.set(telegramId, {
timestamp: Date.now(),
success: true,
});
} catch (error) {
this.logger.error("Error fetching balance:", error);
await ctx.reply("❌ Не удалось получить баланс. Попробуйте позже.");
this.lastBalanceCheck.set(telegramId, {
timestamp: Date.now(),
success: false,
});
}
}
private async displayBalance(ctx: Context, balanceData: any) {
const balance = balanceData.data.balance.availableAmount;
const phone = balanceData.data.phone;
const history = balanceData.data.history;
let message = `💳 Баланс карты\n\n`;
message += `📱 Телефон: ${phone}\n\n`;
message += `📊 Последние транзакции:\n`;
message += `━━━━━━━━━━━━━━━━━━━━\n\n`;
const recentTransactions = history.slice(0, 10).reverse();
for (const transaction of recentTransactions) {
message += this.kartaSoupService.formatTransaction(transaction);
message += `\n━━━━━━━━━━━━━━━━━━━━\n\n`;
}
if (history.length > 10) {
message += `... и еще ${history.length - 10} транзакций\n\n`;
}
message += `💰 Доступно: ${this.kartaSoupService.formatBalance(balance)}`;
await ctx.reply(message, this.getMainMenu());
}
private getMainMenu() {
return Markup.inlineKeyboard([
[Markup.button.callback("💰 Проверить баланс", "check_balance")],
[Markup.button.callback("🔄 Изменить код", "change_code")],
]);
}
}

View File

@ -0,0 +1,28 @@
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
telegramId: string;
@Column({ nullable: true })
kartaSoupCode: string;
@Column({ nullable: true })
username: string;
@Column({ nullable: true })
firstName: string;
@Column({ nullable: true })
lastName: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,29 @@
export interface Transaction {
time: string;
amount: number;
locationName: string[];
trnType: number;
mcc: string;
currency: string;
merchantId: string;
reversal: boolean;
posRechargeReceipt: any;
credit: boolean;
locationCity: string;
}
export interface BalanceData {
phone: string;
balance: {
availableAmount: number;
};
history: Transaction[];
smsInfoStatus: string;
smsNotificationAvailable: boolean;
cardType: string;
}
export interface BalanceResponse {
status: string;
data: BalanceData;
}

13
src/main.ts Normal file
View File

@ -0,0 +1,13 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
async function bootstrap() {
const logger = new Logger('Bootstrap');
const app = await NestFactory.create(AppModule);
await app.listen(3000);
logger.log('🚀 Telegram bot is running...');
}
bootstrap();

138
src/scripts/broadcast.ts Normal file
View File

@ -0,0 +1,138 @@
import { Telegraf } from "telegraf";
import { DataSource } from "typeorm";
import { User } from "../entities/user.entity";
import * as dotenv from "dotenv";
import * as readline from "readline";
dotenv.config();
const dataSource = new DataSource({
type: "postgres",
host: process.env.DATABASE_HOST || "localhost",
port: parseInt(process.env.DATABASE_PORT || "5432"),
username: process.env.DATABASE_USER || "postgres",
password: process.env.DATABASE_PASSWORD || "postgres",
database: process.env.DATABASE_NAME || "karta_soup",
entities: [User],
synchronize: false,
});
async function askQuestion(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer);
});
});
}
async function broadcast() {
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
console.error("❌ TELEGRAM_BOT_TOKEN не найден в .env файле");
process.exit(1);
}
const bot = new Telegraf(token);
try {
await dataSource.initialize();
console.log("✅ Подключение к базе данных установлено");
const userRepository = dataSource.getRepository(User);
const users = await userRepository.find();
console.log(`\n📊 Найдено пользователей: ${users.length}\n`);
if (users.length === 0) {
console.log("❌ Нет зарегистрированных пользователей");
await dataSource.destroy();
process.exit(0);
}
console.log(
"Введите сообщение для рассылки (для многострочного сообщения используйте \\n):"
);
const message = await askQuestion("> ");
if (!message.trim()) {
console.log("❌ Сообщение не может быть пустым");
await dataSource.destroy();
process.exit(1);
}
const formattedMessage = message.replace(/\\n/g, "\n");
console.log("\n📝 Предпросмотр сообщения:");
console.log("─".repeat(40));
console.log(formattedMessage);
console.log("─".repeat(40));
const confirm = await askQuestion(
`\nОтправить это сообщение ${users.length} пользователям? (да/нет): `
);
if (
confirm.toLowerCase() !== "да" &&
confirm.toLowerCase() !== "yes" &&
confirm.toLowerCase() !== "y"
) {
console.log("❌ Рассылка отменена");
await dataSource.destroy();
process.exit(0);
}
console.log("\n🚀 Начинаю рассылку...\n");
let successCount = 0;
let failCount = 0;
const failedUsers: string[] = [];
for (const user of users) {
try {
await bot.telegram.sendMessage(user.telegramId, formattedMessage);
successCount++;
console.log(
`✅ Отправлено: ${user.username || user.firstName || user.telegramId}`
);
await new Promise((resolve) => setTimeout(resolve, 50));
} catch (error: any) {
failCount++;
const errorMessage =
error.description || error.message || "Unknown error";
failedUsers.push(
`${user.telegramId} (${user.username || "no username"}): ${errorMessage}`
);
console.log(
`❌ Ошибка: ${user.username || user.firstName || user.telegramId} - ${errorMessage}`
);
}
}
console.log("\n" + "═".repeat(40));
console.log("📊 Результаты рассылки:");
console.log(`✅ Успешно: ${successCount}`);
console.log(`❌ Ошибок: ${failCount}`);
console.log("═".repeat(40));
if (failedUsers.length > 0) {
console.log("\n❌ Не удалось отправить:");
failedUsers.forEach((f) => console.log(` - ${f}`));
}
await dataSource.destroy();
process.exit(0);
} catch (error) {
console.error("❌ Ошибка:", error);
await dataSource.destroy();
process.exit(1);
}
}
broadcast();

View File

@ -0,0 +1,58 @@
import { Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
import { BalanceResponse } from '../interfaces/balance.interface';
@Injectable()
export class KartaSoupService {
private readonly logger = new Logger(KartaSoupService.name);
private readonly baseUrl = 'https://meal.gift-cards.ru/api/1/cards';
async getBalance(code: string): Promise<BalanceResponse> {
try {
const response = await axios.get(`${this.baseUrl}/${code}?limit=100`, {
headers: {
'accept': 'application/json;text/html;*/*',
'accept-language': 'en-US,en;q=0.9,ru;q=0.8,ar;q=0.7',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'priority': 'u=1, i',
'referer': 'https://meal.gift-cards.ru/balance',
'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
'x-requested-with': 'XMLHttpRequest'
}
});
return response.data;
} catch (error) {
this.logger.error(`Failed to fetch balance for code ${code}:`, error.message);
throw new Error('Не удалось получить баланс. Проверьте правильность кода.');
}
}
formatBalance(balance: number): string {
return `${balance.toFixed(2)}`;
}
formatTransaction(transaction: any): string {
const date = new Date(transaction.time);
const formattedDate = date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const amount = transaction.amount > 0 ? `+${transaction.amount}` : transaction.amount;
const location = transaction.locationName.join(', ');
const city = transaction.locationCity;
return `${formattedDate}\n${amount} ₽ | ${location}\n${city}`;
}
}

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async findByTelegramId(telegramId: string): Promise<User> {
return this.userRepository.findOne({ where: { telegramId } });
}
async createUser(telegramId: string, username?: string, firstName?: string, lastName?: string): Promise<User> {
const user = this.userRepository.create({
telegramId,
username,
firstName,
lastName,
});
return this.userRepository.save(user);
}
async updateKartaSoupCode(telegramId: string, code: string): Promise<User> {
let user = await this.findByTelegramId(telegramId);
if (!user) {
user = await this.createUser(telegramId);
}
user.kartaSoupCode = code;
return this.userRepository.save(user);
}
async getKartaSoupCode(telegramId: string): Promise<string | null> {
const user = await this.findByTelegramId(telegramId);
return user?.kartaSoupCode || null;
}
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}