Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use descriptive link text instead of "here".

Per static analysis (MD059), the link text "here" is not accessible or descriptive. Consider something like "reference implementation" or "C# bot implementation".

-  - [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] group-specific moderation (eg. #cerco #vendo in polihouse) see [reference implementation](https://github.com/PoliNetworkOrg/PoliNetworkBot_CSharp/blob/03c7434f06323ffdec301cb105d1d3b2c1ed4a95/PoliNetworkBot_CSharp/Code/Bots/Moderation/Blacklist/Blacklist.cs#L84)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [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] group-specific moderation (eg. #cerco #vendo in polihouse) see [reference implementation](https://github.com/PoliNetworkOrg/PoliNetworkBot_CSharp/blob/03c7434f06323ffdec301cb105d1d3b2c1ed4a95/PoliNetworkBot_CSharp/Code/Bots/Moderation/Blacklist/Blacklist.cs#L84)
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

32-32: Link text should be descriptive

(MD059, descriptive-link-text)

🤖 Prompt for AI Agents
In TODO.md around line 32, the checklist item contains a markdown link using the
text "here", which is non-descriptive; replace the link text with a descriptive
phrase like "reference implementation" or "C# bot implementation" (e.g., change
"[here](URL)" to "[C# bot implementation](URL)") so the link text conveys
destination and resolves MD059.

- [x] role management
- [x] setrole: set role for some username (only Direttivo, maybe HR)
- [x] getrole: get user role
Expand Down
2 changes: 2 additions & 0 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) => {
Expand Down
119 changes: 119 additions & 0 deletions src/middlewares/group-specific-actions.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> = {
alloggi: -1001175999519,
ripetizioni: -1001495422899,
books: -1001164044303,
} as const
Comment on lines +10 to +14
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Hard-coded group IDs make deployments and environment changes brittle. Consider moving these IDs to configuration (e.g., env variables or a config file) and parsing them at startup to avoid code changes per environment.

Copilot uses AI. Check for mistakes.

const TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix formatting: remove trailing semicolon.

The pipeline is failing due to a formatting mismatch. The project style doesn't use semicolons.

-const TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS));
+const TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS));
const TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS))
🧰 Tools
🪛 GitHub Actions: Test

[error] 16-16: File content differs from formatting output. Formatting check failed (e.g., Prettier/formatter output mismatch).

🤖 Prompt for AI Agents
In src/middlewares/group-specific-actions.ts around line 16, remove the trailing
semicolon from the line initializing TARGET_GROUP_IDS_SET (currently "const
TARGET_GROUP_IDS_SET = new Set(Object.values(TARGET_GROUPS));") so it matches
the project's no-semicolon formatting style; update the line to end without a
semicolon and ensure file passes linter/formatter.


export class GroupSpecificActions<C extends Context> implements MiddlewareObj<C> {
private composer = new Composer<C>()

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
Comment on lines +25 to +30
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx.from can be undefined for messages sent on behalf of a channel (sender_chat) or anonymous admins in supergroups, causing a runtime error when accessing ctx.from.id. Add a guard (e.g., if (!ctx.from) return next()) before using ctx.from and use optional chaining for the bot check; also avoid calling getChatMember when ctx.from is absent.

Copilot uses AI. Check for mistakes.

let check: Result<void, string>
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<C, "message">): Result<void, string> {
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."
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message for alloggi omits hashtags that are accepted by code (#search and #offer). Update the message to list all accepted tags, e.g., '... #cerco #searching #search #offro #offering #offer ...' to match the validation logic.

Suggested change
"You must include one of the following hashtags in your message:\n #cerco #searching #offro #offering \nCheck rules for more info."
"You must include one of the following hashtags in your message:\n #cerco #searching #search #offro #offering #offer \nCheck rules for more info."

Copilot uses AI. Check for mistakes.
)

return ok()
}
Comment on lines +71 to +87
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The hashtag validation logic is duplicated across checkAlloggi, checkRipetizioni, and checkBooks, which risks drift (already visible in the mismatched message). Consider extracting a generic helper like checkHashtags(ctx, allowed, errorMessage) and driving it from a per-group config map to keep logic and messages in sync.

Copilot uses AI. Check for mistakes.

checkRipetizioni(ctx: Filter<C, "message">): Result<void, string> {
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<C, "message">): Result<void, string> {
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()
Comment on lines +71 to +113
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The three check methods duplicate the same pattern. Consider extracting a reusable helper, e.g. checkHashtags(ctx, allowed, errorMessage), to centralize the entity extraction and membership logic and make future changes safer.

Suggested change
checkAlloggi(ctx: Filter<C, "message">): Result<void, string> {
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<C, "message">): Result<void, string> {
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<C, "message">): Result<void, string> {
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()
private checkHashtags(
ctx: Filter<C, "message">,
allowed: string[],
errorMessage: string
): Result<void, string> {
const hashtags = ctx.entities("hashtag").map((e) => e.text.toLowerCase())
if (!hashtags.some((tag) => allowed.includes(tag))) {
return err(errorMessage)
}
return ok()
}
checkAlloggi(ctx: Filter<C, "message">): Result<void, string> {
return this.checkHashtags(
ctx,
[
"#cerco",
"#searching",
"#search",
"#offro",
"#offering",
"#offer",
],
"You must include one of the following hashtags in your message:\n #cerco #searching #offro #offering \nCheck rules for more info."
)
}
checkRipetizioni(ctx: Filter<C, "message">): Result<void, string> {
return this.checkHashtags(
ctx,
[
"#richiesta",
"#offerta",
"#request",
"#offer",
],
"You must include one of the following hashtags in your message:\n #richiesta #request #offerta #offer \nCheck rules for more info."
)
}
checkBooks(ctx: Filter<C, "message">): Result<void, string> {
return this.checkHashtags(
ctx,
[
"#cerco",
"#vendo",
],
"Devi includere uno di questi hashtags nel tuo messaggio:\n #cerco #vendo \nControlla le regole per maggiori indicazioni."
)

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to @toto04

}

middleware() {
return this.composer.middleware()
}
}
Loading