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