Skip to content

Commit dbf78a5

Browse files
feat: implement user sync (#54)
* feat: user sync * refactor: rename middleware update backend package * refactor: extract type alias on DBUsers * fix: match new APIs
1 parent 5ed61fd commit dbf78a5

File tree

9 files changed

+137
-43
lines changed

9 files changed

+137
-43
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"@grammyjs/menu": "^1.3.1",
3838
"@grammyjs/parse-mode": "^1.11.1",
3939
"@grammyjs/runner": "^2.0.3",
40-
"@polinetwork/backend": "^0.10.0",
40+
"@polinetwork/backend": "^0.12.0",
4141
"@t3-oss/env-core": "^0.13.4",
4242
"@trpc/client": "^11.5.1",
4343
"@types/ssdeep.js": "^0.0.2",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bot.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { AutoModerationStack } from "./middlewares/auto-moderation-stack"
1414
import { BotMembershipHandler } from "./middlewares/bot-membership-handler"
1515
import { checkUsername } from "./middlewares/check-username"
1616
import { messageLink } from "./middlewares/message-link"
17-
import { MessageStorage } from "./middlewares/message-storage"
17+
import { MessageUserStorage } from "./middlewares/message-user-storage"
1818
import { UIActionsLogger } from "./middlewares/ui-actions-logger"
1919
import { redis } from "./redis"
2020
import { once } from "./utils/once"
@@ -87,7 +87,7 @@ bot.on("message", async (ctx, next) => {
8787
})
8888

8989
bot.on("message", messageLink({ channelIds: [TEST_CHAT_ID] })) // now is configured a test group
90-
bot.on("message", MessageStorage.getInstance())
90+
bot.on("message", MessageUserStorage.getInstance())
9191
bot.on("message", checkUsername)
9292
// bot.on("message", async (ctx, next) => { console.log(ctx.message); return await next() })
9393

@@ -120,7 +120,7 @@ const runner = run(bot, {
120120

121121
const terminate = once(async (signal: NodeJS.Signals) => {
122122
logger.warn(`Received ${signal}, shutting down...`)
123-
const p1 = MessageStorage.getInstance().sync()
123+
const p1 = MessageUserStorage.getInstance().sync()
124124
const p2 = redis.quit()
125125
const p3 = runner.isRunning() && runner.stop()
126126
await Promise.all([p1, p2, p3])

src/commands/_base.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ export const _commandsBase = new ManagedCommands<Role>({
3535
}
3636
}
3737

38-
const { role } = await api.tg.permissions.getRole.query({ userId: ctx.from.id })
39-
if (role === "user") return false // TODO: maybe we should do this differently
38+
const { roles } = await api.tg.permissions.getRoles.query({ userId: ctx.from.id })
39+
if (!roles) return false
4040

41-
const userRole = role as Role
42-
if (allowedRoles && !allowedRoles.includes(userRole)) return false
43-
if (excludedRoles?.includes(userRole)) return false
41+
// blacklist is stronger than whitelist
42+
if (allowedRoles?.every((r) => !roles.includes(r))) return false
43+
if (excludedRoles?.some((r) => roles.includes(r))) return false
4444

4545
return true
4646
},

src/commands/role.ts

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ const numberOrString = z.string().transform((s) => {
1313

1414
_commandsBase
1515
.createCommand({
16-
trigger: "getrole",
16+
trigger: "getroles",
1717
scope: "private",
18-
description: "Get role of userid",
18+
description: "Get roles of an user",
1919
args: [
2020
{
2121
key: "username",
@@ -33,24 +33,26 @@ _commandsBase
3333
}
3434

3535
try {
36-
const { role } = await api.tg.permissions.getRole.query({ userId })
37-
await context.reply(fmt(({ b }) => [`Role:`, b`${role}`]))
36+
const { roles } = await api.tg.permissions.getRoles.query({ userId })
37+
await context.reply(
38+
fmt(({ b }) => (roles?.length ? [`Roles:`, b`${roles.join(" ")}`] : "This user has no roles"))
39+
)
3840
} catch (err) {
3941
await context.reply(`There was an error: \n${String(err)}`)
4042
}
4143
},
4244
})
4345
.createCommand({
44-
trigger: "setrole",
46+
trigger: "addrole",
4547
scope: "private",
46-
description: "Set role of username",
48+
description: "Add role to user",
4749
args: [
4850
{
4951
key: "username",
5052
type: numberOrString,
5153
description: "The username or the user id of the user you want to update the role",
5254
},
53-
{ key: "role", type: z.enum<Role[]>(["direttivo", "hr", "admin"]) },
55+
{ key: "role", type: z.enum<Role[]>(["owner", "president", "direttivo", "hr", "admin"]) },
5456
],
5557
permissions: {
5658
allowedRoles: ["owner", "direttivo"],
@@ -65,16 +67,73 @@ _commandsBase
6567
}
6668

6769
try {
68-
const { role: prev } = await api.tg.permissions.getRole.query({ userId })
69-
await api.tg.permissions.setRole.query({ userId, adderId: context.from.id, role: args.role })
70+
const { roles, error } = await api.tg.permissions.addRole.mutate({
71+
userId,
72+
adderId: context.from.id,
73+
role: args.role,
74+
})
75+
76+
if (error) {
77+
await context.reply(fmt(({ n }) => n`There was an error: ${error}`))
78+
return
79+
}
80+
81+
await context.reply(
82+
fmt(
83+
({ b, n }) => [b`✅ Role added!`, n`${b`Username:`} ${args.username}`, n`${b`Updated roles:`} ${roles}`],
84+
{
85+
sep: "\n",
86+
}
87+
)
88+
)
89+
await context.deleteMessage()
90+
} catch (err) {
91+
await context.reply(`There was an error: \n${String(err)}`)
92+
}
93+
},
94+
})
95+
.createCommand({
96+
trigger: "delrole",
97+
scope: "private",
98+
description: "Remove role from an user",
99+
args: [
100+
{
101+
key: "username",
102+
type: numberOrString,
103+
description: "The username or the user id of the user you want to remove the role from",
104+
},
105+
{ key: "role", type: z.enum<Role[]>(["owner", "president", "direttivo", "hr", "admin"]) },
106+
],
107+
permissions: {
108+
allowedRoles: ["owner", "direttivo"],
109+
},
110+
handler: async ({ context, args }) => {
111+
const userId: number | null =
112+
typeof args.username === "string" ? await getTelegramId(args.username.replaceAll("@", "")) : args.username
113+
114+
if (userId === null) {
115+
await context.reply("Not a valid userId or username not in our cache")
116+
return
117+
}
118+
119+
try {
120+
const { roles, error } = await api.tg.permissions.removeRole.mutate({
121+
userId,
122+
removerId: context.from.id,
123+
role: args.role,
124+
})
125+
126+
if (error) {
127+
await context.reply(fmt(({ n }) => n`There was an error: ${error}`))
128+
return
129+
}
130+
70131
await context.reply(
71132
fmt(
72-
({ b, n }) => [
73-
b`✅ Role set!`,
74-
n`${b`Username:`} ${args.username}`,
75-
n`${b`Role:`} ${prev} -> ${args.role}`,
76-
],
77-
{ sep: "\n" }
133+
({ b, n }) => [b`✅ Role removed!`, n`${b`Username:`} ${args.username}`, n`${b`Updated roles:`} ${roles}`],
134+
{
135+
sep: "\n",
136+
}
78137
)
79138
)
80139
await context.deleteMessage()

src/middlewares/auto-moderation-stack/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { fmt, fmtUser } from "@/utils/format"
1212
import { createFakeMessage, getText } from "@/utils/messages"
1313
import type { Context } from "@/utils/types"
1414
import { wait } from "@/utils/wait"
15-
import { MessageStorage } from "../message-storage"
15+
import { MessageUserStorage } from "../message-user-storage"
1616
import { AIModeration } from "./ai-moderation"
1717
import { MULTI_CHAT_SPAM, NON_LATIN } from "./constants"
1818
import { checkForAllowedLinks } from "./functions"
@@ -206,7 +206,7 @@ export class AutoModerationStack<C extends Context> implements MiddlewareObj<C>
206206
.map(([hash, chatId, messageId]) => ({ hash, chatId: Number(chatId), messageId: Number(messageId) }))
207207
.filter((v) => ssdeep.similarity(v.hash, hash) > MULTI_CHAT_SPAM.SIMILARITY_THR)
208208
.map(async (v) => {
209-
const msg = await MessageStorage.getInstance().get(v.chatId, v.messageId)
209+
const msg = await MessageUserStorage.getInstance().get(v.chatId, v.messageId)
210210
const message = createFakeMessage(v.chatId, v.messageId, ctx.from, msg?.timestamp)
211211
return message
212212
})

src/middlewares/message-link.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { logger } from "@/logger"
55
import { padChatId } from "@/utils/chat"
66
import { fmt, fmtChat } from "@/utils/format"
77
import type { Context } from "@/utils/types"
8-
import { MessageStorage } from "./message-storage"
8+
import { MessageUserStorage } from "./message-user-storage"
99

1010
// --- Configuration ---
1111
const LINK_REGEX = /https?:\/\/t\.me\/c\/(-?\d+)\/(\d+)(?:\/(\d+))?/gi // Regex with global and case-insensitive flags
@@ -102,7 +102,7 @@ async function makeResponse(
102102
const inviteLink =
103103
chat.invite_link ?? (await api.tg.groups.getById.query({ telegramId: chat.id }))[0].link ?? undefined
104104

105-
const message = await MessageStorage.getInstance().get(chatId, messageId)
105+
const message = await MessageUserStorage.getInstance().get(chatId, messageId)
106106
if (message === null) {
107107
return {
108108
message: fmt(
Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import { Cron } from "croner"
22
import { Composer, type Context, type MiddlewareObj } from "grammy"
3-
import { api } from "@/backend"
3+
import type { User } from "grammy/types"
4+
import { type ApiInput, api } from "@/backend"
45
import { logger } from "@/logger"
56
import { padChatId } from "@/utils/chat"
67

78
export type Message = Parameters<typeof api.tg.messages.add.mutate>[0]["messages"][0]
9+
type DBUsers = ApiInput["tg"]["users"]["add"]["users"]
810

9-
export class MessageStorage<C extends Context> implements MiddlewareObj<C> {
10-
private static instance: MessageStorage<Context> | null = null
11-
static getInstance<C extends Context>(): MessageStorage<C> {
12-
if (!MessageStorage.instance) {
13-
MessageStorage.instance = new MessageStorage<Context>()
11+
export class MessageUserStorage<C extends Context> implements MiddlewareObj<C> {
12+
private static instance: MessageUserStorage<Context> | null = null
13+
static getInstance<C extends Context>(): MessageUserStorage<C> {
14+
if (!MessageUserStorage.instance) {
15+
MessageUserStorage.instance = new MessageUserStorage<Context>()
1416
}
15-
return MessageStorage.instance as unknown as MessageStorage<C>
17+
return MessageUserStorage.instance as unknown as MessageUserStorage<C>
1618
}
1719

1820
private composer: Composer<C> = new Composer<C>()
19-
private memoryStorage: Message[]
21+
private memoryStorage: Message[] = []
22+
private userStorage: Map<number, User> = new Map()
2023
private constructor() {
21-
this.memoryStorage = []
2224
new Cron("0 */1 * * * *", () => this.sync())
2325

2426
this.composer.on(["message:text", "message:caption"], (ctx, next) => {
@@ -36,6 +38,7 @@ export class MessageStorage<C extends Context> implements MiddlewareObj<C> {
3638
timestamp: new Date(ctx.message.date * 1000),
3739
})
3840

41+
this.userStorage.set(ctx.from.id, ctx.from)
3942
return next()
4043
})
4144
}
@@ -60,6 +63,10 @@ export class MessageStorage<C extends Context> implements MiddlewareObj<C> {
6063
}
6164

6265
async sync(): Promise<void> {
66+
await Promise.all([this.syncMessages(), this.syncUsers()])
67+
}
68+
69+
private async syncMessages(): Promise<void> {
6370
if (this.memoryStorage.length === 0) return
6471
const { error } = await api.tg.messages.add.mutate({ messages: this.memoryStorage })
6572
if (error) {
@@ -73,6 +80,34 @@ export class MessageStorage<C extends Context> implements MiddlewareObj<C> {
7380
this.memoryStorage = []
7481
}
7582

83+
private async syncUsers(): Promise<void> {
84+
if (this.userStorage.size === 0) return
85+
const users: DBUsers = this.userStorage
86+
.values()
87+
.toArray()
88+
.map((u) => ({
89+
id: u.id,
90+
firstName: u.first_name,
91+
lastName: u.last_name,
92+
username: u.username,
93+
isBot: u.is_bot,
94+
langCode: u.language_code,
95+
}))
96+
97+
this.userStorage.clear()
98+
99+
const { error } = await api.tg.users.add.mutate({ users })
100+
if (error === "ENCRYPT_ERROR") {
101+
logger.error("userStorage: There was an error while encrypting users in the backend, users voided")
102+
return
103+
} else if (error === "INTERNAL_SERVER_ERROR") {
104+
logger.error("userStorage: There was an UNEXPECTED error while saving users in backend, users voided")
105+
return
106+
}
107+
108+
logger.debug(`userStorage: ${users.length} users upserted in the database`)
109+
}
110+
76111
middleware() {
77112
return this.composer.middleware()
78113
}

src/utils/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ export type ContextWith<P extends OptionalPropertyOf<TContext>> = Exclude<TConte
1515
export type MaybePromise<T> = T | Promise<T>
1616

1717
export type Context = ManagedCommandsFlavor<TContext>
18-
export type Role = ApiInput["tg"]["permissions"]["setRole"]["role"]
18+
export type Role = ApiInput["tg"]["permissions"]["addRole"]["role"]

0 commit comments

Comments
 (0)