230 lines
6.7 KiB
TypeScript
230 lines
6.7 KiB
TypeScript
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/scheme.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,
|
|
trx: Transaction<Database>,
|
|
cfg?: BotConfig,
|
|
isMicChat?: boolean,
|
|
): Promise<CheckUserOut> => {
|
|
const out: CheckUserOut = {
|
|
isBlocked: false,
|
|
isTimeout: false,
|
|
isNewUser: false,
|
|
isChatParticipant: true,
|
|
isCaptchaSolved: false,
|
|
}
|
|
|
|
if (!ctx.chat || !ctx.from) return out
|
|
if (cfg === undefined) {
|
|
isMicChat = false
|
|
} else {
|
|
isMicChat ??= ctx.chat.id === cfg.chat_id
|
|
}
|
|
|
|
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(trx, user)
|
|
|
|
if (!user.is_chat_participant && isMicChat) {
|
|
await trx.updateTable('users').set({
|
|
is_chat_participant: true,
|
|
is_captcha_passed: true,
|
|
}).where('tg_id', '=', user.tg_id).execute()
|
|
}
|
|
|
|
out.isBlocked = userRestrictions.isBlocked
|
|
out.isTimeout = userRestrictions.isTimeout
|
|
out.isChatParticipant = user.is_chat_participant || isMicChat
|
|
out.isCaptchaSolved = user.is_captcha_passed
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
export const checkUserTrx = async (
|
|
ctx: Ctx,
|
|
db: Kysely<Database>,
|
|
cfg?: BotConfig,
|
|
isMicChat?: boolean,
|
|
): Promise<CheckUserOut> => {
|
|
let res: CheckUserOut
|
|
await db.transaction().execute(async trx => {
|
|
res = await checkUser(ctx, trx, cfg, isMicChat)
|
|
})
|
|
return res!
|
|
}
|
|
|
|
|
|
|
|
interface CheckUserOnNewChatMemberOut extends CheckUserOut {
|
|
isExpectedUserJoined: boolean
|
|
expectedUserTgId: number
|
|
}
|
|
|
|
// Extends checkUser function with checking invite link functionality
|
|
export const checkUserOnNewChatMember = async (
|
|
ctx: Filter<Ctx, "chat_member">,
|
|
db: Kysely<Database> | Transaction<Database>,
|
|
): Promise<CheckUserOnNewChatMemberOut> => {
|
|
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)
|
|
if (userInfo.isBlocked || userInfo.isNewUser) {
|
|
return
|
|
}
|
|
await trx.updateTable('users').set({
|
|
is_chat_participant: true
|
|
}).where('tg_id', '=', ctx.from.id).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
|
|
await db.deleteFrom('invite_links').where('link', '=', invite_link).execute()
|
|
await ctx.revokeChatInviteLink(invite_link)
|
|
}
|
|
|
|
if (!out.isExpectedUserJoined) {
|
|
await db.updateTable('users').set({
|
|
is_banned: true
|
|
}).where(eb => eb.or([
|
|
eb('tg_id', '=', out.expectedUserTgId),
|
|
eb('tg_id', '=', ctx.chatMember.from.id),
|
|
])).execute()
|
|
await ctx.banChatMember(ctx.chatMember.from.id).catch()
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
|
|
|
|
export interface CheckUserOnStartOut extends CheckUserOut {
|
|
activeInviteLink: string | null
|
|
}
|
|
export const checkUserOnStart = async (
|
|
ctx: CommandContext<Ctx>,
|
|
db: Kysely<Database>,
|
|
): Promise<CheckUserOnStartOut> => {
|
|
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<Ctx, "chat_member">,
|
|
db: Kysely<Database>,
|
|
) => {
|
|
await db.updateTable('users').set({
|
|
is_chat_participant: false
|
|
}).where('tg_id', '=', ctx.chatMember.from.id).execute()
|
|
}
|