import { Queue } from 'bull'; import * as admin from 'firebase-admin'; import { DataSource, EntityManager, Repository } from 'typeorm'; import { InjectQueue } from '@nestjs/bull'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { InjectDataSource, InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { AuthDataValidator, objectToAuthDataMap } from '@telegram-auth/server'; import { RefCode, RefCodeWhiteList, User } from './database/entities'; import { QuestType } from './funki-id.dto'; import { InjectRedis } from './libs/redis'; import IORedis from 'ioredis'; @Injectable() export class FunkiIdTelegramService { constructor( @Inject('FIREBASE_AUTH') private readonly firebaseAuth: admin.auth.Auth, @Inject('TELEGRAM_BOT_TOKEN') private readonly telegramBotToken: string, @InjectRepository(User, 'funki-id') private readonly userRepository: Repository, @InjectDataSource('funki-id') private dataSource: DataSource, @InjectQueue('validate-quest-by-type-queue') private readonly validateQueue: Queue, @InjectEntityManager('funki-id') private readonly entityManager: EntityManager, @InjectRedis() private readonly redis: IORedis, ) {} private dateRanges = [ { time: 1514764800, id: 433418920 }, { time: 1524787200, id: 594609909 }, { time: 1543622400, id: 654347640 }, { time: 1558915200, id: 757850032 }, { time: 1572566400, id: 950795629 }, { time: 1583193600, id: 952482509 }, { time: 1600128000, id: 1006700021 }, { time: 1604275200, id: 1035873657 }, { time: 1609459200, id: 1565295243 }, { time: 1612483200, id: 1710497692 }, { time: 1617840000, id: 1722157744 }, { time: 1621209600, id: 1870540461 }, { time: 1622505600, id: 1824557330 }, { time: 1628640000, id: 1709596835 }, { time: 1629849600, id: 1951363047 }, { time: 1631059200, id: 1967135729 }, { time: 1636329600, id: 2146646076 }, { time: 1636934400, id: 2144972936 }, { time: 1637280000, id: 1079547515 }, { time: 1647475200, id: 5180189356 }, { time: 1674432000, id: 2047400233 }, { time: 1677283200, id: 5141773756 }, { time: 1682553600, id: 5648773695 }, { time: 1684627200, id: 5941997741 }, { time: 1704585600, id: 7494348399 }, ]; private getCreationTime(i: number, telegramId: number): number { if (i === 0) { return this.dateRanges[i].time; } const prevRange = this.dateRanges[i - 1]; const currentRange = this.dateRanges[i]; const timeDiff = prevRange.time - currentRange.time; const idDiff = prevRange.id - currentRange.id; const idOffset = telegramId - prevRange.id; return prevRange.time + (timeDiff / idDiff) * idOffset; } private getAgesByTelegramId(telegramId: number): number { if (!telegramId) { return 0; } const now = Math.floor(Date.now() / 1000); let creationTime = this.dateRanges[0].time; for (let i = 0; i < this.dateRanges.length; i++) { if (telegramId <= this.dateRanges[i].id) { creationTime = this.getCreationTime(i, telegramId); break; } } const ageInSeconds = now - creationTime; const ageInYears = Math.round(ageInSeconds / (365.25 * 24 * 60 * 60) + 0.5); return ageInYears; } async authorize(payload: Record, currentUser?: User) { const validator = new AuthDataValidator({ botToken: this.telegramBotToken, }); const data = objectToAuthDataMap(payload || {}); const telegramUser = await validator.validate(data); if (!telegramUser || !telegramUser.id) { throw new BadRequestException('Invalid hash'); } const { id, is_bot } = telegramUser; if (is_bot) { throw new BadRequestException('Bot not allowed'); } const isWebAppData = data.has('user') && data.has('query_id'); const allowSendPM = !isWebAppData; let user = null; const ages = this.getAgesByTelegramId(id); // find a user that has the same telegram id const existingTelegramUser = await this.getUserByTelegramId(id); // case 1: link telegram account to current user if (currentUser) { // link telegram account to current user const { telegram } = currentUser; if (telegram && telegram.id) { throw new BadRequestException('This account is already linked to a telegram account'); } if (existingTelegramUser) { throw new BadRequestException('Another user is already linked to this telegram account'); } // update user with telegram data user = await this.userRepository.save({ uid: currentUser.uid, telegramId: id.toString(), telegram: { id: id.toString(), username: telegramUser.username, avatar: telegramUser.photo_url ?? undefined, allowSendPM: allowSendPM === true, ages, }, }); } else { // case 2: sign in/up with telegram account if (!existingTelegramUser) { const email = `${telegramUser.id}@telegram.org`; let firebaseUser = await this.firebaseAuth.getUserByEmail(email).catch(() => null); if (!firebaseUser) { firebaseUser = await this.firebaseAuth.createUser({ email, emailVerified: true, displayName: telegramUser.username, photoURL: telegramUser.photo_url ?? undefined, }); } user = await this.entityManager.transaction(async txManager => { const user = await txManager.getRepository(User).findOne({ where: { uid: firebaseUser.uid } }); if (user) { if (!user?.telegram) { user.name = telegramUser.username; user.telegram = { id: telegramUser.id.toString(), username: telegramUser.username, avatar: telegramUser.photo_url ?? null, allowSendPM: allowSendPM === true, ages, }; await txManager.getRepository(User).save(user); } else if (!user?.telegram?.allowSendPM && allowSendPM === true) { user.telegram.allowSendPM = true; await this.userRepository.save(user); } return user; } const whitelistCodes = await txManager.getRepository(RefCodeWhiteList).findBy({ telegramId: id }); const newUser = await txManager.getRepository(User).save({ uid: firebaseUser.uid, name: telegramUser.username, email: firebaseUser.email, telegramId: telegramUser.id.toString(), telegram: { id: telegramUser.id.toString(), username: telegramUser.username, avatar: telegramUser.photo_url ?? (await this.redis.get(`telegram:${telegramUser.id}:photo`).catch(() => null)), allowSendPM: allowSendPM === true, ages, }, // if user is whitelisted, set onboarded to true onboarded: whitelistCodes.length > 0, }); // add ref code and wallet if user is whitelisted const refCodes = whitelistCodes.map(code => ({ ownerUid: firebaseUser.uid, code: code.code, isPermanent: true, isUsed: false, })); if (refCodes.length > 0) { await txManager.getRepository(RefCode).save(refCodes); } return { ...newUser, refCodes, }; }); } else { // update telegram bot send pm permission user = existingTelegramUser; if (!!existingTelegramUser?.uid && allowSendPM && !user?.telegram?.allowSendPM) { user = await this.updateTelegramSendPMPermission(user.uid, true); } } } const customToken = await this.firebaseAuth.createCustomToken(user.uid, { telegram_id: telegramUser.id, displayName: telegramUser.username, }); await this.validateQueue.add('validate-quest-by-type-job', { funkiId: user.uid, type: QuestType.LINK_SOCIAL, }); return { customToken }; } public async getUserByTelegramId(telegramId: number): Promise { return this.userRepository .createQueryBuilder('user') .where(`user.telegramId = :telegramId`, { telegramId }) .getOne(); } public async updateTelegramSendPMPermission(userUid: string, allowSendPM: boolean) { const user = await this.userRepository.findOne({ where: { uid: userUid } }); user.telegram.allowSendPM = allowSendPM; await this.userRepository.save(user); return user; } }