-
Notifications
You must be signed in to change notification settings - Fork 0
feat: new handlers for bot #158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import type { Ticket, User } from '@roll-stack/database' | ||
| import type { Context } from 'grammy' | ||
| import { repository } from '@roll-stack/database' | ||
| import { Bot } from 'grammy' | ||
|
|
@@ -9,12 +10,12 @@ const { telegram } = useRuntimeConfig() | |
| let bot: Bot | null = null | ||
|
|
||
| export async function useCreateWasabiBot() { | ||
| const botInDb = await repository.telegram.findBot(telegram.wasabiBotId) | ||
| if (!botInDb?.token) { | ||
| const token = await getBotToken() | ||
| if (!token) { | ||
| throw new Error('Wasabi bot is not configured') | ||
| } | ||
|
|
||
| bot = new Bot(botInDb.token) | ||
| bot = new Bot(token) | ||
|
|
||
| bot.on('message:text', async (ctx) => { | ||
| if (ctx.hasCommand('start')) { | ||
|
|
@@ -24,6 +25,22 @@ export async function useCreateWasabiBot() { | |
| return handleMessage(ctx) | ||
| }) | ||
|
|
||
| bot.on('message:photo', async (ctx) => { | ||
| return handlePhoto(ctx) | ||
| }) | ||
|
|
||
| bot.on('message:video', async (ctx) => { | ||
| return handleVideo(ctx) | ||
| }) | ||
|
|
||
| bot.on('message:document', async (ctx) => { | ||
| return handleFile(ctx) | ||
| }) | ||
|
|
||
| bot.on('message:file', async (ctx) => { | ||
| return handleFile(ctx) | ||
| }) | ||
|
|
||
| // Somebody invited bot to a group | ||
| bot.on('my_chat_member', async (ctx) => { | ||
| logger.log('my_chat_member', ctx.update) | ||
|
|
@@ -84,11 +101,102 @@ async function handleMessage(ctx: Context) { | |
| return | ||
| } | ||
|
|
||
| const telegramUser = await repository.telegram.findUserByTelegramIdAndBotId(ctx.message.from.id.toString(), telegram.wasabiBotId) | ||
| if (!telegramUser?.user) { | ||
| const data = await getUserAndTicket(ctx.message.from.id.toString()) | ||
| if (!data) { | ||
| return | ||
| } | ||
|
|
||
| await repository.ticket.createMessage({ | ||
| ticketId: data.ticket.id, | ||
| userId: data.user.id, | ||
| text: ctx.message.text, | ||
| }) | ||
|
|
||
| logger.log('message', data.user.id, ctx.message.from.id, ctx.message.text) | ||
| ctx.reply('Сообщение передано в службу поддержки.') | ||
| } | ||
|
|
||
| async function handlePhoto(ctx: Context) { | ||
| if (!ctx.message?.photo?.length) { | ||
| return | ||
| } | ||
|
|
||
| const data = await getUserAndTicket(ctx.message.from.id.toString()) | ||
| if (!data) { | ||
| return | ||
| } | ||
|
|
||
| const bestQuality = ctx.message.photo.pop() | ||
| if (!bestQuality) { | ||
| return | ||
| } | ||
|
|
||
| const botToken = await getBotToken() | ||
| if (!botToken) { | ||
| return null | ||
| } | ||
|
|
||
| const downloadUrl = await getFileDownloadUrl({ ctx, fileId: bestQuality.file_id, botToken }) | ||
|
|
||
| await repository.ticket.createMessage({ | ||
| ticketId: data.ticket.id, | ||
| userId: data.user.id, | ||
| text: JSON.stringify({ downloadUrl, photo: ctx.message.photo }), | ||
| }) | ||
|
|
||
| // Save photo? | ||
| logger.log('photo', data.user.id, ctx.message.from.id, ctx.message.text, ctx.message.photo, downloadUrl) | ||
| ctx.reply('Фото передано в службу поддержки.') | ||
| } | ||
|
|
||
|
Comment on lines
+119
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not persist or log Telegram download URLs (they embed the bot token). Apply this diff: async function handlePhoto(ctx: Context) {
if (!ctx.message?.photo?.length) {
return
}
const data = await getUserAndTicket(ctx.message.from.id.toString())
if (!data) {
return
}
- const bestQuality = ctx.message.photo.pop()
+ const bestQuality = ctx.message.photo[ctx.message.photo.length - 1]
if (!bestQuality) {
return
}
-
- const botToken = await getBotToken()
- if (!botToken) {
- return null
- }
-
- const downloadUrl = await getFileDownloadUrl({ ctx, fileId: bestQuality.file_id, botToken })
+ // Fetch file metadata without creating a URL that embeds the bot token
+ const file = await ctx.api.getFile(bestQuality.file_id)
+ if (!file?.file_path) {
+ logger.warn('photo:file_path missing for file_id', bestQuality.file_id)
+ return
+ }
await repository.ticket.createMessage({
ticketId: data.ticket.id,
userId: data.user.id,
- text: JSON.stringify({ downloadUrl, photo: ctx.message.photo }),
+ text: JSON.stringify({
+ fileId: bestQuality.file_id,
+ filePath: file.file_path,
+ sizes: ctx.message.photo,
+ caption: ctx.message.caption,
+ }),
})
// Save photo?
- logger.log('photo', data.user.id, ctx.message.from.id, ctx.message.text, ctx.message.photo, downloadUrl)
- ctx.reply('Фото передано в службу поддержки.')
+ logger.log('photo', {
+ userId: data.user.id,
+ tgUserId: ctx.message.from.id,
+ fileId: bestQuality.file_id,
+ filePath: file.file_path,
+ })
+ await ctx.reply('Фото передано в службу поддержки.')
}Follow-ups:
|
||
| async function handleVideo(ctx: Context) { | ||
| if (!ctx.message?.video) { | ||
| return | ||
| } | ||
|
|
||
| const data = await getUserAndTicket(ctx.message.from.id.toString()) | ||
| if (!data) { | ||
| return | ||
| } | ||
|
|
||
| await repository.ticket.createMessage({ | ||
| ticketId: data.ticket.id, | ||
| userId: data.user.id, | ||
| text: JSON.stringify(ctx.message.video), | ||
| }) | ||
|
|
||
| // Save video? | ||
| logger.log('video', data.user.id, ctx.message.from.id, ctx.message.text, ctx.message.video) | ||
| ctx.reply('Видео передано в службу поддержки.') | ||
| } | ||
|
|
||
| async function handleFile(ctx: Context) { | ||
| if (!ctx.message?.document) { | ||
| return | ||
| } | ||
|
|
||
| const data = await getUserAndTicket(ctx.message.from.id.toString()) | ||
| if (!data) { | ||
| return | ||
| } | ||
|
|
||
| await repository.ticket.createMessage({ | ||
| ticketId: data.ticket.id, | ||
| userId: data.user.id, | ||
| text: JSON.stringify(ctx.message.document), | ||
| }) | ||
|
|
||
| // Save file? | ||
| logger.log('file', data.user.id, ctx.message.from.id, ctx.message.text, ctx.message.document) | ||
| ctx.reply('Файл передан в службу поддержки.') | ||
| } | ||
|
|
||
| async function getUserAndTicket(telegramId: string): Promise<{ user: User, ticket: Ticket } | null> { | ||
| const telegramUser = await repository.telegram.findUserByTelegramIdAndBotId(telegramId, telegram.wasabiBotId) | ||
| if (!telegramUser?.user) { | ||
| return null | ||
| } | ||
|
|
||
| // Get last ticket | ||
| const tickets = await repository.ticket.listOpenedByUser(telegramUser.user.id) | ||
| let ticket = tickets?.[0] | ||
|
|
@@ -102,17 +210,29 @@ async function handleMessage(ctx: Context) { | |
| }) | ||
| } | ||
| if (!ticket) { | ||
| return | ||
| return null | ||
| } | ||
|
|
||
| await repository.ticket.createMessage({ | ||
| ticketId: ticket.id, | ||
| userId: telegramUser.user.id, | ||
| text: ctx.message.text, | ||
| }) | ||
| return { user: telegramUser.user, ticket } | ||
| } | ||
|
|
||
| logger.log('message', telegramUser.user.id, ctx.message.from.id, ctx.message.text) | ||
| ctx.reply('Сообщение передано в службу поддержки.') | ||
| async function getFileDownloadUrl(data: { ctx: Context, fileId: string, botToken: string }) { | ||
| // https://api.telegram.org/file/bot<token>/<file_path> | ||
| const file = await data.ctx.api.getFile(data.fileId) | ||
| if (!file) { | ||
| return null | ||
| } | ||
|
|
||
| return `https://api.telegram.org/file/bot${data.botToken}/${file.file_path}` | ||
| } | ||
|
|
||
| async function getBotToken(): Promise<string | null> { | ||
| const botInDb = await repository.telegram.findBot(telegram.wasabiBotId) | ||
| if (!botInDb?.token) { | ||
| return null | ||
| } | ||
|
|
||
| return botInDb.token | ||
| } | ||
|
|
||
| export function useWasabiBot(): Bot { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make bot initialization idempotent to prevent duplicate handlers and multiple start() calls.
If useCreateWasabiBot() can be called more than once (hot reload, startup race, health-checks), you'll re-register handlers and call start() again. Guard and early-return if already initialized.
Apply this diff:
export async function useCreateWasabiBot() { + if (bot) { + logger.info('Wasabi bot is already initialized; skipping re-init') + return + } const token = await getBotToken() if (!token) { throw new Error('Wasabi bot is not configured') } bot = new Bot(token) + bot.catch((err) => logger.error('Unhandled Wasabi bot error:', err))📝 Committable suggestion
🤖 Prompt for AI Agents