Stop ticking Telegram login message
All checks were successful
Build and deploy Backend / build (push) Successful in 28s

This commit is contained in:
Ruslan Bakiev
2026-05-13 21:15:54 +07:00
parent bea31fe8f2
commit c8af4e895a

View File

@@ -38,7 +38,7 @@ type TelegramSentMessage = {
}; };
const loginPrefix = 'login_'; const loginPrefix = 'login_';
const loginTimerHandles = new Map<string, ReturnType<typeof setTimeout>>(); const loginExpirationHandles = new Map<string, ReturnType<typeof setTimeout>>();
function randomToken() { function randomToken() {
return randomBytes(32).toString('base64url'); return randomBytes(32).toString('base64url');
@@ -124,12 +124,12 @@ function formatRemaining(expiresAt: Date) {
return `${minutes}:${rest}`; return `${minutes}:${rest}`;
} }
function loginReplyMarkup(token: string, expiresAt: Date) { function loginReplyMarkup(token: string) {
return { return {
inline_keyboard: [ inline_keyboard: [
[ [
{ {
text: 'Открыть MapFlow', text: 'Войти',
url: `${config.webAppUrl}?telegram_login=${encodeURIComponent(token)}`, url: `${config.webAppUrl}?telegram_login=${encodeURIComponent(token)}`,
}, },
], ],
@@ -138,12 +138,7 @@ function loginReplyMarkup(token: string, expiresAt: Date) {
} }
function loginMessageText(expiresAt: Date) { function loginMessageText(expiresAt: Date) {
return [ return [formatRemaining(expiresAt), '', 'Войти в MapFlow'].join('\n');
'Вход в MapFlow',
'',
'Перейдите по ссылке ниже.',
`Ссылка активна: ${formatRemaining(expiresAt)}`,
].join('\n');
} }
async function sendLoginMessage( async function sendLoginMessage(
@@ -152,7 +147,7 @@ async function sendLoginMessage(
token?: string, token?: string,
expiresAt?: Date, expiresAt?: Date,
) { ) {
const replyMarkup = token && expiresAt ? loginReplyMarkup(token, expiresAt) : undefined; const replyMarkup = token && expiresAt ? loginReplyMarkup(token) : undefined;
return callTelegram<TelegramSentMessage>('sendMessage', { return callTelegram<TelegramSentMessage>('sendMessage', {
chat_id: chatId, chat_id: chatId,
@@ -165,67 +160,79 @@ async function editLoginMessage(
chatId: string, chatId: string,
messageId: number, messageId: number,
text: string, text: string,
token?: string,
expiresAt?: Date,
) { ) {
const replyMarkup = token && expiresAt
? loginReplyMarkup(token, expiresAt)
: { inline_keyboard: [] };
await callTelegram('editMessageText', { await callTelegram('editMessageText', {
chat_id: chatId, chat_id: chatId,
message_id: messageId, message_id: messageId,
text, text,
reply_markup: replyMarkup, reply_markup: { inline_keyboard: [] },
}); });
} }
function scheduleLoginMessageTimer( function scheduleLoginExpiration(
requestId: string, requestId: string,
token: string,
chatId: string, chatId: string,
messageId: number, messageId: number,
expiresAt: Date, expiresAt: Date,
) { ) {
const existingHandle = loginTimerHandles.get(requestId); const existingHandle = loginExpirationHandles.get(requestId);
if (existingHandle) { if (existingHandle) {
clearTimeout(existingHandle); clearTimeout(existingHandle);
} }
const tick = async () => { const handle = setTimeout(() => {
const request = await prisma.telegramLoginRequest.findUnique({ void (async () => {
where: { id: requestId },
select: { status: true },
});
if (!request || request.status !== 'CONFIRMED') {
loginTimerHandles.delete(requestId);
return;
}
if (expiresAt <= new Date()) {
const updated = await prisma.telegramLoginRequest.updateMany({ const updated = await prisma.telegramLoginRequest.updateMany({
where: { id: requestId, status: 'CONFIRMED' }, where: { id: requestId, status: 'CONFIRMED' },
data: { status: 'EXPIRED' }, data: { status: 'EXPIRED' },
}); });
loginTimerHandles.delete(requestId); loginExpirationHandles.delete(requestId);
if (updated.count > 0) { if (updated.count > 0) {
await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.'); await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.');
} }
return; })();
}, Math.max(0, expiresAt.getTime() - Date.now()));
loginExpirationHandles.set(requestId, handle);
}
function cancelLoginExpiration(requestId: string) {
const handle = loginExpirationHandles.get(requestId);
if (handle) {
clearTimeout(handle);
loginExpirationHandles.delete(requestId);
} }
}
await editLoginMessage(chatId, messageId, loginMessageText(expiresAt), token, expiresAt); async function expireLoginMessageNow(
const handle = setTimeout(() => { requestId: string,
void tick(); chatId: string,
}, 1000); messageId: number,
loginTimerHandles.set(requestId, handle); ) {
}; cancelLoginExpiration(requestId);
const updated = await prisma.telegramLoginRequest.updateMany({
where: { id: requestId, status: 'CONFIRMED' },
data: { status: 'EXPIRED' },
});
const handle = setTimeout(() => { if (updated.count > 0) {
void tick(); await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.');
}, 1000); }
loginTimerHandles.set(requestId, handle); }
async function refreshExpiredLoginMessage(
requestId: string,
chatId: string,
messageId: number,
) {
const request = await prisma.telegramLoginRequest.findUnique({
where: { id: requestId },
select: { status: true },
});
if (request?.status === 'CONFIRMED') {
await expireLoginMessageNow(requestId, chatId, messageId);
}
} }
export async function createTelegramBotLogin() { export async function createTelegramBotLogin() {
@@ -257,10 +264,18 @@ export async function completeTelegramBotLogin(token: string) {
} }
if (request.expiresAt <= new Date()) { if (request.expiresAt <= new Date()) {
if (request.telegramChatId && request.telegramMessageId) {
await refreshExpiredLoginMessage(
request.id,
request.telegramChatId,
request.telegramMessageId,
);
} else {
await prisma.telegramLoginRequest.update({ await prisma.telegramLoginRequest.update({
where: { id: request.id }, where: { id: request.id },
data: { status: 'EXPIRED' }, data: { status: 'EXPIRED' },
}); });
}
throw new Error('Telegram login token is expired.'); throw new Error('Telegram login token is expired.');
} }
@@ -268,6 +283,7 @@ export async function completeTelegramBotLogin(token: string) {
throw new Error('Telegram login is not confirmed.'); throw new Error('Telegram login is not confirmed.');
} }
cancelLoginExpiration(request.id);
await prisma.telegramLoginRequest.update({ await prisma.telegramLoginRequest.update({
where: { id: request.id }, where: { id: request.id },
data: { status: 'USED' }, data: { status: 'USED' },
@@ -314,7 +330,10 @@ export async function handleTelegramBotWebhook(
}); });
if (!request || request.status !== 'PENDING' || request.expiresAt <= new Date()) { if (!request || request.status !== 'PENDING' || request.expiresAt <= new Date()) {
await sendLoginMessage(chatId, 'Ссылка входа устарела.\nВернитесь на сайт и начните вход заново.'); await sendLoginMessage(
chatId,
'Ссылка входа устарела.\nВернитесь на сайт и начните вход заново.',
);
return; return;
} }
@@ -349,9 +368,8 @@ export async function handleTelegramBotWebhook(
}, },
}); });
scheduleLoginMessageTimer( scheduleLoginExpiration(
request.id, request.id,
token,
chatId.toString(), chatId.toString(),
sentMessage.message_id, sentMessage.message_id,
request.expiresAt, request.expiresAt,