mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bdb76d4639 | ||
![]() |
15634412ef | ||
![]() |
bbcf9649fa | ||
![]() |
e845d7314e | ||
![]() |
6927b1c94f | ||
![]() |
a09c6acd0d | ||
![]() |
0963650ccb | ||
![]() |
380688b353 | ||
![]() |
ad5466bff8 | ||
![]() |
a83652bf3f | ||
![]() |
c632de314d | ||
![]() |
259c9610d5 | ||
![]() |
e9936c5524 | ||
![]() |
3f60440e72 | ||
![]() |
71a15f92fb | ||
![]() |
32bc0dd820 | ||
![]() |
20d1ac9d01 | ||
![]() |
18baf89e0e | ||
![]() |
3a1d1f2e59 | ||
![]() |
e9a048721d | ||
![]() |
68f0c7ff1a | ||
![]() |
2875fe94ea | ||
![]() |
1870427c0f | ||
![]() |
636568fd30 | ||
![]() |
bbc2391bf8 | ||
![]() |
401684542a | ||
![]() |
870edb2513 | ||
![]() |
7ad09169ea | ||
![]() |
c1a0f8915b | ||
![]() |
dcdab8e5a1 | ||
![]() |
eb3278fdab | ||
![]() |
34db3af48d | ||
![]() |
198da960dd | ||
![]() |
cb83918fb3 | ||
![]() |
f59a48540b | ||
![]() |
ccf9c1a5fb | ||
![]() |
ba6a85142a | ||
![]() |
440baccd2a | ||
![]() |
690c073328 | ||
![]() |
3f0730ed4f | ||
![]() |
01d5663bc8 | ||
![]() |
49806cd00e | ||
![]() |
935b0848e5 | ||
![]() |
5ca20a89a2 | ||
![]() |
e89a2266ec |
@@ -53,6 +53,8 @@ _Modern protocol-side framework implemented based on NTQQ._
|
|||||||
|
|
||||||
+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
|
+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
|
||||||
|
|
||||||
|
+ [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
|
||||||
|
|
||||||
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
|
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.7.58",
|
"version": "4.7.66",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -92,7 +92,9 @@ const MusicInsert = () => {
|
|||||||
className="w-96"
|
className="w-96"
|
||||||
fullWidth
|
fullWidth
|
||||||
selectedKey={mode}
|
selectedKey={mode}
|
||||||
onSelectionChange={setMode}
|
onSelectionChange={(key) => {
|
||||||
|
if (key !== null) setMode(key)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
|
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
|
||||||
<Select
|
<Select
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.7.58",
|
"version": "4.7.66",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"commander": "^13.0.0",
|
"commander": "^13.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.4",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-import-resolver-typescript": "^4.0.0",
|
"eslint-import-resolver-typescript": "^4.0.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
@@ -50,7 +50,6 @@
|
|||||||
"fast-xml-parser": "^4.3.6",
|
"fast-xml-parser": "^4.3.6",
|
||||||
"file-type": "^20.0.0",
|
"file-type": "^20.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"image-size": "^1.1.1",
|
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
@@ -4,10 +4,10 @@ import { execFile } from 'child_process';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import type { VideoInfo } from './video';
|
import type { VideoInfo } from './video';
|
||||||
import { fileTypeFromFile } from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import imageSize from 'image-size';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { platform } from 'node:os';
|
import { platform } from 'node:os';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
|
import { imageSizeFallBack } from '@/image-size';
|
||||||
const currentPath = dirname(fileURLToPath(import.meta.url));
|
const currentPath = dirname(fileURLToPath(import.meta.url));
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const getFFmpegPath = (tool: string): string => {
|
const getFFmpegPath = (tool: string): string => {
|
||||||
@@ -157,7 +157,7 @@ export class FFmpegService {
|
|||||||
try {
|
try {
|
||||||
await this.extractThumbnail(videoPath, thumbnailPath);
|
await this.extractThumbnail(videoPath, thumbnailPath);
|
||||||
// 获取图片尺寸
|
// 获取图片尺寸
|
||||||
const dimensions = imageSize(thumbnailPath);
|
const dimensions = await imageSizeFallBack(thumbnailPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
format: fileType?.ext ?? 'mp4',
|
format: fileType?.ext ?? 'mp4',
|
||||||
|
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '4.7.58';
|
export const napCatVersion = '4.7.66';
|
||||||
|
@@ -17,8 +17,6 @@ import fs from 'fs';
|
|||||||
import fsPromises from 'fs/promises';
|
import fsPromises from 'fs/promises';
|
||||||
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
||||||
import { fileTypeFromFile } from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import imageSize from 'image-size';
|
|
||||||
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
|
||||||
import { RkeyManager } from '@/core/helper/rkey';
|
import { RkeyManager } from '@/core/helper/rkey';
|
||||||
import { calculateFileMD5 } from '@/common/file';
|
import { calculateFileMD5 } from '@/common/file';
|
||||||
import pathLib from 'node:path';
|
import pathLib from 'node:path';
|
||||||
@@ -30,6 +28,7 @@ import { FFmpegService } from '@/common/ffmpeg';
|
|||||||
import { rkeyDataType } from '../types/file';
|
import { rkeyDataType } from '../types/file';
|
||||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
import { FileId } from '../packet/transformer/proto/misc/fileid';
|
import { FileId } from '../packet/transformer/proto/misc/fileid';
|
||||||
|
import { imageSizeFallBack } from '@/image-size';
|
||||||
|
|
||||||
export class NTQQFileApi {
|
export class NTQQFileApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
@@ -209,7 +208,7 @@ export class NTQQFileApi {
|
|||||||
if (fileSize === 0) {
|
if (fileSize === 0) {
|
||||||
throw new Error('文件异常,大小为0');
|
throw new Error('文件异常,大小为0');
|
||||||
}
|
}
|
||||||
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
|
const imageSize = await imageSizeFallBack(picPath);
|
||||||
context.deleteAfterSentFiles.push(path);
|
context.deleteAfterSentFiles.push(path);
|
||||||
return {
|
return {
|
||||||
elementType: ElementType.PIC,
|
elementType: ElementType.PIC,
|
||||||
@@ -437,19 +436,6 @@ export class NTQQFileApi {
|
|||||||
return completeRetData.filePath;
|
return completeRetData.filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
imageSize(filePath, (err: Error | null, dimensions) => {
|
|
||||||
if (err) {
|
|
||||||
reject(new Error(err.message));
|
|
||||||
} else if (!dimensions) {
|
|
||||||
reject(new Error('获取图片尺寸失败'));
|
|
||||||
} else {
|
|
||||||
resolve(dimensions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
|
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
|
||||||
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
|
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
|
||||||
|
16
src/core/external/appid.json
vendored
16
src/core/external/appid.json
vendored
@@ -286,5 +286,21 @@
|
|||||||
"9.9.19-34958": {
|
"9.9.19-34958": {
|
||||||
"appid": 537290742,
|
"appid": 537290742,
|
||||||
"qua": "V1_WIN_NQ_9.9.19_34958_GW_B"
|
"qua": "V1_WIN_NQ_9.9.19_34958_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.17-35184": {
|
||||||
|
"appid": 537291084,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.17_35184_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.19-35184": {
|
||||||
|
"appid": 537291048,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.19_35184_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.17-35341": {
|
||||||
|
"appid": 537291383,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.17_35341_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.19-35341": {
|
||||||
|
"appid": 537291347,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.19_35341_GW_B"
|
||||||
}
|
}
|
||||||
}
|
}
|
24
src/core/external/offset.json
vendored
24
src/core/external/offset.json
vendored
@@ -366,5 +366,29 @@
|
|||||||
"9.9.19-34958-x64": {
|
"9.9.19-34958-x64": {
|
||||||
"send": "3BDD8D0",
|
"send": "3BDD8D0",
|
||||||
"recv": "3BE20D0"
|
"recv": "3BE20D0"
|
||||||
|
},
|
||||||
|
"3.2.17-35184-x64": {
|
||||||
|
"send": "AE0DDE0",
|
||||||
|
"recv": "AE11800"
|
||||||
|
},
|
||||||
|
"3.2.17-35184-arm64": {
|
||||||
|
"send": "7776028",
|
||||||
|
"recv": "7779958"
|
||||||
|
},
|
||||||
|
"9.9.19-35184-x64": {
|
||||||
|
"send": "3BE5A10",
|
||||||
|
"recv": "3BEA210"
|
||||||
|
},
|
||||||
|
"9.9.19-35341-x64": {
|
||||||
|
"send": "3BE5A10",
|
||||||
|
"recv": "3BEA210"
|
||||||
|
},
|
||||||
|
"3.2.17-35341-x64": {
|
||||||
|
"send": "AE2F700",
|
||||||
|
"recv": "AE33120"
|
||||||
|
},
|
||||||
|
"3.2.17-35341-arm64": {
|
||||||
|
"send": "778D840",
|
||||||
|
"recv": "7791170"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -30,13 +30,8 @@ export class PacketOperationContext {
|
|||||||
return await this.context.client.sendOidbPacket(pkt, rsp);
|
return await this.context.client.sendOidbPacket(pkt, rsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async GroupPoke(groupUin: number, uin: number) {
|
async SendPoke(is_group: boolean, peer: number, target?: number) {
|
||||||
const req = trans.SendPoke.build(uin, groupUin);
|
const req = trans.SendPoke.build(is_group, peer, target ?? peer);
|
||||||
await this.context.client.sendOidbPacket(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
async FriendPoke(uin: number) {
|
|
||||||
const req = trans.SendPoke.build(uin);
|
|
||||||
await this.context.client.sendOidbPacket(req);
|
await this.context.client.sendOidbPacket(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,13 +8,13 @@ class SendPoke extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
build(peer: number, group?: number): OidbPacket {
|
build(is_group: boolean, peer: number, target: number): OidbPacket {
|
||||||
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode({
|
const payload = {
|
||||||
uin: peer,
|
uin: target,
|
||||||
groupUin: group,
|
ext: 0,
|
||||||
friendUin: group ?? peer,
|
...(is_group ? { groupUin: peer } : { friendUin: peer })
|
||||||
ext: 0
|
};
|
||||||
});
|
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode(payload);
|
||||||
return OidbBase.build(0xED3, 1, data);
|
return OidbBase.build(0xED3, 1, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ class OidbBase extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
|
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, _isLafter: boolean = false): OidbPacket {
|
||||||
const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({
|
const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({
|
||||||
command: cmd,
|
command: cmd,
|
||||||
subCommand: subCmd,
|
subCommand: subCmd,
|
||||||
|
426
src/image-size/index.ts
Normal file
426
src/image-size/index.ts
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
export interface ImageSize {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageType {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
PNG = 'png',
|
||||||
|
BMP = 'bmp',
|
||||||
|
GIF = 'gif',
|
||||||
|
WEBP = 'webp',
|
||||||
|
UNKNOWN = 'unknown',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageParser {
|
||||||
|
readonly type: ImageType;
|
||||||
|
canParse(buffer: Buffer): boolean;
|
||||||
|
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 魔术匹配
|
||||||
|
function matchMagic(buffer: Buffer, magic: number[], offset = 0): boolean {
|
||||||
|
if (buffer.length < offset + magic.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < magic.length; i++) {
|
||||||
|
if (buffer[offset + i] !== magic[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PNG解析器
|
||||||
|
class PngParser implements ImageParser {
|
||||||
|
readonly type = ImageType.PNG;
|
||||||
|
// PNG 魔术头:89 50 4E 47 0D 0A 1A 0A
|
||||||
|
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.PNG_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(24) as Buffer;
|
||||||
|
if (!buf || buf.length < 24) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt32BE(16);
|
||||||
|
const height = buf.readUInt32BE(20);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JPEG解析器
|
||||||
|
class JpegParser implements ImageParser {
|
||||||
|
readonly type = ImageType.JPEG;
|
||||||
|
// JPEG 魔术头:FF D8
|
||||||
|
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
|
||||||
|
|
||||||
|
// JPEG标记常量
|
||||||
|
private readonly SOF_MARKERS = {
|
||||||
|
SOF0: 0xC0, // 基线DCT
|
||||||
|
SOF1: 0xC1, // 扩展顺序DCT
|
||||||
|
SOF2: 0xC2, // 渐进式DCT
|
||||||
|
SOF3: 0xC3, // 无损
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 非SOF标记
|
||||||
|
private readonly NON_SOF_MARKERS: number[] = [
|
||||||
|
0xC4, // DHT
|
||||||
|
0xC8, // JPEG扩展
|
||||||
|
0xCC, // DAC
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.JPEG_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSOFMarker(marker: number): boolean {
|
||||||
|
return (
|
||||||
|
marker === this.SOF_MARKERS.SOF0 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF1 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF2 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isNonSOFMarker(marker: number): boolean {
|
||||||
|
return this.NON_SOF_MARKERS.includes(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise<ImageSize | undefined>((resolve, reject) => {
|
||||||
|
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
let offset = 0;
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
stream.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理数据块
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
// 追加新数据到缓冲区
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
|
||||||
|
const bufferSize = buffer.length;
|
||||||
|
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
|
||||||
|
|
||||||
|
// 从JPEG头部后开始扫描
|
||||||
|
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
|
||||||
|
// 寻找FF标记
|
||||||
|
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
|
||||||
|
const marker = buffer[offset + 1];
|
||||||
|
if (!marker) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 跳过非SOF标记
|
||||||
|
if (this.isNonSOFMarker(marker)) {
|
||||||
|
offset += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理SOF标记 (包含尺寸信息)
|
||||||
|
if (this.isSOFMarker(marker)) {
|
||||||
|
// 确保缓冲区中有足够数据读取尺寸
|
||||||
|
if (offset + 9 < bufferSize) {
|
||||||
|
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
|
||||||
|
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
|
||||||
|
const height = buffer.readUInt16BE(offset + 5);
|
||||||
|
const width = buffer.readUInt16BE(offset + 7);
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
stream.destroy();
|
||||||
|
resolve({ width, height });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 如果缓冲区内数据不够,保留当前位置等待更多数据
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓冲区管理: 如果处理了许多数据但没找到标记,
|
||||||
|
// 保留最后N字节用于跨块匹配,丢弃之前的数据
|
||||||
|
if (offset > BUFFER_SIZE) {
|
||||||
|
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
|
||||||
|
if (offset > KEEP_BYTES) {
|
||||||
|
buffer = buffer.subarray(offset - KEEP_BYTES);
|
||||||
|
offset = KEEP_BYTES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理流结束
|
||||||
|
stream.on('end', () => {
|
||||||
|
if (!found) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BMP解析器
|
||||||
|
class BmpParser implements ImageParser {
|
||||||
|
readonly type = ImageType.BMP;
|
||||||
|
// BMP 魔术头:42 4D (BM)
|
||||||
|
private readonly BMP_SIGNATURE = [0x42, 0x4D];
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.BMP_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(26) as Buffer;
|
||||||
|
if (!buf || buf.length < 26) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt32LE(18);
|
||||||
|
const height = buf.readUInt32LE(22);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIF解析器
|
||||||
|
class GifParser implements ImageParser {
|
||||||
|
readonly type = ImageType.GIF;
|
||||||
|
// GIF87a 魔术头:47 49 46 38 37 61
|
||||||
|
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
|
||||||
|
// GIF89a 魔术头:47 49 46 38 39 61
|
||||||
|
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
|
||||||
|
matchMagic(buffer, this.GIF89A_SIGNATURE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(10) as Buffer;
|
||||||
|
if (!buf || buf.length < 10) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt16LE(6);
|
||||||
|
const height = buf.readUInt16LE(8);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
|
||||||
|
class WebpParser implements ImageParser {
|
||||||
|
readonly type = ImageType.WEBP;
|
||||||
|
// WEBP RIFF 头:52 49 46 46 (RIFF)
|
||||||
|
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
|
||||||
|
// WEBP 魔术头:57 45 42 50 (WEBP)
|
||||||
|
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
|
||||||
|
|
||||||
|
// WEBP 块头
|
||||||
|
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
|
||||||
|
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
|
||||||
|
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
buffer.length >= 12 &&
|
||||||
|
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
|
||||||
|
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isChunkType(buffer: Buffer, offset: number, chunkType: number[]): boolean {
|
||||||
|
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 需要读取足够的字节来检测所有三种格式
|
||||||
|
const MAX_HEADER_SIZE = 32;
|
||||||
|
let totalBytes = 0;
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
stream.on('error', reject);
|
||||||
|
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
buffer = Buffer.concat([buffer, chunkBuffer]);
|
||||||
|
totalBytes += chunk.length;
|
||||||
|
|
||||||
|
// 检查是否有足够的字节进行格式检测
|
||||||
|
if (totalBytes >= MAX_HEADER_SIZE) {
|
||||||
|
stream.destroy();
|
||||||
|
|
||||||
|
// 检查基本的WEBP签名
|
||||||
|
if (!this.canParse(buffer)) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查chunk头部,位于字节12-15
|
||||||
|
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
|
||||||
|
// VP8格式 - 标准WebP
|
||||||
|
// 宽度和高度在帧头中
|
||||||
|
const width = buffer.readUInt16LE(26) & 0x3FFF;
|
||||||
|
const height = buffer.readUInt16LE(28) & 0x3FFF;
|
||||||
|
return resolve({ width, height });
|
||||||
|
|
||||||
|
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
|
||||||
|
// VP8L格式 - 无损WebP
|
||||||
|
// 1字节标记后是14位宽度和14位高度
|
||||||
|
const bits = buffer.readUInt32LE(21);
|
||||||
|
const width = 1 + (bits & 0x3FFF);
|
||||||
|
const height = 1 + ((bits >> 14) & 0x3FFF);
|
||||||
|
return resolve({ width, height });
|
||||||
|
|
||||||
|
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
|
||||||
|
// VP8X格式 - 扩展WebP
|
||||||
|
// 24位宽度和高度(减去1)
|
||||||
|
if (!buffer[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
const width = 1 + ((buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) & 0xFFFFFF);
|
||||||
|
const height = 1 + ((buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) & 0xFFFFFF);
|
||||||
|
return resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
// 未知的WebP子格式
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
// 如果没有读到足够的字节
|
||||||
|
if (totalBytes < MAX_HEADER_SIZE) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsers: ReadonlyArray<ImageParser> = [
|
||||||
|
new PngParser(),
|
||||||
|
new JpegParser(),
|
||||||
|
new BmpParser(),
|
||||||
|
new GifParser(),
|
||||||
|
new WebpParser(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function detectImageType(filePath: string): Promise<ImageType> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const stream = fs.createReadStream(filePath, {
|
||||||
|
highWaterMark: 64, // 优化读取buffer大小
|
||||||
|
start: 0,
|
||||||
|
end: 63
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer: Buffer | null = null;
|
||||||
|
|
||||||
|
stream.once('error', (err) => {
|
||||||
|
stream.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('readable', () => {
|
||||||
|
buffer = stream.read(64) as Buffer;
|
||||||
|
stream.destroy();
|
||||||
|
|
||||||
|
if (!buffer) {
|
||||||
|
return resolve(ImageType.UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parser of parsers) {
|
||||||
|
if (parser.canParse(buffer)) {
|
||||||
|
return resolve(parser.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(ImageType.UNKNOWN);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('end', () => {
|
||||||
|
if (!buffer) {
|
||||||
|
resolve(ImageType.UNKNOWN);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imageSizeFromFile(filePath: string): Promise<ImageSize | undefined> {
|
||||||
|
try {
|
||||||
|
// 先检测类型
|
||||||
|
const type = await detectImageType(filePath);
|
||||||
|
const parser = parsers.find(p => p.type === type);
|
||||||
|
if (!parser) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用流式方式解析尺寸
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
try {
|
||||||
|
return await parser.parseSize(stream);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`解析图片尺寸出错: ${err}`);
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
if (!stream.destroyed) {
|
||||||
|
stream.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`检测图片类型出错: ${err}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imageSizeFallBack(
|
||||||
|
filePath: string,
|
||||||
|
fallback: ImageSize = {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
}
|
||||||
|
): Promise<ImageSize> {
|
||||||
|
return await imageSizeFromFile(filePath) ?? fallback;
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { normalize, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
import { ContextMode, normalize, ReturnDataType, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
||||||
import { OB11PostSendMsg } from '@/onebot/types';
|
import { OB11PostSendMsg } from '@/onebot/types';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
|
||||||
@@ -19,8 +19,14 @@ export class GoCQHTTPSendForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
|||||||
}
|
}
|
||||||
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
||||||
override actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
|
override actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Private);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
||||||
override actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
|
override actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
|
||||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
|
||||||
user_id: Type.Union([Type.Number(), Type.String()]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
export class GroupPoke extends GetPacketStatusDepends<Payload, void> {
|
|
||||||
override actionName = ActionName.GroupPoke;
|
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
|
||||||
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +1,19 @@
|
|||||||
import { ContextMode, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
import { ContextMode, ReturnDataType, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
||||||
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||||
import { OB11PostSendMsg } from '@/onebot/types';
|
import { OB11PostSendMsg } from '@/onebot/types';
|
||||||
|
|
||||||
// 未检测参数
|
// 未检测参数
|
||||||
class SendGroupMsg extends SendMsgBase {
|
class SendGroupMsg extends SendMsgBase {
|
||||||
override actionName = ActionName.SendGroupMsg;
|
override actionName = ActionName.SendGroupMsg;
|
||||||
override contextMode: ContextMode = ContextMode.Group;
|
|
||||||
|
|
||||||
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||||
delete payload.user_id;
|
delete payload.user_id;
|
||||||
payload.message_type = 'group';
|
payload.message_type = 'group';
|
||||||
return super.check(payload);
|
return super.check(payload);
|
||||||
}
|
}
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SendGroupMsg;
|
export default SendGroupMsg;
|
||||||
|
@@ -78,7 +78,6 @@ import { GetGroupFileSystemInfo } from '@/onebot/action/go-cqhttp/GetGroupFileSy
|
|||||||
import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles';
|
import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles';
|
||||||
import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder';
|
import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder';
|
||||||
import { GetGroupSystemMsg } from './system/GetSystemMsg';
|
import { GetGroupSystemMsg } from './system/GetSystemMsg';
|
||||||
import { GroupPoke } from './group/GroupPoke';
|
|
||||||
import { GetUserStatus } from './extends/GetUserStatus';
|
import { GetUserStatus } from './extends/GetUserStatus';
|
||||||
import { GetRkey } from './extends/GetRkey';
|
import { GetRkey } from './extends/GetRkey';
|
||||||
import { SetSpecialTitle } from './extends/SetSpecialTitle';
|
import { SetSpecialTitle } from './extends/SetSpecialTitle';
|
||||||
@@ -86,7 +85,6 @@ import { GetGroupShutList } from './group/GetGroupShutList';
|
|||||||
import { GetGroupMemberList } from './group/GetGroupMemberList';
|
import { GetGroupMemberList } from './group/GetGroupMemberList';
|
||||||
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
|
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
|
||||||
import { GetPacketStatus } from '@/onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatus } from '@/onebot/action/packet/GetPacketStatus';
|
||||||
import { FriendPoke } from '@/onebot/action/user/FriendPoke';
|
|
||||||
import { GetCredentials } from './system/GetCredentials';
|
import { GetCredentials } from './system/GetCredentials';
|
||||||
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
|
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
|
||||||
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
|
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
|
||||||
@@ -102,7 +100,7 @@ import { GetGuildList } from './guild/GetGuildList';
|
|||||||
import { GetGuildProfile } from './guild/GetGuildProfile';
|
import { GetGuildProfile } from './guild/GetGuildProfile';
|
||||||
import { GetClientkey } from './extends/GetClientkey';
|
import { GetClientkey } from './extends/GetClientkey';
|
||||||
import { SendPacket } from './extends/SendPacket';
|
import { SendPacket } from './extends/SendPacket';
|
||||||
import { SendPoke } from '@/onebot/action/packet/SendPoke';
|
import { FriendPoke, GroupPoke, SendPoke } from '@/onebot/action/packet/SendPoke';
|
||||||
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
|
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
|
||||||
import { BotExit } from './extends/BotExit';
|
import { BotExit } from './extends/BotExit';
|
||||||
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
|
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
|
||||||
|
@@ -104,8 +104,6 @@ function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
||||||
contextMode = ContextMode.Normal;
|
|
||||||
|
|
||||||
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||||
const messages = normalize(payload.message);
|
const messages = normalize(payload.message);
|
||||||
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
|
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
|
||||||
@@ -117,12 +115,13 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
}
|
}
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
this.contextMode = ContextMode.Normal;
|
return this.base_handle(payload);
|
||||||
if (payload.message_type === 'group') this.contextMode = ContextMode.Group;
|
}
|
||||||
if (payload.message_type === 'private') this.contextMode = ContextMode.Private;
|
async base_handle(payload: OB11PostSendMsg, contextMode: ContextMode = ContextMode.Normal): Promise<ReturnDataType> {
|
||||||
const peer = await createContext(this.core, payload, this.contextMode);
|
if (payload.message_type === 'group') contextMode = ContextMode.Group;
|
||||||
|
if (payload.message_type === 'private') contextMode = ContextMode.Private;
|
||||||
|
const peer = await createContext(this.core, payload, contextMode);
|
||||||
|
|
||||||
const messages = normalize(
|
const messages = normalize(
|
||||||
payload.message,
|
payload.message,
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
import { ContextMode, SendMsgBase } from './SendMsg';
|
import { ContextMode, ReturnDataType, SendMsgBase } from './SendMsg';
|
||||||
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||||
import { OB11PostSendMsg } from '@/onebot/types';
|
import { OB11PostSendMsg } from '@/onebot/types';
|
||||||
|
|
||||||
// 未检测参数
|
// 未检测参数
|
||||||
class SendPrivateMsg extends SendMsgBase {
|
class SendPrivateMsg extends SendMsgBase {
|
||||||
override actionName = ActionName.SendPrivateMsg;
|
override actionName = ActionName.SendPrivateMsg;
|
||||||
override contextMode: ContextMode = ContextMode.Private;
|
|
||||||
|
|
||||||
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||||
payload.message_type = 'private';
|
payload.message_type = 'private';
|
||||||
return super.check(payload);
|
return super.check(payload);
|
||||||
}
|
}
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Private);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SendPrivateMsg;
|
export default SendPrivateMsg;
|
||||||
|
@@ -3,21 +3,36 @@ import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
|||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const SchemaData = Type.Object({
|
||||||
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
group_id: Type.Optional(Type.String()),
|
||||||
user_id: Type.Union([Type.Number(), Type.String()]),
|
user_id: Type.Optional(Type.String()),
|
||||||
|
target_id: Type.Optional(Type.String()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
export class SendPokeBase extends GetPacketStatusDepends<Payload, void> {
|
||||||
export class SendPoke extends GetPacketStatusDepends<Payload, void> {
|
|
||||||
override actionName = ActionName.SendPoke;
|
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
if (payload.group_id) {
|
// 这里的 !! 可以传入空字符串 忽略这些数据有利用接口统一接口
|
||||||
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
|
const target_id = !!payload.target_id ? payload.target_id : payload.user_id;
|
||||||
} else {
|
const peer_id = !!payload.group_id ? payload.group_id : payload.user_id;
|
||||||
await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
|
|
||||||
|
const is_group = !!payload.group_id;
|
||||||
|
if (!target_id || !peer_id) {
|
||||||
|
throw new Error('请检查参数,缺少 user_id 或 group_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.core.apis.PacketApi.pkt.operation.SendPoke(is_group, +peer_id, +target_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class SendPoke extends SendPokeBase {
|
||||||
|
override actionName = ActionName.SendPoke;
|
||||||
|
}
|
||||||
|
export class GroupPoke extends SendPokeBase {
|
||||||
|
override actionName = ActionName.GroupPoke;
|
||||||
|
}
|
||||||
|
export class FriendPoke extends SendPokeBase {
|
||||||
|
override actionName = ActionName.FriendPoke;
|
||||||
|
}
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
|
||||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
|
||||||
user_id: Type.Union([Type.Number(), Type.String()])
|
|
||||||
});
|
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
export class FriendPoke extends GetPacketStatusDepends<Payload, void> {
|
|
||||||
override actionName = ActionName.FriendPoke;
|
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
|
||||||
await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,4 +2,5 @@ import { EventType, OneBotEvent } from '@/onebot/event/OneBotEvent';
|
|||||||
|
|
||||||
export abstract class OB11BaseNoticeEvent extends OneBotEvent {
|
export abstract class OB11BaseNoticeEvent extends OneBotEvent {
|
||||||
post_type = EventType.NOTICE;
|
post_type = EventType.NOTICE;
|
||||||
}
|
abstract notice_type: string;
|
||||||
|
}
|
||||||
|
6
src/onebot/event/request/OB11BaseRequestEvent.ts
Normal file
6
src/onebot/event/request/OB11BaseRequestEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { EventType, OneBotEvent } from '@/onebot/event/OneBotEvent';
|
||||||
|
|
||||||
|
export abstract class OB11BaseRequestEvent extends OneBotEvent {
|
||||||
|
readonly post_type = EventType.REQUEST;
|
||||||
|
abstract request_type: string;
|
||||||
|
}
|
@@ -1,10 +1,8 @@
|
|||||||
import { OB11BaseNoticeEvent } from '@/onebot/event/notice/OB11BaseNoticeEvent';
|
|
||||||
import { EventType } from '@/onebot/event/OneBotEvent';
|
|
||||||
import { NapCatCore } from '@/core';
|
import { NapCatCore } from '@/core';
|
||||||
|
import { OB11BaseRequestEvent } from './OB11BaseRequestEvent';
|
||||||
|
|
||||||
export class OB11FriendRequestEvent extends OB11BaseNoticeEvent {
|
export class OB11FriendRequestEvent extends OB11BaseRequestEvent {
|
||||||
override post_type = EventType.REQUEST;
|
override request_type = 'friend';
|
||||||
request_type = 'friend';
|
|
||||||
|
|
||||||
user_id: number;
|
user_id: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
import { OB11GroupNoticeEvent } from '@/onebot/event/notice/OB11GroupNoticeEvent';
|
|
||||||
import { EventType } from '@/onebot/event/OneBotEvent';
|
|
||||||
import { NapCatCore } from '@/core';
|
import { NapCatCore } from '@/core';
|
||||||
|
import { OB11BaseRequestEvent } from './OB11BaseRequestEvent';
|
||||||
|
|
||||||
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent {
|
export class OB11GroupRequestEvent extends OB11BaseRequestEvent {
|
||||||
override post_type = EventType.REQUEST;
|
override readonly request_type = 'group' as const;
|
||||||
request_type = 'group';
|
|
||||||
|
|
||||||
override user_id: number;
|
group_id: number;
|
||||||
|
user_id: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
flag: string;
|
flag: string;
|
||||||
sub_type: string;
|
sub_type: string;
|
||||||
|
|
||||||
constructor(core: NapCatCore, groupId: number, userId: number, sub_type: string, comment: string, flag: string) {
|
constructor(core: NapCatCore, groupId: number, userId: number, sub_type: string, comment: string, flag: string) {
|
||||||
super(core, groupId, userId);
|
super(core);
|
||||||
|
this.group_id = groupId;
|
||||||
this.user_id = userId;
|
this.user_id = userId;
|
||||||
this.sub_type = sub_type;
|
this.sub_type = sub_type;
|
||||||
this.comment = comment;
|
this.comment = comment;
|
||||||
|
@@ -122,7 +122,9 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
|
|
||||||
// ------------挂载路由------------
|
// ------------挂载路由------------
|
||||||
// 挂载静态路由(前端),路径为 /webui
|
// 挂载静态路由(前端),路径为 /webui
|
||||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
app.use('/webui', express.static(pathWrapper.staticPath, {
|
||||||
|
maxAge: '1d'
|
||||||
|
}));
|
||||||
// 初始化WebSocket服务器
|
// 初始化WebSocket服务器
|
||||||
const sslCerts = await checkCertificates(logger);
|
const sslCerts = await checkCertificates(logger);
|
||||||
const isHttps = !!sslCerts;
|
const isHttps = !!sslCerts;
|
||||||
|
Reference in New Issue
Block a user