Fixes I guess?

This commit is contained in:
Dmitry Anderson 2024-10-25 19:35:12 +02:00
parent d545318aed
commit ce6802faff
8 changed files with 260 additions and 253 deletions

View File

@ -1,13 +1,8 @@
import { Bot, Context, session } from "https://deno.land/x/grammy/mod.ts"; import { Bot, Context, session, BotError, GrammyError, HttpError } from "https://deno.land/x/grammy/mod.ts";
import { BotError } from "https://deno.land/x/grammy@v1.30.0/bot.ts";
import { Ctx, defaultSessionData } from "./ctx.ts"; import { Ctx, defaultSessionData } from "./ctx.ts";
import * as config from "../cfg/config.ts" import * as config from "../cfg/config.ts"
import { ERR_CODES, Err } from "../utils/errors.ts"; import { ERR_CODES, Err } from "../utils/errors.ts";
class BotUnknownRuntimeErr extends Err {
code: ERR_CODES = ERR_CODES.UnknownErr;
override cause?: BotError<Context>;
}
class BotUnknownOnStartErr extends Err { class BotUnknownOnStartErr extends Err {
code: ERR_CODES = ERR_CODES.UnknownErr; code: ERR_CODES = ERR_CODES.UnknownErr;
} }
@ -16,16 +11,23 @@ export const runBot = async (cfg: config.BotConfig, initMode: (bot: Bot<Ctx>) =>
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>) => {
throw new BotUnknownRuntimeErr("Unknown error while running the bot", const ctx = err.ctx;
console.error(`Error while handling update ${ctx.update.update_id}:`);
const e = err.error;
if (e instanceof GrammyError) {
if (e.error_code === 400 && e.method === "deleteMessage") return
console.error("Error in request:", e.description);
} else if (e instanceof HttpError) {
console.error("Could not contact Telegram:", e);
} else {
throw new BotUnknownOnStartErr("Unknown error while starting the bot",
{ cause: err }) { cause: err })
}
}) })
initMode(bot) initMode(bot)
await bot.init() await bot.init()
console.log(`starting bot ${bot.botInfo.username}`) console.log(`starting bot ${bot.botInfo.username}`)
await bot.start().catch(err => { await bot.start().catch(err => console.log(err))
throw new BotUnknownOnStartErr("Unknown error while starting the bot",
{ cause: err })
})
} }

View File

@ -1,11 +1,11 @@
import { Filter } from "https://deno.land/x/grammy@v1.30.0/mod.ts"; import { Filter } from "https://deno.land/x/grammy/mod.ts";
import { BotConfig } from "../../cfg/config.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, type CheckUserOnStartOut } from "./user_managment.ts";
const CAPTCHA_FAILS_LIMIT = 3 const CAPTCHA_FAILS_LIMIT = 3
const TIMEOUT_AFTER_FAIL_MINS = 5 const TIMEOUT_AFTER_FAIL_MINS = 5
@ -46,11 +46,10 @@ const isSolutionCorrect = (expectSolution: string, solution: string): boolean =>
const captchaPassed = async (ctx: Ctx, db: Kysely<Database>, cfg: BotConfig) => { const captchaPassed = async (ctx: Ctx, db: Kysely<Database>, cfg: BotConfig) => {
if (!ctx.from) return
if (ctx.chatId && ctx.session.captcha_data) { if (ctx.chatId && ctx.session.captcha_data) {
ctx.api.deleteMessage( await ctx.api.deleteMessage(
ctx.chatId, ctx.session.captcha_data.message_id ctx.chatId, ctx.session.captcha_data.message_id
).catch() )
} }
ctx.session.captcha_data = undefined ctx.session.captcha_data = undefined
@ -73,32 +72,21 @@ const captchaPassed = async (ctx: Ctx, db: Kysely<Database>, cfg: BotConfig) =>
}).execute() }).execute()
}) })
ctx.reply(LangManager.getLang(ctx.from.language_code) ctx.reply(LangManager.getLang(ctx.from!.language_code)
.replies.captcha.passed(link.invite_link)) .replies.captcha.passed(link.invite_link))
ctx.session.captcha_data = undefined ctx.session.captcha_data = undefined
} }
const initUserCaptcha = async (ctx: Ctx, db: Kysely<Database>, user: CheckUserOut, cfg: BotConfig) => { const initUserCaptcha = async (ctx: Ctx, db: Kysely<Database>, user: CheckUserOnStartOut, cfg: BotConfig) => {
if (!ctx.msg || !ctx.msg.from) return
if (user.isChatParticipant) { if (user.isChatParticipant) {
ctx.reply(LangManager.getLang(ctx.msg.from.language_code) ctx.reply(LangManager.getLang(ctx.from!.language_code)
.replies.captcha.already_in_chat) .replies.captcha.already_in_chat)
return } else if (user.activeInviteLink) {
} await ctx.reply(user.activeInviteLink)
if (user.isCaptchaSolved) { } else if (user.isCaptchaSolved) {
let activeLink: string | undefined;
await db.transaction().execute(async trx => {
activeLink = await getActiveInviteLink(trx, ctx.msg!.from!.id)
})
if (activeLink) {
ctx.reply(activeLink)
return
}
await captchaPassed(ctx, db, cfg) await captchaPassed(ctx, db, cfg)
return } else {
}
const captcha = new Captcha() const captcha = new Captcha()
const msg = await ctx.reply(captcha._text) const msg = await ctx.reply(captcha._text)
ctx.session.captcha_data = { ctx.session.captcha_data = {
@ -108,6 +96,7 @@ const initUserCaptcha = async (ctx: Ctx, db: Kysely<Database>, user: CheckUserOu
solution: captcha._solution.toString(), solution: captcha._solution.toString(),
} }
} }
}
// returns true if captcha is response to a captcha; else -- returns false // returns true if captcha is response to a captcha; else -- returns false
@ -116,18 +105,19 @@ const checkCaptchaSolution = async (ctx: Filter<Ctx, "message:text">, db: Kysely
const users = await db.selectFrom('users').selectAll().where('tg_id', '=', ctx.msg.from.id).execute() const users = await db.selectFrom('users').selectAll().where('tg_id', '=', ctx.msg.from.id).execute()
if (users.length < 1) return // TODO: Maybe create new user here, idk if (users.length < 1) return // TODO: Maybe create new user here, idk
const userRestrictions = await checkUserRestrictions(db, users[0]) const user = users[0]
const userRestrictions = await checkUserRestrictions(db, user)
if (userRestrictions.isBlocked || userRestrictions.isTimeout) return if (userRestrictions.isBlocked || userRestrictions.isTimeout) return
if (isSolutionCorrect(ctx.session.captcha_data!.solution, ctx.message.text)) { if (isSolutionCorrect(ctx.session.captcha_data!.solution, ctx.message.text)) {
await captchaPassed(ctx, db, cfg) await captchaPassed(ctx, db, cfg)
} else { } else {
ctx.session.captcha_data!.tries_failed++ ctx.session.captcha_data!.tries_failed++
ctx.api.deleteMessage(ctx.chatId, ctx.msg.message_id).catch() await ctx.api.deleteMessage(ctx.chatId, ctx.msg.message_id)
if (ctx.session.captcha_data!.tries_failed > CAPTCHA_FAILS_LIMIT) { if (ctx.session.captcha_data!.tries_failed > CAPTCHA_FAILS_LIMIT) {
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() await ctx.api.deleteMessage(ctx.chatId, ctx.session.captcha_data!.message_id)
// TODO: Add timeout // TODO: Add timeout
} }
} }

View File

@ -1,12 +1,11 @@
import { ChatMember } from "https://deno.land/x/grammy_types/manage.ts";
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 { Database } from "../../repo/exports.ts"; import { Database } from "../../repo/exports.ts";
import { checkUser, checkUserOnNewChatMember, onMemberLeftChat } from "./user_managment.ts"; import { CompiledConfig } from "../../cfg/config.ts";
import { checkUser, checkUserOnNewChatMember, onMemberLeftChat, checkUserOnStart } from "./user_managment.ts";
import { checkCaptchaSolution, initUserCaptcha } from "./captcha.ts"; import { checkCaptchaSolution, initUserCaptcha } from "./captcha.ts";
import { checkUserOnStart } from "./user_managment.ts";
import { ChatMember } from "https://deno.land/x/grammy_types@v3.14.0/manage.ts";
interface ChatMemberUpdateStatus { interface ChatMemberUpdateStatus {
joined: boolean, joined: boolean,
@ -52,19 +51,15 @@ export const init = (bot: Bot<Ctx>, db: Kysely<Database>, cfg: CompiledConfig) =
bot.command('start', async ctx => { bot.command('start', async ctx => {
const userInfo = await checkUserOnStart(ctx, db) const userInfo = await checkUserOnStart(ctx, db)
if (userInfo.isBlocked || userInfo.isTimeout) return if (userInfo.isBlocked || userInfo.isTimeout) return
// TODO: if (userInfo.isNewUser) { /* Some hello message? */ } // TODO: if (userInfo.isNewUser) { /* Some hello message? */ }
if (!ctx.session.captcha_data) {
await initUserCaptcha(ctx, db, userInfo, botCfg) await initUserCaptcha(ctx, db, userInfo, botCfg)
}
}) })
bot.on('chat_member', async (ctx, next) => { bot.on('chat_member', async (ctx, next) => {
if (ctx.from.is_bot) return if (ctx.from.is_bot) return
if (ctx.chat.id !== botCfg.chat_id) return if (ctx.chat.id !== botCfg.chat_id) return
const chatMemberUpdateStatus = getChatMemberUpdateStatus( const { old_chat_member, new_chat_member } = ctx.chatMember
ctx.chatMember.old_chat_member, ctx.chatMember.new_chat_member const chatMemberUpdateStatus = getChatMemberUpdateStatus(old_chat_member, new_chat_member)
)
if (chatMemberUpdateStatus.joined) { if (chatMemberUpdateStatus.joined) {
console.log(`New Chat member ${ctx.chatMember.from.id}:'+ console.log(`New Chat member ${ctx.chatMember.from.id}:'+

View File

@ -93,9 +93,20 @@ export const checkUserOnNewChatMember = async (
expectedUserTgId: -1, expectedUserTgId: -1,
} }
const userInfo = await checkUser(ctx, db, cfg, true) let userInfo: CheckUserOut
await db.transaction().execute(async trx => {
userInfo = await checkUser(ctx, trx, cfg, true)
if (userInfo.isBlocked || userInfo.isNewUser) {
return
}
await trx.updateTable('users').where('tg_id', '=', ctx.from.id).set({
is_chat_participant: true
}).execute()
})
userInfo = userInfo!
if (userInfo.isBlocked) { if (userInfo.isBlocked) {
ctx.banChatMember(ctx.chatMember.from.id).catch( await ctx.banChatMember(ctx.chatMember.from.id).catch(
err => console.log(err) err => console.log(err)
) )
out.isBlocked = true out.isBlocked = true
@ -135,43 +146,50 @@ export const checkUserOnNewChatMember = async (
export interface CheckUserOnStartOut extends CheckUserOut {
activeInviteLink: string | null
}
export const checkUserOnStart = async ( export const checkUserOnStart = async (
ctx: CommandContext<Ctx>, ctx: CommandContext<Ctx>,
db: Kysely<Database>, db: Kysely<Database>,
): Promise<CheckUserOut> => { ): Promise<CheckUserOnStartOut> => {
let isBlocked: boolean = false let isBlocked: boolean = false
let isTimeout: boolean = false let isTimeout: boolean = false
let isNewUser: boolean = false let isNewUser: boolean = false
let isChatParticipant: boolean = false let isChatParticipant: boolean = false
let isCaptchaSolved: boolean = false let isCaptchaSolved: boolean = false
let activeInviteLink: string | null = null
await db.transaction().execute(async trx => { await db.transaction().execute(async trx => {
const users = await trx.selectFrom('users') const users = await trx.selectFrom('users')
.select([ .where('tg_id', '=', ctx.from!.id)
'id', .leftJoin('invite_links as il', join => join.onRef(
'tg_id', 'il.expect_user_tg_id', '=', 'users.tg_id'
'is_captcha_passed', )).selectAll().execute()
'is_banned',
'is_timeout',
'is_chat_participant',
'timeout_until'
])
.where('tg_id', '=', ctx.msg.from!.id).execute()
if (users.length < 1) { if (users.length < 1) {
// TODO -- referal links support // TODO -- referal links support
// const referal = ctx.match // const referal = ctx.match
await trx.insertInto('users').values({ await trx.insertInto('users').values({
tg_id: ctx.msg.from?.id tg_id: ctx.from!.id
}).execute() }).execute()
isNewUser = true isNewUser = true
} else { } else {
const now = new Date()
const user = users[0] const user = users[0]
const userRestrictions = await checkUserRestrictions(db, user) const userRestrictions = await checkUserRestrictions(db, user)
isBlocked = userRestrictions.isBlocked isBlocked = userRestrictions.isBlocked
isTimeout = userRestrictions.isTimeout isTimeout = userRestrictions.isTimeout
isChatParticipant = user.is_chat_participant isChatParticipant = user.is_chat_participant
isCaptchaSolved = user.is_captcha_passed isCaptchaSolved = user.is_captcha_passed
// TODO: move invite link related values
// (eg. valid_until and link) into it's own namespace
// if possible
if (user.valid_until && user.valid_until < now) {
await trx.deleteFrom('invite_links').where('link', '=', user.link).execute()
} else {
activeInviteLink = user.link
}
} }
}) })
return { return {
@ -179,7 +197,8 @@ export const checkUserOnStart = async (
isTimeout, isTimeout,
isNewUser, isNewUser,
isChatParticipant, isChatParticipant,
isCaptchaSolved isCaptchaSolved,
activeInviteLink
} }
} }

View File

@ -3,11 +3,11 @@ import { CompiledConfig } from "../../cfg/config.ts";
import { Ctx } from "../ctx.ts"; import { Ctx } from "../ctx.ts";
export const init = (bot: Bot<Ctx>, _: CompiledConfig) => { export const init = (bot: Bot<Ctx>, _: CompiledConfig) => {
bot.command("getchat", ctx => { bot.command("getchat", async ctx => {
if (!ctx.from || !ctx.message) return if (!ctx.from || !ctx.message) return
if (ctx.message.chat.type != "group" if (ctx.message.chat.type != "group"
&& ctx.message.chat.type != "supergroup") return && ctx.message.chat.type != "supergroup") return
ctx.deleteMessage() await ctx.deleteMessage()
console.log(`Chat ${ctx.message.chat.title} ID: ${ctx.message.chat.id}`) console.log(`Chat ${ctx.message.chat.title} ID: ${ctx.message.chat.id}`)
}) })

View File

@ -150,7 +150,8 @@
} }
}, },
"redirects": { "redirects": {
"https://deno.land/x/grammy/mod.ts": "https://deno.land/x/grammy@v1.30.0/mod.ts" "https://deno.land/x/grammy/mod.ts": "https://deno.land/x/grammy@v1.30.0/mod.ts",
"https://deno.land/x/grammy_types/manage.ts": "https://deno.land/x/grammy_types@v3.14.0/manage.ts"
}, },
"remote": { "remote": {
"https://cdn.jsdelivr.net/npm/kysely/dist/esm/dialect/database-introspector.js": "dce8b6ada28af6b75864ca3fcc2c61bbacb8976def6f715b2f96c5aa7a9331ce", "https://cdn.jsdelivr.net/npm/kysely/dist/esm/dialect/database-introspector.js": "dce8b6ada28af6b75864ca3fcc2c61bbacb8976def6f715b2f96c5aa7a9331ce",
@ -193,7 +194,6 @@
"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",
"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",
@ -530,7 +530,8 @@
"https://deno.land/x/postgres@v0.17.0/query/transaction.ts": "8e75c3ce0aca97da7fe126e68f8e6c08d640e5c8d2016e62cee5c254bebe7fe8", "https://deno.land/x/postgres@v0.17.0/query/transaction.ts": "8e75c3ce0aca97da7fe126e68f8e6c08d640e5c8d2016e62cee5c254bebe7fe8",
"https://deno.land/x/postgres@v0.17.0/query/types.ts": "a6dc8024867fe7ccb0ba4b4fa403ee5d474c7742174128c8e689c3b5e5eaa933", "https://deno.land/x/postgres@v0.17.0/query/types.ts": "a6dc8024867fe7ccb0ba4b4fa403ee5d474c7742174128c8e689c3b5e5eaa933",
"https://deno.land/x/postgres@v0.17.0/utils/deferred.ts": "dd94f2a57355355c47812b061a51b55263f72d24e9cb3fdb474c7519f4d61083", "https://deno.land/x/postgres@v0.17.0/utils/deferred.ts": "dd94f2a57355355c47812b061a51b55263f72d24e9cb3fdb474c7519f4d61083",
"https://deno.land/x/postgres@v0.17.0/utils/utils.ts": "19c3527ddd5c6c4c49ae36397120274c7f41f9d3cbf479cb36065d23329e9f90" "https://deno.land/x/postgres@v0.17.0/utils/utils.ts": "19c3527ddd5c6c4c49ae36397120274c7f41f9d3cbf479cb36065d23329e9f90",
"npm:kysely": "ba2b09d8e5e4ae121ef77cfd971477858381650c26bcf28dfd3a8e5ed0535e8a"
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [