This commit is contained in:
Dmitry Anderson 2024-10-24 10:35:13 +02:00
parent 06f395d6ec
commit c7e82c8212
13 changed files with 79 additions and 49 deletions

View File

@ -1,14 +1,15 @@
FROM denoland/deno:2.0.2 FROM denoland/deno:1.46.3
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN deno cache ./main.ts --allow-import RUN deno cache ./main.ts
# RUN deno cache ./main.ts --allow-import
RUN mkdir bin RUN mkdir bin
# Not working # Not working
# RUN deno compile ./main.ts -o ./bin/mic --allow-all --allow-import # RUN deno compile ./main.ts -o ./bin/mic --allow-all --allow-import
# ENTRYPOINT [ "./bin/mic" ] # ENTRYPOINT [ "./bin/mic" ]
ENTRYPOINT [ "deno", "run", "--allow-all", "main.ts" ] ENTRYPOINT [ "deno", "run", "--allow-all", "main.ts" ]

View File

@ -13,7 +13,6 @@ class BotUnknownOnStartErr extends Err {
} }
export const runBot = async (cfg: config.BotConfig, initMode: (bot: Bot<Ctx>) => void) => { export const runBot = async (cfg: config.BotConfig, initMode: (bot: Bot<Ctx>) => void) => {
// Bot initialization & setup
const bot = new Bot<Ctx>(cfg.bot_token) const bot = new Bot<Ctx>(cfg.bot_token)
bot.use(session({ initial: defaultSessionData })) bot.use(session({ initial: defaultSessionData }))
bot.catch((err: BotError<Context>) => { bot.catch((err: BotError<Context>) => {
@ -22,8 +21,9 @@ export const runBot = async (cfg: config.BotConfig, initMode: (bot: Bot<Ctx>) =>
}) })
initMode(bot) initMode(bot)
await bot.init()
console.log("Starting bot") console.log(`starting bot ${bot.botInfo.username}`)
await bot.start().catch(err => { await bot.start().catch(err => {
throw new BotUnknownOnStartErr("Unknown error while starting the bot", throw new BotUnknownOnStartErr("Unknown error while starting the bot",
{ cause: err }) { cause: err })

View File

@ -1,8 +1,8 @@
import { Filter } from "https://deno.land/x/grammy@v1.30.0/mod.ts"; import { Filter } from "https://deno.land/x/grammy@v1.30.0/mod.ts";
import { BotConfig } from "../../cfg/bot.ts" import { BotConfig } from "../../cfg/config.ts";
import { Ctx } from "../ctx.ts"; import { Ctx } from "../ctx.ts";
import { LangManager } from "../lang/export.ts"; import { LangManager } from "../lang/export.ts";
import { Kysely } from "npm:kysely"; import { Kysely } from 'npm:kysely';
import { Database } from "../../repo/exports.ts"; import { Database } from "../../repo/exports.ts";
import { checkUserRestrictions, getActiveInviteLink } from "../../core/users.ts"; import { checkUserRestrictions, getActiveInviteLink } from "../../core/users.ts";
import { CheckUserOut } from "./user_managment.ts"; import { CheckUserOut } from "./user_managment.ts";
@ -128,6 +128,7 @@ const checkCaptchaSolution = async (ctx: Filter<Ctx, "message:text">, db: Kysely
ctx.reply(LangManager.getLang(ctx.msg.from.language_code) ctx.reply(LangManager.getLang(ctx.msg.from.language_code)
.replies.captcha.failed(TIMEOUT_AFTER_FAIL_MINS)) .replies.captcha.failed(TIMEOUT_AFTER_FAIL_MINS))
ctx.api.deleteMessage(ctx.chatId, ctx.session.captcha_data!.message_id).catch() ctx.api.deleteMessage(ctx.chatId, ctx.session.captcha_data!.message_id).catch()
// TODO: Add timeout
} }
} }
return return

View File

@ -1,5 +1,5 @@
import { Bot } from "https://deno.land/x/grammy/mod.ts"; import { Bot } from "https://deno.land/x/grammy/mod.ts";
import { Kysely } from "npm:kysely"; import { Kysely } from 'npm:kysely';
import { Ctx } from "../ctx.ts"; import { Ctx } from "../ctx.ts";
import { CompiledConfig } from "../../cfg/config.ts"; import { CompiledConfig } from "../../cfg/config.ts";
import { Database } from "../../repo/exports.ts"; import { Database } from "../../repo/exports.ts";
@ -9,6 +9,7 @@ import { checkUserOnStart } from "./user_managment.ts";
export const init = (bot: Bot<Ctx>, db: Kysely<Database>, cfg: CompiledConfig) => { export const init = (bot: Bot<Ctx>, db: Kysely<Database>, cfg: CompiledConfig) => {
console.log(`initializing normal mode`)
const { botCfg } = cfg const { botCfg } = cfg
bot.on('message', ctx => { bot.on('message', ctx => {
@ -26,13 +27,14 @@ export const init = (bot: Bot<Ctx>, db: Kysely<Database>, cfg: CompiledConfig) =
}) })
bot.on('message:text', ctx => { bot.on('message:text', async ctx => {
console.log(`From: ${ctx.from.id} Message: ${ctx.msg.text}`)
if (ctx.from.is_bot || ctx.hasCommand('start')) return if (ctx.from.is_bot || ctx.hasCommand('start')) return
if (ctx.chat.id === botCfg.chat_id) { if (ctx.chat.id === botCfg.chat_id) {
checkUser(ctx, db, botCfg, true) await checkUser(ctx, db, botCfg, true)
} else if (ctx.message.chat.type == "private" && ctx.session.captcha_data) { } else if (ctx.message.chat.type == "private" && ctx.session.captcha_data) {
checkCaptchaSolution(ctx, db, botCfg) await checkCaptchaSolution(ctx, db, botCfg)
} }
}) })
@ -41,6 +43,8 @@ export const init = (bot: Bot<Ctx>, db: Kysely<Database>, cfg: CompiledConfig) =
if (!ctx.from || ctx.from.is_bot) return if (!ctx.from || ctx.from.is_bot) return
if (ctx.chat.type !== 'private') return if (ctx.chat.type !== 'private') return
console.log(`Start called by ${ctx.from.id}`)
const userInfo = await checkUserOnStart(ctx, db) const userInfo = await checkUserOnStart(ctx, db)
if (userInfo.isBlocked || userInfo.isTimeout) return if (userInfo.isBlocked || userInfo.isTimeout) return

View File

@ -1,10 +1,10 @@
import { CommandContext, Filter } from "https://deno.land/x/grammy/mod.ts"; import { CommandContext, Filter } from "https://deno.land/x/grammy/mod.ts";
import { Kysely, Transaction } from "npm:kysely"; import { Kysely, Transaction } from 'npm:kysely';
import { Ctx } from "../ctx.ts"; import { Ctx } from "../ctx.ts";
import { Database } from "../../repo/exports.ts"; import { Database } from "../../repo/exports.ts";
import { checkUserRestrictions } from "../../core/users.ts"; import { checkUserRestrictions } from "../../core/users.ts";
import { UserCheckedRestrictions } from "../../core/entities.ts"; import { UserCheckedRestrictions } from "../../core/entities.ts";
import { BotConfig } from "../../cfg/bot.ts"; import { BotConfig } from "../../cfg/config.ts";
export interface CheckUserOut extends UserCheckedRestrictions { export interface CheckUserOut extends UserCheckedRestrictions {

View File

@ -1,7 +0,0 @@
export enum BotMode{ setup, normal }
export type BotConfig = {
mode: BotMode,
bot_token: string,
chat_id: number,
admin_ids: number[],
}

View File

@ -1,16 +1,23 @@
import type { PoolConfig } from "npm:@types/pg"; import type * as pg from "npm:pg";
import { strToBool } from "../utils/convert.ts"; import { strToBool } from "../utils/convert.ts";
import { BadConfigErr } from "../utils/errors.ts"; import { BadConfigErr } from "../utils/errors.ts";
import { readJsonFileSync } from "../utils/io.ts"; import { readJsonFileSync } from "../utils/io.ts";
import { BotConfig } from "./bot.ts"; import * as path from "jsr:@std/path";
export const POSTGRES_PASSWORD = "POSTGRES_PASSWORD" export const POSTGRES_PASSWORD = "POSTGRES_PASSWORD"
export const MIC_APPLY_MIGRATIONS = "MIC_APPLY_MIGRATIONS" export const MIC_APPLY_MIGRATIONS = "MIC_APPLY_MIGRATIONS"
export const DEFAULT_CONFIG_FILE_PATH = "DEFAULT_CONFIG_FILE_PATH" export const DEFAULT_CONFIG_FILE_PATH = import.meta.dirname ? path.join(import.meta.dirname, '../config.json') : '/app/config.json'
export const MIC_DROP_DB = "MIC_DROP_DB" export const MIC_DROP_DB = "MIC_DROP_DB"
export const BOT_TOKEN = "BOT_TOKEN" export const BOT_TOKEN = "BOT_TOKEN"
export const MIC_CONFIG_PATH = "MIC_CONFIG_PATH" export const MIC_CONFIG_PATH = "MIC_CONFIG_PATH"
export enum BotMode{ setup, normal }
export type BotConfig = {
mode: BotMode,
bot_token: string,
chat_id: number,
admin_ids: number[],
}
export interface EnvConfig { export interface EnvConfig {
db_password?: string, db_password?: string,
@ -50,7 +57,7 @@ export interface MigrationConfig {
} }
export interface CompiledConfig { export interface CompiledConfig {
pgPoolCfg: PoolConfig pgPoolCfg: pg.PoolConfig
botCfg: BotConfig botCfg: BotConfig
migrationCfg?: MigrationConfig migrationCfg?: MigrationConfig
} }
@ -81,7 +88,7 @@ export const getConfig = (): Config => {
if (fileCfg.db_password_file) { if (fileCfg.db_password_file) {
const decoder = new TextDecoder("utf-8") const decoder = new TextDecoder("utf-8")
const data = Deno.readFileSync(fileCfg.db_password_file) const data = Deno.readFileSync(fileCfg.db_password_file)
db_password = decoder.decode(data).toString().slice(0, -1) db_password = decoder.decode(data).toString()
} else if (envCfg.db_password) { } else if (envCfg.db_password) {
db_password = envCfg.db_password db_password = envCfg.db_password
} else throw new BadConfigErr( } else throw new BadConfigErr(
@ -112,6 +119,4 @@ export const getConfig = (): Config => {
bot_token, bot_token,
} }
} }
export * from "./bot.ts"

View File

@ -1,7 +1,6 @@
import type { PoolConfig } from "npm:@types/pg"; import type { PoolConfig } from "npm:@types/pg";
import { getConfig } from "./cfg/config.ts"; import { BotMode, getConfig } from "./cfg/config.ts";
import type { BotConfig, CompiledConfig } from "./cfg/config.ts"; import type { BotConfig, CompiledConfig } from "./cfg/config.ts";
import { BotMode } from "./cfg/bot.ts";
import * as path from "jsr:@std/path"; import * as path from "jsr:@std/path";
const MIC_CHAT_ID = -1002438254268 const MIC_CHAT_ID = -1002438254268
@ -16,7 +15,7 @@ export const DEFAULT_DB_TLS = false
export const loadConfig = (): CompiledConfig => { export const loadConfig = (): CompiledConfig => {
const cfg = getConfig() const cfg = getConfig()
const pgPoolCfg: PoolConfig = { const pgPoolCfg: PoolConfig = {
host: cfg.db_name || DEFAULT_DB_HOST, host: cfg.db_host || DEFAULT_DB_HOST,
user: cfg.db_user || DEFAULT_DB_USER, user: cfg.db_user || DEFAULT_DB_USER,
database: cfg.db_name || DEFAULT_DB_NAME, database: cfg.db_name || DEFAULT_DB_NAME,
port: cfg.db_port || DEFAULT_DB_PORT, port: cfg.db_port || DEFAULT_DB_PORT,
@ -36,7 +35,8 @@ export const loadConfig = (): CompiledConfig => {
migrationCfg: { migrationCfg: {
dropDb: cfg.drop_db, dropDb: cfg.drop_db,
applyMigrations: cfg.apply_migrations, applyMigrations: cfg.apply_migrations,
migrationsPath: path.join(__dirname, "migrations"), migrationsPath: import.meta.dirname ?
path.join(import.meta.dirname, 'migrations') : '/app/migrations',
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { Kysely, Transaction } from "npm:kysely"; import { Kysely, Transaction } from 'npm:kysely';
import { Database } from "../repo/exports.ts"; import { Database } from "../repo/exports.ts";
import { UserCheckedRestrictions, UserRestrictionsInfo } from "./entities.ts"; import { UserCheckedRestrictions, UserRestrictionsInfo } from "./entities.ts";

View File

@ -193,7 +193,7 @@
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/expression/expression-builder.js": "da9c44985b130c4d9ebd2dabff36b66e748e5afa8a785313e0d9318967ef24d7", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/expression/expression-builder.js": "da9c44985b130c4d9ebd2dabff36b66e748e5afa8a785313e0d9318967ef24d7",
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/expression/expression-wrapper.js": "35742f42558a1cbb369ecf7b3cb863f4486fa0c2384fb23e278244440f3c12c9", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/expression/expression-wrapper.js": "35742f42558a1cbb369ecf7b3cb863f4486fa0c2384fb23e278244440f3c12c9",
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/expression/expression.js": "7c024b0c9f292bbd0aefe486c81328d7b18ece24cb569afc3171e06035f4f970", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/expression/expression.js": "7c024b0c9f292bbd0aefe486c81328d7b18ece24cb569afc3171e06035f4f970",
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/index.js": "ba2b09d8e5e4ae121ef77cfd971477858381650c26bcf28dfd3a8e5ed0535e8a", "npm:kysely": "ba2b09d8e5e4ae121ef77cfd971477858381650c26bcf28dfd3a8e5ed0535e8a",
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/kysely.js": "d6b10df0da4bb8d853d1ff946b8a1a5e4c254c7a2e26d3a5ecacce82c9e3c7eb", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/kysely.js": "d6b10df0da4bb8d853d1ff946b8a1a5e4c254c7a2e26d3a5ecacce82c9e3c7eb",
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/migration/file-migration-provider.js": "04be6f4d0bb587f254b270875b529bbf30bbdf19af2d95b06272cd5e6b01e56e", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/migration/file-migration-provider.js": "04be6f4d0bb587f254b270875b529bbf30bbdf19af2d95b06272cd5e6b01e56e",
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/migration/migrator.js": "35c171e46ae9d18b9b9459148f626be5bc88f22ee5f7844b9b6818dd53ecb77a", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/migration/migrator.js": "35c171e46ae9d18b9b9459148f626be5bc88f22ee5f7844b9b6818dd53ecb77a",

View File

@ -13,6 +13,8 @@ services:
restart: always restart: always
depends_on: depends_on:
- postgres - postgres
environment:
- MIC_APPLY_MIGRATIONS=y
networks: networks:
- mic_bot - mic_bot
secrets: secrets:

View File

@ -3,14 +3,14 @@ import { init as initNormalMode } from "./bot/normal_mode/init.ts"
import { init as initSetupMode } from "./bot/setup_mode/init.ts" import { init as initSetupMode } from "./bot/setup_mode/init.ts"
import { setupDB } from "./repo/exports.ts"; import { setupDB } from "./repo/exports.ts";
import { loadConfig } from "./config.ts"; import { loadConfig } from "./config.ts";
import { BotMode } from "./cfg/bot.ts"; import { BotMode } from "./cfg/config.ts";
const main = async () => { const main = async () => {
const cfg = loadConfig() const cfg = loadConfig()
const { botCfg } = cfg const { botCfg } = cfg
const { db } = await setupDB(cfg) const { db } = await setupDB(cfg)
runBot(botCfg, (bot) => { await runBot(botCfg, (bot) => {
switch (botCfg.mode) { switch (botCfg.mode) {
case BotMode.normal: case BotMode.normal:
initNormalMode(bot, db, cfg) initNormalMode(bot, db, cfg)

View File

@ -3,37 +3,61 @@ import {
Migrator, Migrator,
PostgresDialect, PostgresDialect,
FileMigrationProvider FileMigrationProvider
} from 'https://cdn.jsdelivr.net/npm/kysely/dist/esm/index.js' } from 'npm:kysely'
/// <reference types="npm:@types/pg" /> /// <reference types="npm:@types/pg" />
// @deno-types="npm:@types/pg" import pg from "npm:pg";
import { Pool } from "npm:pg";
import { Database } from "./scheme.ts" import { Database } from "./scheme.ts"
import { CompiledConfig } from "../cfg/config.ts"; import { CompiledConfig } from "../cfg/config.ts";
import { promises as fs } from 'node:fs'
import * as path from 'node:path'
export type SetupDbOutput = { export type SetupDbOutput = {
pool: Pool, pool: pg.Pool,
db: Kysely<Database>, db: Kysely<Database>,
} }
export const setupDB = async (cfg: CompiledConfig): Promise<SetupDbOutput> => { export const setupDB = async (cfg: CompiledConfig): Promise<SetupDbOutput> => {
const pool = new Pool(cfg.pgPoolCfg) console.log("creating db instance")
const pool = new pg.Pool(cfg.pgPoolCfg)
const dialect = new PostgresDialect({ pool }) const dialect = new PostgresDialect({ pool })
const db = new Kysely<Database>({ dialect }) const db = new Kysely<Database>({ dialect })
if (cfg.migrationCfg && cfg.migrationCfg.applyMigrations) { if (cfg.migrationCfg?.applyMigrations) {
console.log("applying db migrations", cfg.migrationCfg.migrationsPath)
const migrator = new Migrator({ const migrator = new Migrator({
db, db,
provider: new FileMigrationProvider({ provider: new FileMigrationProvider({
fs, fs: {
path, readdir(path) {
return Promise.resolve(
[...Deno.readDirSync(path)].map((file) => file.name)
)
},
},
path: {
join(...path) {
return path.join('/')
},
},
migrationFolder: cfg.migrationCfg.migrationsPath, migrationFolder: cfg.migrationCfg.migrationsPath,
}), }),
allowUnorderedMigrations: true allowUnorderedMigrations: true
}) })
await migrator.migrateToLatest()
const { error, results } = await migrator.migrateToLatest()
results?.forEach((it) => {
if (it.status === 'Success') {
console.log(`migration "${it.migrationName}" was executed successfully`)
} else if (it.status === 'Error') {
console.error(`failed to execute migration "${it.migrationName}"`)
}
})
if (error) {
console.error('failed to migrate')
console.error(error)
throw error
}
} }
return { pool, db } return { pool, db }
} }