diff --git a/backend/package.json b/backend/package.json index 87aa9a6..95a8dca 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,9 +16,9 @@ "@grammyjs/auto-retry": "^1.1.1", "@grammyjs/runner": "^2.0.3", "@prisma/client": "5.11.0", - "@telegum/grammy-buttons": "^0.4.0", - "@telegum/grammy-messages": "^0.4.0", - "@telegum/tgx": "^0.1.1", + "@telegum/grammy-buttons": "^0.5.1", + "@telegum/grammy-messages": "^0.5.1", + "@telegum/tgx": "^0.2.1", "axios": "^1.6.8", "dotenv": "^16.4.5", "grammy": "^1.22.4", diff --git a/backend/src/bot/handlers/commands/start.ts b/backend/src/bot/handlers/commands/menu.ts similarity index 93% rename from backend/src/bot/handlers/commands/start.ts rename to backend/src/bot/handlers/commands/menu.ts index 9b9afcf..ce52ef8 100644 --- a/backend/src/bot/handlers/commands/start.ts +++ b/backend/src/bot/handlers/commands/menu.ts @@ -4,7 +4,7 @@ import views from '~/bot/handlers/views' export default handler((composer) => { composer - .command('start') + .command('menu') .filter(filters.pm) .use(async (ctx) => { await ctx diff --git a/backend/src/bot/handlers/commands/start.tsx b/backend/src/bot/handlers/commands/start.tsx new file mode 100644 index 0000000..90f7ebb --- /dev/null +++ b/backend/src/bot/handlers/commands/start.tsx @@ -0,0 +1,49 @@ +import views from '~/bot/handlers/views' +import { handler } from '~/bot/handlers' +import filters from '~/bot/filters' +import { TelegramNotLinkedToInnohassleAccountError } from '~/domain/errors' +import { InnohassleLoginButton } from '~/bot/login-button' + +export default handler((composer) => { + composer + .command('start') + .filter(filters.pm) + .use(async (ctx) => { + let authorized + try { + authorized = await ctx.domain.isUserAuthorized(ctx.from.id) + } catch (error) { + authorized = false + if (error instanceof TelegramNotLinkedToInnohassleAccountError) { + // ignore + } else { + ctx.logger.error({ + msg: 'failed to check whether user is authorized', + error: error, + }) + } + } + + let content + if (authorized) { + content = await views.main.render(ctx, {}) + } else { + content = ( + <> + {ctx.t['WelcomeMessage.Unauthorized']} + + + {ctx.t['Buttons.LoginWithInnohassle']} + + + + ) + } + + await ctx.send(content).to(ctx.chat.id) + }) +}) diff --git a/backend/src/bot/handlers/root.ts b/backend/src/bot/handlers/root.tsx similarity index 53% rename from backend/src/bot/handlers/root.ts rename to backend/src/bot/handlers/root.tsx index e912cea..37bd668 100644 --- a/backend/src/bot/handlers/root.ts +++ b/backend/src/bot/handlers/root.tsx @@ -1,27 +1,24 @@ -import { InlineKeyboard } from 'grammy' import commands from './commands' import views from './views' import { handler } from '.' import { TelegramNotLinkedToInnohassleAccountError } from '~/domain/errors' +import { InnohassleLoginButton } from '~/bot/login-button' export default handler((composer) => { composer = composer.errorBoundary(async (err) => { if (err.error instanceof TelegramNotLinkedToInnohassleAccountError) { const ctx = err.ctx - const loginUrl = new URL(ctx.config.innohassle.telegramLoginUrl) - loginUrl.searchParams.set('bot', `${ctx.me.username}?start=_`) - await ctx.reply(ctx.t['InNoHassle.LinkAccountsRequest.Message'], { - reply_markup: new InlineKeyboard([[ - { - text: ctx.t['InNoHassle.LinkAccountsRequest.Button'], - login_url: { - url: loginUrl.toString(), - bot_username: ctx.config.innohassle.telegramBotUsername, - forward_text: ctx.t['InNoHassle.LinkAccountsRequest.ForwardText'], - }, - }, - ]]), - }) + await ctx + .send( + + {ctx.t['Buttons.LoginWithInnohassle']} + , + ) + .to(ctx.chat!.id) } else { throw err.error } diff --git a/backend/src/bot/login-button.tsx b/backend/src/bot/login-button.tsx new file mode 100644 index 0000000..203b9c7 --- /dev/null +++ b/backend/src/bot/login-button.tsx @@ -0,0 +1,24 @@ +export function InnohassleLoginButton({ + loginUrl: loginUrlStr, + loginBotUsername, + returnBotUsername, + children, +}: { + loginUrl: string + loginBotUsername: string + returnBotUsername: string + children: string +}) { + const loginUrl = new URL(loginUrlStr) + loginUrl.searchParams.set('bot', `${returnBotUsername}?start=_`) + return ( + + ) +} diff --git a/backend/src/domain/index.ts b/backend/src/domain/index.ts index 4779577..e9a72cd 100644 --- a/backend/src/domain/index.ts +++ b/backend/src/domain/index.ts @@ -142,6 +142,10 @@ export class Domain { }) } + public async isUserAuthorized(telegramId: number): Promise { + return !!(await this.getUserSportData(telegramId)) + } + private async requestSport( telegramId: number, method: M, diff --git a/backend/src/translations/_en.tsx b/backend/src/translations/_en.tsx index 4f737d4..fb58135 100644 --- a/backend/src/translations/_en.tsx +++ b/backend/src/translations/_en.tsx @@ -7,13 +7,26 @@ import { tgxFromHtml } from '~/utils/tgx-from-html' function dateLong(date: Date): string { const day = date.toLocaleDateString('en-US', { weekday: 'long', timeZone: TIMEZONE }) const month = date.toLocaleDateString('en-US', { month: 'long', timeZone: TIMEZONE }) - const dayOfMonth = date.getDate() - const year = date.getFullYear() + const dayOfMonth = date.toLocaleDateString('en-US', { day: 'numeric', timeZone: TIMEZONE }) + const year = date.toLocaleDateString('en-US', { year: 'numeric', timeZone: TIMEZONE }) return `${day}, ${month} ${dayOfMonth}, ${year}` } +function dateAndTimeShort( + startsAt: Date, + endsAt: Date, +): string { + const weekDayShort = startsAt.toLocaleDateString('en-US', { weekday: 'short', timeZone: TIMEZONE }) + const monthShort = startsAt.toLocaleDateString('en-US', { month: 'short', timeZone: TIMEZONE }) + const dayOfMonth = startsAt.toLocaleDateString('en-US', { day: 'numeric', timeZone: TIMEZONE }) + + return `${weekDayShort} ${monthShort} ${dayOfMonth}, ${clockTime(startsAt, TIMEZONE)}—${clockTime(endsAt, TIMEZONE)}` +} + export default { + 'WelcomeMessage.Unauthorized': 'Welcome to IU Sport Bot.\n\nPlease, login:', + 'Weekday.TwoLetters': (weekday: Weekday) => { switch (weekday) { case 'mon': return 'Mo' @@ -27,20 +40,21 @@ export default { }, 'Buttons.Back': '← Back', + 'Buttons.LoginWithInnohassle': 'Login with InNoHassle', 'HowGoodAmI.Thinking': 'Hmm... Let me think 🤔', 'HowGoodAmI.Answer': (percent: number) => `You're better than ${percent}% of students!`, 'HowGoodAmI.Failed': 'I don\'t know 🤷‍♂️', - 'Alert.CheckInSuccessful': (training: TrainingDetailed) => [ - '✅ Check-in successful', + 'Alert.CheckInSuccessful': ({ title, startsAt, endsAt }: TrainingDetailed) => [ + '✅ Check-in successful ✅', '', - `${training.title} at ${training.startsAt}`, + `${title}\n${dateAndTimeShort(startsAt, endsAt)}`, ].join('\n'), - 'Alert.CheckInCancelled': (training: TrainingDetailed) => [ - '❌ Check-in cancelled', + 'Alert.CheckInCancelled': ({ title, startsAt, endsAt }: TrainingDetailed) => [ + '❌ Check-in cancelled ❌', '', - `${training.title} at ${training.startsAt}`, + `${title}\n${dateAndTimeShort(startsAt, endsAt)}`, ].join('\n'), 'Alert.CheckInUnavailable': 'You cannot check-in for this training.', 'Alert.AlreadyCheckedIn': 'You are already checked in for this training.', @@ -103,8 +117,4 @@ export default { ), 'Views.Training.Buttons.CheckIn': 'Check-in', 'Views.Training.Buttons.CancelCheckIn': 'Cancel check-in', - - 'InNoHassle.LinkAccountsRequest.Message': 'To use this Telegram bot, please login to InNoHassle with your Telegram account. When you\'re done, send /start to continue!', - 'InNoHassle.LinkAccountsRequest.Button': 'Login with InNoHassle', - 'InNoHassle.LinkAccountsRequest.ForwardText': 'Link your Telegram to InNoHassle.', } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bd8175..2eb7f2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,14 +48,14 @@ importers: specifier: 5.11.0 version: 5.11.0(prisma@5.11.0) '@telegum/grammy-buttons': - specifier: ^0.4.0 - version: 0.4.0(@telegum/tgx@0.1.1)(grammy@1.22.4) + specifier: ^0.5.1 + version: 0.5.1(@telegum/tgx@0.2.1)(grammy@1.22.4) '@telegum/grammy-messages': - specifier: ^0.4.0 - version: 0.4.0(@telegum/tgx@0.1.1)(grammy@1.22.4) + specifier: ^0.5.1 + version: 0.5.1(@telegum/tgx@0.2.1)(grammy@1.22.4) '@telegum/tgx': - specifier: ^0.1.1 - version: 0.1.1 + specifier: ^0.2.1 + version: 0.2.1 axios: specifier: ^1.6.8 version: 1.6.8 @@ -1343,28 +1343,28 @@ packages: - typescript dev: true - /@telegum/grammy-buttons@0.4.0(@telegum/tgx@0.1.1)(grammy@1.22.4): - resolution: {integrity: sha512-zQUjwZ5wkxiRC1N22bfhR3jf0v6res758Gw+FMiX21jXszAFUd0lHxXHoFklaxvkzKGCdLsnZih9/kn82jO2DA==} + /@telegum/grammy-buttons@0.5.1(@telegum/tgx@0.2.1)(grammy@1.22.4): + resolution: {integrity: sha512-EHDyv37fKc4A43kJZuR2KSv0RFrC1sEPxHx5jC83Kqaq2Fe9jyy7W2D0jJG60FysMsjdK/96tzRussUBLnrecQ==} peerDependencies: - '@telegum/tgx': ^0.1.0 + '@telegum/tgx': ^0.2.1 grammy: ^1.22.4 dependencies: - '@telegum/tgx': 0.1.1 + '@telegum/tgx': 0.2.1 grammy: 1.22.4 dev: false - /@telegum/grammy-messages@0.4.0(@telegum/tgx@0.1.1)(grammy@1.22.4): - resolution: {integrity: sha512-XUA77yrstJlmcxWiKDZngdRhBFvtWx2ZJCrF1qtNmKVpdf3pYZ21dqqgs5pRNH/ey3Efv+StXEkogob18rHylA==} + /@telegum/grammy-messages@0.5.1(@telegum/tgx@0.2.1)(grammy@1.22.4): + resolution: {integrity: sha512-cmCjiACeR5/FadiVtEYwMaJveG/W9VDiXCuHJs6uKCFIFWyf9xRwbn1MlE9tY2Od4z8p+A28pRMvNg3XS/FsUw==} peerDependencies: - '@telegum/tgx': ^0.1.0 + '@telegum/tgx': ^0.2.1 grammy: ^1.22.4 dependencies: - '@telegum/tgx': 0.1.1 + '@telegum/tgx': 0.2.1 grammy: 1.22.4 dev: false - /@telegum/tgx@0.1.1: - resolution: {integrity: sha512-4MKeCaop+Bi7sLefbc1TWxkrFoyINmyUwZjcAVUfmVyRsavt2sV5rXJ4hlnVDVjkUNNjECTNUWp/BhVKn0aJdg==} + /@telegum/tgx@0.2.1: + resolution: {integrity: sha512-17mhEkEwwH2zIiXRwstfd0BgHOYCvwmGzqXAtrYtn5cVbFc+hzMAyDOzudtGO9SgNsJvr4rtWyuLV4Yfubh0RA==} dev: false /@tufjs/canonical-json@2.0.0: