diff --git a/README.md b/README.md index 256637c..1fbdb9a 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,6 @@ Our new telegram bot. ```sh pnpm run dev ``` + +### Maybe useful references +- [How to send private messages](https://github.com/PoliNetworkOrg/PoliNetworkBot_CSharp/blob/03c7434f06323ffdec301cb105d1d3b2c1ed4a95/PoliNetworkBot_CSharp/Code/Utils/SendMessage.cs#L90) diff --git a/TODO.md b/TODO.md index 927a400..a7434bf 100644 --- a/TODO.md +++ b/TODO.md @@ -29,7 +29,7 @@ - [ ] exception to send our whatsapp links? - [ ] do not delete Direttivo's allowed messages - [x] check if user has username - - [ ] group-specific moderation (eg. #cerco #vendo in polihouse) see [here](https://github.com/PoliNetworkOrg/PoliNetworkBot_CSharp/blob/03c7434f06323ffdec301cb105d1d3b2c1ed4a95/PoliNetworkBot_CSharp/Code/Bots/Moderation/Blacklist/Blacklist.cs#L84) + - [x] group-specific moderation (eg. #cerco #vendo in polihouse) see [here](https://github.com/PoliNetworkOrg/PoliNetworkBot_CSharp/blob/03c7434f06323ffdec301cb105d1d3b2c1ed4a95/PoliNetworkBot_CSharp/Code/Bots/Moderation/Blacklist/Blacklist.cs#L84) - [x] role management - [x] setrole: set role for some username (only Direttivo, maybe HR) - [x] getrole: get user role diff --git a/src/bot.ts b/src/bot.ts index 6cfd4da..ef6df2c 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -12,6 +12,7 @@ import { logger } from "./logger" 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 { messageLink } from "./middlewares/message-link" import { MessageUserStorage } from "./middlewares/message-user-storage" import { UIActionsLogger } from "./middlewares/ui-actions-logger" @@ -76,6 +77,7 @@ bot.use(MenuGenerator.getInstance()) bot.use(commands) bot.use(new BotMembershipHandler()) bot.use(new AutoModerationStack()) +bot.use(new GroupSpecificActions()) bot.use(new UIActionsLogger()) bot.on("message", async (ctx, next) => { diff --git a/src/middlewares/group-specific-actions.ts b/src/middlewares/group-specific-actions.ts new file mode 100644 index 0000000..b9334bb --- /dev/null +++ b/src/middlewares/group-specific-actions.ts @@ -0,0 +1,119 @@ +import { Composer, type Filter, type MiddlewareObj } from "grammy" +import { err, ok, type Result } from "neverthrow" +import { api } from "@/backend" +import { logger } from "@/logger" +import { modules } from "@/modules" +import { fmt, fmtUser } from "@/utils/format" +import type { Context } from "@/utils/types" +import { wait } from "@/utils/wait" + +const TARGET_GROUPS: Record = { + alloggi: -1001175999519, + ripetizioni: -1001495422899, + books: -1001164044303, +} as const + +const TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS)); + +export class GroupSpecificActions implements MiddlewareObj { + private composer = new Composer() + + constructor() { + this.composer + .filter((ctx) => !!ctx.chatId && TARGET_GROUP_IDS_SET.has(ctx.chatId)) + .on("message", async (ctx, next) => { + if (ctx.from.id === ctx.me.id) return next() // skip if bot + const { roles } = await api.tg.permissions.getRoles.query({ userId: ctx.from.id }) + if (roles && roles.length > 0) return next() // skip if admin or other roles + + const chatMember = await ctx.getChatMember(ctx.from.id) + if (chatMember.status === "administrator" || chatMember.status === "creator") return next() // skip if group-admin + + let check: Result + switch (ctx.chatId) { + case TARGET_GROUPS.alloggi: + check = this.checkAlloggi(ctx) + break + + case TARGET_GROUPS.ripetizioni: + check = this.checkRipetizioni(ctx) + break + + case TARGET_GROUPS.books: + check = this.checkBooks(ctx) + break + + default: + logger.error( + { chatId: ctx.chatId, targetGroupsMap: TARGET_GROUPS }, + "GroupSpecificActions: target group matched, but no handler set. This is an unimplemented feature" + ) + return next() + } + + if (check.isOk()) return next() + + modules.get("tgLogger").delete([ctx.message], `User did not follow group rules:\n${check.error}`, ctx.me) + const reply = await ctx.reply( + fmt(({ b, n }) => [b`${fmtUser(ctx.from)} you sent an invalid message`, b`Reason:`, n`${check.error}`], { + sep: "\n", + }), + { disable_notification: false, reply_markup: { force_reply: true } } + ) + + // delete error msg after 2 min without blocking the middleware stack + void wait(120_000) + .then(() => reply.delete()) + .catch(() => {}) + }) + } + + checkAlloggi(ctx: Filter): Result { + const hashtags = ctx.entities("hashtag").map((e) => e.text.toLowerCase()) + + if ( + !hashtags.includes("#cerco") && + !hashtags.includes("#searching") && + !hashtags.includes("#search") && + !hashtags.includes("#offro") && + !hashtags.includes("#offering") && + !hashtags.includes("#offer") + ) + return err( + "You must include one of the following hashtags in your message:\n #cerco #searching #offro #offering \nCheck rules for more info." + ) + + return ok() + } + + checkRipetizioni(ctx: Filter): Result { + const hashtags = ctx.entities("hashtag").map((e) => e.text.toLowerCase()) + + if ( + !hashtags.includes("#richiesta") && + !hashtags.includes("#offerta") && + !hashtags.includes("#request") && + !hashtags.includes("#offer") + ) + return err( + "You must include one of the following hashtags in your message:\n #richiesta #request #offerta #offer \nCheck rules for more info." + ) + + return ok() + } + + checkBooks(ctx: Filter): Result { + const hashtags = ctx.entities("hashtag").map((e) => e.text.toLowerCase()) + + if (!hashtags.includes("#cerco") && !hashtags.includes("#vendo")) + return err( + "Devi includere uno di questi hashtags nel tuo messaggio:\n #cerco #vendo \nControlla le regole per maggiori indicazioni." + ) + + return ok() + } + + middleware() { + return this.composer.middleware() + } +}