commit 7725f47f2a624c522acfd8d34260684737c95bd6 Author: Kurbanov Bulat Date: Mon Dec 13 02:06:36 2021 +0300 Init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3fd3067 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.vscode + +build + +node_modules + +package-lock.json diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml new file mode 100644 index 0000000..41be1da --- /dev/null +++ b/.github/workflows/build_docker_image.yml @@ -0,0 +1,49 @@ +name: Build docker image + +on: + push: + branches: + - 'main' + +jobs: + Build-Docker-Image: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - id: repository_name + uses: ASzc/change-string-case-action@v1 + with: + string: ${{ github.repository }} + + - + name: Login to ghcr.io + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - + name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + env: + IMAGE: ${{ steps.repository_name.outputs.lowercase }} + with: + push: true + tags: ghcr.io/${{ env.IMAGE }}:latest + context: . + file: ./docker/build.dockerfile + + - + name: Invoke deployment hook + uses: joelwmale/webhook-action@master + with: + url: ${{ secrets.WEBHOOK_URL }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fd3067 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode + +build + +node_modules + +package-lock.json diff --git a/docker/build.dockerfile b/docker/build.dockerfile new file mode 100644 index 0000000..f220339 --- /dev/null +++ b/docker/build.dockerfile @@ -0,0 +1,22 @@ +FROM node:lts-alpine as build-image + +WORKDIR /root/app + +COPY ./package.json ./ +COPY ./tsconfig.json ./ +COPY ./src ./src + +RUN npm i && npm run build + + +FROM node:lts-alpine as runtime-image + +WORKDIR /root/app + +COPY ./package.json ./ + +RUN npm i --only=production + +COPY --from=build-image /root/app/build ./build + +CMD npm run run diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa2d609 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "flibusta_bot", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "type": "module", + "scripts": { + "build": "esbuild ./src/main.ts --bundle --platform=node --outfile=./build/main.cjs", + "build-watch": "npm run build -- --watch", + "run": "node ./build/main.cjs", + "run-watch": "nodemon build/main.cjs" + }, + "author": "", + "license": "ISC", + "dependencies": { + "envalid": "^7.2.2", + "express": "^4.17.1", + "got": "^11.8.3", + "safe-compare": "^1.1.4", + "telegraf": "^4.4.2" + }, + "devDependencies": { + "@types/express": "^4.17.13", + "@types/node": "^16.11.9", + "@types/safe-compare": "^1.1.0", + "esbuild": "^0.14.2", + "nodemon": "^2.0.15", + "typescript": "^4.5.2" + } +} diff --git a/src/bots/factory/bots/approved/callback_data.ts b/src/bots/factory/bots/approved/callback_data.ts new file mode 100644 index 0000000..942ec62 --- /dev/null +++ b/src/bots/factory/bots/approved/callback_data.ts @@ -0,0 +1,8 @@ +export const SETTINGS_LANGUAGES = 'settings.languages'; + +export const SEARCH_BOOK_PREFIX = 'sb_'; +export const SEARCH_AUTHORS_PREFIX = 'sa_'; +export const SEARCH_SERIES_PREFIX = 'ss_'; + +export const AUTHOR_BOOKS_PREFIX = 'ba_'; +export const SEQUENCE_BOOKS_PREFIX = 'bs_'; diff --git a/src/bots/factory/bots/approved/format.ts b/src/bots/factory/bots/approved/format.ts new file mode 100644 index 0000000..a41c9f1 --- /dev/null +++ b/src/bots/factory/bots/approved/format.ts @@ -0,0 +1,55 @@ +import { AuthorBook, Book, Author, Sequence } from './services/book_library'; + + +function isBook(item: AuthorBook | Book): item is Book { + return 'authors' in item; +} + + +export function formatBook(book: AuthorBook | Book): string { + let response: string[] = []; + + response.push(`📖 ${book.title} | ${book.lang}`); + + if (book.annotation_exists) { + response.push(`📝 Аннотация: /b_info_${book.id}`) + } + + if (isBook(book) && book.authors.length > 0) { + response.push('Авторы:') + book.authors.forEach(author => response.push(`͏👤 ${author.last_name} ${author.first_name} ${author.middle_name}`)); + } + + if (book.translators.length > 0) { + response.push('Переводчики:'); + book.translators.forEach(author => response.push(`͏👤 ${author.last_name} ${author.first_name} ${author.middle_name}`)); + } + + book.available_types.forEach(a_type => response.push(`📥 ${a_type}: /d_${a_type}_${book.id}`)); + + return response.join('\n'); +} + + +export function formatAuthor(author: Author): string { + let response = []; + + response.push(`👤 ${author.last_name} ${author.first_name} ${author.middle_name}`); + response.push(`/a_${author.id}`); + + if (author.annotation_exists) { + response.push(`📝 Аннотация: /a_info_${author.id}`); + } + + return response.join('\n'); +} + + +export function formatSequence(sequence: Sequence): string { + let response = []; + + response.push(`📚 ${sequence.name}`); + response.push(`/s_${sequence.id}`); + + return response.join('\n'); +} diff --git a/src/bots/factory/bots/approved/index.ts b/src/bots/factory/bots/approved/index.ts new file mode 100644 index 0000000..3526833 --- /dev/null +++ b/src/bots/factory/bots/approved/index.ts @@ -0,0 +1,142 @@ +import { Context, Telegraf, Markup } from 'telegraf'; + +import { BotState } from '@/bots/manager'; + +import env from '@/config'; + +import * as Messages from "./messages"; + +import * as CallbackData from "./callback_data"; + +import * as BookLibrary from "./services/book_library"; +import { CachedMessage, getBookCache } from './services/book_cache'; +import { getBookCacheBuffer } from './services/book_cache_buffer'; +import { formatBook, formatAuthor, formatSequence } from './format'; +import { getPaginatedMessage, registerPaginationCommand } from './utils'; + + +export async function createApprovedBot(token: string, state: BotState): Promise { + const bot = new Telegraf(token, { + telegram: { + apiRoot: env.TELEGRAM_BOT_API_ROOT, + } + }); + + await bot.telegram.setMyCommands([ + {command: "random_book", description: "Случайная книга"}, + {command: "update_log", description: "Информация об обновлении каталога"}, + {command: "settings", description: "Настройки"}, + {command: "help", description: "Помощь"}, + ]); + + bot.help((ctx: Context) => ctx.reply(Messages.HELP_MESSAGE)); + + bot.start((ctx: Context) => { + if (!ctx.message) { + return; + } + + const name = ctx.message.from.first_name || ctx.message.from.username || 'пользователь'; + ctx.telegram.sendMessage(ctx.message.chat.id, + Messages.START_MESSAGE.replace('{name}', name), { + reply_to_message_id: ctx.message.message_id, + } + ); + }); + + registerPaginationCommand(bot, CallbackData.SEARCH_BOOK_PREFIX, BookLibrary.searchByBookName, formatBook); + registerPaginationCommand(bot, CallbackData.SEARCH_AUTHORS_PREFIX, BookLibrary.searchAuthors, formatAuthor); + registerPaginationCommand(bot, CallbackData.SEARCH_SERIES_PREFIX, BookLibrary.searchSequences, formatSequence); + registerPaginationCommand(bot, CallbackData.AUTHOR_BOOKS_PREFIX, BookLibrary.getAuthorBooks, formatBook); + registerPaginationCommand(bot, CallbackData.SEQUENCE_BOOKS_PREFIX, BookLibrary.getSequenceBooks, formatBook); + + bot.hears(/^\/d_[a-zA-Z0-9]+_[\d]+$/gm, async (ctx: Context) => { + if (!ctx.message || !('text' in ctx.message)) { + return; + } + + const [_, format, id] = ctx.message.text.split('_'); + + let cache: CachedMessage; + + if (state.privileged) { + cache = await getBookCache(parseInt(id), format); + } else { + cache = await getBookCacheBuffer(parseInt(id), format); + } + + ctx.telegram.copyMessage(ctx.message.chat.id, cache.chat_id, cache.message_id, { + allow_sending_without_reply: true, + }) + }); + + bot.hears(/^\/b_info_[\d]+$/gm, async (ctx: Context) => { + if (!ctx.message || !('text' in ctx.message)) { + return; + } + + const bookId = ctx.message.text.split('_')[2]; + + const annotation = await BookLibrary.getBookAnnotation(parseInt(bookId)); + + ctx.reply(annotation.text); + }); + + bot.hears(/^\/a_[\d]+$/gm, async (ctx: Context) => { + if (!ctx.message || !('text' in ctx.message)) { + return; + } + + const authorId = ctx.message.text.split('_')[1]; + + const pMessage = await getPaginatedMessage(CallbackData.AUTHOR_BOOKS_PREFIX, authorId, 1, BookLibrary.getAuthorBooks, formatBook); + + await ctx.reply(pMessage.message, { + reply_markup: pMessage.keyboard.reply_markup + }); + }); + + bot.hears(/^\/s_[\d]+$/gm, async (ctx: Context) => { + if (!ctx.message || !('text' in ctx.message)) { + return; + } + + const sequenceId = ctx.message.text.split('_')[1]; + + const pMessage = await getPaginatedMessage(CallbackData.SEQUENCE_BOOKS_PREFIX, sequenceId, 1, BookLibrary.getSequenceBooks, formatBook); + + await ctx.reply(pMessage.message, { + reply_markup: pMessage.keyboard.reply_markup + }); + }); + + bot.on("message", async (ctx: Context) => { + if (!ctx.message || !('text' in ctx.message)) { + return; + } + + const query = ctx.message.text.substring(0, 64 - 7); + + let keyboard = Markup.inlineKeyboard([ + [ + Markup.button.callback('Книгу', `${CallbackData.SEARCH_BOOK_PREFIX}${query}_1`) + ], + [ + Markup.button.callback('Автора', `${CallbackData.SEARCH_AUTHORS_PREFIX}${query}_1`), + ], + [ + Markup.button.callback('Серию', `${CallbackData.SEARCH_SERIES_PREFIX}${query}_1`) + ], + [ + Markup.button.callback('Переводчика', '# ToDO'), + ] + ]); + + await ctx.telegram.sendMessage(ctx.message.chat.id, Messages.SEARCH_MESSAGE, { + reply_to_message_id: ctx.message.message_id, + reply_markup: keyboard.reply_markup, + }); + }); + + return bot; +} diff --git a/src/bots/factory/bots/approved/keyboard.ts b/src/bots/factory/bots/approved/keyboard.ts new file mode 100644 index 0000000..67356bf --- /dev/null +++ b/src/bots/factory/bots/approved/keyboard.ts @@ -0,0 +1,32 @@ +import { Markup } from 'telegraf'; +import { InlineKeyboardMarkup } from 'typegram'; + + +export function getPaginationKeyboard(prefix: string, query: string, page: number, totalPages: number): Markup.Markup { + function getRow(delta: number) { + const row = []; + + if (page - delta > 0) { + row.push(Markup.button.callback(`-${delta}`, `${prefix}${query}_${page - delta}`)); + } + if (page + delta <= totalPages) { + row.push(Markup.button.callback(`+${delta}`, `${prefix}${query}_${page + delta}`)); + } + + return row; + } + + const rows = []; + + const row1 = getRow(1); + if (row1) { + rows.push(row1); + } + + const row5 = getRow(5); + if (row5) { + rows.push(row5); + } + + return Markup.inlineKeyboard(rows); +} \ No newline at end of file diff --git a/src/bots/factory/bots/approved/messages.ts b/src/bots/factory/bots/approved/messages.ts new file mode 100644 index 0000000..ae986aa --- /dev/null +++ b/src/bots/factory/bots/approved/messages.ts @@ -0,0 +1,11 @@ +export const START_MESSAGE = 'Привет, {name}! \n' + + 'Этот бот поможет тебе загружать книги.\n' + + 'Узнать, как со мной работать /help.\n' + + 'Настройки языков для поиска /settings.\n'; + +export const HELP_MESSAGE = 'Лучше один раз увидеть, чем сто раз услышать.\n' + + 'https://youtu.be/HV6Wm87D6_A'; + +export const SETTINGS_MESSAGE = 'Настройки:'; + +export const SEARCH_MESSAGE = 'Что ищем?'; diff --git a/src/bots/factory/bots/approved/services/book_cache.ts b/src/bots/factory/bots/approved/services/book_cache.ts new file mode 100644 index 0000000..99d6e45 --- /dev/null +++ b/src/bots/factory/bots/approved/services/book_cache.ts @@ -0,0 +1,37 @@ +import got from 'got'; + +import env from '@/config'; + + +export interface CachedMessage { + message_id: number, + chat_id: string | number, +} + + +interface BookCache { + id: number; + object_id: number; + object_type: string; + data: CachedMessage & { + file_token: string | null, + } +} + + +async function _makeRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { + const response = await got(`${env.CACHE_SERVER_URL}${url}`, { + searchParams, + headers: { + 'Authorization': env.CACHE_SERVER_API_KEY, + }, + responseType: 'json', + }); + + return response.body; +} + + +export async function getBookCache(bookId: number, fileType: string): Promise { + return (await _makeRequest(`/api/v1/${bookId}/${fileType}`)).data; +} diff --git a/src/bots/factory/bots/approved/services/book_cache_buffer.ts b/src/bots/factory/bots/approved/services/book_cache_buffer.ts new file mode 100644 index 0000000..212dac9 --- /dev/null +++ b/src/bots/factory/bots/approved/services/book_cache_buffer.ts @@ -0,0 +1,22 @@ +import got from 'got'; + +import env from '@/config'; +import { CachedMessage } from './book_cache'; + + +async function _makeRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { + const response = await got(`${env.BUFFER_SERVER_URL}${url}`, { + searchParams, + headers: { + 'Authorization': env.BUFFER_SERVER_API_KEY, + }, + responseType: 'json', + }); + + return response.body; +} + + +export async function getBookCacheBuffer(bookId: number, fileType: string): Promise { + return _makeRequest(`/api/v1/${bookId}/${fileType}`); +} diff --git a/src/bots/factory/bots/approved/services/book_download.ts b/src/bots/factory/bots/approved/services/book_download.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/bots/factory/bots/approved/services/book_library.ts b/src/bots/factory/bots/approved/services/book_library.ts new file mode 100644 index 0000000..ffc7b8e --- /dev/null +++ b/src/bots/factory/bots/approved/services/book_library.ts @@ -0,0 +1,121 @@ +import got from 'got'; + +import env from '@/config'; + + +const PAGE_SIZE = 7; + + +export interface Page { + items: T[]; + page: number; + size: number; + total: number; + total_pages: number; +} + + +interface BookAuthor { + id: number; + first_name: string; + last_name: string; + middle_name: string; +} + + +export interface AuthorBook { + id: number; + title: string; + lang: string; + file_type: string; + available_types: string[]; + uploaded: string; + annotation_exists: boolean; + translators: BookAuthor[]; +} + + +export interface Book extends AuthorBook { + authors: BookAuthor[]; +} + + +export interface Author { + id: number; + last_name: string; + first_name: string; + middle_name: string; + annotation_exists: boolean; +} + + +export interface Sequence { + id: number; + name: string; +} + + +export interface BookAnnotation { + id: number; + title: string; + text: string; + file: string | null; +} + + +async function _makeRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { + const response = await got(`${env.BOOK_SERVER_URL}${url}`, { + searchParams, + headers: { + 'Authorization': env.BOOK_SERVER_API_KEY, + }, + responseType: 'json', + }); + + return response.body; +} + + +export async function searchByBookName(query: string, page: number): Promise> { + return _makeRequest>(`/api/v1/books/search/${query}`, { + page: page, + size: PAGE_SIZE, + }) +} + + +export async function searchAuthors(query: string, page: number): Promise> { + return _makeRequest>(`/api/v1/authors/search/${query}`, { + page: page, + size: PAGE_SIZE, + }); +} + + +export async function searchSequences(query: string, page: number): Promise> { + return _makeRequest>(`/api/v1/sequences/search/${query}`, { + page: page, + size: PAGE_SIZE, + }); +} + + +export async function getBookAnnotation(bookId: number): Promise { + return _makeRequest(`/api/v1/books/${bookId}/annotation`); +} + + +export async function getAuthorBooks(authorId: number, page: number): Promise> { + return _makeRequest>(`/api/v1/authors/${authorId}/books`, { + page: page, + size: PAGE_SIZE, + }); +} + + +export async function getSequenceBooks(sequenceId: number, page: number): Promise> { + return _makeRequest>(`/api/v1/sequences/${sequenceId}/books`, { + page: page, + size: PAGE_SIZE, + }); +} diff --git a/src/bots/factory/bots/approved/utils.ts b/src/bots/factory/bots/approved/utils.ts new file mode 100644 index 0000000..ffe8cd8 --- /dev/null +++ b/src/bots/factory/bots/approved/utils.ts @@ -0,0 +1,55 @@ +import { Context, Markup, Telegraf, TelegramError } from 'telegraf'; +import { InlineKeyboardMarkup } from 'typegram'; + +import { getPaginationKeyboard } from './keyboard'; +import * as BookLibrary from "./services/book_library"; + + +interface PreparedMessage { + message: string; + keyboard: Markup.Markup; +} + + +export async function getPaginatedMessage( + prefix: string, + data: any, + page: number, + itemsGetter: (data: any, page: number) => Promise>, + itemFormater: (item: T) => string, +): Promise { + const itemsPage = await itemsGetter(data, page); + + const formatedItems = itemsPage.items.map(itemFormater).join('\n\n\n'); + const message = formatedItems + `\n\nСтраница ${page}/${itemsPage.total_pages}`; + + const keyboard = getPaginationKeyboard(prefix, data, page, itemsPage.total_pages); + + return { + message, + keyboard + }; +} + +export function registerPaginationCommand( + bot: Telegraf, + prefix: string, + itemsGetter: (data: any, page: number) => Promise>, + itemFormater: (item: T) => string, +) { + bot.action(new RegExp(prefix), async (ctx: Context) => { + if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; + + const [_, query, sPage] = ctx.callbackQuery.data.split('_'); + + const pMessage = await getPaginatedMessage(prefix, query, parseInt(sPage), itemsGetter, itemFormater); + + try { + await ctx.editMessageText(pMessage.message, { + reply_markup: pMessage.keyboard.reply_markup + }); + } catch (err) { + // console.log(err); + } + }) +} diff --git a/src/bots/factory/bots/blocked.ts b/src/bots/factory/bots/blocked.ts new file mode 100644 index 0000000..aad82f2 --- /dev/null +++ b/src/bots/factory/bots/blocked.ts @@ -0,0 +1,16 @@ +import { Telegraf, Context } from 'telegraf'; + +import { BotState } from '@/bots/manager'; + + +export async function createBlockedBot(token: string, state: BotState): Promise { + const bot = new Telegraf(token); + + await bot.telegram.deleteMyCommands(); + + bot.on("message", async (ctx: Context) => { + await ctx.reply('Бот заблокирован!'); + }); + + return bot; +} diff --git a/src/bots/factory/bots/pending.ts b/src/bots/factory/bots/pending.ts new file mode 100644 index 0000000..2a037f1 --- /dev/null +++ b/src/bots/factory/bots/pending.ts @@ -0,0 +1,16 @@ +import { Telegraf, Context } from 'telegraf'; + +import { BotState } from '@/bots/manager'; + + +export async function createPendingBot(token: string, state: BotState): Promise { + const bot = new Telegraf(token); + + await bot.telegram.deleteMyCommands(); + + bot.on("message", async (ctx: Context) => { + await ctx.reply('Бот зарегистрирован, но не подтвержден администратором!'); + }); + + return bot; +} diff --git a/src/bots/factory/index.ts b/src/bots/factory/index.ts new file mode 100644 index 0000000..aa19a62 --- /dev/null +++ b/src/bots/factory/index.ts @@ -0,0 +1,25 @@ +import { Telegraf } from "telegraf"; + +import { BotState } from '@/bots/manager'; + +import { createPendingBot } from './bots/pending'; +import { createBlockedBot } from './bots/blocked'; +import { createApprovedBot } from './bots/approved/index'; + + +export enum BotStatuses { + PENDING = 'pending', + APPROVED = 'approved', + BLOCKED = 'blocked', +} + + +export default async function getBot(token: string, state: BotState): Promise { + const handlers = { + [BotStatuses.PENDING]: createPendingBot, + [BotStatuses.BLOCKED]: createBlockedBot, + [BotStatuses.APPROVED]: createApprovedBot, + }; + + return handlers[state.status](token, state); +} diff --git a/src/bots/manager.ts b/src/bots/manager.ts new file mode 100644 index 0000000..923c7dd --- /dev/null +++ b/src/bots/manager.ts @@ -0,0 +1,114 @@ +import express, { Response, Request, NextFunction } from 'express'; + +import got from 'got'; + +import { Telegraf } from 'telegraf'; + +import env from '@/config'; +import getBot, { BotStatuses } from './factory/index'; +import { Server } from 'http'; + + +export interface BotState { + id: number; + token: string; + status: BotStatuses; + privileged: boolean; + created_time: string; +} + + +async function _makeSyncRequest(): Promise { + try { + const response = await got(env.MANAGER_URL, { + headers: { + 'Authorization': env.MANAGER_API_KEY + }, + responseType: 'json', + }); + + return response.body; + } catch (err) { + return null; + } +} + + +export default class BotsManager { + static bots: {[key: number]: Telegraf} = {}; + static botsStates: {[key: number]: BotStatuses} = {}; + static syncInterval: NodeJS.Timer | null = null; + static server: Server | null = null; + + static async start() { + await this.sync(); + + this.launch(); + + await this.sync(); + if (this.syncInterval === null) { + this.syncInterval = setInterval(() => this.sync(), 30_000); + } + } + + static async sync() { + const botsData = await _makeSyncRequest(); + + if (botsData !== null) { + await Promise.all(botsData.map((state) => this.updateBotState(state))); + } + } + + static async updateBotState(state: BotState) { + const isExists = this.bots[state.id] !== undefined; + + if (isExists && this.botsStates[state.id] === state.status) { + return; + } + + const bot = await getBot(state.token, state); + + this.bots[state.id] = bot; + this.botsStates[state.id] = state.status; + + try { + const oldBot = new Telegraf(bot.telegram.token); + await oldBot.telegram.deleteWebhook(); + await oldBot.telegram.logOut(); + } catch (e) { + console.log(e); + } + + await bot.telegram.setWebhook( + `${env.WEBHOOK_BASE_URL}:${env.WEBHOOK_PORT}/${state.id}/${bot.telegram.token}` + ); + } + + static async handleUpdate(req: Request, res: Response, next: NextFunction) { + const botIdStr = req.url.split("/")[1]; + const bot = this.bots[parseInt(botIdStr)]; + await bot.webhookCallback(`/${botIdStr}/${bot.telegram.token}`)(req, res); + } + + static async launch() { + const application = express(); + application.use((req: Request, res: Response, next: NextFunction) => this.handleUpdate(req, res, next)); + this.server = application.listen(env.WEBHOOK_PORT); + console.log("Server started!"); + + process.once('SIGINT', () => this.stop()); + process.once('SIGTERM', () => this.stop()); + } + + static stop() { + Object.keys(this.bots).forEach(key => this.bots[parseInt(key)].telegram.deleteWebhook()); + + if (this.syncInterval) { + clearInterval(this.syncInterval); + this.syncInterval = null; + } + + this.server?.close(); + this.server = null; + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..3fc3c77 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,16 @@ +import { cleanEnv, str, num } from 'envalid'; + + +export default cleanEnv(process.env, { + WEBHOOK_BASE_URL: str(), + WEBHOOK_PORT: num(), + TELEGRAM_BOT_API_ROOT: str({ default: "https://api.telegram.org" }), + MANAGER_URL: str(), + MANAGER_API_KEY: str(), + BOOK_SERVER_URL: str(), + BOOK_SERVER_API_KEY: str(), + CACHE_SERVER_URL: str(), + CACHE_SERVER_API_KEY: str(), + BUFFER_SERVER_URL: str(), + BUFFER_SERVER_API_KEY: str() +}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ea8d345 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,4 @@ +import BotsManager from './bots/manager'; + + +BotsManager.start(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7d0d7de --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,104 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNEXT", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "ESNEXT", /* Specify what module code is generated. */ + // "rootDir": "./src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "@/*": ["./*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [ + // "./src", + // ], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./build/", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}