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:
devilreef 2026-02-17 21:27:35 +06:00
parent 1eb2200fbe
commit 85c7410ffb
21 changed files with 638 additions and 364 deletions

View file

@ -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(

View file

@ -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),

View file

@ -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`

View file

@ -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`)
})

View 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)}`)
})
}

View 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, ' ')}
`)
})
}

View file

@ -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) => {

View file

@ -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')
}

View file

@ -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"

View file

@ -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')
}

View file

@ -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"