From 896fa7c2aac03e2e18591bd6e6e0cf731950f230 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 11 Apr 2026 18:57:24 +0200 Subject: [PATCH 1/3] feat: allow ban/mute first arg to be a user instead of repliedTo --- src/commands/moderation/ban.ts | 38 +++++++++++++++++++++++++++------ src/commands/moderation/mute.ts | 38 +++++++++++++++++++++++++++------ src/utils/users.ts | 33 +++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 96302ab..dc5f988 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -6,27 +6,53 @@ import { fmt } from "@/utils/format" import { ephemeral } from "@/utils/messages" import { getTelegramId } from "@/utils/telegram-id" import { numberOrString, type Role } from "@/utils/types" -import { getUser } from "@/utils/users" +import { getOverloadUser, getUser } from "@/utils/users" export const ban = new CommandsCollection("Banning") .createCommand({ trigger: "ban", - args: [{ key: "reason", optional: true, description: "Optional reason to ban the user" }], + args: [ + { + key: "reasonOrUser", + optional: true, + description: + "If the message is a reply, this argument is the reason. Otherwise, it's the username or user id of the user to ban", + type: numberOrString, + }, + { key: "reason", optional: true, description: "Optional reason to ban the user" }, + ], description: "Permanently ban a user from a group", scope: "group", - reply: "required", + reply: "optional", permissions: { allowedRoles: ["owner", "direttivo"], excludedRoles: ["creator"], allowGroupAdmins: true, }, handler: async ({ args, context, repliedTo }) => { - if (!repliedTo.from) { - logger.error("ban: no repliedTo.from field (the msg was sent in a channel)") + const userOverload = await getOverloadUser(context, repliedTo, args.reasonOrUser, args.reason) + if (userOverload.isErr()) { + await ephemeral( + context.reply( + repliedTo + ? fmt(({ n }) => n`There was an error`) + : fmt(({ n }) => n`Target user not found, please try replying to a their message`) + ) + ) + logger.error({ args, repliedTo }, `BAN: ${userOverload.error}`) return } - const res = await Moderation.ban(repliedTo.from, context.chat, context.from, null, [repliedTo], args.reason) + const { user, reason } = userOverload.value + + const res = await Moderation.ban( + user, + context.chat, + context.from, + null, + repliedTo ? [repliedTo] : undefined, + reason + ) if (res.isErr()) await ephemeral(context.reply(res.error.fmtError)) }, }) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 837d662..d3fa1be 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -6,7 +6,7 @@ import { fmt } from "@/utils/format" import { ephemeral } from "@/utils/messages" import { getTelegramId } from "@/utils/telegram-id" import { numberOrString, type Role } from "@/utils/types" -import { getUser } from "@/utils/users" +import { getOverloadUser, getUser } from "@/utils/users" export const mute = new CommandsCollection("Muting") .createCommand({ @@ -47,22 +47,48 @@ export const mute = new CommandsCollection("Muting") }) .createCommand({ trigger: "mute", - args: [{ key: "reason", optional: true, description: "Optional reason to mute the user" }], + args: [ + { + key: "reasonOrUser", + optional: true, + description: + "If the message is a reply, this argument is the reason. Otherwise, it's the username or user id of the user to mute", + type: numberOrString, + }, + { key: "reason", optional: true, description: "Optional reason to mute the user" }, + ], description: "Permanently mute a user from a group", scope: "group", - reply: "required", + reply: "optional", permissions: { allowedRoles: ["owner", "direttivo"], excludedRoles: ["creator"], allowGroupAdmins: true, }, handler: async ({ args, context, repliedTo }) => { - if (!repliedTo.from) { - logger.error("mute: no repliedTo.from field (the msg was sent in a channel)") + const userOverload = await getOverloadUser(context, repliedTo, args.reasonOrUser, args.reason) + if (userOverload.isErr()) { + await ephemeral( + context.reply( + repliedTo + ? fmt(({ n }) => n`There was an error`) + : fmt(({ n }) => n`Target user not found, please try replying to a their message`) + ) + ) + logger.error({ args, repliedTo }, `MUTE: ${userOverload.error}`) return } - const res = await Moderation.mute(repliedTo.from, context.chat, context.from, null, [repliedTo], args.reason) + const { user, reason } = userOverload.value + + const res = await Moderation.mute( + user, + context.chat, + context.from, + null, + repliedTo ? [repliedTo] : undefined, + reason + ) if (res.isErr()) await ephemeral(context.reply(res.error.fmtError)) }, }) diff --git a/src/utils/users.ts b/src/utils/users.ts index a5b3fbd..fc92169 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,6 +1,9 @@ import type { Context } from "grammy" -import type { User } from "grammy/types" +import type { Message, User } from "grammy/types" +import { Err, Ok, type Result } from "neverthrow" +import { logger } from "@/logger" import { MessageUserStorage } from "@/middlewares/message-user-storage" +import { getTelegramId } from "./telegram-id" export async function getUser(userId: number, ctx: C | null): Promise { // TODO: check if this works correctly @@ -26,3 +29,31 @@ export function printCtxFrom(ctx: C): string { if (!ctx.from) return "" return printUsername(ctx.from) } + +export async function getOverloadUser( + context: C, + repliedTo: Message | null, + firstArg?: string | number, + secondArg?: string +): Promise> { + if (repliedTo) { + if (!repliedTo.from) { + // error + return new Err("[getOverloadUser] no repliedTo.from field (the msg was sent in a channel)") + } + return new Ok({ user: repliedTo.from, reason: [firstArg, secondArg].filter(Boolean).join(" ") }) + } + + if (!firstArg) return new Err("[getOverloadUser] No firstArg passed (without repliedTo)") + + const userId = typeof firstArg === "number" ? firstArg : await getTelegramId(firstArg) + if (!userId) return new Err("[getOverloadUser] Cannot retrieve the userId from arg or redis") + + const user = await getUser(userId, context) + if (!user) return new Err("[getOverloadUser] Cannot retrieve the User from chatMember or storage") + + return new Ok({ + user, + reason: secondArg, + }) +} From e169b505dd1c6c78df3bced7f4a07a98ff8f4587 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 11 Apr 2026 19:02:18 +0200 Subject: [PATCH 2/3] fix: add a couple of catch --- src/utils/users.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/users.ts b/src/utils/users.ts index fc92169..93a34ea 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -46,10 +46,10 @@ export async function getOverloadUser( if (!firstArg) return new Err("[getOverloadUser] No firstArg passed (without repliedTo)") - const userId = typeof firstArg === "number" ? firstArg : await getTelegramId(firstArg) + const userId = typeof firstArg === "number" ? firstArg : await getTelegramId(firstArg).catch(() => null) if (!userId) return new Err("[getOverloadUser] Cannot retrieve the userId from arg or redis") - const user = await getUser(userId, context) + const user = await getUser(userId, context).catch(() => null) if (!user) return new Err("[getOverloadUser] Cannot retrieve the User from chatMember or storage") return new Ok({ From 4e9cd4da780b59dc50fb48faf8e7315c18126f29 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 11 Apr 2026 19:04:08 +0200 Subject: [PATCH 3/3] fix: typo --- src/commands/moderation/ban.ts | 2 +- src/commands/moderation/mute.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index dc5f988..00d4254 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -36,7 +36,7 @@ export const ban = new CommandsCollection("Banning") context.reply( repliedTo ? fmt(({ n }) => n`There was an error`) - : fmt(({ n }) => n`Target user not found, please try replying to a their message`) + : fmt(({ n }) => n`Target user not found, please try replying to their message`) ) ) logger.error({ args, repliedTo }, `BAN: ${userOverload.error}`) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index d3fa1be..00ac7f1 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -72,7 +72,7 @@ export const mute = new CommandsCollection("Muting") context.reply( repliedTo ? fmt(({ n }) => n`There was an error`) - : fmt(({ n }) => n`Target user not found, please try replying to a their message`) + : fmt(({ n }) => n`Target user not found, please try replying to their message`) ) ) logger.error({ args, repliedTo }, `MUTE: ${userOverload.error}`)