From 84d9da1a657cfd25bb765e28f94a255808cb3f93 Mon Sep 17 00:00:00 2001 From: Tommaso Morganti Date: Mon, 13 Apr 2026 19:53:08 +0200 Subject: [PATCH 1/2] fix: more robust link message parsing --- package.json | 2 +- pnpm-lock.yaml | 10 +-- src/middlewares/message-link.ts | 110 ++++++++++++++++++-------------- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index fdbc8a2..ebd05de 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@grammyjs/parse-mode": "^1.11.1", "@grammyjs/runner": "^2.0.3", "@influxdata/influxdb-client": "^1.35.0", - "@polinetwork/backend": "^0.15.17", + "@polinetwork/backend": "^0.15.18", "@socket.io/devalue-parser": "^0.1.0", "@t3-oss/env-core": "^0.13.4", "@trpc/client": "^11.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1072a1b..b4ef934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^1.35.0 version: 1.35.0 '@polinetwork/backend': - specifier: ^0.15.17 - version: 0.15.17 + specifier: ^0.15.18 + version: 0.15.18 '@socket.io/devalue-parser': specifier: ^0.1.0 version: 0.1.0 @@ -434,8 +434,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@polinetwork/backend@0.15.17': - resolution: {integrity: sha512-yZ2UvPpDTYds6ivOtF03/+u/BUaZimiPMzHFUsbuF5jiARimJb1Bz7t2OQQ9ajcCCaOi8Jy3D7Izb+8Zk6fxzw==} + '@polinetwork/backend@0.15.18': + resolution: {integrity: sha512-gT4TRMWIFNhwQi2mOx1OiH9HTP9BoorZHSvTti8vTCpk0qgKlL39ntAHQvFtDq1RbJ7rvS+DRd8fRBZkLVtOOw==} '@redis/bloom@1.2.0': resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} @@ -1841,7 +1841,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@polinetwork/backend@0.15.17': {} + '@polinetwork/backend@0.15.18': {} '@redis/bloom@1.2.0(@redis/client@1.6.0)': dependencies: diff --git a/src/middlewares/message-link.ts b/src/middlewares/message-link.ts index 24c9064..d0adc1b 100644 --- a/src/middlewares/message-link.ts +++ b/src/middlewares/message-link.ts @@ -5,14 +5,35 @@ import { logger } from "@/logger" import { TrackedMiddleware } from "@/modules/telemetry" import { padChatId } from "@/utils/chat" import { fmt, fmtChat, fmtUser } from "@/utils/format" -import { getText } from "@/utils/messages" import type { Context } from "@/utils/types" import { MessageUserStorage } from "./message-user-storage" // --- Configuration --- -const LINK_REGEX = /https?:\/\/t\.me\/c\/(-?\d+)\/(\d+)(?:\/(\d+))?/gi // Regex with global and case-insensitive flags +const LINK_REGEX = /https?:\/\/t\.me\/(?:c\/(-?\d+)|([\w\d]+))\/(\d+)(?:\/(\d+))?/i // Regex with global and case-insensitive flags const CHAR_LIMIT = 400 // How many initial rows of text to include +export async function parseTelegramMessageLink(link: string): Promise<{ + chatId: number + messageId: number +} | null> { + const match = link.match(LINK_REGEX) + if (!match) return null + + const chatHandle = match[2] + const chatId = chatHandle + ? await api.tg.groups.getByTag + .query({ tag: chatHandle }) + .then((r) => r?.telegramId ?? null) + .catch(() => null) + : parseInt(match[1], 10) + const messageId = match[4] ? parseInt(match[4], 10) : parseInt(match[3], 10) + + if (chatId === null) return null + if (Number.isNaN(chatId) || Number.isNaN(messageId)) return null + + return { chatId, messageId } +} + type Config = { chatIds: number[] textRowsLimit?: number @@ -24,55 +45,48 @@ export class MessageLink extends TrackedMiddleware { this.composer .filter((ctx) => !!ctx.chatId && config.chatIds.includes(ctx.chatId)) - .on(["message:text", "message:caption"]) - .filter( - (ctx) => getText(ctx.message).text.match(LINK_REGEX) !== null, - async (ctx, next) => { - logger.debug("[message-link] found a link to parse") - - const messageText = getText(ctx.message).text - - const matches = messageText.matchAll(LINK_REGEX) - const processedLinks: { chatId: number; messageId: number }[] = [] // Track processed links to avoid duplicates - - for (const match of matches) { - // Ensure we have capture groups - if (!match[1] || !match[2]) { - logger.warn(`[message-link] Regex matched but missing capture groups: ${match.join(" - ")}`) - continue - } - - const chatId = Number(match[1]) - const messageId = match[3] ? Number(match[3]) : Number(match[2]) - - // Skip if we've already processed this link in this message (e.g., multiple occurrences) - if (processedLinks.some((link) => link.chatId === chatId && link.messageId === messageId)) { - continue - } - logger.info( - { chatId, messageId, reporter: { username: ctx.from.username, id: ctx.from.id } }, - "[message-link] link parsed and sending response" - ) - processedLinks.push({ chatId, messageId }) - - const { message, inviteLink } = await makeResponse(ctx, chatId, messageId, ctx.from) - - const inlineKeyboard = new InlineKeyboard() - if (inviteLink) { - inlineKeyboard.url("Join Group", inviteLink) - } - - await ctx.reply(message, { - reply_markup: inlineKeyboard, - link_preview_options: { is_disabled: true }, // Prevent previewing the invite link in the reply itself - message_thread_id: ctx.chat.is_forum ? ctx.message.message_thread_id : undefined, - }) - await ctx.deleteMessage() + .on(["message:entities:url", "message:entities:text_link"]) + .use(async (ctx, next) => { + logger.debug("[message-link] found a link to parse") + + const links = ctx + .entities("text_link") + .map((e) => e.url) + .concat(ctx.entities("url").map((e) => e.text)) + + const processedLinks: { chatId: number; messageId: number }[] = [] // Track processed links to avoid duplicates + for (const link of links) { + const parsed = await parseTelegramMessageLink(link) + if (!parsed) continue + logger.debug({ parsed }, `[message-link] parsed link with regex`) + const { chatId, messageId } = parsed + + // Skip if we've already processed this link in this message (e.g., multiple occurrences) + if (processedLinks.some((link) => link.chatId === chatId && link.messageId === messageId)) { + continue + } + logger.info( + { chatId, messageId, reporter: { username: ctx.from.username, id: ctx.from.id } }, + "[message-link] link parsed and sending response" + ) + processedLinks.push({ chatId, messageId }) + + const { message, inviteLink } = await makeResponse(ctx, chatId, messageId, ctx.from) + + const inlineKeyboard = new InlineKeyboard() + if (inviteLink) { + inlineKeyboard.url("Join Group", inviteLink) } - return next() + await ctx.reply(message, { + reply_markup: inlineKeyboard, + link_preview_options: { is_disabled: true }, // Prevent previewing the invite link in the reply itself + message_thread_id: ctx.chat.is_forum ? ctx.message.message_thread_id : undefined, + }) + await ctx.deleteMessage() } - ) + return next() + }) } } From 56bb8c44ed19d7d2b1b23973ac6a0210eb38afcc Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Mon, 13 Apr 2026 20:02:32 +0200 Subject: [PATCH 2/2] fix: some fix --- src/bot.ts | 2 +- src/lib/group-management/index.ts | 1 + src/middlewares/message-link.ts | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index bb0fa91..af3d647 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -85,6 +85,7 @@ bot.use(new AutoModerationStack()) bot.use(new GroupSpecificActions()) bot.use(Moderation) bot.use(new MentionListener()) +bot.use(new MessageLink({ chatIds: [POLIADMINS] })) bot.on("message", async (ctx, next) => { const { username, id } = ctx.message.from @@ -93,7 +94,6 @@ bot.on("message", async (ctx, next) => { await next() }) -bot.on("message", new MessageLink({ chatIds: [POLIADMINS] })) bot.on("message", MessageUserStorage.getInstance()) bot.on("message", checkUsername) // bot.on("message", async (ctx, next) => { console.log(ctx.message); return await next() }) diff --git a/src/lib/group-management/index.ts b/src/lib/group-management/index.ts index 1234b54..5541f7c 100644 --- a/src/lib/group-management/index.ts +++ b/src/lib/group-management/index.ts @@ -18,6 +18,7 @@ function stripChatInfo(chat: ChatFullInfo) { is_forum: chat.is_forum, type: chat.type, invite_link: chat.invite_link, + username: chat.username, } } diff --git a/src/middlewares/message-link.ts b/src/middlewares/message-link.ts index d0adc1b..1dd9421 100644 --- a/src/middlewares/message-link.ts +++ b/src/middlewares/message-link.ts @@ -3,7 +3,7 @@ import type { User } from "grammy/types" import { api } from "@/backend" import { logger } from "@/logger" import { TrackedMiddleware } from "@/modules/telemetry" -import { padChatId } from "@/utils/chat" +import { padChatId, stripChatId } from "@/utils/chat" import { fmt, fmtChat, fmtUser } from "@/utils/format" import type { Context } from "@/utils/types" import { MessageUserStorage } from "./message-user-storage" @@ -23,7 +23,7 @@ export async function parseTelegramMessageLink(link: string): Promise<{ const chatId = chatHandle ? await api.tg.groups.getByTag .query({ tag: chatHandle }) - .then((r) => r?.telegramId ?? null) + .then((r) => stripChatId(r?.telegramId) ?? null) .catch(() => null) : parseInt(match[1], 10) const messageId = match[4] ? parseInt(match[4], 10) : parseInt(match[3], 10) @@ -71,7 +71,7 @@ export class MessageLink extends TrackedMiddleware { ) processedLinks.push({ chatId, messageId }) - const { message, inviteLink } = await makeResponse(ctx, chatId, messageId, ctx.from) + const { message, inviteLink } = await makeResponse(ctx, link, chatId, messageId, ctx.from) const inlineKeyboard = new InlineKeyboard() if (inviteLink) { @@ -94,13 +94,15 @@ type Response = { message: string inviteLink?: string } -async function makeResponse(ctx: Context, chatId: number, messageId: number, reporter: User): Promise { +async function makeResponse( + ctx: Context, + link: string, + chatId: number, + messageId: number, + reporter: User +): Promise { const headerRes = fmt( - ({ b, n }) => [ - b`Message link reported`, - n`${b`Reporter:`} ${fmtUser(reporter)}`, - n`${b`Link:`} https://t.me/c/${chatId}/${messageId}`, - ], + ({ b, n }) => [b`🚩 Message link reported`, n`${b`Reporter:`} ${fmtUser(reporter)}`, n`${b`Link:`} ${link}`], { sep: "\n" } )