Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2d31faa
chore: just for fun
lorenzocorallo Jan 29, 2026
02a2a38
refactor: move delete perform outside of tgLogger
lorenzocorallo Jan 29, 2026
3c82dfb
refactor: move moderation functions inside a Moderation obj
lorenzocorallo Jan 29, 2026
6abc4d2
refactor: moderation class
lorenzocorallo Jan 29, 2026
e6ae507
feat: getUser function, use Moderation in tgLogger menus
lorenzocorallo Jan 29, 2026
25e8442
fix: some revision
lorenzocorallo Jan 29, 2026
4754d7a
fix: typo and wrong field in multiChatSpam
lorenzocorallo Jan 29, 2026
94edd3e
fix: handle API call error in getUser
lorenzocorallo Jan 29, 2026
f10da09
feat: support retrieving user from memoryStorage
lorenzocorallo Jan 29, 2026
9761004
fix: new getUser return type
lorenzocorallo Jan 29, 2026
e356233
feat: handle preDel abort, better Moderation errors
lorenzocorallo Jan 30, 2026
2845b5d
fix: missing moderator in moderationAction log
lorenzocorallo Jan 30, 2026
3cad9b5
fix: command /del not using the new Moderation method
lorenzocorallo Jan 30, 2026
9d80057
feat: delete preLogs when forwarded but not deleted message(s)
lorenzocorallo Jan 30, 2026
44b31fb
fix: username arg with custom type, pass ctx to getUser
lorenzocorallo Jan 30, 2026
14928d2
fix: getUser ctx undefined propagation in optional chaining
lorenzocorallo Jan 30, 2026
bfe9a23
fix: typo
lorenzocorallo Jan 30, 2026
4ea2b98
fix: typos
lorenzocorallo Jan 30, 2026
db11e3d
refactor: moved UI actions middleware in new moderation stack
toto04 Jan 30, 2026
1badc83
docs: todoooooooo
toto04 Jan 30, 2026
6a51733
fix: use moderation stack in `group-specific-actions`
toto04 Jan 30, 2026
d18fed1
refactor: types
toto04 Jan 30, 2026
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
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- [x] /report to allow user to report (@admin is not implemented)
- [x] track ban, mute and kick done via telegram UI (not by command)
- [ ] controlled moderation flow (see #42)
- [ ] audit log (implemented, need to audit every mod action)
- [x] audit log (implemented, need to audit every mod action)
- [ ] send in-chat action log (deprived of chat ids and stuff)
- [x] automatic moderation
- [x] delete non-latin alphabet characters
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@grammyjs/menu": "^1.3.1",
"@grammyjs/parse-mode": "^1.11.1",
"@grammyjs/runner": "^2.0.3",
"@polinetwork/backend": "^0.15.2",
"@polinetwork/backend": "^0.15.3",
"@t3-oss/env-core": "^0.13.4",
"@trpc/client": "^11.5.1",
"@types/ssdeep.js": "^0.0.2",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ 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"
import { modules, sharedDataInit } from "./modules"
import { Moderation } from "./modules/moderation"
import { redis } from "./redis"
import { once } from "./utils/once"
import { setTelegramId } from "./utils/telegram-id"
Expand Down Expand Up @@ -78,7 +78,7 @@ bot.use(commands)
bot.use(new BotMembershipHandler())
bot.use(new AutoModerationStack())
bot.use(new GroupSpecificActions())
bot.use(new UIActionsLogger())
bot.use(Moderation)

bot.on("message", async (ctx, next) => {
const { username, id } = ctx.message.from
Expand Down
71 changes: 31 additions & 40 deletions src/commands/ban.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { logger } from "@/logger"
import { ban, unban } from "@/modules/moderation"
import { Moderation } from "@/modules/moderation"
import { duration } from "@/utils/duration"
import { fmt } from "@/utils/format"
import { getTelegramId } from "@/utils/telegram-id"
import { numberOrString } from "@/utils/types"
import { getUser } from "@/utils/users"
import { wait } from "@/utils/wait"

import { _commandsBase } from "./_base"

_commandsBase
Expand All @@ -25,22 +26,10 @@ _commandsBase
return
}

const res = await ban({
ctx: context,
target: repliedTo.from,
from: context.from,
message: repliedTo,
reason: args.reason,
})

if (res.isErr()) {
const msg = await context.reply(res.error)
await wait(5000)
await msg.delete()
return
}

await context.reply(res.value)
const res = await Moderation.ban(repliedTo.from, context.chat, context.from, null, [repliedTo], args.reason)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
.createCommand({
Expand Down Expand Up @@ -68,28 +57,22 @@ _commandsBase
return
}

const res = await ban({
ctx: context,
target: repliedTo.from,
from: context.from,
message: repliedTo,
duration: args.duration,
reason: args.reason,
})

if (res.isErr()) {
const msg = await context.reply(res.error)
await wait(5000)
await msg.delete()
return
}

await context.reply(res.value)
const res = await Moderation.ban(
repliedTo.from,
context.chat,
context.from,
args.duration,
[repliedTo],
args.reason
)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
.createCommand({
trigger: "unban",
args: [{ key: "username", optional: false, description: "Username (or user id) to unban" }],
args: [{ key: "username", type: numberOrString, description: "Username (or user id) to unban" }],
description: "Unban a user from a group",
scope: "group",
permissions: {
Expand All @@ -98,7 +81,9 @@ _commandsBase
},
handler: async ({ args, context }) => {
await context.deleteMessage()
const userId = args.username.startsWith("@") ? await getTelegramId(args.username) : parseInt(args.username, 10)
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}`)
const msg = await context.reply(fmt(({ b }) => b`@${context.from.username} user not found`))
Expand All @@ -107,12 +92,18 @@ _commandsBase
return
}

const res = await unban({ ctx: context, from: context.from, targetId: userId })
if (res.isErr()) {
const msg = await context.reply(res.error)
const user = await getUser(userId, context)
if (!user) {
const msg = await context.reply("Error: cannot find this user")
logger.error({ userId }, "UNBAN: cannot retrieve the user")
await wait(5000)
await msg.delete()
return
}

const res = await Moderation.unban(user, context.chat, context.from)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
11 changes: 8 additions & 3 deletions src/commands/del.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { logger } from "@/logger"
import { modules } from "@/modules"
import { Moderation } from "@/modules/moderation"
import { getText } from "@/utils/messages"
import { wait } from "@/utils/wait"
import { _commandsBase } from "./_base"

_commandsBase.createCommand({
Expand All @@ -13,6 +14,7 @@ _commandsBase.createCommand({
description: "Deletes the replied to message",
reply: "required",
handler: async ({ repliedTo, context }) => {
await context.deleteMessage()
const { text, type } = getText(repliedTo)
logger.info({
action: "delete_message",
Expand All @@ -21,7 +23,10 @@ _commandsBase.createCommand({
sender: repliedTo.from?.username,
})

await modules.get("tgLogger").delete([repliedTo], "Command /del", context.from) // actual message to delete
await context.deleteMessage() // /del message
const res = await Moderation.deleteMessages([repliedTo], context.from, "Command /del")
// TODO: better error and ok response
const msg = await context.reply(res.isErr() ? "Cannot delete the message" : "OK")
await wait(5000)
await msg.delete()
},
})
21 changes: 5 additions & 16 deletions src/commands/kick.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from "@/logger"
import { kick } from "@/modules/moderation"
import { Moderation } from "@/modules/moderation"
import { wait } from "@/utils/wait"

import { _commandsBase } from "./_base"
Expand All @@ -21,20 +21,9 @@ _commandsBase.createCommand({
return
}

const res = await kick({
ctx: context,
target: repliedTo.from,
from: context.from,
message: repliedTo,
reason: args.reason,
})
if (res.isErr()) {
const msg = await context.reply(res.error)
await wait(5000)
await msg.delete()
return
}

await context.reply(res.value)
const res = await Moderation.kick(repliedTo.from, context.chat, context.from, [repliedTo], args.reason)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
66 changes: 30 additions & 36 deletions src/commands/mute.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { logger } from "@/logger"
import { mute, unmute } from "@/modules/moderation"
import { Moderation } from "@/modules/moderation"
import { duration } from "@/utils/duration"
import { fmt } from "@/utils/format"
import { getTelegramId } from "@/utils/telegram-id"
import { numberOrString } from "@/utils/types"
import { getUser } from "@/utils/users"
import { wait } from "@/utils/wait"

import { _commandsBase } from "./_base"

_commandsBase
Expand Down Expand Up @@ -33,21 +34,17 @@ _commandsBase
return
}

const res = await mute({
ctx: context,
target: repliedTo.from,
message: repliedTo,
from: context.from,
duration: args.duration,
reason: args.reason,
})

if (res.isErr()) {
const msg = await context.reply(res.error)
await wait(5000)
await msg.delete()
return
}
const res = await Moderation.mute(
repliedTo.from,
context.chat,
context.from,
args.duration,
[repliedTo],
args.reason
)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
.createCommand({
Expand All @@ -67,25 +64,15 @@ _commandsBase
return
}

const res = await mute({
ctx: context,
target: repliedTo.from,
message: repliedTo,
from: context.from,
reason: args.reason,
})

if (res.isErr()) {
const msg = await context.reply(res.error)
await wait(5000)
await msg.delete()
return
}
const res = await Moderation.mute(repliedTo.from, context.chat, context.from, null, [repliedTo], args.reason)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
.createCommand({
trigger: "unmute",
args: [{ key: "username", optional: false, description: "Username (or user id) to unmute" }],
args: [{ key: "username", type: numberOrString, description: "Username (or user id) to unmute" }],
description: "Unmute a user from a group",
scope: "group",
permissions: {
Expand All @@ -94,7 +81,8 @@ _commandsBase
},
handler: async ({ args, context }) => {
await context.deleteMessage()
const userId = args.username.startsWith("@") ? await getTelegramId(args.username) : parseInt(args.username, 10)
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`))
Expand All @@ -103,12 +91,18 @@ _commandsBase
return
}

const res = await unmute({ ctx: context, from: context.from, targetId: userId })
if (res.isErr()) {
const msg = await context.reply(res.error)
const user = await getUser(userId, context)
if (!user) {
const msg = await context.reply("Error: cannot find this user")
logger.error({ userId }, "UNMUTE: cannot retrieve the user")
await wait(5000)
await msg.delete()
return
}

const res = await Moderation.unmute(user, context.chat, context.from)
const msg = await context.reply(res.isErr() ? res.error.fmtError : "OK")
await wait(5000)
await msg.delete()
},
})
Loading