From a9a3fac6b8d5193957e7e5dfe777b28428d1070d Mon Sep 17 00:00:00 2001 From: Tommaso Morganti Date: Sat, 11 Apr 2026 18:38:51 +0200 Subject: [PATCH] feat: `ban` and `mute` command args overloading --- src/commands/moderation/ban.ts | 66 ++++++++++++++++++++++---------- src/commands/moderation/mute.ts | 67 +++++++++++++++++++++++---------- src/utils/users.ts | 15 ++++++++ 3 files changed, 108 insertions(+), 40 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 96302ab..a3d4cfc 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,17 +1,30 @@ +import type { User } from "grammy/types" import { CommandsCollection } from "@/lib/managed-commands" import { logger } from "@/logger" import { Moderation } from "@/modules/moderation" import { duration } from "@/utils/duration" 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 { getUserFromIdOrUsername } 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, + type: numberOrString, + 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", + }, + { + key: "reason", + optional: true, + description: "Reason to mute the user (only if the first argument is the username or user id)", + }, + ], description: "Permanently ban a user from a group", scope: "group", reply: "required", @@ -21,12 +34,34 @@ export const ban = new CommandsCollection("Banning") allowGroupAdmins: true, }, handler: async ({ args, context, repliedTo }) => { - if (!repliedTo.from) { - logger.error("ban: no repliedTo.from field (the msg was sent in a channel)") - return - } + let user: User | null = null + let reason: string | undefined - const res = await Moderation.ban(repliedTo.from, context.chat, context.from, null, [repliedTo], args.reason) + if (repliedTo) { + if (!repliedTo.from) { + logger.error("BAN: no repliedTo.from field (the msg was sent in a channel)") + return + } + user = repliedTo.from + reason = [args.reasonOrUser, args.reason].filter(Boolean).join(" ") ?? undefined + } else { + if (!args.reasonOrUser) { + const msg = await context.reply( + fmt(({ b }) => b`You must specify a user to ban or reply to one of their messages`) + ) + await ephemeral(msg) + return + } + user = await getUserFromIdOrUsername(args.reasonOrUser, context) + if (!user) { + const msg = await context.reply(fmt(({ n }) => n`Error: cannot find this user`)) + logger.error({ user: args.reasonOrUser }, "BAN: cannot retrieve the user") + await ephemeral(msg) + return + } + reason = args.reason + } + const res = await Moderation.ban(user, context.chat, context.from, null, repliedTo ? [repliedTo] : [], reason) if (res.isErr()) await ephemeral(context.reply(res.error.fmtError)) }, }) @@ -51,7 +86,7 @@ export const ban = new CommandsCollection("Banning") }, handler: async ({ args, context, repliedTo }) => { if (!repliedTo.from) { - logger.error("ban: no repliedTo.from field (the msg was sent in a channel)") + logger.error("TBAN: no repliedTo.from field (the msg was sent in a channel)") return } @@ -77,18 +112,9 @@ export const ban = new CommandsCollection("Banning") allowGroupAdmins: true, }, handler: async ({ args, context }) => { - const userId: number | null = - typeof args.username === "string" ? await getTelegramId(args.username.replaceAll("@", "")) : args.username - - if (!userId) { - logger.debug(`unban: no userId for username ${args.username}`) - await ephemeral(context.reply(fmt(({ b }) => b`@${context.from.username} user not found`))) - return - } - - const user = await getUser(userId, context) + const user = await getUserFromIdOrUsername(args.username, context) if (!user) { - logger.error({ userId }, "UNBAN: cannot retrieve the user") + logger.error({ user: args.username }, "UNBAN: cannot retrieve the user") await ephemeral(context.reply(fmt(({ n }) => [n`Error: cannot find this user`]))) return } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 837d662..23933de 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,12 +1,12 @@ +import type { User } from "grammy/types" import { CommandsCollection } from "@/lib/managed-commands" import { logger } from "@/logger" import { Moderation } from "@/modules/moderation" import { duration } from "@/utils/duration" 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 { getUserFromIdOrUsername } from "@/utils/users" export const mute = new CommandsCollection("Muting") .createCommand({ @@ -30,7 +30,7 @@ export const mute = new CommandsCollection("Muting") }, handler: async ({ args, context, repliedTo }) => { if (!repliedTo.from) { - logger.error("tmute: no repliedTo.from field (the msg was sent in a channel)") + logger.error("TMUTE: no repliedTo.from field (the msg was sent in a channel)") return } @@ -47,22 +47,58 @@ 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, + type: numberOrString, + 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", + }, + { + key: "reason", + optional: true, + description: "Reason to mute the user (only if the first argument is the username or user id)", + }, + ], 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)") - return + let user: User | null = null + let reason: string | undefined + + if (repliedTo) { + if (!repliedTo.from) { + logger.error("MUTE: no repliedTo.from field (the msg was sent in a channel)") + return + } + user = repliedTo.from + reason = [args.reasonOrUser, args.reason].filter(Boolean).join(" ") ?? undefined + } else { + if (!args.reasonOrUser) { + const msg = await context.reply( + fmt(({ b }) => b`You must specify a user to mute or reply to one of their messages`) + ) + await ephemeral(msg) + return + } + user = await getUserFromIdOrUsername(args.reasonOrUser, context) + if (!user) { + const msg = await context.reply(fmt(({ n }) => n`Error: cannot find this user`)) + logger.error({ user: args.reasonOrUser }, "MUTE: cannot retrieve the user") + await ephemeral(msg) + return + } + reason = args.reason } - const res = await Moderation.mute(repliedTo.from, context.chat, context.from, null, [repliedTo], args.reason) + const res = await Moderation.mute(user, context.chat, context.from, null, repliedTo ? [repliedTo] : [], reason) if (res.isErr()) await ephemeral(context.reply(res.error.fmtError)) }, }) @@ -77,19 +113,10 @@ export const mute = new CommandsCollection("Muting") allowGroupAdmins: true, }, handler: async ({ args, context }) => { - const userId: number | null = - typeof args.username === "string" ? await getTelegramId(args.username.replaceAll("@", "")) : args.username - if (!userId) { - logger.debug(`unmute: no userId for username ${args.username}`) - const msg = await context.reply(fmt(({ b }) => b`@${context.from.username} user not found`)) - await ephemeral(msg) - return - } - - const user = await getUser(userId, context) + const user = await getUserFromIdOrUsername(args.username, context) if (!user) { const msg = await context.reply(fmt(({ n }) => n`Error: cannot find this user`)) - logger.error({ userId }, "UNMUTE: cannot retrieve the user") + logger.error({ user: args.username }, "UNMUTE: cannot retrieve the user") await ephemeral(msg) return } diff --git a/src/utils/users.ts b/src/utils/users.ts index a5b3fbd..fa6c8d1 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,6 +1,8 @@ import type { Context } from "grammy" import type { User } from "grammy/types" +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 @@ -8,6 +10,19 @@ export async function getUser(userId: number, ctx: C | null): return chatUser?.user ?? MessageUserStorage.getInstance().getStoredUser(userId) } +export async function getUserFromIdOrUsername( + idOrUsername: number | string, + ctx: C | null +): Promise { + const userId = typeof idOrUsername === "string" ? await getTelegramId(idOrUsername.replaceAll("@", "")) : idOrUsername + if (!userId) { + logger.debug(`unmute: no userId for username ${idOrUsername}`) + return null + } + + return await getUser(userId, ctx) +} + /** * Formats a user's username and ID for logging. * @param user grammY User object