Conversation
WalkthroughThe Wasabi Telegram bot now initializes its token from the database, adds handlers for text, photos, videos, and files, and persists incoming content to user tickets. New helper functions map Telegram users to app users/tickets, build Telegram file download URLs, and centralize token retrieval. Bot start is wrapped with error logging. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant TG as Telegram User
participant Bot as Wasabi Bot
participant Svc as WasabiBot Service
participant Repo as repository.telegram / tickets
participant TGAPI as Telegram API
Note over Bot,Svc: Startup
Bot->>Svc: start()
Svc->>Repo: getBotToken(botId)
alt token found
Svc-->>Bot: init with token
else no token
Svc-->>Bot: throw "Wasabi bot is not configured"
end
rect rgba(220,245,255,0.5)
Note over TG,Bot: Incoming message (text/photo/video/file)
TG-->>Bot: message
Bot->>Svc: handleMessage/Photo/Video/File(ctx)
Svc->>Repo: getUserAndTicket(telegramId)
alt user & open ticket
opt media (photo/video/document)
Svc->>TGAPI: getFile(fileId) (photo: choose best size)
Svc->>Svc: getFileDownloadUrl(file.file_id, token)
Svc-->>Repo: persist ticket message (JSON payload)
end
Svc-->>Repo: persist ticket message (text)
Svc-->>TG: reply ack
else not mapped
Svc-->>TG: reply not configured/ignored
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web-app/server/services/telegram/wasabi-bot.ts (2)
200-211: Guard for undefined list, and avoid duplicate ticket races.
- listOpenedByUser may return undefined; you already used optional chaining, but the subsequent length access is unsafe.
- Two concurrent messages from a user with no open ticket can create two tickets. Gate creation with a transaction or a unique partial index on (userId, status='opened').
Apply this diff to harden the null path:
- const tickets = await repository.ticket.listOpenedByUser(telegramUser.user.id) + const tickets = (await repository.ticket.listOpenedByUser(telegramUser.user.id)) ?? [] let ticket = tickets?.[0] if (!tickets.length || !ticket) { // Create ticket ticket = await repository.ticket.create({Operational follow-up:
- Add a DB constraint or repository-level “findOrCreateOpenedTicket(userId)” that retries on unique‑violation.
49-56: Do not await bot.start(); it stays pending during long-polling. apps/web-app/server/services/telegram/wasabi-bot.ts (lines 49–56): removeawait bot.start()— callbot.start()(no await) and handle startup errors viabot.catch(...)or theonStart/start options.
🧹 Nitpick comments (4)
apps/web-app/server/services/telegram/wasabi-bot.ts (4)
104-117: Await replies to handle errors and ensure ordering.
These handlers are async; not awaiting ctx.reply may drop errors and race with process exit.Apply this diff:
logger.log('message', data.user.id, ctx.message.from.id, ctx.message.text) - ctx.reply('Сообщение передано в службу поддержки.') + await ctx.reply('Сообщение передано в службу поддержки.')
152-171: Await reply and consider persisting a compact subset of video fields.
Await the reply; storing the entire object may bloat the DB—prefer a minimal subset (file_id, file_unique_id, mime, size, caption).Apply this minimal diff:
- ctx.reply('Видео передано в службу поддержки.') + await ctx.reply('Видео передано в службу поддержки.')Optional (example shape, adjust as needed):
- text: JSON.stringify(ctx.message.video), + text: JSON.stringify({ + fileId: ctx.message.video.file_id, + fileUniqueId: ctx.message.video.file_unique_id, + mimeType: ctx.message.video.mime_type, + fileSize: ctx.message.video.file_size, + width: ctx.message.video.width, + height: ctx.message.video.height, + duration: ctx.message.video.duration, + caption: ctx.message.caption, + }),
173-192: Await reply and trim stored document fields.
Await the reply; consider storing only file_id, mime_type, file_size, file_name, caption.Apply this minimal diff:
- ctx.reply('Файл передан в службу поддержки.') + await ctx.reply('Файл передан в службу поддержки.')
229-236: Avoid hot-path DB lookups for the bot token.
getBotToken() is invoked inside handlers in the current code. Either:
- Fetch once at startup and reuse, or
- Cache with a TTL and add an admin-triggered refresh/rotation path.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web-app/server/services/telegram/wasabi-bot.ts(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web-app/server/services/telegram/wasabi-bot.ts (2)
packages/database/src/repository/index.ts (1)
repository(63-63)packages/database/src/types.ts (2)
User(44-44)Ticket(179-179)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web-app/server/services/telegram/wasabi-bot.ts (3)
1-1: Types import looks good.
No issues with the added types import.
28-43: Do not remove the 'message:file' handler — grammY supports it.
message:file (or :file) is a valid shortcut that matches any message containing a file (photo, animation, audio, document, video, video_note, voice, sticker); keep it if you want to catch all file types, or use message:document only when you intend to handle documents exclusively.Likely an incorrect or invalid review comment.
219-227: Return file_path instead of token-bearing URL.Helpers must not construct or return URLs containing bot tokens; return the Telegram File.file_path and let the backend produce ephemeral/proxied links.
Apply this diff:
-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 getFilePath(data: { ctx: Context, fileId: string }) { + const file = await data.ctx.api.getFile(data.fileId) + if (!file?.file_path) { + return null + } + return file.file_path +}Reminder: update call sites accordingly (handlePhoto should call ctx.api.getFile or use the new getFilePath result as appropriate).
| 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) | ||
|
|
There was a problem hiding this comment.
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
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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) | |
| 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)) |
🤖 Prompt for AI Agents
In apps/web-app/server/services/telegram/wasabi-bot.ts around lines 12 to 19,
the code always constructs a new Bot(token) and registers handlers/start which
can run multiple times on hot reloads; make initialization idempotent by
checking an existing bot instance and returning early if already initialized
(optionally verify the existing bot was created with the same token), only
create new Bot and register handlers/start when no bot exists, and ensure any
exported/shared state (bot variable) is set once so duplicate handlers and
start() calls are avoided.
| 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('Фото передано в службу поддержки.') | ||
| } | ||
|
|
There was a problem hiding this comment.
Do not persist or log Telegram download URLs (they embed the bot token).
Storing/logging downloadUrl leaks the bot token (a secret). Also, using Array.pop() mutates ctx.message.photo and drops the highest-quality size from the array you later persist. Fix both.
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:
- If you need a usable link for agents, generate it on-demand server-side (proxy endpoint) so the token never leaves the backend.
- Scrub past logs/DB entries created by this branch that may already contain the token. Rotate the bot token after deploying the fix.



Summary by CodeRabbit