import { CommandContext, Filter } from "https://deno.land/x/grammy/mod.ts"; import { Kysely, Transaction } from 'npm:kysely'; import { Ctx } from "../ctx.ts"; import { Database } from "../../repo/exports.ts"; import { checkUserRestrictions } from "../../core/users.ts"; import { UserCheckedRestrictions } from "../../core/entities.ts"; import { BotConfig } from "../../cfg/config.ts"; export interface CheckUserOut extends UserCheckedRestrictions { isNewUser: boolean isChatParticipant: boolean isCaptchaSolved: boolean } // Checks user if it's banned, timeout and if it exists overall // 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, ): Promise => { 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 } }) return out } interface CheckUserOnNewChatMemberOut extends CheckUserOut { isExpectedUserJoined: boolean expectedUserTgId: number } // Extends checkUser function with checking invite link functionality export const checkUserOnNewChatMember = async ( 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, } 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 let activeInviteLink: string | null = null 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.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 } } export const onMemberLeftChat = async ( ctx: Filter, db: Kysely, ) => { await db.updateTable('users') .where('tg_id', '=', ctx.chatMember.from.id).set({ is_chat_participant: false }).execute() }