diff --git a/bot/bot.ts b/bot/bot.ts index ab68e5d..ba4e3d3 100644 --- a/bot/bot.ts +++ b/bot/bot.ts @@ -1,13 +1,8 @@ -import { Bot, Context, session } from "https://deno.land/x/grammy/mod.ts"; -import { BotError } from "https://deno.land/x/grammy@v1.30.0/bot.ts"; +import { Bot, Context, session, BotError, GrammyError, HttpError } from "https://deno.land/x/grammy/mod.ts"; import { Ctx, defaultSessionData } from "./ctx.ts"; import * as config from "../cfg/config.ts" import { ERR_CODES, Err } from "../utils/errors.ts"; -class BotUnknownRuntimeErr extends Err { - code: ERR_CODES = ERR_CODES.UnknownErr; - override cause?: BotError; -} class BotUnknownOnStartErr extends Err { code: ERR_CODES = ERR_CODES.UnknownErr; } @@ -16,16 +11,23 @@ export const runBot = async (cfg: config.BotConfig, initMode: (bot: Bot) => const bot = new Bot(cfg.bot_token) bot.use(session({ initial: defaultSessionData })) bot.catch((err: BotError) => { - throw new BotUnknownRuntimeErr("Unknown error while running the bot", - { cause: err }) + 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 }) + } }) initMode(bot) await bot.init() console.log(`starting bot ${bot.botInfo.username}`) - await bot.start().catch(err => { - throw new BotUnknownOnStartErr("Unknown error while starting the bot", - { cause: err }) - }) + await bot.start().catch(err => console.log(err)) } diff --git a/bot/normal_mode/captcha.ts b/bot/normal_mode/captcha.ts index 249e7ac..c6328e8 100644 --- a/bot/normal_mode/captcha.ts +++ b/bot/normal_mode/captcha.ts @@ -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 { Ctx } from "../ctx.ts"; import { LangManager } from "../lang/export.ts"; import { Kysely } from 'npm:kysely'; import { Database } from "../../repo/exports.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 TIMEOUT_AFTER_FAIL_MINS = 5 @@ -46,11 +46,10 @@ const isSolutionCorrect = (expectSolution: string, solution: string): boolean => const captchaPassed = async (ctx: Ctx, db: Kysely, cfg: BotConfig) => { - if (!ctx.from) return if (ctx.chatId && ctx.session.captcha_data) { - ctx.api.deleteMessage( + await ctx.api.deleteMessage( ctx.chatId, ctx.session.captcha_data.message_id - ).catch() + ) } ctx.session.captcha_data = undefined @@ -73,39 +72,29 @@ const captchaPassed = async (ctx: Ctx, db: Kysely, cfg: BotConfig) => }).execute() }) - ctx.reply(LangManager.getLang(ctx.from.language_code) + ctx.reply(LangManager.getLang(ctx.from!.language_code) .replies.captcha.passed(link.invite_link)) ctx.session.captcha_data = undefined } -const initUserCaptcha = async (ctx: Ctx, db: Kysely, user: CheckUserOut, cfg: BotConfig) => { - if (!ctx.msg || !ctx.msg.from) return +const initUserCaptcha = async (ctx: Ctx, db: Kysely, user: CheckUserOnStartOut, cfg: BotConfig) => { 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) - return - } - 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 - } + } else if (user.activeInviteLink) { + await ctx.reply(user.activeInviteLink) + } else if (user.isCaptchaSolved) { await captchaPassed(ctx, db, cfg) - return - } - - const captcha = new Captcha() - const msg = await ctx.reply(captcha._text) - ctx.session.captcha_data = { - message_id: msg.message_id, - tries_failed: 0, - generated_at: captcha._generated_at, - solution: captcha._solution.toString(), + } else { + const captcha = new Captcha() + const msg = await ctx.reply(captcha._text) + ctx.session.captcha_data = { + message_id: msg.message_id, + tries_failed: 0, + generated_at: captcha._generated_at, + solution: captcha._solution.toString(), + } } } @@ -116,18 +105,19 @@ const checkCaptchaSolution = async (ctx: Filter, db: Kysely 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 - const userRestrictions = await checkUserRestrictions(db, users[0]) + const user = users[0] + const userRestrictions = await checkUserRestrictions(db, user) if (userRestrictions.isBlocked || userRestrictions.isTimeout) return if (isSolutionCorrect(ctx.session.captcha_data!.solution, ctx.message.text)) { await captchaPassed(ctx, db, cfg) } else { 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) { ctx.reply(LangManager.getLang(ctx.msg.from.language_code) .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 } } diff --git a/bot/normal_mode/init.ts b/bot/normal_mode/init.ts index 99d370b..eace69e 100644 --- a/bot/normal_mode/init.ts +++ b/bot/normal_mode/init.ts @@ -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 { Kysely } from 'npm:kysely'; import { Ctx } from "../ctx.ts"; -import { CompiledConfig } from "../../cfg/config.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 { checkUserOnStart } from "./user_managment.ts"; -import { ChatMember } from "https://deno.land/x/grammy_types@v3.14.0/manage.ts"; interface ChatMemberUpdateStatus { joined: boolean, @@ -17,68 +16,64 @@ interface ChatMemberUpdateStatus { } function getChatMemberUpdateStatus(oldMember: ChatMember, newMember: ChatMember): ChatMemberUpdateStatus { - return { - joined: oldMember.status === "left" && - newMember.status !== "left" && newMember.status !== "kicked", - left: oldMember.status !== "left" && newMember.status === "left", - kicked: newMember.status === "kicked", - restricted: oldMember.status !== "restricted" && newMember.status === "restricted", - roleChanged: oldMember.status !== newMember.status && - (newMember.status !== "left" && newMember.status !== "kicked") - }; + return { + joined: oldMember.status === "left" && + newMember.status !== "left" && newMember.status !== "kicked", + left: oldMember.status !== "left" && newMember.status === "left", + kicked: newMember.status === "kicked", + restricted: oldMember.status !== "restricted" && newMember.status === "restricted", + roleChanged: oldMember.status !== newMember.status && + (newMember.status !== "left" && newMember.status !== "kicked") + }; } export const init = (bot: Bot, db: Kysely, cfg: CompiledConfig) => { - console.log(`initializing normal mode`) - const { botCfg } = cfg + console.log(`initializing normal mode`) + const { botCfg } = cfg - bot.on('message', async (ctx, next) => { - if (ctx.from?.is_bot) return - console.log(`Chat ID: ${ctx.msg.chat.id} From: ${ctx.from.id} Message: ${ctx.msg.text}`) - await next() - }) + bot.on('message', async (ctx, next) => { + if (ctx.from?.is_bot) return + console.log(`Chat ID: ${ctx.msg.chat.id} From: ${ctx.from.id} Message: ${ctx.msg.text}`) + await next() + }) - bot.on('message:text', async (ctx, next) => { - if (ctx.chat.id === botCfg.chat_id) { - await checkUser(ctx, db, botCfg, true) - } else if (ctx.message.chat.type == "private" && ctx.session.captcha_data) { - await checkCaptchaSolution(ctx, db, botCfg) - } else { - await next() - } - }) + bot.on('message:text', async (ctx, next) => { + if (ctx.chat.id === botCfg.chat_id) { + await checkUser(ctx, db, botCfg, true) + } else if (ctx.message.chat.type == "private" && ctx.session.captcha_data) { + await checkCaptchaSolution(ctx, db, botCfg) + } else { + await next() + } + }) - bot.command('start', async ctx => { - const userInfo = await checkUserOnStart(ctx, db) - if (userInfo.isBlocked || userInfo.isTimeout) return + bot.command('start', async ctx => { + const userInfo = await checkUserOnStart(ctx, db) + if (userInfo.isBlocked || userInfo.isTimeout) return + // TODO: if (userInfo.isNewUser) { /* Some hello message? */ } + await initUserCaptcha(ctx, db, userInfo, botCfg) + }) - // TODO: if (userInfo.isNewUser) { /* Some hello message? */ } - if (!ctx.session.captcha_data) { - await initUserCaptcha(ctx, db, userInfo, botCfg) - } - }) + bot.on('chat_member', async (ctx, next) => { + if (ctx.from.is_bot) return + if (ctx.chat.id !== botCfg.chat_id) return + const { old_chat_member, new_chat_member } = ctx.chatMember + const chatMemberUpdateStatus = getChatMemberUpdateStatus(old_chat_member, new_chat_member) - bot.on('chat_member', async (ctx, next) => { - if (ctx.from.is_bot) return - if (ctx.chat.id !== botCfg.chat_id) return - const chatMemberUpdateStatus = getChatMemberUpdateStatus( - ctx.chatMember.old_chat_member, ctx.chatMember.new_chat_member - ) - - if (chatMemberUpdateStatus.joined) { - console.log(`New Chat member ${ctx.chatMember.from.id}:'+ - '${ctx.from.first_name} ${ctx.from.last_name}`) - await checkUserOnNewChatMember(ctx, db, botCfg) - // TODO: - // const userInfo = await checkUserOnNewChatMember(ctx, db, botCfg) - //if (userInfo.isNewUser) { /* Some hello message? */ } - } else if (chatMemberUpdateStatus.left) { - console.log(`Chat member left ${ctx.chatMember.from.id}:'+ - '${ctx.from.first_name} ${ctx.from.last_name}`) - await onMemberLeftChat(ctx, db) - // TODO: /* POBEG S DURKI! */ - } - await next() - }) -} \ No newline at end of file + if (chatMemberUpdateStatus.joined) { + console.log(`New Chat member ${ctx.chatMember.from.id}:'+ + '${ctx.from.first_name} ${ctx.from.last_name}`) + await checkUserOnNewChatMember(ctx, db, botCfg) + // TODO: + // const userInfo = await checkUserOnNewChatMember(ctx, db, botCfg) + //if (userInfo.isNewUser) { /* Some hello message? */ } + } else if (chatMemberUpdateStatus.left) { + console.log(`Chat member left ${ctx.chatMember.from.id}:'+ + '${ctx.from.first_name} ${ctx.from.last_name}`) + await onMemberLeftChat(ctx, db) + // TODO: /* POBEG S DURKI! */ + } + await next() + }) +} diff --git a/bot/normal_mode/user_managment.ts b/bot/normal_mode/user_managment.ts index d545887..0b31d33 100644 --- a/bot/normal_mode/user_managment.ts +++ b/bot/normal_mode/user_managment.ts @@ -18,169 +18,188 @@ export interface CheckUserOut extends UserCheckedRestrictions { // Creates new user if it doesn't exist yet // Ignores the start command export const checkUser = async ( - ctx: Ctx, - db: Kysely, - cfg: BotConfig, - isMicChat?: boolean, + ctx: Ctx, + db: Kysely, + cfg: BotConfig, + isMicChat?: boolean, ): Promise => { - const out: CheckUserOut = { - isBlocked: false, - isTimeout: false, - isNewUser: false, - isChatParticipant: true, - isCaptchaSolved: false, + const out: CheckUserOut = { + isBlocked: false, + isTimeout: false, + isNewUser: false, + isChatParticipant: true, + isCaptchaSolved: false, + } + + if (!ctx.chat || !ctx.from) return out + isMicChat ??= ctx.chat.id === cfg.chat_id + + await db.transaction().execute(async trx => { + const users = await trx.selectFrom('users') + .selectAll().where('tg_id', '=', ctx.from!.id).execute() + if (users.length < 1) { + await trx.insertInto('users').values({ + tg_id: ctx.from!.id, + joined_by_referal_from_user_id: null, + is_chat_participant: isMicChat, + is_captcha_passed: isMicChat, + joined_chat_at: new Date, + timeout_until: null + }).execute() + out.isNewUser = true + out.isChatParticipant = isMicChat + out.isCaptchaSolved = out.isChatParticipant + } else { + const user = users[0] + const userRestrictions = await checkUserRestrictions(db, user) + + if (!user.is_chat_participant && isMicChat) { + await trx.updateTable('users').set({ + is_chat_participant: true, + is_captcha_passed: true, + }).execute() + } + + out.isBlocked = userRestrictions.isBlocked + out.isTimeout = userRestrictions.isTimeout + out.isChatParticipant = user.is_chat_participant || isMicChat + out.isCaptchaSolved = user.is_captcha_passed } + }) - if (!ctx.chat || !ctx.from) return out - isMicChat ??= ctx.chat.id === cfg.chat_id - - await db.transaction().execute(async trx => { - const users = await trx.selectFrom('users') - .selectAll().where('tg_id', '=', ctx.from!.id).execute() - if (users.length < 1) { - await trx.insertInto('users').values({ - tg_id: ctx.from!.id, - joined_by_referal_from_user_id: null, - is_chat_participant: isMicChat, - is_captcha_passed: isMicChat, - joined_chat_at: new Date, - timeout_until: null - }).execute() - out.isNewUser = true - out.isChatParticipant = isMicChat - out.isCaptchaSolved = out.isChatParticipant - } else { - const user = users[0] - const userRestrictions = await checkUserRestrictions(db, user) - - if (!user.is_chat_participant && isMicChat) { - await trx.updateTable('users').set({ - is_chat_participant: true, - is_captcha_passed: true, - }).execute() - } - - out.isBlocked = userRestrictions.isBlocked - out.isTimeout = userRestrictions.isTimeout - out.isChatParticipant = user.is_chat_participant || isMicChat - out.isCaptchaSolved = user.is_captcha_passed - } - }) - - return out + return out } interface CheckUserOnNewChatMemberOut extends CheckUserOut { - isExpectedUserJoined: boolean - expectedUserTgId: number + isExpectedUserJoined: boolean + expectedUserTgId: number } // Extends checkUser function with checking invite link functionality export const checkUserOnNewChatMember = async ( - ctx: Filter, - db: Kysely | Transaction, - cfg: BotConfig + ctx: Filter, + db: Kysely | Transaction, + cfg: BotConfig ): Promise => { - const out: CheckUserOnNewChatMemberOut = { - isBlocked: false, - isTimeout: false, - isNewUser: false, - isCaptchaSolved: false, - isExpectedUserJoined: true, - isChatParticipant: true, - expectedUserTgId: -1, - } - - const userInfo = await checkUser(ctx, db, cfg, true) - if (userInfo.isBlocked) { - ctx.banChatMember(ctx.chatMember.from.id).catch( - err => console.log(err) - ) - out.isBlocked = true - out.isChatParticipant = false - return out - } - out.isTimeout = userInfo.isTimeout - out.isCaptchaSolved = userInfo.isCaptchaSolved - out.isNewUser = userInfo.isNewUser - - if (!ctx.chatMember.invite_link) return out - const { invite_link } = ctx.chatMember.invite_link - - const inviteLinksData = await db.selectFrom('invite_links') - .select('expect_user_tg_id') - .where('link', '=', invite_link).execute() - if (inviteLinksData.length === 0) { - out.expectedUserTgId = -1 - } else { - const { expect_user_tg_id } = inviteLinksData[0] - out.isExpectedUserJoined = expect_user_tg_id === ctx.chatMember.from.id - out.expectedUserTgId = expect_user_tg_id - db.deleteFrom('invite_links').where('link', '=', invite_link).execute() - ctx.revokeChatInviteLink(invite_link) - } - - if (!out.isExpectedUserJoined) { - db.updateTable('users').where(eb => eb.or([ - eb('tg_id', '=', out.expectedUserTgId), - eb('tg_id', '=', ctx.chatMember.from.id), - ])).execute() - ctx.banChatMember(ctx.chatMember.from.id).catch() + const out: CheckUserOnNewChatMemberOut = { + isBlocked: false, + isTimeout: false, + isNewUser: false, + isCaptchaSolved: false, + isExpectedUserJoined: true, + isChatParticipant: true, + expectedUserTgId: -1, + } + + 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) { + await ctx.banChatMember(ctx.chatMember.from.id).catch( + err => console.log(err) + ) + out.isBlocked = true + out.isChatParticipant = false return out + } + out.isTimeout = userInfo.isTimeout + out.isCaptchaSolved = userInfo.isCaptchaSolved + out.isNewUser = userInfo.isNewUser + + if (!ctx.chatMember.invite_link) return out + const { invite_link } = ctx.chatMember.invite_link + + const inviteLinksData = await db.selectFrom('invite_links') + .select('expect_user_tg_id') + .where('link', '=', invite_link).execute() + if (inviteLinksData.length === 0) { + out.expectedUserTgId = -1 + } else { + const { expect_user_tg_id } = inviteLinksData[0] + out.isExpectedUserJoined = expect_user_tg_id === ctx.chatMember.from.id + out.expectedUserTgId = expect_user_tg_id + db.deleteFrom('invite_links').where('link', '=', invite_link).execute() + ctx.revokeChatInviteLink(invite_link) + } + + if (!out.isExpectedUserJoined) { + db.updateTable('users').where(eb => eb.or([ + eb('tg_id', '=', out.expectedUserTgId), + eb('tg_id', '=', ctx.chatMember.from.id), + ])).execute() + ctx.banChatMember(ctx.chatMember.from.id).catch() + } + + return out } +export interface CheckUserOnStartOut extends CheckUserOut { + activeInviteLink: string | null +} export const checkUserOnStart = async ( - ctx: CommandContext, - db: Kysely, -): Promise => { - let isBlocked: boolean = false - let isTimeout: boolean = false - let isNewUser: boolean = false - let isChatParticipant: boolean = false - let isCaptchaSolved: boolean = false + ctx: CommandContext, + db: Kysely, +): Promise => { + let isBlocked: boolean = false + let isTimeout: boolean = false + let isNewUser: boolean = false + let isChatParticipant: boolean = false + let isCaptchaSolved: boolean = false + let activeInviteLink: string | null = null - await db.transaction().execute(async trx => { - const users = await trx.selectFrom('users') - .select([ - 'id', - 'tg_id', - 'is_captcha_passed', - 'is_banned', - 'is_timeout', - 'is_chat_participant', - 'timeout_until' - ]) - .where('tg_id', '=', ctx.msg.from!.id).execute() - if (users.length < 1) { - // TODO -- referal links support - // const referal = ctx.match + await db.transaction().execute(async trx => { + const users = await trx.selectFrom('users') + .where('tg_id', '=', ctx.from!.id) + .leftJoin('invite_links as il', join => join.onRef( + 'il.expect_user_tg_id', '=', 'users.tg_id' + )).selectAll().execute() + if (users.length < 1) { + // TODO -- referal links support + // const referal = ctx.match - await trx.insertInto('users').values({ - tg_id: ctx.msg.from?.id - }).execute() - isNewUser = true - } else { - const user = users[0] - const userRestrictions = await checkUserRestrictions(db, user) - isBlocked = userRestrictions.isBlocked - isTimeout = userRestrictions.isTimeout - isChatParticipant = user.is_chat_participant - isCaptchaSolved = user.is_captcha_passed - } - }) - return { - isBlocked, - isTimeout, - isNewUser, - isChatParticipant, - isCaptchaSolved + await trx.insertInto('users').values({ + tg_id: ctx.from!.id + }).execute() + isNewUser = true + } else { + const now = new Date() + const user = users[0] + const userRestrictions = await checkUserRestrictions(db, user) + isBlocked = userRestrictions.isBlocked + isTimeout = userRestrictions.isTimeout + isChatParticipant = user.is_chat_participant + 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 { + isBlocked, + isTimeout, + isNewUser, + isChatParticipant, + isCaptchaSolved, + activeInviteLink + } } @@ -192,4 +211,4 @@ export const onMemberLeftChat = async ( .where('tg_id', '=', ctx.chatMember.from.id).set({ is_chat_participant: false }).execute() -} \ No newline at end of file +} diff --git a/bot/setup_mode/init.ts b/bot/setup_mode/init.ts index 56ba2b9..1cc5e13 100644 --- a/bot/setup_mode/init.ts +++ b/bot/setup_mode/init.ts @@ -3,11 +3,11 @@ import { CompiledConfig } from "../../cfg/config.ts"; import { Ctx } from "../ctx.ts"; export const init = (bot: Bot, _: CompiledConfig) => { - bot.command("getchat", ctx => { + bot.command("getchat", async ctx => { if (!ctx.from || !ctx.message) return if (ctx.message.chat.type != "group" && ctx.message.chat.type != "supergroup") return - ctx.deleteMessage() + await ctx.deleteMessage() console.log(`Chat ${ctx.message.chat.title} ID: ${ctx.message.chat.id}`) }) @@ -15,4 +15,4 @@ export const init = (bot: Bot, _: CompiledConfig) => { if (!ctx.from) return console.log(`User ${ctx.from.first_name} ${ctx.from.last_name} ID: ${ctx.from.id}`) }) -} \ No newline at end of file +} diff --git a/config.ts b/config.ts index f472111..4c31b26 100644 --- a/config.ts +++ b/config.ts @@ -39,4 +39,4 @@ export const loadConfig = (): CompiledConfig => { path.join(import.meta.dirname, 'migrations') : '/app/migrations', } } -} \ No newline at end of file +} diff --git a/core/users.ts b/core/users.ts index ee9933a..8cb2ecb 100644 --- a/core/users.ts +++ b/core/users.ts @@ -40,4 +40,4 @@ export const getActiveInviteLink = async ( return } return link.link -} \ No newline at end of file +} diff --git a/deno.lock b/deno.lock index 1924e71..eaeda38 100644 --- a/deno.lock +++ b/deno.lock @@ -150,7 +150,8 @@ } }, "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": { "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-wrapper.js": "35742f42558a1cbb369ecf7b3cb863f4486fa0c2384fb23e278244440f3c12c9", "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/migration/file-migration-provider.js": "04be6f4d0bb587f254b270875b529bbf30bbdf19af2d95b06272cd5e6b01e56e", "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/types.ts": "a6dc8024867fe7ccb0ba4b4fa403ee5d474c7742174128c8e689c3b5e5eaa933", "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": { "dependencies": [