From d4e5201913bef93f7e7326f1a655bff02b6e3a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sat, 18 May 2024 11:48:38 +0800 Subject: [PATCH] feat: action check data --- package.json | 2 + src/common/utils/type.ts | 31 +++++++++++++ src/onebot11/action/BaseAction.ts | 17 ++++++-- .../action/extends/SetOnlineStatus.ts | 24 ++++++++--- src/onebot11/action/extends/SetQQAvatar.ts | 31 ++++++++----- src/onebot11/action/file/GetFile.ts | 35 ++++++++++++--- src/onebot11/action/go-cqhttp/DownloadFile.ts | 43 ++++++++++++++----- .../action/go-cqhttp/GetForwardMsg.ts | 21 ++++++--- 8 files changed, 158 insertions(+), 46 deletions(-) create mode 100644 src/common/utils/type.ts diff --git a/package.json b/package.json index 61d68251..bd35c3d0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "vite-tsconfig-paths": "^4.3.2" }, "dependencies": { + "ajv": "^8.13.0", "commander": "^12.0.0", "cors": "^2.8.5", "express": "^5.0.0-beta.2", @@ -55,6 +56,7 @@ "file-type": "^19.0.0", "fluent-ffmpeg": "^2.1.2", "image-size": "^1.1.1", + "json-schema-to-ts": "^3.1.0", "log4js": "^6.9.1", "qrcode-terminal": "^0.12.0", "silk-wasm": "^3.3.4", diff --git a/src/common/utils/type.ts b/src/common/utils/type.ts new file mode 100644 index 00000000..bcc54a9b --- /dev/null +++ b/src/common/utils/type.ts @@ -0,0 +1,31 @@ +/** + * 运行时类型转换与检查类 + */ +export class TypeCheck { + static isEmpty(value: any): boolean { + return value === null || value === undefined || value === '' || + (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0); + } +} + +export class TypeConvert { + static toNumber(value: any): number { + const num = Number(value); + if (isNaN(num)) { + throw new Error(`无法将输入转换为数字: ${value}`); + } + return num; + } + + static toString(value: any): string { + return String(value); + } + + static toBoolean(value: any): boolean { + return Boolean(value); + } + + static toArray(value: any): any[] { + return Array.isArray(value) ? value : [value]; + } +} \ No newline at end of file diff --git a/src/onebot11/action/BaseAction.ts b/src/onebot11/action/BaseAction.ts index 2fad8ea6..cc19217a 100644 --- a/src/onebot11/action/BaseAction.ts +++ b/src/onebot11/action/BaseAction.ts @@ -1,16 +1,25 @@ import { ActionName, BaseCheckResult } from './types'; import { OB11Response } from './OB11Response'; import { OB11Return } from '../types'; - import { log, logError } from '../../common/utils/log'; +import Ajv from 'ajv'; class BaseAction { actionName!: ActionName; - + private validate: any = undefined; + PayloadSchema: any = undefined; protected async check(payload: PayloadType): Promise { + if (this.validate && !this.validate(payload)) { + return { + valid: false, + message: this.validate.errors?.map((e: { message: any; }) => e.message).join(',') as string + } + } else if (this.PayloadSchema) { + this.validate = new Ajv().compile(this.PayloadSchema); + } return { - valid: true, - }; + valid: true + } } public async handle(payload: PayloadType): Promise> { diff --git a/src/onebot11/action/extends/SetOnlineStatus.ts b/src/onebot11/action/extends/SetOnlineStatus.ts index 09410a28..99248dbf 100644 --- a/src/onebot11/action/extends/SetOnlineStatus.ts +++ b/src/onebot11/action/extends/SetOnlineStatus.ts @@ -2,17 +2,27 @@ import { OB11User } from '../../types'; import { OB11Constructor } from '../../constructor'; import { friends } from '@/core/data'; import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; +import { ActionName, BaseCheckResult } from '../types'; import { NTQQUserApi } from '@/core/apis'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import Ajv from "ajv" // 设置在线状态 -interface Payload { - status: number; - extStatus: number; - batteryStatus: number; -} + +const SchemaData = { + type: 'object', + properties: { + status: { type: 'number' }, + extStatus: { type: 'number' }, + batteryStatus: { type: 'number' } + }, + required: ['status', 'extStatus', 'batteryStatus'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + export class SetOnlineStatus extends BaseAction { actionName = ActionName.SetOnlineStatus; - + PayloadSchema = SchemaData; protected async _handle(payload: Payload) { // 可设置状态 // { status: 10, extStatus: 1027, batteryStatus: 0 } diff --git a/src/onebot11/action/extends/SetQQAvatar.ts b/src/onebot11/action/extends/SetQQAvatar.ts index fcb8746f..397a2528 100644 --- a/src/onebot11/action/extends/SetQQAvatar.ts +++ b/src/onebot11/action/extends/SetQQAvatar.ts @@ -1,27 +1,38 @@ import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; +import { ActionName, BaseCheckResult } from '../types'; import * as fs from 'node:fs'; import { NTQQUserApi } from '@/core/apis/user'; import { checkFileReceived, uri2local } from '@/common/utils/file'; // import { log } from "../../../common/utils"; interface Payload { - file: string + file: string } export default class SetAvatar extends BaseAction { actionName = ActionName.SetQQAvatar; - + // 用不着复杂检测 + protected async check(payload: Payload): Promise { + if (!payload.file || typeof payload.file != "string") { + return { + valid: false, + message: 'file字段不能为空或者类型错误', + }; + } + return { + valid: true, + }; + } protected async _handle(payload: Payload): Promise { const { path, isLocal, errMsg } = (await uri2local(payload.file)); - if (errMsg){ + if (errMsg) { throw `头像${payload.file}设置失败,file字段可能格式不正确`; } if (path) { await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 const ret = await NTQQUserApi.setQQAvatar(path); - if (!isLocal){ - fs.unlink(path, () => {}); + if (!isLocal) { + fs.unlink(path, () => { }); } if (!ret) { throw `头像${payload.file}设置失败,api无返回`; @@ -29,15 +40,15 @@ export default class SetAvatar extends BaseAction { // log(`头像设置返回:${JSON.stringify(ret)}`) if (ret['result'] == 1004022) { throw `头像${payload.file}设置失败,文件可能不是图片格式`; - } else if(ret['result'] != 0) { + } else if (ret['result'] != 0) { throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`; } } else { - if (!isLocal){ - fs.unlink(path, () => {}); + if (!isLocal) { + fs.unlink(path, () => { }); } throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`; - } + } return null; } } diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index e4ff4d03..1e77858a 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -5,9 +5,11 @@ import { ob11Config } from '@/onebot11/config'; import { log, logDebug } from '@/common/utils/log'; import { sleep } from '@/common/utils/helper'; import { uri2local } from '@/common/utils/file'; -import { ActionName } from '../types'; +import { ActionName, BaseCheckResult } from '../types'; import { FileElement, RawMessage, VideoElement } from '@/core/entities'; import { NTQQFileApi } from '@/core/apis'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import Ajv from 'ajv'; export interface GetFilePayload { file: string; // 文件名或者fileUuid @@ -20,8 +22,16 @@ export interface GetFileResponse { file_name?: string; base64?: string; } +const GetFileBase_PayloadSchema = { + type: 'object', + properties: { + file: { type: 'string' } + }, + required: ['file'] +} as const satisfies JSONSchema; export class GetFileBase extends BaseAction { + PayloadSchema: any = GetFileBase_PayloadSchema; private getElement(msg: RawMessage): { id: string, element: VideoElement | FileElement } { let element = msg.elements.find(e => e.fileElement); if (!element) { @@ -34,7 +44,6 @@ export class GetFileBase extends BaseAction { } return { id: element.elementId, element: element.fileElement }; } - protected async _handle(payload: GetFilePayload): Promise { let cache = await dbUtil.getFileCacheByName(payload.file); if (!cache) { @@ -102,13 +111,25 @@ export class GetFileBase extends BaseAction { } } +const GetFile_PayloadSchema = { + type: 'object', + properties: { + file_id: { type: 'string' }, + file: { type: 'string' } + }, + required: ['file_id'] +} as const satisfies JSONSchema; + +type GetFile_Payload_Internal = FromSchema; + +interface GetFile_Payload extends GetFile_Payload_Internal { + file: string +} + export default class GetFile extends GetFileBase { actionName = ActionName.GetFile; - - protected async _handle(payload: { file_id: string, file: string }): Promise { - if (!payload.file_id) { - throw new Error('file_id 不能为空'); - } + PayloadSchema = GetFile_PayloadSchema; + protected async _handle(payload: GetFile_Payload): Promise { payload.file = payload.file_id; return super._handle(payload); } diff --git a/src/onebot11/action/go-cqhttp/DownloadFile.ts b/src/onebot11/action/go-cqhttp/DownloadFile.ts index 1e8ecfe2..c4a0cbe8 100644 --- a/src/onebot11/action/go-cqhttp/DownloadFile.ts +++ b/src/onebot11/action/go-cqhttp/DownloadFile.ts @@ -1,25 +1,46 @@ import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; +import { ActionName, BaseCheckResult } from '../types'; import fs from 'fs'; import { join as joinPath } from 'node:path'; import { calculateFileMD5, getTempDir, httpDownload } from '@/common/utils/file'; import { v4 as uuid4 } from 'uuid'; - -interface Payload { - thread_count?: number; - url?: string; - base64?: string; - name?: string; - headers?: string | string[]; -} - +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import Ajv from 'ajv'; interface FileResponse { file: string; } +const PayloadSchema = { + type: 'object', + properties: { + thread_count: { type: 'number' }, + url: { type: 'string' }, + base64: { type: 'string' }, + name: { type: 'string' }, + headers: { + type: "array", + items: { + type: "string" + } + } + }, +} as const satisfies JSONSchema; + +type Payload = FromSchema; export default class GoCQHTTPDownloadFile extends BaseAction { actionName = ActionName.GoCQHTTP_DownloadFile; - + validateDownload = new Ajv().compile(PayloadSchema); + // 这里重写是为了兼容 headers可能出现 string string[] + protected async check(payload: Payload): Promise { + if (payload.headers) { + // 如果存在headers 为数组则开始兼容string string[] + payload.headers = payload?.headers && Array.isArray(payload.headers) ? payload.headers : [payload.headers as unknown as string]; + } + if (!this.validateDownload(payload)) { + return { valid: false, message: this.validateDownload.errors?.map(e => e.message).join(', ') as string }; + } + return { valid: true }; + } protected async _handle(payload: Payload): Promise { const isRandomName = !payload.name; const name = payload.name || uuid4(); diff --git a/src/onebot11/action/go-cqhttp/GetForwardMsg.ts b/src/onebot11/action/go-cqhttp/GetForwardMsg.ts index 935a94e0..980faf83 100644 --- a/src/onebot11/action/go-cqhttp/GetForwardMsg.ts +++ b/src/onebot11/action/go-cqhttp/GetForwardMsg.ts @@ -3,12 +3,19 @@ import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'; import { NTQQMsgApi } from '@/core/apis'; import { dbUtil } from '@/core/utils/db'; import { OB11Constructor } from '../../constructor'; -import { ActionName } from '../types'; +import { ActionName, BaseCheckResult } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import Ajv from 'ajv'; -interface Payload { - message_id: string; // long msg id - id?: string; // short msg id -} +const SchemaData = { + type: 'object', + properties: { + message_id: { type: 'string' }, + id: { type: 'string' } + }, +} as const satisfies JSONSchema; + +type Payload = FromSchema; interface Response { messages: (OB11Message & { content: OB11MessageData })[]; @@ -16,7 +23,7 @@ interface Response { export class GoCQHTTGetForwardMsgAction extends BaseAction { actionName = ActionName.GoCQHTTP_GetForwardMsg; - + PayloadSchema = SchemaData; protected async _handle(payload: Payload): Promise { const msgId = payload.message_id || payload.id; if (!msgId) { @@ -25,7 +32,7 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction { let rootMsg = await dbUtil.getMsgByLongId(msgId); if (!rootMsg) { rootMsg = await dbUtil.getMsgByShortId(parseInt(msgId)); - if (!rootMsg){ + if (!rootMsg) { throw Error('msg not found'); } }