From 5a59d14cc26fad6e06ab0c5e3f991291f482d294 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sun, 15 Mar 2026 19:41:50 +0100 Subject: [PATCH 1/5] feat: mention listener, handler for @admin --- src/bot.ts | 2 ++ src/commands/report.ts | 30 ++++++++++++++++--------- src/middlewares/mention-listener.ts | 34 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 src/middlewares/mention-listener.ts diff --git a/src/bot.ts b/src/bot.ts index 0527610..cfd3376 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -21,6 +21,7 @@ import { redis } from "./redis" import { once } from "./utils/once" import { setTelegramId } from "./utils/telegram-id" import type { Context, ModuleShared } from "./utils/types" +import { MentionListener } from "./middlewares/mention-listener" const TEST_CHAT_ID = -1002669533277 const ALLOWED_UPDATES: ReadonlyArray> = [ @@ -79,6 +80,7 @@ bot.use(new BotMembershipHandler()) bot.use(new AutoModerationStack()) bot.use(new GroupSpecificActions()) bot.use(Moderation) +bot.use(new MentionListener()) bot.on("message", async (ctx, next) => { const { username, id } = ctx.message.from diff --git a/src/commands/report.ts b/src/commands/report.ts index 9ebb0cb..f9dfbfb 100644 --- a/src/commands/report.ts +++ b/src/commands/report.ts @@ -1,8 +1,27 @@ +import type { Context, Filter } from "grammy" +import type { Message } from "grammy/types" +import type { ConversationContext } from "@/lib/managed-commands" import { logger } from "@/logger" import { modules } from "@/modules" import { fmt } from "@/utils/format" import { _commandsBase } from "./_base" +export const report = async ( + context: Filter | ConversationContext<"group"> | ConversationContext<"supergroup">, + repliedTo: Message +) => { + const reportSent = await modules.get("tgLogger").report(repliedTo, context.from) + await context.reply( + reportSent + ? fmt(({ b, n }) => [b`✅ Message reported!`, n`Moderators have been notified.`], { sep: "\n" }) + : fmt(({ b, n }) => [b`⚠️ Report not sent`, n`Please try again in a moment.`], { sep: "\n" }), + { + disable_notification: false, + reply_parameters: { message_id: repliedTo.message_id }, + } + ) +} + _commandsBase.createCommand({ trigger: "report", description: "Report a message to admins", @@ -15,15 +34,6 @@ _commandsBase.createCommand({ return } - const reportSent = await modules.get("tgLogger").report(repliedTo, context.from) - await context.reply( - reportSent - ? fmt(({ b, n }) => [b`✅ Message reported!`, n`Moderators have been notified.`], { sep: "\n" }) - : fmt(({ b, n }) => [b`⚠️ Report not sent`, n`Please try again in a moment.`], { sep: "\n" }), - { - disable_notification: false, - reply_parameters: { message_id: repliedTo.message_id }, - } - ) + await report(context, repliedTo) }, }) diff --git a/src/middlewares/mention-listener.ts b/src/middlewares/mention-listener.ts new file mode 100644 index 0000000..589ce25 --- /dev/null +++ b/src/middlewares/mention-listener.ts @@ -0,0 +1,34 @@ +import { Composer, type Filter, type MiddlewareObj } from "grammy" +import { report } from "@/commands/report" +import { logger } from "@/logger" +import type { Context } from "@/utils/types" + +type MentionContext = Filter +export class MentionListener implements MiddlewareObj { + private composer = new Composer() + + constructor() { + this.composer + .on("message:entities:mention") + .fork() + .filter( + (ctx) => ctx.entities("mention").some((m) => m.text === "@admin"), + (ctx) => this.handleReport(ctx) + ) + } + + middleware() { + return this.composer.middleware() + } + + private async handleReport(ctx: MentionContext) { + await ctx.deleteMessage() + const repliedTo = ctx.message.reply_to_message + if (!repliedTo?.from) { + logger.error("report: no repliedTo or repliedTo.from field (the msg was sent in a channel)") + return + } + + await report(ctx, repliedTo) + } +} From c6927f28fb6a15f404497c1c2ea197d129815904 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 18 Mar 2026 16:44:01 +0100 Subject: [PATCH 2/5] chore: biome --- src/bot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot.ts b/src/bot.ts index cfd3376..5371c9c 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,6 +13,7 @@ import { AutoModerationStack } from "./middlewares/auto-moderation-stack" import { BotMembershipHandler } from "./middlewares/bot-membership-handler" import { checkUsername } from "./middlewares/check-username" import { GroupSpecificActions } from "./middlewares/group-specific-actions" +import { MentionListener } from "./middlewares/mention-listener" import { messageLink } from "./middlewares/message-link" import { MessageUserStorage } from "./middlewares/message-user-storage" import { modules, sharedDataInit } from "./modules" @@ -21,7 +22,6 @@ import { redis } from "./redis" import { once } from "./utils/once" import { setTelegramId } from "./utils/telegram-id" import type { Context, ModuleShared } from "./utils/types" -import { MentionListener } from "./middlewares/mention-listener" const TEST_CHAT_ID = -1002669533277 const ALLOWED_UPDATES: ReadonlyArray> = [ From 0f7fdbc5407f84d99e4839db8325e3a80771cc9b Mon Sep 17 00:00:00 2001 From: Tommaso Morganti Date: Wed, 18 Mar 2026 17:09:28 +0100 Subject: [PATCH 3/5] fix: report method signature --- src/commands/report.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/commands/report.ts b/src/commands/report.ts index f9dfbfb..f1eafd8 100644 --- a/src/commands/report.ts +++ b/src/commands/report.ts @@ -1,15 +1,12 @@ import type { Context, Filter } from "grammy" import type { Message } from "grammy/types" -import type { ConversationContext } from "@/lib/managed-commands" +import type { CommandScopedContext } from "@/lib/managed-commands" import { logger } from "@/logger" import { modules } from "@/modules" import { fmt } from "@/utils/format" import { _commandsBase } from "./_base" -export const report = async ( - context: Filter | ConversationContext<"group"> | ConversationContext<"supergroup">, - repliedTo: Message -) => { +export const report = async (context: Filter | CommandScopedContext, repliedTo: Message) => { const reportSent = await modules.get("tgLogger").report(repliedTo, context.from) await context.reply( reportSent From 43c81866f0a85d3e312a92813d353167d7d96f43 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 18 Mar 2026 17:19:39 +0100 Subject: [PATCH 4/5] docs: update TODO.md --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index b2f4b01..997f9dc 100644 --- a/TODO.md +++ b/TODO.md @@ -17,7 +17,7 @@ - [x] advanced moderation - [x] ban_all - [x] unban_all - - [x] /report to allow user to report (@admin is not implemented) + - [x] /report to allow user to report - [x] track ban, mute and kick done via telegram UI (not by command) - [ ] controlled moderation flow (see #42) - [x] audit log (implemented, need to audit every mod action) From 1670376ddac9638e6d8a313a8beb60f28d807c06 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 18 Mar 2026 17:25:38 +0100 Subject: [PATCH 5/5] fix: merge --- src/commands/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index e3f7206..54420af 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -10,7 +10,7 @@ import { printCtxFrom } from "@/utils/users" import { linkAdminDashboard } from "./link-admin-dashboard" import { management } from "./management" import { moderation } from "./moderation" -import { logReport } from "./report" +import { report } from "./report" import { search } from "./search" const adapter = new RedisFallbackAdapter>({ @@ -23,13 +23,13 @@ export const commands = new ManagedCommands({ adapter, hooks: { wrongScope: async ({ context, command }) => { - await context.deleteMessage().catch(() => { }) + await context.deleteMessage().catch(() => {}) logger.info( `[ManagedCommands] Command '/${command.trigger}' with scope '${command.scope}' invoked by ${printCtxFrom(context)} in a '${context.chat.type}' chat` ) }, missingPermissions: async ({ context, command }) => { - await context.deleteMessage().catch(() => { }) + await context.deleteMessage().catch(() => {}) logger.info( { command_permissions: command.permissions }, `[ManagedCommands] Command '/${command.trigger}' invoked by ${printCtxFrom(context)} without permissions` @@ -45,7 +45,7 @@ export const commands = new ManagedCommands({ beforeHandler: async ({ context }) => { if (context.chat.type !== "private") { // silently delete the command trigger if the command is used in a group, to reduce noise - context.deleteMessage().catch(() => { }) + context.deleteMessage().catch(() => {}) } }, overrideGroupAdminCheck: async (userId, groupId, ctx) => { @@ -69,4 +69,4 @@ export const commands = new ManagedCommands({ await context.reply("pong") }, }) - .withCollection(linkAdminDashboard, logReport, search, management, moderation) + .withCollection(linkAdminDashboard, report, search, management, moderation)