refactor: simplify codebase with unified patterns and reduced duplication
- unify ChatConfigService setters into generic setPermission method - replace PermissionService switch statements with data-driven checkers map - deduplicate config.ts callback handlers via loop-based registration (414→254 lines) - extract shared updateChatCommands utility from role-add/role-delete - add proper generics to chunk utility, fix mention loop indexing - extract isHiddenAdmin helper and ANONYMOUS_ADMIN_ID constant in perms - remove debug console.logs, standardize arrow exports and type imports - add roleAdminPermission schema and migration
This commit is contained in:
parent
1eb2200fbe
commit
85c7410ffb
21 changed files with 638 additions and 364 deletions
2
drizzle/0002_bored_stephen_strange.sql
Normal file
2
drizzle/0002_bored_stephen_strange.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
CREATE TYPE "public"."role_admin_permission" AS ENUM('everyone', 'all_admins', 'only_owner');--> statement-breakpoint
|
||||
ALTER TABLE "chat_configs" ADD COLUMN "role_admin_permission" "role_admin_permission" DEFAULT 'all_admins' NOT NULL;
|
||||
228
drizzle/meta/0002_snapshot.json
Normal file
228
drizzle/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
{
|
||||
"id": "92655a53-6225-4b92-87d3-8df7ba9778e8",
|
||||
"prevId": "fb792b24-bbbd-4b38-9191-24a83d05d277",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,13 @@
|
|||
"when": 1764313289570,
|
||||
"tag": "0001_legal_blob",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1765468835132,
|
||||
"tag": "0002_bored_stephen_strange",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2,20 +2,17 @@ import type { BotType } from '@/bot/index.js'
|
|||
import { bold, format } from 'gramio'
|
||||
import { isChatAdmin, refreshAdminCache } from '@/bot/utilities/perms.js'
|
||||
|
||||
export default function (bot: BotType) {
|
||||
// Command to refresh admin cache
|
||||
export default (bot: BotType) => {
|
||||
bot.command('reload', async (ctx) => {
|
||||
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||
return ctx.reply('This command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
// Any admin can refresh cache
|
||||
const isAdmin = await isChatAdmin(ctx.chat.id, ctx.from.id)
|
||||
if (!isAdmin) {
|
||||
return ctx.reply('This command can only be used by admins')
|
||||
}
|
||||
|
||||
// Refresh the admin cache for this chat
|
||||
refreshAdminCache(ctx.chat.id)
|
||||
|
||||
return ctx.reply(
|
||||
|
|
|
|||
|
|
@ -1,43 +1,81 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { bold, format, InlineKeyboard, italic } from 'gramio'
|
||||
import { isChatAdmin, isChatOwner } from '@/bot/utilities/perms.js'
|
||||
import { ChatConfigService, type RoleManagePermission, type RoleMentionPermission } from '@/shared/services/chatConfig.js'
|
||||
import { ChatConfigService, type ChatConfig, type RoleAdminPermission, type RoleManagePermission, type RoleMentionPermission } from '@/shared/services/chatConfig.js'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { bot } from '@/bot/index.js'
|
||||
|
||||
// Shared keyboard builders for consistent UI
|
||||
const MANAGE_PERMISSION_OPTIONS: { value: RoleManagePermission, label: string }[] = [
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'all_admins', label: 'All Admins' },
|
||||
{ value: 'admin_can_promote_members', label: 'Admins: Promote Members' },
|
||||
{ value: 'admin_can_change_info', label: 'Admins: Change Info' },
|
||||
{ value: 'admin_can_manage_chat', label: 'Admins: Manage Chat' },
|
||||
{ value: 'only_owner', label: 'Only Owner' },
|
||||
]
|
||||
type PermissionType = 'manage' | 'mention' | 'admin'
|
||||
|
||||
const MENTION_PERMISSION_OPTIONS: { value: RoleMentionPermission, label: string }[] = [
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'all_admins', label: 'All Admins' },
|
||||
{ value: 'only_owner', label: 'Only Owner' },
|
||||
]
|
||||
|
||||
function buildManageKeyboard(currentValue: RoleManagePermission): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
for (const option of MANAGE_PERMISSION_OPTIONS) {
|
||||
const isSelected = option.value === currentValue
|
||||
const label = isSelected ? `✓ ${option.label}` : option.label
|
||||
keyboard.text(label, `config:manage:${option.value}`).row()
|
||||
}
|
||||
keyboard.text('« Back to Config', 'config:back')
|
||||
return keyboard
|
||||
interface PermissionOption<T> {
|
||||
value: T
|
||||
label: string
|
||||
}
|
||||
|
||||
function buildMentionKeyboard(currentValue: RoleMentionPermission): InlineKeyboard {
|
||||
interface PermissionConfig<T extends string> {
|
||||
key: PermissionType
|
||||
displayTitle: string
|
||||
selectPrompt: string
|
||||
options: PermissionOption<T>[]
|
||||
getField: (config: ChatConfig) => T
|
||||
setPermission: (chatId: number, value: T) => Promise<ChatConfig | null>
|
||||
describePermission: (value: T) => string
|
||||
}
|
||||
|
||||
const PERMISSION_CONFIGS: Record<PermissionType, PermissionConfig<any>> = {
|
||||
manage: {
|
||||
key: 'manage',
|
||||
displayTitle: 'Role Management Permission',
|
||||
selectPrompt: 'Select who can add/delete roles:',
|
||||
options: [
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'all_admins', label: 'All Admins' },
|
||||
{ value: 'admin_can_promote_members', label: 'Admins: Promote Members' },
|
||||
{ value: 'admin_can_change_info', label: 'Admins: Change Info' },
|
||||
{ value: 'admin_can_manage_chat', label: 'Admins: Manage Chat' },
|
||||
{ value: 'only_owner', label: 'Only Owner' },
|
||||
] as PermissionOption<RoleManagePermission>[],
|
||||
getField: (config) => config.roleManagePermission,
|
||||
setPermission: ChatConfigService.setRoleManagePermission.bind(ChatConfigService),
|
||||
describePermission: PermissionService.describeRoleManagePermission.bind(PermissionService),
|
||||
},
|
||||
mention: {
|
||||
key: 'mention',
|
||||
displayTitle: 'Role Mention Permission',
|
||||
selectPrompt: 'Select who can mention roles:',
|
||||
options: [
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'all_admins', label: 'All Admins' },
|
||||
{ value: 'only_owner', label: 'Only Owner' },
|
||||
] as PermissionOption<RoleMentionPermission>[],
|
||||
getField: (config) => config.roleMentionPermission,
|
||||
setPermission: ChatConfigService.setRoleMentionPermission.bind(ChatConfigService),
|
||||
describePermission: PermissionService.describeRoleMentionPermission.bind(PermissionService),
|
||||
},
|
||||
admin: {
|
||||
key: 'admin',
|
||||
displayTitle: 'Role Admin Permission',
|
||||
selectPrompt: 'Select who can use role admin commands (rolemembers, rolekick):',
|
||||
options: [
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'all_admins', label: 'All Admins' },
|
||||
{ value: 'only_owner', label: 'Only Owner' },
|
||||
] as PermissionOption<RoleAdminPermission>[],
|
||||
getField: (config) => config.roleAdminPermission,
|
||||
setPermission: ChatConfigService.setRoleAdminPermission.bind(ChatConfigService),
|
||||
describePermission: PermissionService.describeRoleAdminPermission.bind(PermissionService),
|
||||
},
|
||||
}
|
||||
|
||||
function buildPermissionKeyboard<T extends string>(
|
||||
permConfig: PermissionConfig<T>,
|
||||
currentValue: T,
|
||||
): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
for (const option of MENTION_PERMISSION_OPTIONS) {
|
||||
for (const option of permConfig.options) {
|
||||
const isSelected = option.value === currentValue
|
||||
const label = isSelected ? `✓ ${option.label}` : option.label
|
||||
keyboard.text(label, `config:mention:${option.value}`).row()
|
||||
keyboard.text(label, `config:${permConfig.key}:${option.value}`).row()
|
||||
}
|
||||
keyboard.text('« Back to Config', 'config:back')
|
||||
return keyboard
|
||||
|
|
@ -47,19 +85,18 @@ function buildMainConfigKeyboard(isOwner: boolean): InlineKeyboard {
|
|||
const keyboard = new InlineKeyboard()
|
||||
if (isOwner) {
|
||||
keyboard.text('⚙️ Role Management', 'config:show:manage').row()
|
||||
keyboard.text('📢 Role Mentions', 'config:show:mention')
|
||||
keyboard.text('📢 Role Mentions', 'config:show:mention').row()
|
||||
keyboard.text('👥 Role Admin', 'config:show:admin')
|
||||
}
|
||||
return keyboard
|
||||
}
|
||||
|
||||
export default function (bot: BotType) {
|
||||
// Command to show current configuration
|
||||
export default (bot: BotType) => {
|
||||
bot.command('config', async (ctx) => {
|
||||
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||
return ctx.reply('This command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
// Any admin can view config
|
||||
const isAdmin = await isChatAdmin(ctx.chat.id, ctx.from.id)
|
||||
if (!isAdmin) {
|
||||
return ctx.reply('This command can only be used by admins')
|
||||
|
|
@ -78,6 +115,9 @@ export default function (bot: BotType) {
|
|||
${bold('Role Mentions:')}
|
||||
${italic(PermissionService.describeRoleMentionPermission(config.roleMentionPermission))}
|
||||
|
||||
${bold('Role Admin:')}
|
||||
${italic(PermissionService.describeRoleAdminPermission(config.roleAdminPermission))}
|
||||
|
||||
${isOwner ? '' : italic('\nOnly the chat owner can change settings.')}
|
||||
`,
|
||||
{
|
||||
|
|
@ -86,190 +126,98 @@ export default function (bot: BotType) {
|
|||
)
|
||||
})
|
||||
|
||||
// Handle navigation to manage settings
|
||||
bot.callbackQuery('config:show:manage', async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
for (const permType of Object.keys(PERMISSION_CONFIGS) as PermissionType[]) {
|
||||
const permConfig = PERMISSION_CONFIGS[permType]
|
||||
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
if (!isOwner) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Only the chat owner can change settings',
|
||||
show_alert: true,
|
||||
bot.callbackQuery(`config:show:${permType}`, async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
if (!isOwner) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Only the chat owner can change settings',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
const currentValue = permConfig.getField(config)
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold(permConfig.displayTitle)}
|
||||
|
||||
Current: ${italic(permConfig.describePermission(currentValue))}
|
||||
|
||||
${permConfig.selectPrompt}
|
||||
`,
|
||||
reply_markup: buildPermissionKeyboard(permConfig, currentValue),
|
||||
})
|
||||
}
|
||||
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold('Role Management Permission')}
|
||||
|
||||
Current: ${italic(PermissionService.describeRoleManagePermission(config.roleManagePermission))}
|
||||
|
||||
Select who can add/delete roles:
|
||||
`,
|
||||
reply_markup: buildManageKeyboard(config.roleManagePermission),
|
||||
return ctx.answerCallbackQuery()
|
||||
})
|
||||
|
||||
return ctx.answerCallbackQuery()
|
||||
})
|
||||
bot.callbackQuery(new RegExp(`^config:${permType}:(.+)$`), async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
|
||||
// Handle navigation to mention settings
|
||||
bot.callbackQuery('config:show:mention', async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
if (!isOwner) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Only the chat owner can change this setting',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
if (!isOwner) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Only the chat owner can change settings',
|
||||
show_alert: true,
|
||||
const permission = ctx.queryPayload.split(':')[2]
|
||||
const validOptions = permConfig.options.map(opt => opt.value)
|
||||
|
||||
if (!validOptions.includes(permission)) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Invalid option',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updated = await permConfig.setPermission(chatId, permission)
|
||||
|
||||
if (!updated) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Failed to update configuration',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const currentValue = permConfig.getField(updated)
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold(permConfig.displayTitle)}
|
||||
|
||||
Current: ${italic(permConfig.describePermission(currentValue))}
|
||||
|
||||
${permConfig.selectPrompt}
|
||||
`,
|
||||
reply_markup: buildPermissionKeyboard(permConfig, currentValue),
|
||||
})
|
||||
}
|
||||
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold('Role Mention Permission')}
|
||||
|
||||
Current: ${italic(PermissionService.describeRoleMentionPermission(config.roleMentionPermission))}
|
||||
|
||||
Select who can mention roles:
|
||||
`,
|
||||
reply_markup: buildMentionKeyboard(config.roleMentionPermission),
|
||||
return ctx.answerCallbackQuery({
|
||||
text: `Updated: ${permConfig.describePermission(currentValue)}`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.answerCallbackQuery()
|
||||
})
|
||||
|
||||
// Handle manage permission selection
|
||||
bot.callbackQuery(/^config:manage:(.+)$/, async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
if (!isOwner) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Only the chat owner can change this setting',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const permission = ctx.queryPayload.split(':')[2] as RoleManagePermission
|
||||
|
||||
const validOptions: RoleManagePermission[] = [
|
||||
'everyone',
|
||||
'all_admins',
|
||||
'admin_can_promote_members',
|
||||
'admin_can_change_info',
|
||||
'admin_can_manage_chat',
|
||||
'only_owner',
|
||||
]
|
||||
|
||||
if (!validOptions.includes(permission)) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Invalid option',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updated = await ChatConfigService.setRoleManagePermission(chatId, permission)
|
||||
|
||||
if (!updated) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Failed to update configuration',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold('Role Management Permission')}
|
||||
|
||||
Current: ${italic(PermissionService.describeRoleManagePermission(updated.roleManagePermission))}
|
||||
|
||||
Select who can add/delete roles:
|
||||
`,
|
||||
reply_markup: buildManageKeyboard(updated.roleManagePermission),
|
||||
})
|
||||
|
||||
return ctx.answerCallbackQuery({
|
||||
text: `Updated: ${PermissionService.describeRoleManagePermission(updated.roleManagePermission)}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Handle mention permission selection
|
||||
bot.callbackQuery(/^config:mention:(.+)$/, async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
if (!isOwner) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Only the chat owner can change this setting',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const permission = ctx.queryPayload.split(':')[2] as RoleMentionPermission
|
||||
|
||||
const validOptions: RoleMentionPermission[] = ['everyone', 'all_admins', 'only_owner']
|
||||
|
||||
if (!validOptions.includes(permission)) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Invalid option',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updated = await ChatConfigService.setRoleMentionPermission(chatId, permission)
|
||||
|
||||
if (!updated) {
|
||||
return ctx.answerCallbackQuery({
|
||||
text: 'Failed to update configuration',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold('Role Mention Permission')}
|
||||
|
||||
Current: ${italic(PermissionService.describeRoleMentionPermission(updated.roleMentionPermission))}
|
||||
|
||||
Select who can mention roles:
|
||||
`,
|
||||
reply_markup: buildMentionKeyboard(updated.roleMentionPermission),
|
||||
})
|
||||
|
||||
return ctx.answerCallbackQuery({
|
||||
text: `Updated: ${PermissionService.describeRoleMentionPermission(updated.roleMentionPermission)}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Handle back button to main config
|
||||
bot.callbackQuery('config:back', async (ctx) => {
|
||||
const chatId = ctx.message?.chat.id
|
||||
const messageId = ctx.message?.id
|
||||
|
|
@ -292,6 +240,9 @@ export default function (bot: BotType) {
|
|||
${bold('Role Mentions:')}
|
||||
${italic(PermissionService.describeRoleMentionPermission(config.roleMentionPermission))}
|
||||
|
||||
${bold('Role Admin:')}
|
||||
${italic(PermissionService.describeRoleAdminPermission(config.roleAdminPermission))}
|
||||
|
||||
${isOwner ? '' : italic('\nOnly the chat owner can change settings.')}
|
||||
`,
|
||||
reply_markup: buildMainConfigKeyboard(isOwner),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
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 { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
export default function (bot: BotType) {
|
||||
export default (bot: BotType) => {
|
||||
bot.command('roleadd', async (ctx) => {
|
||||
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||
return ctx.reply('This command can only be used in groups or supergroups')
|
||||
|
|
@ -40,19 +41,7 @@ export default function (bot: BotType) {
|
|||
return ctx.reply('failed to create role, skill issue')
|
||||
}
|
||||
|
||||
setImmediate(async () => {
|
||||
const roles = await RoleService.getByChatId(ctx.chatId)
|
||||
await bot.api.setMyCommands({
|
||||
scope: {
|
||||
chat_id: ctx.chatId,
|
||||
type: 'chat',
|
||||
},
|
||||
commands: roles.map(role => ({
|
||||
command: role.slug,
|
||||
description: `Mention all members of this role`,
|
||||
})),
|
||||
})
|
||||
})
|
||||
updateChatCommands(bot, ctx.chatId)
|
||||
|
||||
return ctx.reply(
|
||||
format`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { bold, code, format } from 'gramio'
|
||||
import { updateChatCommands } from '@/bot/utilities/commands.js'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
|
|
@ -34,19 +35,7 @@ export default (bot: BotType) => {
|
|||
return ctx.reply('failed to delete role, skill issue')
|
||||
}
|
||||
|
||||
setImmediate(async () => {
|
||||
const roles = await RoleService.getByChatId(ctx.chatId)
|
||||
await bot.api.setMyCommands({
|
||||
scope: {
|
||||
chat_id: ctx.chatId,
|
||||
type: 'chat',
|
||||
},
|
||||
commands: roles.map(role => ({
|
||||
command: role.slug,
|
||||
description: `Mention all members of this role`,
|
||||
})),
|
||||
})
|
||||
})
|
||||
updateChatCommands(bot, ctx.chatId)
|
||||
|
||||
return ctx.reply(format`role ${bold(role.slug)} deleted`)
|
||||
})
|
||||
|
|
|
|||
54
src/bot/commands/admin/role-kick.ts
Normal file
54
src/bot/commands/admin/role-kick.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { bold, code, format, mention } from 'gramio'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('rolekick', async (ctx) => {
|
||||
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||
return ctx.reply('this command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
const canAdmin = await PermissionService.canAdminRoles(ctx.chat.id, ctx.from.id)
|
||||
if (!canAdmin) {
|
||||
return ctx.reply('You don\'t have permission to use role admin commands')
|
||||
}
|
||||
|
||||
// Check if this is a reply to a message
|
||||
const replyMessage = ctx.replyMessage
|
||||
if (!replyMessage) {
|
||||
return ctx.reply(format`reply to a user's message to kick them from a role\nusage: ${code('/rolekick <rolename>')} (as reply)`)
|
||||
}
|
||||
|
||||
const targetUser = replyMessage.from
|
||||
if (!targetUser) {
|
||||
return ctx.reply('could not determine the user from the replied message')
|
||||
}
|
||||
|
||||
let [roleSlug] = (ctx.args ?? '').trim().split(' ')
|
||||
if (!roleSlug) {
|
||||
return ctx.reply(format`where is the role name bro?\nusage: ${code('/rolekick <rolename>')} (as reply)`)
|
||||
}
|
||||
|
||||
roleSlug = roleSlug.toLowerCase()
|
||||
|
||||
const role = await RoleService.getBySlugOrAlias(roleSlug, ctx.chatId)
|
||||
if (!role) {
|
||||
return ctx.reply('role not found')
|
||||
}
|
||||
|
||||
// Check if user is a member of the role
|
||||
const membership = await RoleService.getMemberById(role.id!, ctx.chatId, targetUser.id)
|
||||
if (!membership) {
|
||||
return ctx.reply(format`${mention(targetUser.firstName, { id: targetUser.id, is_bot: targetUser.isBot(), first_name: targetUser.firstName })} is not a member of ${bold(role.slug)}`)
|
||||
}
|
||||
|
||||
// Remove user from role
|
||||
const removed = await RoleService.removeMember(role.id!, targetUser.id)
|
||||
if (!removed) {
|
||||
return ctx.reply('failed to remove user from role, skill issue')
|
||||
}
|
||||
|
||||
return ctx.reply(format`${mention(targetUser.firstName, { id: targetUser.id, is_bot: targetUser.isBot(), first_name: targetUser.firstName })} has been kicked from ${bold(role.slug)}`)
|
||||
})
|
||||
}
|
||||
43
src/bot/commands/admin/role-members.ts
Normal file
43
src/bot/commands/admin/role-members.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { bold, code, format, italic, join, mention } from 'gramio'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('rolemembers', async (ctx) => {
|
||||
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||
return ctx.reply('this command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
const canAdmin = await PermissionService.canAdminRoles(ctx.chat.id, ctx.from.id)
|
||||
if (!canAdmin) {
|
||||
return ctx.reply('You don\'t have permission to use role admin commands')
|
||||
}
|
||||
|
||||
let [roleSlug] = (ctx.args ?? '').trim().split(' ')
|
||||
if (!roleSlug) {
|
||||
return ctx.reply(format`where is the role name bro?\nusage: ${code('/rolemembers <name>')}`)
|
||||
}
|
||||
|
||||
roleSlug = roleSlug.toLowerCase()
|
||||
|
||||
const role = await RoleService.getBySlugOrAlias(roleSlug, ctx.chatId)
|
||||
if (!role) {
|
||||
return ctx.reply('role not found')
|
||||
}
|
||||
|
||||
const memberIds = await RoleService.getMemberIds(roleSlug, ctx.chatId)
|
||||
|
||||
if (memberIds.length === 0) {
|
||||
return ctx.reply(format`role ${bold(role.slug)} has no members`)
|
||||
}
|
||||
|
||||
const mentions = memberIds.map(id => mention('👤', { id, is_bot: false, first_name: '' }))
|
||||
|
||||
return ctx.reply(format`
|
||||
${bold(`Members of ${role.slug}`)} ${italic(`(${memberIds.length})`)}
|
||||
|
||||
${join(mentions, entity => entity, ' ')}
|
||||
`)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { BotType } from "@/bot/index.js"
|
||||
import type { BotType } from "@/bot/index.js"
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('resetautocomplete', async (ctx) => {
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ export default (bot: BotType) => {
|
|||
}
|
||||
|
||||
const role = await RoleService.getBySlugOrAlias(slug, ctx.chatId)
|
||||
console.log(role)
|
||||
if (!role) {
|
||||
return ctx.reply('role not found')
|
||||
}
|
||||
|
||||
const isMember = await RoleService.getMemberById(role.id!, ctx.chatId, ctx.from.id)
|
||||
console.log(isMember)
|
||||
if (isMember !== null) {
|
||||
return ctx.reply('you are already in this role')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BotType } from "@/bot/index.js"
|
||||
import type { BotType } from "@/bot/index.js"
|
||||
import { RoleService } from "@/shared/services/role.js"
|
||||
import { format, join } from "gramio"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BotType } from "@/bot/index.js"
|
||||
import type { BotType } from "@/bot/index.js"
|
||||
import { RoleService } from "@/shared/services/role.js"
|
||||
import { bold, code, format } from "gramio"
|
||||
|
||||
|
|
@ -21,7 +21,6 @@ export default (bot: BotType) => {
|
|||
}
|
||||
|
||||
const isMember = await RoleService.getMemberById(role.id!, ctx.chatId, ctx.from.id)
|
||||
console.log(isMember)
|
||||
if (isMember === null) {
|
||||
return ctx.reply('you are not a member of this role')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BotType } from "@/bot/index.js"
|
||||
import type { BotType } from "@/bot/index.js"
|
||||
import { RoleService } from "@/shared/services/role.js"
|
||||
import { format, join } from "gramio"
|
||||
|
||||
|
|
|
|||
18
src/bot/utilities/commands.ts
Normal file
18
src/bot/utilities/commands.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { BotType } from '@/bot/index.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
export function updateChatCommands(bot: BotType, chatId: number): void {
|
||||
setImmediate(async () => {
|
||||
const roles = await RoleService.getByChatId(chatId)
|
||||
await bot.api.setMyCommands({
|
||||
scope: {
|
||||
chat_id: chatId,
|
||||
type: 'chat',
|
||||
},
|
||||
commands: roles.map(role => ({
|
||||
command: role.slug,
|
||||
description: `Mention all members of this role`,
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
import type { TelegramChatMember } from 'gramio'
|
||||
import { bot } from '../index.js'
|
||||
|
||||
// Type definitions for admin permissions
|
||||
const ANONYMOUS_ADMIN_ID = 1087968824
|
||||
|
||||
interface TelegramAdminMember extends TelegramChatMember {
|
||||
can_promote_members?: boolean
|
||||
can_change_info?: boolean
|
||||
can_manage_chat?: boolean
|
||||
}
|
||||
export interface AdminInfo {
|
||||
userId: number
|
||||
status: 'creator' | 'administrator'
|
||||
|
|
@ -51,14 +57,16 @@ export function refreshAdminCache(chatId: number): void {
|
|||
adminCache.delete(chatId)
|
||||
}
|
||||
|
||||
function isHiddenAdmin(chatId: number, userId: number): boolean {
|
||||
return userId === chatId || userId === ANONYMOUS_ADMIN_ID
|
||||
}
|
||||
|
||||
export async function isChatAdmin(chatId: number, userId: number) {
|
||||
try {
|
||||
// handle hidden administrators
|
||||
if (userId === chatId || userId === 1087968824)
|
||||
if (isHiddenAdmin(chatId, userId))
|
||||
return true
|
||||
|
||||
const chatAdmins = await getChatAdminsWithCache(chatId)
|
||||
|
||||
return chatAdmins.some(admin => admin.user.id === userId)
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -67,15 +75,10 @@ export async function isChatAdmin(chatId: number, userId: number) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is the chat owner/creator
|
||||
*/
|
||||
export async function isChatOwner(chatId: number, userId: number): Promise<boolean> {
|
||||
try {
|
||||
// Handle special cases (hidden admins)
|
||||
if (userId === chatId || userId === 1087968824) {
|
||||
if (isHiddenAdmin(chatId, userId))
|
||||
return true
|
||||
}
|
||||
|
||||
const chatAdmins = await getChatAdminsWithCache(chatId)
|
||||
const userAdmin = chatAdmins.find(admin => admin.user.id === userId)
|
||||
|
|
@ -87,13 +90,9 @@ export async function isChatOwner(chatId: number, userId: number): Promise<boole
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed admin info for a user
|
||||
*/
|
||||
export async function getAdminInfo(chatId: number, userId: number): Promise<AdminInfo | null> {
|
||||
try {
|
||||
// Handle special cases
|
||||
if (userId === chatId || userId === 1087968824) {
|
||||
if (isHiddenAdmin(chatId, userId)) {
|
||||
return {
|
||||
userId,
|
||||
status: 'creator',
|
||||
|
|
@ -106,19 +105,20 @@ export async function getAdminInfo(chatId: number, userId: number): Promise<Admi
|
|||
}
|
||||
|
||||
const chatAdmins = await getChatAdminsWithCache(chatId)
|
||||
const userAdmin = chatAdmins.find(admin => admin.user.id === userId)
|
||||
const userAdmin = chatAdmins.find(admin => admin.user.id === userId) as TelegramAdminMember | undefined
|
||||
|
||||
if (!userAdmin) {
|
||||
if (!userAdmin)
|
||||
return null
|
||||
}
|
||||
|
||||
const isCreator = userAdmin.status === 'creator'
|
||||
|
||||
return {
|
||||
userId,
|
||||
status: userAdmin.status as 'creator' | 'administrator',
|
||||
permissions: {
|
||||
canPromoteMembers: userAdmin.status === 'creator' || (userAdmin as any).can_promote_members || false,
|
||||
canChangeInfo: userAdmin.status === 'creator' || (userAdmin as any).can_change_info || false,
|
||||
canManageChat: userAdmin.status === 'creator' || (userAdmin as any).can_manage_chat || false,
|
||||
canPromoteMembers: isCreator || userAdmin.can_promote_members || false,
|
||||
canChangeInfo: isCreator || userAdmin.can_change_info || false,
|
||||
canManageChat: isCreator || userAdmin.can_manage_chat || false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -128,9 +128,6 @@ export async function getAdminInfo(chatId: number, userId: number): Promise<Admi
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific Telegram admin permission
|
||||
*/
|
||||
export async function hasAdminPermission(
|
||||
chatId: number,
|
||||
userId: number,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ export const roleMentionPermissionEnum = pgEnum('role_mention_permission', [
|
|||
'only_owner',
|
||||
])
|
||||
|
||||
export const roleAdminPermissionEnum = pgEnum('role_admin_permission', [
|
||||
'everyone',
|
||||
'all_admins',
|
||||
'only_owner',
|
||||
])
|
||||
|
||||
export const roleSchema = pgTable('roles', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
slug: varchar('slug', { length: 32 }).notNull(),
|
||||
|
|
@ -48,6 +54,11 @@ export const chatConfigSchema = pgTable('chat_configs', {
|
|||
.notNull()
|
||||
.default('everyone'),
|
||||
|
||||
// Who can use admin commands (rolemembers, rolekick)
|
||||
roleAdminPermission: roleAdminPermissionEnum('role_admin_permission')
|
||||
.notNull()
|
||||
.default('all_admins'),
|
||||
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,11 +15,17 @@ export type RoleMentionPermission
|
|||
| 'all_admins'
|
||||
| 'only_owner'
|
||||
|
||||
export type RoleAdminPermission
|
||||
= | 'everyone'
|
||||
| 'all_admins'
|
||||
| 'only_owner'
|
||||
|
||||
export interface ChatConfig {
|
||||
id: string
|
||||
chatId: number
|
||||
roleManagePermission: RoleManagePermission
|
||||
roleMentionPermission: RoleMentionPermission
|
||||
roleAdminPermission: RoleAdminPermission
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
|
@ -46,45 +52,45 @@ export class ChatConfigService {
|
|||
chatId,
|
||||
roleManagePermission: 'all_admins',
|
||||
roleMentionPermission: 'everyone',
|
||||
roleAdminPermission: 'all_admins',
|
||||
})
|
||||
.returning()
|
||||
.then(result => result[0]!) as Promise<ChatConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role management permission
|
||||
*/
|
||||
static async setRoleManagePermission(
|
||||
private static async setPermission<K extends keyof ChatConfig>(
|
||||
chatId: number,
|
||||
permission: RoleManagePermission,
|
||||
field: K,
|
||||
value: ChatConfig[K],
|
||||
): Promise<ChatConfig | null> {
|
||||
// Ensure config exists first
|
||||
await this.getOrCreate(chatId)
|
||||
|
||||
return database
|
||||
.update(chatConfigSchema)
|
||||
.set({ roleManagePermission: permission })
|
||||
.set({ [field]: value })
|
||||
.where(eq(chatConfigSchema.chatId, chatId))
|
||||
.returning()
|
||||
.then(result => (result[0] ?? null) as ChatConfig | null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role mention permission
|
||||
*/
|
||||
static async setRoleManagePermission(
|
||||
chatId: number,
|
||||
permission: RoleManagePermission,
|
||||
): Promise<ChatConfig | null> {
|
||||
return this.setPermission(chatId, 'roleManagePermission', permission)
|
||||
}
|
||||
|
||||
static async setRoleMentionPermission(
|
||||
chatId: number,
|
||||
permission: RoleMentionPermission,
|
||||
): Promise<ChatConfig | null> {
|
||||
// Ensure config exists first
|
||||
await this.getOrCreate(chatId)
|
||||
return this.setPermission(chatId, 'roleMentionPermission', permission)
|
||||
}
|
||||
|
||||
return database
|
||||
.update(chatConfigSchema)
|
||||
.set({ roleMentionPermission: permission })
|
||||
.where(eq(chatConfigSchema.chatId, chatId))
|
||||
.returning()
|
||||
.then(result => (result[0] ?? null) as ChatConfig | null)
|
||||
static async setRoleAdminPermission(
|
||||
chatId: number,
|
||||
permission: RoleAdminPermission,
|
||||
): Promise<ChatConfig | null> {
|
||||
return this.setPermission(chatId, 'roleAdminPermission', permission)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ export class MentionService {
|
|||
const members = await RoleService.getMemberIds(query, chatId)
|
||||
const membersChunks = chunk(members, 5)
|
||||
|
||||
for (const memberChunk of membersChunks) {
|
||||
const chunkIdx = membersChunks.indexOf(memberChunk)
|
||||
for (let i = 0; i < membersChunks.length; i++) {
|
||||
const memberChunk = membersChunks[i]
|
||||
|
||||
const message = format`
|
||||
${bold('Mass mention by')} ${bold(mention('user', { id: caller.id, is_bot: caller.isBot(), first_name: caller.firstName }))} ${membersChunks.length > 1 ? italic(`[${chunkIdx + 1}/${membersChunks.length}]`) : ''}
|
||||
const message = format`${bold('Mass mention by')} ${bold(mention('user', { id: caller.id, is_bot: caller.isBot(), first_name: caller.firstName }))} ${membersChunks.length > 1 ? italic(`[${i + 1}/${membersChunks.length}]`) : ''}
|
||||
|
||||
${join(this.buildMentions(memberChunk), entity => entity, ' ')}
|
||||
`
|
||||
${join(this.buildMentions(memberChunk), entity => entity, ' ')}`
|
||||
|
||||
await bot.api.sendMessage({
|
||||
chat_id: role.chatId,
|
||||
|
|
|
|||
|
|
@ -1,86 +1,73 @@
|
|||
import type { RoleManagePermission, RoleMentionPermission } from './chatConfig.js'
|
||||
import type { ChatConfig, RoleAdminPermission, RoleManagePermission, RoleMentionPermission } from './chatConfig.js'
|
||||
import { hasAdminPermission, isChatAdmin, isChatOwner } from '@/bot/utilities/perms.js'
|
||||
import { ChatConfigService } from './chatConfig.js'
|
||||
|
||||
type PermissionChecker = (chatId: number, userId: number) => Promise<boolean>
|
||||
|
||||
const PERMISSION_CHECKERS: Record<string, PermissionChecker> = {
|
||||
everyone: async () => true,
|
||||
all_admins: isChatAdmin,
|
||||
only_owner: isChatOwner,
|
||||
admin_can_promote_members: (chatId, userId) => hasAdminPermission(chatId, userId, 'can_promote_members'),
|
||||
admin_can_change_info: (chatId, userId) => hasAdminPermission(chatId, userId, 'can_change_info'),
|
||||
admin_can_manage_chat: (chatId, userId) => hasAdminPermission(chatId, userId, 'can_manage_chat'),
|
||||
}
|
||||
|
||||
const PERMISSION_DESCRIPTIONS = {
|
||||
roleManagePermission: {
|
||||
everyone: 'Everyone can manage roles',
|
||||
all_admins: 'All admins can manage roles',
|
||||
admin_can_promote_members: 'Admins with "Promote Members" permission can manage roles',
|
||||
admin_can_change_info: 'Admins with "Change Info" permission can manage roles',
|
||||
admin_can_manage_chat: 'Admins with "Manage Chat" permission can manage roles',
|
||||
only_owner: 'Only the chat owner can manage roles',
|
||||
} satisfies Record<RoleManagePermission, string>,
|
||||
roleMentionPermission: {
|
||||
everyone: 'Everyone can mention roles',
|
||||
all_admins: 'Only admins can mention roles',
|
||||
only_owner: 'Only the chat owner can mention roles',
|
||||
} satisfies Record<RoleMentionPermission, string>,
|
||||
roleAdminPermission: {
|
||||
everyone: 'Everyone can use role admin commands',
|
||||
all_admins: 'Only admins can use role admin commands',
|
||||
only_owner: 'Only the chat owner can use role admin commands',
|
||||
} satisfies Record<RoleAdminPermission, string>,
|
||||
} as const
|
||||
|
||||
export class PermissionService {
|
||||
/**
|
||||
* Check if user can manage roles (add/delete) based on chat config
|
||||
*/
|
||||
private static async checkPermission<K extends keyof ChatConfig>(
|
||||
chatId: number,
|
||||
userId: number,
|
||||
field: K,
|
||||
defaultFallback: PermissionChecker,
|
||||
): Promise<boolean> {
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
const permission = config[field] as string
|
||||
const checker = PERMISSION_CHECKERS[permission]
|
||||
return checker ? checker(chatId, userId) : defaultFallback(chatId, userId)
|
||||
}
|
||||
|
||||
static async canManageRoles(chatId: number, userId: number): Promise<boolean> {
|
||||
// Get chat configuration
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
const permission = config.roleManagePermission
|
||||
|
||||
switch (permission) {
|
||||
case 'everyone':
|
||||
return true
|
||||
|
||||
case 'all_admins':
|
||||
return isChatAdmin(chatId, userId)
|
||||
|
||||
case 'admin_can_promote_members':
|
||||
return hasAdminPermission(chatId, userId, 'can_promote_members')
|
||||
|
||||
case 'admin_can_change_info':
|
||||
return hasAdminPermission(chatId, userId, 'can_change_info')
|
||||
|
||||
case 'admin_can_manage_chat':
|
||||
return hasAdminPermission(chatId, userId, 'can_manage_chat')
|
||||
|
||||
case 'only_owner':
|
||||
return isChatOwner(chatId, userId)
|
||||
|
||||
default:
|
||||
// Default to admin-only for safety
|
||||
return isChatAdmin(chatId, userId)
|
||||
}
|
||||
return this.checkPermission(chatId, userId, 'roleManagePermission', isChatAdmin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can mention roles (@role or /role) based on chat config
|
||||
*/
|
||||
static async canMentionRoles(chatId: number, userId: number): Promise<boolean> {
|
||||
// Get chat configuration
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
const permission = config.roleMentionPermission
|
||||
|
||||
switch (permission) {
|
||||
case 'everyone':
|
||||
return true
|
||||
|
||||
case 'all_admins':
|
||||
return isChatAdmin(chatId, userId)
|
||||
|
||||
case 'only_owner':
|
||||
return isChatOwner(chatId, userId)
|
||||
|
||||
default:
|
||||
// Default to everyone for backwards compatibility
|
||||
return true
|
||||
}
|
||||
return this.checkPermission(chatId, userId, 'roleMentionPermission', async () => true)
|
||||
}
|
||||
|
||||
static async canAdminRoles(chatId: number, userId: number): Promise<boolean> {
|
||||
return this.checkPermission(chatId, userId, 'roleAdminPermission', isChatAdmin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable description of permission level
|
||||
*/
|
||||
static describeRoleManagePermission(permission: RoleManagePermission): string {
|
||||
const descriptions: Record<RoleManagePermission, string> = {
|
||||
everyone: 'Everyone can manage roles',
|
||||
all_admins: 'All admins can manage roles',
|
||||
admin_can_promote_members: 'Admins with "Promote Members" permission can manage roles',
|
||||
admin_can_change_info: 'Admins with "Change Info" permission can manage roles',
|
||||
admin_can_manage_chat: 'Admins with "Manage Chat" permission can manage roles',
|
||||
only_owner: 'Only the chat owner can manage roles',
|
||||
}
|
||||
return descriptions[permission]
|
||||
return PERMISSION_DESCRIPTIONS.roleManagePermission[permission]
|
||||
}
|
||||
|
||||
static describeRoleMentionPermission(permission: RoleMentionPermission): string {
|
||||
const descriptions: Record<RoleMentionPermission, string> = {
|
||||
everyone: 'Everyone can mention roles',
|
||||
all_admins: 'Only admins can mention roles',
|
||||
only_owner: 'Only the chat owner can mention roles',
|
||||
}
|
||||
return descriptions[permission]
|
||||
return PERMISSION_DESCRIPTIONS.roleMentionPermission[permission]
|
||||
}
|
||||
|
||||
static describeRoleAdminPermission(permission: RoleAdminPermission): string {
|
||||
return PERMISSION_DESCRIPTIONS.roleAdminPermission[permission]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export function chunk(arr: any[], size: number) {
|
||||
return Array.from({ length: Math.ceil(arr.length / size) }, (_: any, i: number) =>
|
||||
export function chunk<T>(arr: T[], size: number): T[][] {
|
||||
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
|
||||
arr.slice(i * size, i * size + size))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue