feat: config management
fuck it we're vibecoding now
This commit is contained in:
parent
1ea3f3ba97
commit
1eb2200fbe
14 changed files with 1082 additions and 33 deletions
117
CLAUDE.md
Normal file
117
CLAUDE.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
A Telegram bot built with GramIO that enables role-based mentions in group chats. Users can create custom roles, join/leave them, and mention all role members at once using @mentions or commands.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- `pnpm dev` - Run bot in development mode with hot reload
|
||||
- `pnpm start` - Run bot in production mode
|
||||
- `pnpm drizzle-kit generate --config=src/shared/database/config.ts` - Generate database migrations after schema changes
|
||||
- `pnpm drizzle-kit migrate --config=src/shared/database/config.ts` - Apply pending migrations to database
|
||||
- `pnpm drizzle-kit studio --config=src/shared/database/config.ts` - Open Drizzle Studio for database inspection
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
**Bot Entry Point** (`src/index.ts` → `src/bot/index.ts`)
|
||||
- Bot initialization uses GramIO framework
|
||||
- Commands are auto-loaded from `src/bot/commands/` using `@gramio/autoload`
|
||||
- Two middleware handlers in `src/bot/index.ts`:
|
||||
1. Message handler for @mention syntax (e.g., `@rolename`) - triggers role mentions
|
||||
2. Command handler for bare commands (e.g., `/rolename`) - also triggers role mentions
|
||||
- Bot username is captured on startup to properly handle commands with @botname suffix
|
||||
|
||||
**Database** (PostgreSQL with Drizzle ORM)
|
||||
- Schema defined in `src/shared/database/schema.ts`
|
||||
- Three tables: `roles` (id, slug, aliases, chatId), `role_members` (id, roleId, userId), and `chat_configs` (id, chatId, roleManagePermission, roleMentionPermission)
|
||||
- Configuration in `src/shared/database/config.ts`
|
||||
- Path aliases use `@/*` mapping to `./src/*`
|
||||
|
||||
**Services** (`src/shared/services/`)
|
||||
- `RoleService` - All role and role member database operations
|
||||
- `MentionService` - Handles mentioning all members of a role
|
||||
- Chunks members into groups of 5 to avoid hitting Telegram's limits
|
||||
- Builds mention entities using GramIO's `mention()` helper
|
||||
- Sends multiple messages if role has >5 members, with pagination indicators
|
||||
- `ChatConfigService` - Manages per-chat permission configuration
|
||||
- Lazy initialization via `getOrCreate()` - configs created on first access
|
||||
- Defaults: `all_admins` for role management, `everyone` for mentions
|
||||
- `PermissionService` - Centralized permission checking logic
|
||||
- `canManageRoles()` - Checks if user can add/delete roles based on chat config
|
||||
- `canMentionRoles()` - Checks if user can trigger role mentions based on chat config
|
||||
|
||||
**Commands** (`src/bot/commands/`)
|
||||
- Admin commands require chat admin permissions (checked via `isChatAdmin()`)
|
||||
- `/roleadd <name>` - Create new role (permission configurable, updates bot commands dynamically)
|
||||
- `/roledel` - Delete role (permission configurable)
|
||||
- `/join <name>` - Join a role (always available to everyone)
|
||||
- `/leave <name>` - Leave a role (always available to everyone)
|
||||
- `/myroles` - List your roles
|
||||
- `/roles` - List all roles in chat
|
||||
- `/config` - View and configure permissions (any admin can view, owner can change via inline buttons)
|
||||
- `/reload` - Refresh admin cache (any admin)
|
||||
|
||||
**Role Triggering**
|
||||
- Roles can be triggered two ways:
|
||||
1. Via @mention in any message (e.g., "hey @developers")
|
||||
2. Via bare command (e.g., "/developers" or "/developers@botname")
|
||||
- Both methods call `MentionService.mentionAll()`
|
||||
|
||||
**Utilities**
|
||||
- `src/bot/utilities/perms.ts` - Permission checking utilities
|
||||
- `isChatAdmin()` - Checks if user is admin (uses 1-hour cache)
|
||||
- `isChatOwner()` - Checks if user is chat creator/owner
|
||||
- `getAdminInfo()` - Returns detailed admin info with granular permissions
|
||||
- `hasAdminPermission()` - Checks specific Telegram admin permissions (can_promote_members, can_change_info, can_manage_chat)
|
||||
- `refreshAdminCache()` - Clears admin cache for a specific chat
|
||||
- Admin cache: 1-hour TTL to reduce Telegram API calls, falls back to stale data on error
|
||||
- Handles hidden admins including anonymous admin bot ID 1087968824
|
||||
- `src/bot/utilities/constants.ts` - Role name validation regex and reserved command names
|
||||
- `src/shared/utilities/chunk.ts` - Array chunking utility for member batching
|
||||
|
||||
### Important Patterns
|
||||
|
||||
**Permission System**
|
||||
- Chat owners can configure permissions via `/config` command with inline keyboard navigation
|
||||
- Role management options: `everyone`, `all_admins`, `admin_can_promote_members`, `admin_can_change_info`, `admin_can_manage_chat`, `only_owner`
|
||||
- Role mention options: `everyone`, `all_admins`, `only_owner`
|
||||
- Permissions checked via `PermissionService` which consults `chat_configs` table
|
||||
- Join/leave functionality always available to everyone (no configuration)
|
||||
- Admin cache with 1-hour TTL reduces Telegram API calls - use `/reload` to manually refresh
|
||||
|
||||
**Role Name Validation**
|
||||
- Must match `ROLE_NAME_REGEX`: 4-32 characters, letters only
|
||||
- Cannot use `RESERVED_NAMES` (built-in command names)
|
||||
|
||||
**Chat Scope**
|
||||
- All roles are scoped to `chatId` - same role name can exist in different chats
|
||||
- Bot only works in groups/supergroups, not private chats
|
||||
|
||||
**Dynamic Command Registration**
|
||||
- When admin creates/deletes roles, bot updates chat-specific command list via `setMyCommands()` with scope type 'chat'
|
||||
- This allows autocomplete for role names in that specific chat
|
||||
|
||||
**Environment Variables**
|
||||
- Required: `DATABASE_URL`, `BOT_TOKEN`
|
||||
- Optional: `NODE_ENV` (defaults to 'production')
|
||||
- Loaded via Node's `loadEnvFile()` from `.env` file
|
||||
|
||||
### Database Migrations
|
||||
|
||||
After modifying `src/shared/database/schema.ts`, generate and apply migrations using drizzle-kit with the explicit config path:
|
||||
- `pnpm drizzle-kit generate --config=src/shared/database/config.ts`
|
||||
- `pnpm drizzle-kit migrate --config=src/shared/database/config.ts`
|
||||
|
||||
The migration output directory is `./drizzle/`.
|
||||
|
||||
**Current Schema:**
|
||||
- `roles` - Role definitions with slug, aliases, and chatId
|
||||
- `role_members` - User memberships in roles
|
||||
- `chat_configs` - Per-chat permission configuration (lazy-initialized)
|
||||
- Enums: `role_manage_permission` and `role_mention_permission`
|
||||
- Default values ensure backwards compatibility with existing chats
|
||||
11
drizzle/0001_legal_blob.sql
Normal file
11
drizzle/0001_legal_blob.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
CREATE TYPE "public"."role_manage_permission" AS ENUM('everyone', 'all_admins', 'admin_can_promote_members', 'admin_can_change_info', 'admin_can_manage_chat', 'only_owner');--> statement-breakpoint
|
||||
CREATE TYPE "public"."role_mention_permission" AS ENUM('everyone', 'all_admins', 'only_owner');--> statement-breakpoint
|
||||
CREATE TABLE "chat_configs" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"chat_id" bigint NOT NULL,
|
||||
"role_manage_permission" "role_manage_permission" DEFAULT 'all_admins' NOT NULL,
|
||||
"role_mention_permission" "role_mention_permission" DEFAULT 'everyone' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "chat_configs_chat_id_unique" UNIQUE("chat_id")
|
||||
);
|
||||
211
drizzle/meta/0001_snapshot.json
Normal file
211
drizzle/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
{
|
||||
"id": "fb792b24-bbbd-4b38-9191-24a83d05d277",
|
||||
"prevId": "2a786bb1-c05b-4d36-a401-293ccb96076f",
|
||||
"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'"
|
||||
},
|
||||
"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_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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1763720438133,
|
||||
"tag": "0000_outgoing_magneto",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1764313289570,
|
||||
"tag": "0001_legal_blob",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
25
src/bot/commands/admin/config-refresh.ts
Normal file
25
src/bot/commands/admin/config-refresh.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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
|
||||
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(
|
||||
format`${bold('Admin cache refreshed!')} Permission checks will use fresh data from Telegram.`,
|
||||
)
|
||||
})
|
||||
}
|
||||
302
src/bot/commands/admin/config.ts
Normal file
302
src/bot/commands/admin/config.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
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 { 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' },
|
||||
]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function buildMentionKeyboard(currentValue: RoleMentionPermission): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
for (const option of MENTION_PERMISSION_OPTIONS) {
|
||||
const isSelected = option.value === currentValue
|
||||
const label = isSelected ? `✓ ${option.label}` : option.label
|
||||
keyboard.text(label, `config:mention:${option.value}`).row()
|
||||
}
|
||||
keyboard.text('« Back to Config', 'config:back')
|
||||
return keyboard
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
return keyboard
|
||||
}
|
||||
|
||||
export default function (bot: BotType) {
|
||||
// Command to show current configuration
|
||||
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')
|
||||
}
|
||||
|
||||
const isOwner = await isChatOwner(ctx.chat.id, ctx.from.id)
|
||||
const config = await ChatConfigService.getOrCreate(ctx.chat.id)
|
||||
|
||||
return ctx.reply(
|
||||
format`
|
||||
${bold('Chat Configuration')}
|
||||
|
||||
${bold('Role Management:')}
|
||||
${italic(PermissionService.describeRoleManagePermission(config.roleManagePermission))}
|
||||
|
||||
${bold('Role Mentions:')}
|
||||
${italic(PermissionService.describeRoleMentionPermission(config.roleMentionPermission))}
|
||||
|
||||
${isOwner ? '' : italic('\nOnly the chat owner can change settings.')}
|
||||
`,
|
||||
{
|
||||
reply_markup: buildMainConfigKeyboard(isOwner),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// 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 })
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
// 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 settings',
|
||||
show_alert: true,
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
// 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
|
||||
if (!chatId || !messageId) {
|
||||
return ctx.answerCallbackQuery({ text: 'Error: Could not determine chat', show_alert: true })
|
||||
}
|
||||
|
||||
const isOwner = await isChatOwner(chatId, ctx.from.id)
|
||||
const config = await ChatConfigService.getOrCreate(chatId)
|
||||
|
||||
await bot.api.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: format`
|
||||
${bold('Chat Configuration')}
|
||||
|
||||
${bold('Role Management:')}
|
||||
${italic(PermissionService.describeRoleManagePermission(config.roleManagePermission))}
|
||||
|
||||
${bold('Role Mentions:')}
|
||||
${italic(PermissionService.describeRoleMentionPermission(config.roleMentionPermission))}
|
||||
|
||||
${isOwner ? '' : italic('\nOnly the chat owner can change settings.')}
|
||||
`,
|
||||
reply_markup: buildMainConfigKeyboard(isOwner),
|
||||
})
|
||||
|
||||
return ctx.answerCallbackQuery()
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,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 { isChatAdmin } from '@/bot/utilities/perms.js'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
export default function (bot: BotType) {
|
||||
|
|
@ -10,9 +10,9 @@ export default function (bot: BotType) {
|
|||
return ctx.reply('This command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
const isAdmin = await isChatAdmin(ctx.chat.id, ctx.from.id)
|
||||
if (!isAdmin) {
|
||||
return ctx.reply('this command can only be used by admins')
|
||||
const canManage = await PermissionService.canManageRoles(ctx.chat.id, ctx.from.id)
|
||||
if (!canManage) {
|
||||
return ctx.reply('You don\'t have permission to manage roles')
|
||||
}
|
||||
|
||||
let [slug] = (ctx.args ?? '').split(' ')
|
||||
|
|
@ -45,9 +45,9 @@ export default function (bot: BotType) {
|
|||
await bot.api.setMyCommands({
|
||||
scope: {
|
||||
chat_id: ctx.chatId,
|
||||
type: 'chat'
|
||||
type: 'chat',
|
||||
},
|
||||
commands: roles.map((role) => ({
|
||||
commands: roles.map(role => ({
|
||||
command: role.slug,
|
||||
description: `Mention all members of this role`,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { BotType } from "@/bot/index.js"
|
||||
import { isChatAdmin } from "@/bot/utilities/perms.js"
|
||||
import { RoleService } from "@/shared/services/role.js"
|
||||
import { bold, code, format } from "gramio"
|
||||
import type { BotType } from '@/bot/index.js'
|
||||
import { bold, code, format } from 'gramio'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
export default (bot: BotType) => {
|
||||
bot.command('roledel', async (ctx) => {
|
||||
|
|
@ -9,9 +9,9 @@ export default (bot: BotType) => {
|
|||
return ctx.reply('this command can only be used in groups or supergroups')
|
||||
}
|
||||
|
||||
const isAdmin = await isChatAdmin(ctx.chat.id, ctx.from.id)
|
||||
if (!isAdmin) {
|
||||
return ctx.reply('this command can only be used by admins')
|
||||
const canManage = await PermissionService.canManageRoles(ctx.chat.id, ctx.from.id)
|
||||
if (!canManage) {
|
||||
return ctx.reply('You don\'t have permission to manage roles')
|
||||
}
|
||||
|
||||
let [roleSlug] = (ctx.args ?? '').trim().split(' ')
|
||||
|
|
@ -27,7 +27,7 @@ export default (bot: BotType) => {
|
|||
}
|
||||
|
||||
const members = await RoleService.getMemberIds(roleSlug, ctx.chatId)
|
||||
await Promise.all(members.map((member) => RoleService.removeMember(role.id!, member)))
|
||||
await Promise.all(members.map(member => RoleService.removeMember(role.id!, member)))
|
||||
|
||||
const deletedRole = await RoleService.delete(role.id!)
|
||||
if (!deletedRole) {
|
||||
|
|
@ -36,18 +36,18 @@ export default (bot: BotType) => {
|
|||
|
||||
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`,
|
||||
})),
|
||||
})
|
||||
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`,
|
||||
})),
|
||||
})
|
||||
})
|
||||
|
||||
return ctx.reply(format`role ${bold(role.slug)} deleted`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { autoload } from '@gramio/autoload'
|
|||
import { Bot } from 'gramio'
|
||||
import env from '@/shared/env.js'
|
||||
import { MentionService } from '@/shared/services/mention.js'
|
||||
import { PermissionService } from '@/shared/services/permission.js'
|
||||
import { RoleService } from '@/shared/services/role.js'
|
||||
|
||||
let botUsername: string | undefined
|
||||
|
|
@ -23,6 +24,16 @@ bot.on('message', async (ctx, next) => {
|
|||
if (!mentions.length) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// Check if user has permission to mention roles
|
||||
const canMention = await PermissionService.canMentionRoles(ctx.chatId, ctx.from.id)
|
||||
if (!canMention) {
|
||||
await ctx.reply('You don\'t have permission to mention roles', {
|
||||
reply_parameters: { message_id: ctx.id, allow_sending_without_reply: true },
|
||||
})
|
||||
return // Stop processing
|
||||
}
|
||||
|
||||
const rolesToMention = await Promise.all(mentions.map(mention => RoleService.getBySlugOrAlias(mention.slice(1), ctx.chatId))).then(roles => roles.filter(role => role !== null))
|
||||
|
||||
if (!rolesToMention.length) {
|
||||
|
|
@ -61,6 +72,13 @@ bot.on('message', async (ctx, next) => {
|
|||
return next()
|
||||
}
|
||||
|
||||
// Check if user has permission to mention roles
|
||||
const canMention = await PermissionService.canMentionRoles(ctx.chatId, ctx.from.id)
|
||||
if (!canMention) {
|
||||
await ctx.reply('You don\'t have permission to mention roles')
|
||||
return // Stop processing
|
||||
}
|
||||
|
||||
await MentionService.mentionAll(role.slug, ctx.chatId, ctx.from, ctx.replyMessage?.id ?? undefined)
|
||||
})
|
||||
|
||||
|
|
@ -79,6 +97,8 @@ bot.onStart(async ({ info }) => {
|
|||
{ command: 'leave', description: 'Leave a role' },
|
||||
{ command: 'myroles', description: 'View your roles in this chat' },
|
||||
{ command: 'roles', description: 'View all roles in this chat' },
|
||||
{ command: 'config', description: 'View and configure chat permissions' },
|
||||
{ command: 'reload', description: 'Refresh admin cache' },
|
||||
],
|
||||
suppress: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export const ROLE_NAME_REGEX = /^[a-zA-Z]{4,32}$/
|
||||
export const RESERVED_NAMES = ['roleadd', 'roledel', 'join', 'leave', 'myroles', 'roles']
|
||||
export const ROLE_NAME_REGEX = /^[a-z]{4,32}$/i
|
||||
export const RESERVED_NAMES = ['roleadd', 'roledel', 'join', 'leave', 'myroles', 'roles', 'config', 'reload']
|
||||
|
|
|
|||
|
|
@ -1,14 +1,63 @@
|
|||
import type { TelegramChatMember } from 'gramio'
|
||||
import { bot } from '../index.js'
|
||||
|
||||
// Type definitions for admin permissions
|
||||
export interface AdminInfo {
|
||||
userId: number
|
||||
status: 'creator' | 'administrator'
|
||||
permissions: {
|
||||
canPromoteMembers: boolean
|
||||
canChangeInfo: boolean
|
||||
canManageChat: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Admin cache with 1-hour TTL
|
||||
interface AdminCacheEntry {
|
||||
data: TelegramChatMember[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const adminCache = new Map<number, AdminCacheEntry>()
|
||||
const CACHE_TTL = 60 * 60 * 1000 // 1 hour in milliseconds
|
||||
|
||||
/**
|
||||
* Get chat administrators with caching (1-hour TTL)
|
||||
*/
|
||||
async function getChatAdminsWithCache(chatId: number): Promise<TelegramChatMember[]> {
|
||||
const cached = adminCache.get(chatId)
|
||||
|
||||
// Return cached data if still valid
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.data
|
||||
}
|
||||
|
||||
try {
|
||||
const admins = await bot.api.getChatAdministrators({ chat_id: chatId })
|
||||
adminCache.set(chatId, { data: admins, timestamp: Date.now() })
|
||||
return admins
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching chat administrators:', error)
|
||||
// Return cached data even if expired, or empty array
|
||||
return cached?.data ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh admin cache for a specific chat
|
||||
*/
|
||||
export function refreshAdminCache(chatId: number): void {
|
||||
adminCache.delete(chatId)
|
||||
}
|
||||
|
||||
export async function isChatAdmin(chatId: number, userId: number) {
|
||||
try {
|
||||
// handle hidden administrators
|
||||
if (userId === chatId || userId === 1087968824)
|
||||
return true
|
||||
|
||||
const chatAdmins = await bot.api.getChatAdministrators({
|
||||
chat_id: chatId,
|
||||
})
|
||||
const chatAdmins = await getChatAdminsWithCache(chatId)
|
||||
|
||||
return chatAdmins.some(admin => admin.user.id === userId)
|
||||
}
|
||||
|
|
@ -17,3 +66,91 @@ export async function isChatAdmin(chatId: number, userId: number) {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return true
|
||||
}
|
||||
|
||||
const chatAdmins = await getChatAdminsWithCache(chatId)
|
||||
const userAdmin = chatAdmins.find(admin => admin.user.id === userId)
|
||||
return userAdmin?.status === 'creator'
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error checking if user is chat owner:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return {
|
||||
userId,
|
||||
status: 'creator',
|
||||
permissions: {
|
||||
canPromoteMembers: true,
|
||||
canChangeInfo: true,
|
||||
canManageChat: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const chatAdmins = await getChatAdminsWithCache(chatId)
|
||||
const userAdmin = chatAdmins.find(admin => admin.user.id === userId)
|
||||
|
||||
if (!userAdmin) {
|
||||
return null
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error getting admin info:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific Telegram admin permission
|
||||
*/
|
||||
export async function hasAdminPermission(
|
||||
chatId: number,
|
||||
userId: number,
|
||||
permission: 'can_promote_members' | 'can_change_info' | 'can_manage_chat',
|
||||
): Promise<boolean> {
|
||||
const adminInfo = await getAdminInfo(chatId, userId)
|
||||
if (!adminInfo)
|
||||
return false
|
||||
|
||||
if (adminInfo.status === 'creator')
|
||||
return true
|
||||
|
||||
switch (permission) {
|
||||
case 'can_promote_members':
|
||||
return adminInfo.permissions.canPromoteMembers
|
||||
case 'can_change_info':
|
||||
return adminInfo.permissions.canChangeInfo
|
||||
case 'can_manage_chat':
|
||||
return adminInfo.permissions.canManageChat
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
import { sql } from 'drizzle-orm'
|
||||
import { bigint, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||
import { bigint, pgEnum, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||
|
||||
// Enums for permission levels
|
||||
export const roleManagePermissionEnum = pgEnum('role_manage_permission', [
|
||||
'everyone',
|
||||
'all_admins',
|
||||
'admin_can_promote_members',
|
||||
'admin_can_change_info',
|
||||
'admin_can_manage_chat',
|
||||
'only_owner',
|
||||
])
|
||||
|
||||
export const roleMentionPermissionEnum = pgEnum('role_mention_permission', [
|
||||
'everyone',
|
||||
'all_admins',
|
||||
'only_owner',
|
||||
])
|
||||
|
||||
export const roleSchema = pgTable('roles', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
|
@ -8,7 +23,7 @@ export const roleSchema = pgTable('roles', {
|
|||
chatId: bigint('chat_id', { mode: 'number' }).notNull(),
|
||||
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => sql`now()`),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||
})
|
||||
|
||||
export const roleMembersSchema = pgTable('role_members', {
|
||||
|
|
@ -18,3 +33,21 @@ export const roleMembersSchema = pgTable('role_members', {
|
|||
|
||||
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(),
|
||||
|
||||
// Who can manage roles (add/delete)
|
||||
roleManagePermission: roleManagePermissionEnum('role_manage_permission')
|
||||
.notNull()
|
||||
.default('all_admins'),
|
||||
|
||||
// Who can mention roles (@role or /role commands)
|
||||
roleMentionPermission: roleMentionPermissionEnum('role_mention_permission')
|
||||
.notNull()
|
||||
.default('everyone'),
|
||||
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||
})
|
||||
|
|
|
|||
100
src/shared/services/chatConfig.ts
Normal file
100
src/shared/services/chatConfig.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
import database from '../database/index.js'
|
||||
import { chatConfigSchema } from '../database/schema.js'
|
||||
|
||||
export type RoleManagePermission
|
||||
= | 'everyone'
|
||||
| 'all_admins'
|
||||
| 'admin_can_promote_members'
|
||||
| 'admin_can_change_info'
|
||||
| 'admin_can_manage_chat'
|
||||
| 'only_owner'
|
||||
|
||||
export type RoleMentionPermission
|
||||
= | 'everyone'
|
||||
| 'all_admins'
|
||||
| 'only_owner'
|
||||
|
||||
export interface ChatConfig {
|
||||
id: string
|
||||
chatId: number
|
||||
roleManagePermission: RoleManagePermission
|
||||
roleMentionPermission: RoleMentionPermission
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export class ChatConfigService {
|
||||
/**
|
||||
* Get config for a chat (creates default if doesn't exist)
|
||||
*/
|
||||
static async getOrCreate(chatId: number): Promise<ChatConfig> {
|
||||
const existing = await database
|
||||
.select()
|
||||
.from(chatConfigSchema)
|
||||
.where(eq(chatConfigSchema.chatId, chatId))
|
||||
.then(result => result[0] ?? null)
|
||||
|
||||
if (existing) {
|
||||
return existing as ChatConfig
|
||||
}
|
||||
|
||||
// Create default config
|
||||
return database
|
||||
.insert(chatConfigSchema)
|
||||
.values({
|
||||
chatId,
|
||||
roleManagePermission: 'all_admins',
|
||||
roleMentionPermission: 'everyone',
|
||||
})
|
||||
.returning()
|
||||
.then(result => result[0]!) as Promise<ChatConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role management permission
|
||||
*/
|
||||
static async setRoleManagePermission(
|
||||
chatId: number,
|
||||
permission: RoleManagePermission,
|
||||
): Promise<ChatConfig | null> {
|
||||
// Ensure config exists first
|
||||
await this.getOrCreate(chatId)
|
||||
|
||||
return database
|
||||
.update(chatConfigSchema)
|
||||
.set({ roleManagePermission: permission })
|
||||
.where(eq(chatConfigSchema.chatId, chatId))
|
||||
.returning()
|
||||
.then(result => (result[0] ?? null) as ChatConfig | null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role mention permission
|
||||
*/
|
||||
static async setRoleMentionPermission(
|
||||
chatId: number,
|
||||
permission: RoleMentionPermission,
|
||||
): Promise<ChatConfig | null> {
|
||||
// Ensure config exists first
|
||||
await this.getOrCreate(chatId)
|
||||
|
||||
return database
|
||||
.update(chatConfigSchema)
|
||||
.set({ roleMentionPermission: permission })
|
||||
.where(eq(chatConfigSchema.chatId, chatId))
|
||||
.returning()
|
||||
.then(result => (result[0] ?? null) as ChatConfig | null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current config (without creating)
|
||||
*/
|
||||
static async get(chatId: number): Promise<ChatConfig | null> {
|
||||
return database
|
||||
.select()
|
||||
.from(chatConfigSchema)
|
||||
.where(eq(chatConfigSchema.chatId, chatId))
|
||||
.then(result => (result[0] ?? null) as ChatConfig | null)
|
||||
}
|
||||
}
|
||||
86
src/shared/services/permission.ts
Normal file
86
src/shared/services/permission.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import type { RoleManagePermission, RoleMentionPermission } from './chatConfig.js'
|
||||
import { hasAdminPermission, isChatAdmin, isChatOwner } from '@/bot/utilities/perms.js'
|
||||
import { ChatConfigService } from './chatConfig.js'
|
||||
|
||||
export class PermissionService {
|
||||
/**
|
||||
* Check if user can manage roles (add/delete) based on chat config
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue