feat: global banlist for role creation
GLOBAL_ADMINS env controls who can /gban, /gunban, /gbanlist. Banned users blocked from /roleadd across all chats.
This commit is contained in:
parent
85c7410ffb
commit
d00295b291
11 changed files with 434 additions and 2 deletions
8
drizzle/0003_wakeful_doctor_spectrum.sql
Normal file
8
drizzle/0003_wakeful_doctor_spectrum.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE "banned_users" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
"reason" text,
|
||||
"banned_by" bigint NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "banned_users_user_id_unique" UNIQUE("user_id")
|
||||
);
|
||||
281
drizzle/meta/0003_snapshot.json
Normal file
281
drizzle/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
{
|
||||
"id": "e566bacf-bce1-4a35-b020-5f9724aa4e99",
|
||||
"prevId": "92655a53-6225-4b92-87d3-8df7ba9778e8",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.banned_users": {
|
||||
"name": "banned_users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reason": {
|
||||
"name": "reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"banned_by": {
|
||||
"name": "banned_by",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"banned_users_user_id_unique": {
|
||||
"name": "banned_users_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.chat_configs": {
|
||||
"name": "chat_configs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"chat_id": {
|
||||
"name": "chat_id",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role_manage_permission": {
|
||||
"name": "role_manage_permission",
|
||||
"type": "role_manage_permission",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'all_admins'"
|
||||
},
|
||||
"role_mention_permission": {
|
||||
"name": "role_mention_permission",
|
||||
"type": "role_mention_permission",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'everyone'"
|
||||
},
|
||||
"role_admin_permission": {
|
||||
"name": "role_admin_permission",
|
||||
"type": "role_admin_permission",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'all_admins'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"chat_configs_chat_id_unique": {
|
||||
"name": "chat_configs_chat_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"chat_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.role_members": {
|
||||
"name": "role_members",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"role_id": {
|
||||
"name": "role_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"role_members_role_id_roles_id_fk": {
|
||||
"name": "role_members_role_id_roles_id_fk",
|
||||
"tableFrom": "role_members",
|
||||
"tableTo": "roles",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.roles": {
|
||||
"name": "roles",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(32)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"aliases": {
|
||||
"name": "aliases",
|
||||
"type": "varchar(32)[]",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'"
|
||||
},
|
||||
"chat_id": {
|
||||
"name": "chat_id",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.role_admin_permission": {
|
||||
"name": "role_admin_permission",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"everyone",
|
||||
"all_admins",
|
||||
"only_owner"
|
||||
]
|
||||
},
|
||||
"public.role_manage_permission": {
|
||||
"name": "role_manage_permission",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"everyone",
|
||||
"all_admins",
|
||||
"admin_can_promote_members",
|
||||
"admin_can_change_info",
|
||||
"admin_can_manage_chat",
|
||||
"only_owner"
|
||||
]
|
||||
},
|
||||
"public.role_mention_permission": {
|
||||
"name": "role_mention_permission",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"everyone",
|
||||
"all_admins",
|
||||
"only_owner"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1765468835132,
|
||||
"tag": "0002_bored_stephen_strange",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1771342325809,
|
||||
"tag": "0003_wakeful_doctor_spectrum",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import type { BotType } from '@/bot/index.js'
|
|||
import { bold, code, format, italic } from 'gramio'
|
||||
import { RESERVED_NAMES, ROLE_NAME_REGEX } from '@/bot/utilities/constants.js'
|
||||
import { updateChatCommands } from '@/bot/utilities/commands.js'
|
||||
import { BanlistService } from '@/shared/services/banlist.js'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
|
|
@ -11,6 +12,11 @@ export default (bot: BotType) => {
|
|||
return ctx.reply('This command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
const banned = await BanlistService.isBanned(ctx.from.id)
|
||||
if (banned) {
|
||||
return ctx.reply('you are globally banned from creating roles')
|
||||
}
|
||||
|
||||
const canManage = await PermissionService.canManageRoles(ctx.chat.id, ctx.from.id)
|
||||
if (!canManage) {
|
||||
return ctx.reply('You don\'t have permission to manage roles')
|
||||
|
|
|
|||
36
src/bot/commands/global/gban.ts
Normal file
36
src/bot/commands/global/gban.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { bold, code, format } from 'gramio'
|
||||
import env from '@/shared/env.js'
|
||||
import { BanlistService } from '@/shared/services/banlist.js'
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('gban', async (ctx) => {
|
||||
if (!env.globalAdmins.includes(ctx.from.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const args = (ctx.args ?? '').trim().split(/\s+/)
|
||||
const targetId = Number(args[0])
|
||||
const reason = args.slice(1).join(' ') || undefined
|
||||
|
||||
if (!targetId || Number.isNaN(targetId)) {
|
||||
return ctx.reply(format`usage: ${code('/gban <user_id> [reason]')}`)
|
||||
}
|
||||
|
||||
if (env.globalAdmins.includes(targetId)) {
|
||||
return ctx.reply('can\'t ban a global admin')
|
||||
}
|
||||
|
||||
const existing = await BanlistService.isBanned(targetId)
|
||||
if (existing) {
|
||||
return ctx.reply('user is already banned')
|
||||
}
|
||||
|
||||
const banned = await BanlistService.ban(targetId, ctx.from.id, reason).catch(() => null)
|
||||
if (!banned) {
|
||||
return ctx.reply('failed to ban user')
|
||||
}
|
||||
|
||||
return ctx.reply(format`${bold('user globally banned')}\nid: ${code(String(targetId))}${reason ? `\nreason: ${reason}` : ''}`)
|
||||
})
|
||||
}
|
||||
23
src/bot/commands/global/gbanlist.ts
Normal file
23
src/bot/commands/global/gbanlist.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import env from '@/shared/env.js'
|
||||
import { BanlistService } from '@/shared/services/banlist.js'
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('gbanlist', async (ctx) => {
|
||||
if (!env.globalAdmins.includes(ctx.from.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const banned = await BanlistService.list()
|
||||
|
||||
if (!banned.length) {
|
||||
return ctx.reply('banlist is empty')
|
||||
}
|
||||
|
||||
const lines = banned.map((b, i) =>
|
||||
`${i + 1}. ${b.userId}${b.reason ? ` — ${b.reason}` : ''}`
|
||||
)
|
||||
|
||||
return ctx.reply(`globally banned users:\n${lines.join('\n')}`)
|
||||
})
|
||||
}
|
||||
25
src/bot/commands/global/gunban.ts
Normal file
25
src/bot/commands/global/gunban.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { code, format } from 'gramio'
|
||||
import env from '@/shared/env.js'
|
||||
import { BanlistService } from '@/shared/services/banlist.js'
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('gunban', async (ctx) => {
|
||||
if (!env.globalAdmins.includes(ctx.from.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetId = Number((ctx.args ?? '').trim())
|
||||
|
||||
if (!targetId || Number.isNaN(targetId)) {
|
||||
return ctx.reply(format`usage: ${code('/gunban <user_id>')}`)
|
||||
}
|
||||
|
||||
const unbanned = await BanlistService.unban(targetId)
|
||||
if (!unbanned) {
|
||||
return ctx.reply('user is not banned')
|
||||
}
|
||||
|
||||
return ctx.reply(`user ${targetId} unbanned`)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export const ROLE_NAME_REGEX = /^[a-z]{4,32}$/i
|
||||
export const RESERVED_NAMES = ['roleadd', 'roledel', 'join', 'leave', 'myroles', 'roles', 'config', 'reload']
|
||||
export const RESERVED_NAMES = ['roleadd', 'roledel', 'join', 'leave', 'myroles', 'roles', 'config', 'reload', 'gban', 'gunban', 'gbanlist']
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { bigint, pgEnum, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||
import { bigint, pgEnum, pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||
|
||||
// Enums for permission levels
|
||||
export const roleManagePermissionEnum = pgEnum('role_manage_permission', [
|
||||
|
|
@ -40,6 +40,15 @@ export const roleMembersSchema = pgTable('role_members', {
|
|||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const bannedUsersSchema = pgTable('banned_users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull().unique(),
|
||||
reason: text('reason'),
|
||||
bannedBy: bigint('banned_by', { mode: 'number' }).notNull(),
|
||||
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const chatConfigSchema = pgTable('chat_configs', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
chatId: bigint('chat_id', { mode: 'number' }).notNull().unique(),
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export default {
|
|||
mode: env.get('NODE_ENV').default('production').asString(),
|
||||
databaseUrl: env.get('DATABASE_URL').required().asString(),
|
||||
botToken: env.get('BOT_TOKEN').required().asString(),
|
||||
globalAdmins: env.get('GLOBAL_ADMINS').default('').asString().split(',').filter(Boolean).map(Number),
|
||||
}
|
||||
|
|
|
|||
36
src/shared/services/banlist.ts
Normal file
36
src/shared/services/banlist.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
import database from '../database/index.js'
|
||||
import { bannedUsersSchema } from '../database/schema.js'
|
||||
|
||||
export class BanlistService {
|
||||
static isBanned(userId: number) {
|
||||
return database
|
||||
.select()
|
||||
.from(bannedUsersSchema)
|
||||
.where(eq(bannedUsersSchema.userId, userId))
|
||||
.then(result => result[0] ?? null)
|
||||
}
|
||||
|
||||
static ban(userId: number, bannedBy: number, reason?: string) {
|
||||
return database
|
||||
.insert(bannedUsersSchema)
|
||||
.values({ userId, bannedBy, reason })
|
||||
.returning()
|
||||
.then(result => result[0] ?? null)
|
||||
}
|
||||
|
||||
static unban(userId: number) {
|
||||
return database
|
||||
.delete(bannedUsersSchema)
|
||||
.where(eq(bannedUsersSchema.userId, userId))
|
||||
.returning()
|
||||
.then(result => result[0] ?? null)
|
||||
}
|
||||
|
||||
static list() {
|
||||
return database
|
||||
.select()
|
||||
.from(bannedUsersSchema)
|
||||
.orderBy(bannedUsersSchema.createdAt)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue