import { default as TelegramLoginButton, TelegramLoginResponse } from "react-telegram-login" import { default as nodecrypto } from "crypto" import { AccountTelegram } from "@prisma/client" /** * A {@link TelegramLoginResponse} object extended with various utility methods, and with its fields renamed to follow the camelCase JS convention. */ export class TelegramLoginObject { id: number firstName: string lastName?: string username?: string photoUrl?: string lang?: string authDate: Date hash: string /** * Construct a {@link TelegramLoginObject} object from a {@link TelegramLoginData}. * * @param u The {@link TelegramLoginData} to use. */ constructor(u: TelegramLoginResponse) { if (!u.id) throw new Error("Missing `id`") if (!u.first_name) throw new Error("Missing `first_name`") if (!u.auth_date) throw new Error("Missing `auth_date`") if (!u.hash) throw new Error("Missing `hash`") this.id = u.id this.firstName = u.first_name this.lastName = u.last_name this.username = u.username this.photoUrl = u.photo_url this.authDate = new Date(u.auth_date * 1000) // https://stackoverflow.com/a/12372720/4334568 if (isNaN(this.authDate.getTime())) throw new Error("Invalid `auth_date`") this.hash = u.hash this.lang = u.lang } /** * Convert this object back into a {@link TelegramLoginResponse}. * * @return The {@link TelegramLoginResponse} object, ready to be serialized. */ toObject(): TelegramLoginResponse { return { id: this.id, first_name: this.firstName, last_name: this.lastName, username: this.username, photo_url: this.photoUrl, lang: this.lang, auth_date: this.authDate.getTime() / 1000, hash: this.hash, } } /** * Convert this object into a partial {@link AccountTelegram} database object. */ toDatabase(): Pick { return { telegramId: this.id, firstName: this.firstName, lastName: this.lastName ?? null, username: this.username ?? null, photoUrl: this.photoUrl ?? null, lang: this.lang ?? null, } } /** * Convert this object in a string, using [the format required to verify a Telegram Login](https://core.telegram.org/widgets/login#checking-authorization). * * @param data The data to encode. * @returns The stringified data. */ toString(): string { const string = Object.entries(this.toObject()) .filter(([key, _]) => key !== "hash") .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}=${value}`) .sort() .join("\n") return string } /** * Check if the `authDate` of the response is recent: it must be in the past, but within `maxMs` from the current date. * * @param maxMs The maximum number of milliseconds that may pass after authentication for the response to be considered valid. * @returns `true` if the request was sent within the requested timeframe, `false` otherwise. */ isRecent(maxMs: number): boolean { const diff = new Date().getTime() - this.authDate.getTime() return diff <= maxMs } /** * Calculate the "`hash`" of this object using [the Telegram Login verification procedure](https://core.telegram.org/widgets/login#checking-authorization). * * _Only works on Node.js, due to usage of the `crypto` module._ * * @param token The bot token used to validate the signature. * @returns The calculated value of the `hash` parameter. */ hmac(token: string): string { const hash = nodecrypto.createHash("sha256") hash.update(token) const hmac = nodecrypto.createHmac("sha256", hash.digest()) hmac.update(this.toString()) return hmac.digest("hex") } /** * Validate this object using [the Telegram Login verification procedure](https://core.telegram.org/widgets/login#checking-authorization). * * _Only works on Node.js, due to usage of the `crypto` module._ * * @param token The bot token used to validate the signature. * @returns `true` if the hash matches the value calculated with {@link hmac}, `false` otherwise. */ isValid(token: string): boolean { const received = this.hash const calculated = this.hmac(token) return received === calculated } /** * Get the Telegram "displayed name" of the user represented by this object. */ toTelegramName(): string { if (this.username) return this.username else if (this.lastName) return this.firstName + " " + this.lastName else return this.firstName } }