feat: 完善QQBot权限过滤和账号删除联动

This commit is contained in:
sunlei 2026-06-01 20:26:54 +08:00
parent beec106f4f
commit aa4b46950b
18 changed files with 564 additions and 51 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -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;
}

View File

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

View File

@ -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: {

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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))',
new Brackets((qb) => {
qb.where('permission.targetType = :all', { all: 'all' }).orWhere(
'(permission.targetType IN (:...qqTargetTypes) AND permission.targetId = :userId)',
{
all: 'all',
targetId: message.targetId,
targetType: message.messageType,
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

View File

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

View File

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

View File

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

View File

@ -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 },
};
}
}