feat: 完善QQBot权限过滤和账号删除联动
This commit is contained in:
parent
beec106f4f
commit
aa4b46950b
@ -28,7 +28,7 @@ QQBOT_EVENT_BUS=mqtt
|
||||
QQBOT_REVERSE_WS_PATH=/qqbot/onebot/reverse
|
||||
QQBOT_REVERSE_WS_TOKEN=
|
||||
QQBOT_AUTO_REGISTER_ACCOUNT=true
|
||||
QQBOT_REQUIRE_ALLOWLIST=true
|
||||
QQBOT_REQUIRE_ALLOWLIST=false
|
||||
QQBOT_API_TIMEOUT_MS=10000
|
||||
QQBOT_SEND_RATE_PER_SECOND=1
|
||||
NAPCAT_WEBUI_BASE_URL=http://127.0.0.1:6099
|
||||
|
||||
@ -63,6 +63,17 @@ CREATE TABLE IF NOT EXISTS `qqbot_account_napcat` (
|
||||
KEY `idx_qqbot_account_napcat_container` (`container_id`, `is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_config` (
|
||||
`id` bigint NOT NULL,
|
||||
`config_key` varchar(120) NOT NULL,
|
||||
`config_value` text NOT NULL,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_qqbot_config_key` (`config_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_rule` (
|
||||
`id` bigint NOT NULL,
|
||||
`name` varchar(120) NOT NULL DEFAULT '',
|
||||
@ -146,8 +157,10 @@ CREATE TABLE IF NOT EXISTS `qqbot_send_log` (
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_allowlist` (
|
||||
`id` bigint NOT NULL,
|
||||
`self_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT 'all',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT 'qq',
|
||||
`target_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`user_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`precise_user` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||
@ -160,8 +173,10 @@ CREATE TABLE IF NOT EXISTS `qqbot_allowlist` (
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_blocklist` (
|
||||
`id` bigint NOT NULL,
|
||||
`self_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT 'all',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT 'qq',
|
||||
`target_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`user_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`precise_user` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||
@ -181,6 +196,73 @@ CREATE TABLE IF NOT EXISTS `qqbot_dedupe` (
|
||||
UNIQUE KEY `uk_qqbot_dedupe_event_key` (`event_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_allowlist` ADD COLUMN `user_id` varchar(64) NOT NULL DEFAULT '''' AFTER `target_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_allowlist'
|
||||
AND column_name = 'user_id'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_allowlist` ADD COLUMN `precise_user` tinyint(1) NOT NULL DEFAULT 0 AFTER `user_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_allowlist'
|
||||
AND column_name = 'precise_user'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_blocklist` ADD COLUMN `user_id` varchar(64) NOT NULL DEFAULT '''' AFTER `target_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_blocklist'
|
||||
AND column_name = 'user_id'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_blocklist` ADD COLUMN `precise_user` tinyint(1) NOT NULL DEFAULT 0 AFTER `user_id`',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_blocklist'
|
||||
AND column_name = 'precise_user'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
INSERT INTO `qqbot_config` (`id`, `config_key`, `config_value`, `remark`)
|
||||
VALUES
|
||||
(2041700000000200501, 'permission.allowlistEnabled', 'false', 'QQBot 白名单总开关'),
|
||||
(2041700000000200502, 'permission.blocklistEnabled', 'true', 'QQBot 黑名单总开关')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`config_key` = VALUES(`config_key`);
|
||||
|
||||
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`)
|
||||
VALUES
|
||||
(2041700000000100400, 0, 'QqBot', '/qqbot', NULL, '/qqbot/dashboard', NULL, 'catalog', '{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}', 1, 110),
|
||||
|
||||
@ -99,6 +99,8 @@ export class QqbotAccountController {
|
||||
@ApiOperation({ summary: '删除 QQBot 账号' })
|
||||
@ApiQuery({ name: 'id', type: String })
|
||||
async delete(@Query('id') id: string) {
|
||||
const account = await this.accountService.findById(id);
|
||||
if (account) await this.reverseWsService.kick(account.selfId);
|
||||
return vbenSuccess(await this.accountService.remove(id));
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
QqbotAccountQueryDto,
|
||||
QqbotAccountUpdateDto,
|
||||
} from './qqbot-account.dto';
|
||||
import { QqbotNapcatContainerService } from '../napcat/qqbot-napcat-container.service';
|
||||
import type { QqbotConnectionRole } from '../qqbot.types';
|
||||
import { getPageParams, normalizeNullableString } from '../qqbot.utils';
|
||||
|
||||
@ -16,6 +17,7 @@ export class QqbotAccountService {
|
||||
constructor(
|
||||
@InjectRepository(QqbotAccount)
|
||||
private readonly accountRepository: Repository<QqbotAccount>,
|
||||
private readonly napcatContainerService: QqbotNapcatContainerService,
|
||||
) {}
|
||||
|
||||
async page(query: QqbotAccountQueryDto) {
|
||||
@ -202,14 +204,29 @@ export class QqbotAccountService {
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const account = await this.accountRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (!account) {
|
||||
throwVbenError('QQBot 账号不存在或已删除');
|
||||
}
|
||||
|
||||
const containerResult =
|
||||
await this.napcatContainerService.removeAccountContainers(id);
|
||||
await this.accountRepository.update(
|
||||
{ id },
|
||||
{
|
||||
connectStatus: 'offline',
|
||||
enabled: false,
|
||||
isDeleted: true,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
return {
|
||||
deletedContainers: containerResult.deletedContainers,
|
||||
};
|
||||
}
|
||||
|
||||
async markOnline(
|
||||
|
||||
37
src/qqbot/config/qqbot-config.entity.ts
Normal file
37
src/qqbot/config/qqbot-config.entity.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId } from '@/common';
|
||||
|
||||
@Entity('qqbot_config')
|
||||
@Index('uk_qqbot_config_key', ['configKey'], { unique: true })
|
||||
export class QqbotConfig {
|
||||
@PrimaryColumn({ type: 'bigint' })
|
||||
id: string;
|
||||
|
||||
@Column({ length: 120, name: 'config_key' })
|
||||
configKey: string;
|
||||
|
||||
@Column({ name: 'config_value', type: 'text' })
|
||||
configValue: string;
|
||||
|
||||
@Column({ default: '', length: 255 })
|
||||
remark: string;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
createTime: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: Date;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
ensureSnowflakeId(this);
|
||||
}
|
||||
}
|
||||
90
src/qqbot/config/qqbot-config.service.ts
Normal file
90
src/qqbot/config/qqbot-config.service.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { QqbotConfig } from './qqbot-config.entity';
|
||||
|
||||
const QQBOT_PERMISSION_CONFIG_KEYS = {
|
||||
allowlistEnabled: 'permission.allowlistEnabled',
|
||||
blocklistEnabled: 'permission.blocklistEnabled',
|
||||
} as const;
|
||||
|
||||
export type QqbotPermissionConfig = {
|
||||
allowlistEnabled: boolean;
|
||||
blocklistEnabled: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QqbotConfigService {
|
||||
constructor(
|
||||
@InjectRepository(QqbotConfig)
|
||||
private readonly configRepository: Repository<QqbotConfig>,
|
||||
) {}
|
||||
|
||||
async getPermissionConfig(): Promise<QqbotPermissionConfig> {
|
||||
const [allowlistEnabled, blocklistEnabled] = await Promise.all([
|
||||
this.getBoolean(QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled, false),
|
||||
this.getBoolean(QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled, true),
|
||||
]);
|
||||
|
||||
return { allowlistEnabled, blocklistEnabled };
|
||||
}
|
||||
|
||||
async updatePermissionConfig(
|
||||
config: Partial<QqbotPermissionConfig>,
|
||||
): Promise<QqbotPermissionConfig> {
|
||||
const tasks: Array<Promise<void>> = [];
|
||||
|
||||
if (typeof config.allowlistEnabled === 'boolean') {
|
||||
tasks.push(
|
||||
this.setBoolean(
|
||||
QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled,
|
||||
config.allowlistEnabled,
|
||||
'QQBot 白名单总开关',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (typeof config.blocklistEnabled === 'boolean') {
|
||||
tasks.push(
|
||||
this.setBoolean(
|
||||
QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled,
|
||||
config.blocklistEnabled,
|
||||
'QQBot 黑名单总开关',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
return this.getPermissionConfig();
|
||||
}
|
||||
|
||||
private async getBoolean(configKey: string, defaultValue: boolean) {
|
||||
const record = await this.configRepository.findOne({
|
||||
where: { configKey },
|
||||
});
|
||||
if (!record) return defaultValue;
|
||||
return record.configValue === 'true';
|
||||
}
|
||||
|
||||
private async setBoolean(configKey: string, value: boolean, remark: string) {
|
||||
const exists = await this.configRepository.findOne({
|
||||
where: { configKey },
|
||||
});
|
||||
const configValue = value ? 'true' : 'false';
|
||||
|
||||
if (exists) {
|
||||
await this.configRepository.update(
|
||||
{ id: exists.id },
|
||||
{ configValue, remark },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.configRepository.save(
|
||||
this.configRepository.create({
|
||||
configKey,
|
||||
configValue,
|
||||
remark,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7,20 +7,31 @@ import { toStringId } from '../qqbot.utils';
|
||||
|
||||
export function isOneBotMessageEvent(
|
||||
payload: QqbotOneBotEvent,
|
||||
): payload is QqbotOneBotEvent & { message_type: QqbotMessageType } {
|
||||
return payload?.post_type === 'message' && !!payload.message_type;
|
||||
): payload is QqbotOneBotEvent & { message_type: string } {
|
||||
return (
|
||||
payload?.post_type === 'message' &&
|
||||
!!normalizeMessageType(payload.message_type)
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeOneBotMessage(
|
||||
payload: QqbotOneBotEvent,
|
||||
): QqbotNormalizedMessage {
|
||||
const messageType = payload.message_type as QqbotMessageType;
|
||||
const messageType = normalizeMessageType(payload.message_type) || 'private';
|
||||
const channelId =
|
||||
toStringId(payload.channel_id) || toStringId(payload.guild_id) || undefined;
|
||||
const groupId = toStringId(payload.group_id) || undefined;
|
||||
const userId = toStringId(payload.user_id);
|
||||
const targetId = messageType === 'group' ? groupId || '' : userId;
|
||||
const targetId =
|
||||
messageType === 'group'
|
||||
? groupId || ''
|
||||
: messageType === 'channel'
|
||||
? channelId || ''
|
||||
: userId;
|
||||
const messageText = extractMessageText(payload);
|
||||
|
||||
return {
|
||||
channelId,
|
||||
eventTime: payload.time
|
||||
? new Date(Number(payload.time) * 1000)
|
||||
: new Date(),
|
||||
@ -64,3 +75,13 @@ function extractMessageText(payload: QqbotOneBotEvent) {
|
||||
.join('')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizeMessageType(messageType?: string): QqbotMessageType | null {
|
||||
if (messageType === 'private' || messageType === 'group') {
|
||||
return messageType;
|
||||
}
|
||||
if (messageType === 'channel' || messageType === 'guild') {
|
||||
return 'channel';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import type {
|
||||
QqbotConversationQueryDto,
|
||||
QqbotMessageQueryDto,
|
||||
} from './qqbot-message.dto';
|
||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||
import type { QqbotMessageType, QqbotNormalizedMessage } from '../qqbot.types';
|
||||
import { getPageParams } from '../qqbot.utils';
|
||||
|
||||
@Injectable()
|
||||
@ -110,7 +110,7 @@ export class QqbotMessageService {
|
||||
async saveOutgoing(params: {
|
||||
messageId?: string;
|
||||
messageText: string;
|
||||
messageType: 'group' | 'private';
|
||||
messageType: QqbotMessageType;
|
||||
selfId: string;
|
||||
targetId: string;
|
||||
userId: string;
|
||||
|
||||
@ -100,6 +100,98 @@ export class QqbotNapcatContainerService {
|
||||
);
|
||||
}
|
||||
|
||||
async removeAccountContainers(accountId: string) {
|
||||
const bindings = await this.bindingRepository.find({
|
||||
where: {
|
||||
accountId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (bindings.length <= 0) return { deletedContainers: 0 };
|
||||
|
||||
let deletedContainers = 0;
|
||||
for (const binding of bindings) {
|
||||
const sharedCount = await this.bindingRepository
|
||||
.createQueryBuilder('binding')
|
||||
.where('binding.containerId = :containerId', {
|
||||
containerId: binding.containerId,
|
||||
})
|
||||
.andWhere('binding.accountId != :accountId', { accountId })
|
||||
.andWhere('binding.isDeleted = :isDeleted', { isDeleted: false })
|
||||
.getCount();
|
||||
if (sharedCount > 0) continue;
|
||||
|
||||
const deleted = await this.removeContainer(binding.containerId);
|
||||
if (deleted) deletedContainers += 1;
|
||||
}
|
||||
|
||||
await this.bindingRepository.update(
|
||||
{ accountId, isDeleted: false },
|
||||
{
|
||||
bindStatus: 'disabled',
|
||||
isDeleted: true,
|
||||
isPrimary: false,
|
||||
},
|
||||
);
|
||||
|
||||
return { deletedContainers };
|
||||
}
|
||||
|
||||
private async removeContainer(containerId: string) {
|
||||
const container = await this.containerRepository.findOne({
|
||||
where: {
|
||||
id: containerId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (!container) return false;
|
||||
|
||||
if (this.getManagedMode() === 'ssh') {
|
||||
await this.removeRemoteDockerContainer(container);
|
||||
}
|
||||
|
||||
await this.containerRepository.update(
|
||||
{ id: container.id },
|
||||
{
|
||||
isDeleted: true,
|
||||
lastError: null,
|
||||
status: 'stopped',
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async removeRemoteDockerContainer(container: QqbotNapcatContainer) {
|
||||
const script = this.buildRemoteRemoveScript(container);
|
||||
await this.runProcess('ssh', [...this.getSshArgs(), 'sh -s'], script);
|
||||
}
|
||||
|
||||
private buildRemoteRemoveScript(container: QqbotNapcatContainer) {
|
||||
const dataDir = this.sh(container.dataDir || '');
|
||||
const name = this.sh(container.name);
|
||||
const rootDir = this.sh(this.getRootDir());
|
||||
|
||||
return `
|
||||
set -eu
|
||||
NAME=${name}
|
||||
DATA_DIR=${dataDir}
|
||||
ROOT_DIR=${rootDir}
|
||||
|
||||
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if [ -n "$DATA_DIR" ] && [ "$DATA_DIR" != "/" ]; then
|
||||
case "$DATA_DIR" in
|
||||
"$ROOT_DIR"/*)
|
||||
rm -rf "$DATA_DIR"
|
||||
;;
|
||||
*)
|
||||
echo "skip unsafe data dir: $DATA_DIR" >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
`;
|
||||
}
|
||||
|
||||
private async getPrimaryRuntime(accountId: string) {
|
||||
const binding = await this.bindingRepository.findOne({
|
||||
order: {
|
||||
|
||||
@ -17,12 +17,18 @@ export class QqbotAllowlist {
|
||||
@Column({ default: '', length: 64, name: 'self_id' })
|
||||
selfId: string;
|
||||
|
||||
@Column({ default: 'all', length: 32, name: 'target_type' })
|
||||
@Column({ default: 'qq', length: 32, name: 'target_type' })
|
||||
targetType: QqbotPermissionTargetType;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'target_id' })
|
||||
targetId: string;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'user_id' })
|
||||
userId: string;
|
||||
|
||||
@Column({ default: false, name: 'precise_user' })
|
||||
preciseUser: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
enabled: boolean;
|
||||
|
||||
|
||||
@ -17,12 +17,18 @@ export class QqbotBlocklist {
|
||||
@Column({ default: '', length: 64, name: 'self_id' })
|
||||
selfId: string;
|
||||
|
||||
@Column({ default: 'all', length: 32, name: 'target_type' })
|
||||
@Column({ default: 'qq', length: 32, name: 'target_type' })
|
||||
targetType: QqbotPermissionTargetType;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'target_id' })
|
||||
targetId: string;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'user_id' })
|
||||
userId: string;
|
||||
|
||||
@Column({ default: false, name: 'precise_user' })
|
||||
preciseUser: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
enabled: boolean;
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||
import { vbenSuccess } from '@/common';
|
||||
import {
|
||||
QqbotPermissionBodyDto,
|
||||
QqbotPermissionConfigDto,
|
||||
QqbotPermissionQueryDto,
|
||||
QqbotPermissionUpdateDto,
|
||||
} from './qqbot-permission.dto';
|
||||
@ -24,6 +25,19 @@ import { QqbotPermissionService } from './qqbot-permission.service';
|
||||
export class QqbotPermissionController {
|
||||
constructor(private readonly permissionService: QqbotPermissionService) {}
|
||||
|
||||
@Get('config')
|
||||
@ApiOperation({ summary: 'QQBot 权限名单配置' })
|
||||
async config() {
|
||||
return vbenSuccess(await this.permissionService.getConfig());
|
||||
}
|
||||
|
||||
@Post('config')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '保存 QQBot 权限名单配置' })
|
||||
async updateConfig(@Body() body: QqbotPermissionConfigDto) {
|
||||
return vbenSuccess(await this.permissionService.updateConfig(body));
|
||||
}
|
||||
|
||||
@Get('allowlist')
|
||||
@ApiOperation({ summary: 'QQBot 白名单分页' })
|
||||
async allowlist(@Query() query: QqbotPermissionQueryDto) {
|
||||
|
||||
@ -1,16 +1,30 @@
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
|
||||
import type { QqbotPermissionTargetType } from '../qqbot.types';
|
||||
|
||||
export class QqbotPermissionConfigDto {
|
||||
@ApiPropertyOptional({ default: false })
|
||||
allowlistEnabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
blocklistEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class QqbotPermissionBodyDto {
|
||||
@ApiPropertyOptional({ example: '10000' })
|
||||
selfId?: string;
|
||||
|
||||
@ApiProperty({ default: 'private' })
|
||||
@ApiProperty({ default: 'qq' })
|
||||
targetType: QqbotPermissionTargetType;
|
||||
|
||||
@ApiProperty({ example: '123456' })
|
||||
targetId: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '123456' })
|
||||
userId?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
preciseUser?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
enabled?: boolean;
|
||||
|
||||
@ -40,4 +54,10 @@ export class QqbotPermissionQueryDto {
|
||||
|
||||
@ApiPropertyOptional()
|
||||
targetId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
userId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
preciseUser?: boolean;
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Brackets, Repository } from 'typeorm';
|
||||
import { throwVbenError } from '@/common';
|
||||
import { QqbotAllowlist } from './qqbot-allowlist.entity';
|
||||
import { QqbotBlocklist } from './qqbot-blocklist.entity';
|
||||
import type {
|
||||
QqbotPermissionBodyDto,
|
||||
QqbotPermissionConfigDto,
|
||||
QqbotPermissionQueryDto,
|
||||
QqbotPermissionUpdateDto,
|
||||
} from './qqbot-permission.dto';
|
||||
import { QqbotConfigService } from '../config/qqbot-config.service';
|
||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||
import { getPageParams } from '../qqbot.utils';
|
||||
|
||||
@ -17,12 +20,21 @@ type PermissionEntity = QqbotAllowlist | QqbotBlocklist;
|
||||
@Injectable()
|
||||
export class QqbotPermissionService {
|
||||
constructor(
|
||||
private readonly configService: QqbotConfigService,
|
||||
@InjectRepository(QqbotAllowlist)
|
||||
private readonly allowlistRepository: Repository<QqbotAllowlist>,
|
||||
@InjectRepository(QqbotBlocklist)
|
||||
private readonly blocklistRepository: Repository<QqbotBlocklist>,
|
||||
) {}
|
||||
|
||||
async getConfig() {
|
||||
return this.configService.getPermissionConfig();
|
||||
}
|
||||
|
||||
async updateConfig(body: QqbotPermissionConfigDto) {
|
||||
return this.configService.updatePermissionConfig(body);
|
||||
}
|
||||
|
||||
async page(kind: PermissionKind, query: QqbotPermissionQueryDto) {
|
||||
const { pageNo, pageSize, skip } = getPageParams(query);
|
||||
const repository = this.getRepository(kind);
|
||||
@ -45,6 +57,16 @@ export class QqbotPermissionService {
|
||||
targetId: `%${query.targetId}%`,
|
||||
});
|
||||
}
|
||||
if (query.userId) {
|
||||
builder.andWhere('permission.userId LIKE :userId', {
|
||||
userId: `%${query.userId}%`,
|
||||
});
|
||||
}
|
||||
if (query.preciseUser !== undefined && `${query.preciseUser}` !== '') {
|
||||
builder.andWhere('permission.preciseUser = :preciseUser', {
|
||||
preciseUser: this.normalizeBoolean(query.preciseUser),
|
||||
});
|
||||
}
|
||||
|
||||
const [list, total] = await builder
|
||||
.orderBy('permission.createTime', 'DESC')
|
||||
@ -56,13 +78,10 @@ export class QqbotPermissionService {
|
||||
|
||||
async save(kind: PermissionKind, body: QqbotPermissionBodyDto) {
|
||||
const repository = this.getRepository(kind);
|
||||
const payload = this.normalizeBody(body);
|
||||
const saved = await repository.save(
|
||||
repository.create({
|
||||
enabled: body.enabled ?? true,
|
||||
remark: body.remark || '',
|
||||
selfId: body.selfId || '',
|
||||
targetId: body.targetId || '',
|
||||
targetType: body.targetType || 'all',
|
||||
...payload,
|
||||
} as PermissionEntity),
|
||||
);
|
||||
return saved.id;
|
||||
@ -70,14 +89,11 @@ export class QqbotPermissionService {
|
||||
|
||||
async update(kind: PermissionKind, body: QqbotPermissionUpdateDto) {
|
||||
const repository = this.getRepository(kind);
|
||||
const payload = this.normalizeBody(body);
|
||||
await repository.update(
|
||||
{ id: body.id } as any,
|
||||
{
|
||||
enabled: body.enabled ?? true,
|
||||
remark: body.remark || '',
|
||||
selfId: body.selfId || '',
|
||||
targetId: body.targetId || '',
|
||||
targetType: body.targetType || 'all',
|
||||
...payload,
|
||||
} as any,
|
||||
);
|
||||
return true;
|
||||
@ -90,21 +106,14 @@ export class QqbotPermissionService {
|
||||
}
|
||||
|
||||
async isBlocked(message: QqbotNormalizedMessage) {
|
||||
const config = await this.configService.getPermissionConfig();
|
||||
if (!config.blocklistEnabled) return false;
|
||||
return this.existsMatched(this.blocklistRepository, message);
|
||||
}
|
||||
|
||||
async isAllowed(message: QqbotNormalizedMessage) {
|
||||
const requireAllowlist =
|
||||
`${process.env.QQBOT_REQUIRE_ALLOWLIST ?? 'true'}` !== 'false';
|
||||
if (!requireAllowlist) return true;
|
||||
|
||||
const hasRule = await this.allowlistRepository.count({
|
||||
where: {
|
||||
enabled: true,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (hasRule <= 0) return false;
|
||||
const config = await this.configService.getPermissionConfig();
|
||||
if (!config.allowlistEnabled) return true;
|
||||
return this.existsMatched(this.allowlistRepository, message);
|
||||
}
|
||||
|
||||
@ -121,17 +130,96 @@ export class QqbotPermissionService {
|
||||
selfId: message.selfId,
|
||||
})
|
||||
.andWhere(
|
||||
'(permission.targetType = :all OR (permission.targetType = :targetType AND permission.targetId = :targetId))',
|
||||
{
|
||||
all: 'all',
|
||||
targetId: message.targetId,
|
||||
targetType: message.messageType,
|
||||
},
|
||||
new Brackets((qb) => {
|
||||
qb.where('permission.targetType = :all', { all: 'all' }).orWhere(
|
||||
'(permission.targetType IN (:...qqTargetTypes) AND permission.targetId = :userId)',
|
||||
{
|
||||
qqTargetTypes: ['qq', 'private'],
|
||||
userId: message.userId,
|
||||
},
|
||||
);
|
||||
|
||||
if (message.messageType === 'group') {
|
||||
qb.orWhere(
|
||||
`(permission.targetType = :groupType
|
||||
AND permission.targetId = :targetId
|
||||
AND (
|
||||
permission.preciseUser = :notPrecise
|
||||
OR (permission.preciseUser = :precise AND permission.userId = :userId)
|
||||
))`,
|
||||
{
|
||||
groupType: 'group',
|
||||
notPrecise: false,
|
||||
precise: true,
|
||||
targetId: message.targetId,
|
||||
userId: message.userId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (message.messageType === 'channel') {
|
||||
qb.orWhere(
|
||||
`(permission.targetType = :channelType
|
||||
AND permission.targetId = :targetId
|
||||
AND (
|
||||
permission.preciseUser = :notPrecise
|
||||
OR (permission.preciseUser = :precise AND permission.userId = :userId)
|
||||
))`,
|
||||
{
|
||||
channelType: 'channel',
|
||||
notPrecise: false,
|
||||
precise: true,
|
||||
targetId: message.targetId,
|
||||
userId: message.userId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.getCount();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private normalizeBody(
|
||||
body: Partial<QqbotPermissionBodyDto>,
|
||||
): Partial<PermissionEntity> {
|
||||
const targetType = body.targetType === 'private' ? 'qq' : body.targetType;
|
||||
const normalizedTargetType = targetType || 'qq';
|
||||
const targetId = `${body.targetId || ''}`.trim();
|
||||
const userId = `${body.userId || ''}`.trim();
|
||||
const preciseUser =
|
||||
normalizedTargetType === 'group' || normalizedTargetType === 'channel'
|
||||
? !!body.preciseUser
|
||||
: false;
|
||||
|
||||
if (!targetId) {
|
||||
throwVbenError(
|
||||
normalizedTargetType === 'qq'
|
||||
? '请填写 QQ 号'
|
||||
: normalizedTargetType === 'group'
|
||||
? '请填写群号'
|
||||
: '请填写频道 ID',
|
||||
);
|
||||
}
|
||||
if (preciseUser && !userId) {
|
||||
throwVbenError('开启精确到 QQ 号后必须填写 QQ 号');
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: body.enabled ?? true,
|
||||
preciseUser,
|
||||
remark: body.remark || '',
|
||||
selfId: body.selfId || '',
|
||||
targetId,
|
||||
targetType: normalizedTargetType,
|
||||
userId: preciseUser ? userId : '',
|
||||
} as Partial<PermissionEntity>;
|
||||
}
|
||||
|
||||
private normalizeBoolean(value: unknown) {
|
||||
return value === true || value === 'true' || value === 1 || value === '1';
|
||||
}
|
||||
|
||||
private getRepository(kind: PermissionKind) {
|
||||
return kind === 'allowlist'
|
||||
? this.allowlistRepository
|
||||
|
||||
@ -7,6 +7,8 @@ import { QqbotAccount } from './account/qqbot-account.entity';
|
||||
import { QqbotAccountService } from './account/qqbot-account.service';
|
||||
import { QqbotNapcatLoginService } from './account/qqbot-napcat-login.service';
|
||||
import { QqbotReverseWsService } from './connection/qqbot-reverse-ws.service';
|
||||
import { QqbotConfig } from './config/qqbot-config.entity';
|
||||
import { QqbotConfigService } from './config/qqbot-config.service';
|
||||
import { QqbotDashboardController } from './dashboard/qqbot-dashboard.controller';
|
||||
import { QqbotDashboardService } from './dashboard/qqbot-dashboard.service';
|
||||
import { QqbotDedupe } from './dedupe/qqbot-dedupe.entity';
|
||||
@ -41,6 +43,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
QqbotAccount,
|
||||
QqbotAllowlist,
|
||||
QqbotBlocklist,
|
||||
QqbotConfig,
|
||||
QqbotConversation,
|
||||
QqbotDedupe,
|
||||
QqbotMessage,
|
||||
@ -61,6 +64,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
providers: [
|
||||
QqbotAccountService,
|
||||
QqbotBusService,
|
||||
QqbotConfigService,
|
||||
QqbotDashboardService,
|
||||
QqbotDedupeService,
|
||||
QqbotEventService,
|
||||
|
||||
@ -10,7 +10,7 @@ export type QqbotLoginScanStatus = 'error' | 'expired' | 'pending' | 'success';
|
||||
|
||||
export type QqbotMessageDirection = 'inbound' | 'outbound';
|
||||
|
||||
export type QqbotMessageType = 'group' | 'private';
|
||||
export type QqbotMessageType = 'channel' | 'group' | 'private';
|
||||
|
||||
export type QqbotNapcatContainerStatus =
|
||||
| 'creating'
|
||||
@ -22,17 +22,19 @@ export type QqbotAccountNapcatBindStatus = 'bound' | 'disabled' | 'pending';
|
||||
|
||||
export type QqbotRuleMatchType = 'equals' | 'keyword' | 'regex';
|
||||
|
||||
export type QqbotRuleTargetType = 'all' | 'group' | 'private';
|
||||
export type QqbotRuleTargetType = 'all' | 'channel' | 'group' | 'private';
|
||||
|
||||
export type QqbotSendStatus = 'failed' | 'pending' | 'success';
|
||||
|
||||
export type QqbotPermissionTargetType = 'all' | 'group' | 'private';
|
||||
export type QqbotPermissionTargetType = 'channel' | 'group' | 'private' | 'qq';
|
||||
|
||||
export type QqbotOneBotEvent = Record<string, any> & {
|
||||
channel_id?: number | string;
|
||||
group_id?: number | string;
|
||||
guild_id?: number | string;
|
||||
message?: any;
|
||||
message_id?: number | string;
|
||||
message_type?: QqbotMessageType;
|
||||
message_type?: string;
|
||||
post_type?: string;
|
||||
raw_message?: string;
|
||||
self_id?: number | string;
|
||||
@ -42,6 +44,7 @@ export type QqbotOneBotEvent = Record<string, any> & {
|
||||
};
|
||||
|
||||
export type QqbotNormalizedMessage = {
|
||||
channelId?: string;
|
||||
eventTime: Date;
|
||||
groupId?: string;
|
||||
messageId: string;
|
||||
|
||||
@ -26,6 +26,10 @@ export class QqbotRuleEngineService {
|
||||
await this.ruleService.markHit(rule);
|
||||
try {
|
||||
await this.sendService.sendText({
|
||||
channelId: message.channelId,
|
||||
guildId: message.rawEvent.guild_id
|
||||
? `${message.rawEvent.guild_id}`
|
||||
: undefined,
|
||||
message: rule.replyContent,
|
||||
selfId: message.selfId,
|
||||
targetId: message.targetId,
|
||||
|
||||
@ -78,6 +78,8 @@ export class QqbotSendService {
|
||||
}
|
||||
|
||||
async sendText(params: {
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
message: string;
|
||||
selfId?: string;
|
||||
targetId: string;
|
||||
@ -90,12 +92,7 @@ export class QqbotSendService {
|
||||
|
||||
this.rateLimitService.assertCanSend(account.selfId, params.targetId);
|
||||
|
||||
const action =
|
||||
params.targetType === 'group' ? 'send_group_msg' : 'send_private_msg';
|
||||
const actionParams =
|
||||
params.targetType === 'group'
|
||||
? { group_id: params.targetId, message: params.message }
|
||||
: { message: params.message, user_id: params.targetId };
|
||||
const { action, actionParams } = this.buildAction(params);
|
||||
|
||||
const log = await this.sendLogRepository.save(
|
||||
this.sendLogRepository.create({
|
||||
@ -175,4 +172,34 @@ export class QqbotSendService {
|
||||
strict: false,
|
||||
});
|
||||
}
|
||||
|
||||
private buildAction(params: {
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
message: string;
|
||||
targetId: string;
|
||||
targetType: QqbotMessageType;
|
||||
}) {
|
||||
if (params.targetType === 'group') {
|
||||
return {
|
||||
action: 'send_group_msg',
|
||||
actionParams: { group_id: params.targetId, message: params.message },
|
||||
};
|
||||
}
|
||||
if (params.targetType === 'channel') {
|
||||
const actionParams: Record<string, any> = {
|
||||
channel_id: params.channelId || params.targetId,
|
||||
message: params.message,
|
||||
};
|
||||
if (params.guildId) actionParams.guild_id = params.guildId;
|
||||
return {
|
||||
action: 'send_guild_channel_msg',
|
||||
actionParams,
|
||||
};
|
||||
}
|
||||
return {
|
||||
action: 'send_private_msg',
|
||||
actionParams: { message: params.message, user_id: params.targetId },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user