Compare commits

..

17 Commits

Author SHA1 Message Date
手瓜一十雪
40b2f6bfd6 release: 2.4.7 2024-09-12 18:31:11 +08:00
手瓜一十雪
911e4921e2 fix: 删除旧文件 2024-09-12 18:15:38 +08:00
手瓜一十雪
1db9bb419d fix: 一处异常字段 2024-09-12 10:55:18 +08:00
手瓜一十雪
c6241a94e3 style: lint 2024-09-12 09:28:41 +08:00
手瓜一十雪
1cbf75ca36 style: lint 2024-09-12 09:28:26 +08:00
手瓜一十雪
8f85c897c8 refactor: SysMsg 2024-09-12 09:20:10 +08:00
手瓜一十雪
29c31b7aba release: 2.4.6 2024-09-12 09:01:13 +08:00
手瓜一十雪
402919d6f2 feat: qucikLogin 2024-09-12 09:00:53 +08:00
手瓜一十雪
82608dd5ff fix: build 2024-09-12 00:17:35 +08:00
手瓜一十雪
f312368df2 build: fix2 2024-09-11 23:40:05 +08:00
手瓜一十雪
374fc64427 feat: delFile 2024-09-11 23:29:26 +08:00
手瓜一十雪
95bd74bb0d BUILD: TEST 2024-09-11 23:18:38 +08:00
手瓜一十雪
a9f5069649 Revert "build: debug"
This reverts commit 957f7ffd8d.
2024-09-11 22:41:59 +08:00
手瓜一十雪
957f7ffd8d build: debug 2024-09-11 22:28:28 +08:00
手瓜一十雪
336dd3ce10 chore: 扩展 2024-09-11 22:13:45 +08:00
手瓜一十雪
47a7295477 fix: typo
copilot
2024-09-11 20:24:50 +08:00
手瓜一十雪
341a0e1c2a chore: code 2024-09-11 20:10:52 +08:00
29 changed files with 199 additions and 168 deletions

View File

@@ -5,7 +5,7 @@ if %errorLevel% == 0 (
echo Administrator mode detected. echo Administrator mode detected.
) else ( ) else (
echo Please run this script in administrator mode. echo Please run this script in administrator mode.
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\"' -Verb runAs" powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
exit exit
) )
@@ -35,6 +35,6 @@ if not exist "%QQpath%" (
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > %NAPCAT_LOAD_PATH% echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > %NAPCAT_LOAD_PATH%
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456 REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456

View File

@@ -5,7 +5,7 @@ if %errorLevel% == 0 (
echo Administrator mode detected. echo Administrator mode detected.
) else ( ) else (
echo Please run this script in administrator mode. echo Please run this script in administrator mode.
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\"' -Verb runAs" powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
exit exit
) )
@@ -36,6 +36,4 @@ if not exist "%QQpath%" (
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > %NAPCAT_LOAD_PATH% echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > %NAPCAT_LOAD_PATH%
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456

View File

@@ -0,0 +1,4 @@
@echo off
REM ./launcher.bat 123456
REM ./launcher-win10.bat 123456
REM 带有REM的为注释 删掉你需要的系统的那行REM这三个单词 修改QQ本脚本启动即可

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "2.4.5", "version": "2.4.7",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "2.4.5", "version": "2.4.7",
"scripts": { "scripts": {
"build:framework": "vite build --mode framework", "build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell", "build:shell": "vite build --mode shell",

View File

@@ -1,4 +1,3 @@
import fs from 'fs';
import fsPromise from 'fs/promises'; import fsPromise from 'fs/promises';
import path from 'node:path'; import path from 'node:path';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
@@ -12,7 +11,7 @@ const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
async function guessDuration(pttPath: string, logger: LogWrapper) { async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath); const pttFileInfo = await fsPromise.stat(pttPath);
let duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration); logger.log('通过文件大小估算语音的时长:', duration);
return duration; return duration;
} }
@@ -20,7 +19,7 @@ async function guessDuration(pttPath: string, logger: LogWrapper) {
async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> { async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => { return new Promise<Buffer>((resolve, reject) => {
const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]); const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', err => { cp.on('error', (err: Error) => {
logger.log('FFmpeg处理转换出错: ', err.message); logger.log('FFmpeg处理转换出错: ', err.message);
reject(err); reject(err);
}); });

View File

@@ -160,8 +160,7 @@ type Uri2LocalRes = {
errMsg: string, errMsg: string,
fileName: string, fileName: string,
ext: string, ext: string,
path: string, path: string
isLocal: boolean
} }
export async function checkFileV2(filePath: string) { export async function checkFileV2(filePath: string) {
@@ -194,7 +193,6 @@ export async function checkUriType(Uri: string) {
return undefined; return undefined;
}, Uri); }, Uri);
if (LocalFileRet) return LocalFileRet; if (LocalFileRet) return LocalFileRet;
const OtherFileRet = await solveProblem((uri: string) => { const OtherFileRet = await solveProblem((uri: string) => {
//再判断是否是Http //再判断是否是Http
if (uri.startsWith('http://') || uri.startsWith('https://')) { if (uri.startsWith('http://') || uri.startsWith('https://')) {
@@ -206,13 +204,13 @@ export async function checkUriType(Uri: string) {
} }
if (uri.startsWith('file://')) { if (uri.startsWith('file://')) {
let filePath: string; let filePath: string;
// await fs.copyFile(url.pathname, filePath);
const pathname = decodeURIComponent(new URL(uri).pathname); const pathname = decodeURIComponent(new URL(uri).pathname);
if (process.platform === 'win32') { if (process.platform === 'win32') {
filePath = pathname.slice(1); filePath = pathname.slice(1);
} else { } else {
filePath = pathname; filePath = pathname;
} }
return { Uri: filePath, Type: FileUriType.Local }; return { Uri: filePath, Type: FileUriType.Local };
} }
if (uri.startsWith('data:')) { if (uri.startsWith('data:')) {
@@ -228,35 +226,42 @@ export async function checkUriType(Uri: string) {
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> { export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri); const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
//解析失败 //解析失败
const tempName = randomUUID();
if (!filename) filename = randomUUID();
//解析Http和Https协议
if (UriType == FileUriType.Unknown) { if (UriType == FileUriType.Unknown) {
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '', isLocal: false }; return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
} }
//解析File协议和本地文件 //解析File协议和本地文件
if (UriType == FileUriType.Local) { if (UriType == FileUriType.Local) {
const fileExt = path.extname(HandledUri); const fileExt = path.extname(HandledUri);
let filename = path.basename(HandledUri, fileExt); let filename = path.basename(HandledUri, fileExt);
filename += fileExt; filename += fileExt;
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: HandledUri, isLocal: true }; //复制文件到临时文件并保持后缀
const filenameTemp = tempName + fileExt;
const filePath = path.join(dir, filenameTemp);
fs.copyFileSync(HandledUri, filePath);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
} }
//接下来都要有文件名 //接下来都要有文件名
if (!filename) filename = randomUUID();
//解析Http和Https协议
if (UriType == FileUriType.Remote) { if (UriType == FileUriType.Remote) {
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname)); const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
if (pathInfo.name) { if (pathInfo.name) {
filename = pathInfo.name; const pathlen = 200 - dir.length - pathInfo.name.length;
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
if (pathInfo.ext) { if (pathInfo.ext) {
filename += pathInfo.ext; filename += pathInfo.ext;
} }
} }
filename = filename.replace(/[/\\:*?"<>|]/g, '_'); filename = filename.replace(/[/\\:*?"<>|]/g, '_');
const fileExt = path.extname(HandledUri); const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
const filePath = path.join(dir, filename); const filePath = path.join(dir, tempName + fileExt);
const buffer = await httpDownload(HandledUri); const buffer = await httpDownload(HandledUri);
fs.writeFileSync(filePath, buffer); //没有文件就创建
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath, isLocal: false }; fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
} }
//解析Base64 //解析Base64
if (UriType == FileUriType.Base64) { if (UriType == FileUriType.Base64) {
@@ -271,7 +276,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
fileExt = ext; fileExt = ext;
filename = filename + '.' + ext; filename = filename + '.' + ext;
} }
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath, isLocal: false }; return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
} }
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '', isLocal: false }; return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
} }

View File

@@ -1 +1 @@
export const napCatVersion = '2.4.5'; export const napCatVersion = '2.4.7';

View File

@@ -24,6 +24,7 @@ import pathLib from 'node:path';
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video'; import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
import ffmpeg from 'fluent-ffmpeg'; import ffmpeg from 'fluent-ffmpeg';
import { encodeSilk } from '@/common/audio'; import { encodeSilk } from '@/common/audio';
import { MessageContext } from '@/onebot/api';
export class NTQQFileApi { export class NTQQFileApi {
context: InstanceContext; context: InstanceContext;
@@ -33,7 +34,7 @@ export class NTQQFileApi {
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
this.core = core; this.core = core;
this.rkeyManager = new RkeyManager('https://llob.linyuchen.net/rkey', this.context.logger); this.rkeyManager = new RkeyManager(['https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], this.context.logger);
} }
async copyFile(filePath: string, destPath: string) { async copyFile(filePath: string, destPath: string) {
@@ -71,7 +72,7 @@ export class NTQQFileApi {
file_uuid: '', file_uuid: '',
}); });
await this.copyFile(filePath, mediaPath!); await this.copyFile(filePath, mediaPath);
const fileSize = await this.getFileSize(filePath); const fileSize = await this.getFileSize(filePath);
return { return {
md5: fileMd5, md5: fileMd5,
@@ -82,7 +83,7 @@ export class NTQQFileApi {
}; };
} }
async createValidSendFileElement(filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> { async createValidSendFileElement(context: MessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
const { const {
fileName: _fileName, fileName: _fileName,
path, path,
@@ -91,6 +92,7 @@ export class NTQQFileApi {
if (fileSize === 0) { if (fileSize === 0) {
throw new Error('文件异常大小为0'); throw new Error('文件异常大小为0');
} }
context.deleteAfterSentFiles.push(path);
return { return {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
@@ -103,12 +105,13 @@ export class NTQQFileApi {
}; };
} }
async createValidSendPicElement(picPath: string, summary: string = '', subType: 0 | 1 = 0,): Promise<SendPicElement> { async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: 0 | 1 = 0,): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType); const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
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 this.core.apis.FileApi.getImageSize(picPath);
context.deleteAfterSentFiles.push(path);
return { return {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: '', elementId: '',
@@ -130,26 +133,32 @@ export class NTQQFileApi {
}; };
} }
async createValidSendVideoElement(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> { async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
const logger = this.core.context.logger; const logger = this.core.context.logger;
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
thumb = pathLib.dirname(thumb);
let videoInfo = { let videoInfo = {
width: 1920, height: 1080, width: 1920, height: 1080,
time: 15, time: 15,
format: 'mp4', format: 'mp4',
size: fileSize, size: 0,
filePath, filePath,
}; };
try { try {
videoInfo = await getVideoInfo(path, logger); videoInfo = await getVideoInfo(filePath, logger);
} catch (e) { } catch (e) {
logger.logError('获取视频信息失败,将使用默认值', e); logger.logError('获取视频信息失败,将使用默认值', e);
} }
const newFilePath = filePath + '.mp4';
fs.copyFileSync(filePath, newFilePath);
context.deleteAfterSentFiles.push(newFilePath);
filePath = newFilePath;
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
videoInfo.size = fileSize;
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
thumb = pathLib.dirname(thumb);
const thumbPath = new Map(); const thumbPath = new Map();
const _thumbPath = await new Promise<string | undefined>((resolve, reject) => { const _thumbPath = await new Promise<string | undefined>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`; const thumbFileName = `${md5}_0.png`;
@@ -179,6 +188,7 @@ export class NTQQFileApi {
const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0; const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0;
thumbPath.set(0, _thumbPath); thumbPath.set(0, _thumbPath);
const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : ''; const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : '';
context.deleteAfterSentFiles.push(path);
return { return {
elementType: ElementType.VIDEO, elementType: ElementType.VIDEO,
elementId: '', elementId: '',

View File

@@ -32,10 +32,6 @@ export class NTQQGroupApi {
for (const group of this.groups) { for (const group of this.groups) {
this.groupCache.set(group.groupCode, group); this.groupCache.set(group.groupCode, group);
} }
// let text = await this.context.session.getMsgService().sendSsoCmdReqByContend(
// 'LightAppSvc.mini_app_share.AdaptShareInfo',
// JSON.stringify({ data: 'test' }));
// console.log(text);
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`); this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
} }

View File

@@ -1,4 +1,4 @@
import { GroupMemberRole, Peer } from '@/core'; import { GroupMemberRole } from '@/core';
export interface Peer { export interface Peer {
chatType: ChatType; chatType: ChatType;

View File

@@ -8,7 +8,7 @@ interface ServerRkeyData {
} }
export class RkeyManager { export class RkeyManager {
serverUrl: string = ''; serverUrl: string[] = [];
logger: LogWrapper; logger: LogWrapper;
private rkeyData: ServerRkeyData = { private rkeyData: ServerRkeyData = {
group_rkey: '', group_rkey: '',
@@ -16,7 +16,7 @@ export class RkeyManager {
expired_time: 0, expired_time: 0,
}; };
constructor(serverUrl: string, logger: LogWrapper) { constructor(serverUrl: string[], logger: LogWrapper) {
this.logger = logger; this.logger = logger;
this.serverUrl = serverUrl; this.serverUrl = serverUrl;
} }
@@ -40,6 +40,13 @@ export class RkeyManager {
async refreshRkey(): Promise<any> { async refreshRkey(): Promise<any> {
//刷新rkey //刷新rkey
this.rkeyData = await RequestUtil.HttpGetJson<ServerRkeyData>(this.serverUrl, 'GET'); for (const url of this.serverUrl) {
try {
this.rkeyData = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
} catch (e) {
this.logger.logError(`[Rkey] Get Rkey ${url} Error `, e);
}
}
} }
} }

View File

@@ -145,7 +145,6 @@ export class NapCatCore {
if (Info.status == 20) { if (Info.status == 20) {
this.selfInfo.online = false; this.selfInfo.online = false;
this.context.logger.log("账号状态变更为离线"); this.context.logger.log("账号状态变更为离线");
return;
} else { } else {
this.selfInfo.online = true; this.selfInfo.online = true;
} }

View File

@@ -1,7 +1,7 @@
import * as pb from 'protobufjs'; import * as pb from 'protobufjs';
// Proto: from src/core/proto/ProfileLike.proto // Proto: from src/core/proto/ProfileLike.proto
// Auther: Mlikiowa // Author: Mlikiowa
export interface LikeDetailType { export interface LikeDetailType {
txt: string; txt: string;
@@ -80,4 +80,4 @@ export const likeMsg = new pb.Type("likeMsg")
export const profileLikeTip = new pb.Type("profileLikeTip") export const profileLikeTip = new pb.Type("profileLikeTip")
.add(likeMsg) .add(likeMsg)
.add(new pb.Field("msg", 14, "likeMsg")); .add(new pb.Field("msg", 14, "likeMsg"));

View File

@@ -19,26 +19,21 @@ export class OCRImage extends BaseAction<Payload, any> {
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
const { path, isLocal, success } = (await uri2local(this.core.NapCatTempPath, payload.image)); const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.image));
if (!success) { if (!success) {
throw `OCR ${payload.image}失败,image字段可能格式不正确`; throw `OCR ${payload.image}失败,image字段可能格式不正确`;
} }
if (path) { if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断 await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await this.core.apis.SystemApi.ocrImage(path); const ret = await this.core.apis.SystemApi.ocrImage(path);
if (!isLocal) { fs.unlink(path, () => { });
fs.unlink(path, () => {
});
}
if (!ret) { if (!ret) {
throw `OCR ${payload.file}失败`; throw `OCR ${payload.file}失败`;
} }
return ret.result; return ret.result;
} }
if (!isLocal) { fs.unlink(path, () => { });
fs.unlink(path, () => {
});
}
throw `OCR ${payload.file}失败,文件可能不存在`; throw `OCR ${payload.file}失败,文件可能不存在`;
} }
} }

View File

@@ -24,17 +24,16 @@ export default class SetAvatar extends BaseAction<Payload, null> {
} }
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const { path, isLocal, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file));
if (!success) { if (!success) {
throw `头像${payload.file}设置失败,file字段可能格式不正确`; throw `头像${payload.file}设置失败,file字段可能格式不正确`;
} }
if (path) { if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断 await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await this.core.apis.UserApi.setQQAvatar(path); const ret = await this.core.apis.UserApi.setQQAvatar(path);
if (!isLocal) { fs.unlink(path, () => {
fs.unlink(path, () => { });
});
}
if (!ret) { if (!ret) {
throw `头像${payload.file}设置失败,api无返回`; throw `头像${payload.file}设置失败,api无返回`;
} }
@@ -45,10 +44,8 @@ export default class SetAvatar extends BaseAction<Payload, null> {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`; throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`;
} }
} else { } else {
if (!isLocal) { fs.unlink(path, () => { });
fs.unlink(path, () => {
});
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`; throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
} }
return null; return null;

View File

@@ -31,7 +31,6 @@ export class SendGroupNotice extends BaseAction<Payload, null> {
//公告图逻辑 //公告图逻辑
const { const {
path, path,
isLocal,
success, success,
} = (await uri2local(this.core.NapCatTempPath, payload.image)); } = (await uri2local(this.core.NapCatTempPath, payload.image));
if (!success) { if (!success) {
@@ -45,10 +44,10 @@ export class SendGroupNotice extends BaseAction<Payload, null> {
if (ImageUploadResult.errCode != 0) { if (ImageUploadResult.errCode != 0) {
throw `群公告${payload.image}设置失败,图片上传失败`; throw `群公告${payload.image}设置失败,图片上传失败`;
} }
if (!isLocal) {
unlink(path, () => { unlink(path, () => {
}); });
}
UploadImage = ImageUploadResult.picInfo; UploadImage = ImageUploadResult.picInfo;
} }
@@ -58,7 +57,6 @@ export class SendGroupNotice extends BaseAction<Payload, null> {
const noticeShowEditCard = +(payload.is_show_edit_card ?? 0); const noticeShowEditCard = +(payload.is_show_edit_card ?? 0);
const noticeTipWindowType = +(payload.tip_window_type ?? 0); const noticeTipWindowType = +(payload.tip_window_type ?? 0);
const noticeConfirmRequired = +(payload.confirm_required ?? 1); const noticeConfirmRequired = +(payload.confirm_required ?? 1);
//const publishGroupBulletinResult = await this.core.apis.GroupApi.publishGroupBulletin(payload.group_id.toString(), payload.content, UploadImage, noticePinned, noticeConfirmRequired);
const publishGroupBulletinResult = await this.core.apis.WebApi.setGroupNotice( const publishGroupBulletinResult = await this.core.apis.WebApi.setGroupNotice(
payload.group_id.toString(), payload.group_id.toString(),
payload.content, payload.content,
@@ -72,7 +70,7 @@ export class SendGroupNotice extends BaseAction<Payload, null> {
UploadImage?.height UploadImage?.height
); );
if (!publishGroupBulletinResult || publishGroupBulletinResult.ec != 0) { if (!publishGroupBulletinResult || publishGroupBulletinResult.ec != 0) {
throw `设置群公告失败,错误信息:${publishGroupBulletinResult?.em}`; throw new Error(`设置群公告失败,错误信息:${publishGroupBulletinResult?.em}`);
} }
return null; return null;
} }

View File

@@ -25,17 +25,14 @@ export default class SetGroupPortrait extends BaseAction<Payload, any> {
} }
async _handle(payload: Payload): Promise<any> { async _handle(payload: Payload): Promise<any> {
const { path, isLocal, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file));
if (!success) { if (!success) {
throw `头像${payload.file}设置失败,file字段可能格式不正确`; throw `头像${payload.file}设置失败,file字段可能格式不正确`;
} }
if (path) { if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断 await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await this.core.apis.GroupApi.setGroupAvatar(payload.group_id.toString(), path) as any; const ret = await this.core.apis.GroupApi.setGroupAvatar(payload.group_id.toString(), path) as any;
if (!isLocal) { fs.unlink(path, () => { });
fs.unlink(path, () => {
});
}
if (!ret) { if (!ret) {
throw `头像${payload.file}设置失败,api无返回`; throw `头像${payload.file}设置失败,api无返回`;
} }
@@ -46,10 +43,7 @@ export default class SetGroupPortrait extends BaseAction<Payload, any> {
} }
return ret; return ret;
} else { } else {
if (!isLocal) { fs.unlink(path, () => {});
fs.unlink(path, () => {
});
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`; throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
} }
} }

View File

@@ -1,9 +1,10 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { ChatType } from '@/core/entities'; import { ChatType, Peer } from '@/core/entities';
import fs from 'fs'; import fs from 'fs';
import { uri2local } from '@/common/file'; import { uri2local } from '@/common/file';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageContext } from '@/onebot/api';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -29,14 +30,19 @@ export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
file = `file://${file}`; file = `file://${file}`;
} }
const downloadResult = await uri2local(this.core.NapCatTempPath, file); const downloadResult = await uri2local(this.core.NapCatTempPath, file);
const peer: Peer = {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id.toString(),
};
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg); throw new Error(downloadResult.errMsg);
} }
const sendFileEle = await this.core.apis.FileApi.createValidSendFileElement(downloadResult.path, payload.name, payload.folder_id); const msgContext: MessageContext = {
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId({ peer: peer,
chatType: ChatType.KCHATTYPEGROUP, deleteAfterSentFiles: []
peerUid: payload.group_id.toString(), };
}, [sendFileEle], [], true); const sendFileEle = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, payload.folder_id);
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], [], true);
return null; return null;
} }
} }

View File

@@ -4,6 +4,8 @@ import { ChatType, Peer, SendFileElement } from '@/core/entities';
import fs from 'fs'; import fs from 'fs';
import { uri2local } from '@/common/file'; import { uri2local } from '@/common/file';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageContext } from '@/onebot/api';
import { ContextMode, createContext } from '../msg/SendMsg';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -42,7 +44,15 @@ export default class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null>
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg); throw new Error(downloadResult.errMsg);
} }
const sendFileEle: SendFileElement = await this.core.apis.FileApi.createValidSendFileElement(downloadResult.path, payload.name);
const msgContext: MessageContext = {
peer: await createContext(this.core, {
user_id: payload.user_id.toString(),
group_id: undefined,
}, ContextMode.Private),
deleteAfterSentFiles: []
};
const sendFileEle: SendFileElement = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name);
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], [], true); await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], [], true);
return null; return null;
} }

View File

@@ -3,6 +3,7 @@ import {
OB11MessageDataType, OB11MessageDataType,
OB11MessageMixType, OB11MessageMixType,
OB11MessageNode, OB11MessageNode,
OB11PostContext,
OB11PostSendMsg, OB11PostSendMsg,
} from '@/onebot/types'; } from '@/onebot/types';
import { ActionName, BaseCheckResult } from '@/onebot/action/types'; import { ActionName, BaseCheckResult } from '@/onebot/action/types';
@@ -30,7 +31,7 @@ export function normalize(message: OB11MessageMixType, autoEscape = false): OB11
) : Array.isArray(message) ? message : [message]; ) : Array.isArray(message) ? message : [message];
} }
export async function createContext(core: NapCatCore, payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> { export async function createContext(core: NapCatCore, payload: OB11PostContext, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id, // This function determines the type of message by the existence of user_id / group_id,
// not message_type. // not message_type.
// This redundant design of Ob11 here should be blamed. // This redundant design of Ob11 here should be blamed.

View File

@@ -142,22 +142,22 @@ export class OneBotGroupApi {
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
const type = json.items[json.items.length - 1]?.txt; const type = json.items[json.items.length - 1]?.txt;
switch (type) { switch (type) {
case "头衔": { case "头衔": {
const memberUin = json.items[1].param[0]; const memberUin = json.items[1].param[0];
const title = json.items[3].txt; const title = json.items[3].txt;
logger.logDebug('收到群成员新头衔消息', json); logger.logDebug('收到群成员新头衔消息', json);
return new OB11GroupTitleEvent( return new OB11GroupTitleEvent(
this.core, this.core,
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(memberUin), parseInt(memberUin),
title, title,
); );
}; }
case "移出": case "移出":
logger.logDebug('收到机器人被踢消息', json); logger.logDebug('收到机器人被踢消息', json);
return; return;
default: default:
logger.logWarn('收到未知的灰条消息', json); logger.logWarn('收到未知的灰条消息', json);
} }
} }
} }

View File

@@ -34,6 +34,7 @@ import { RequestUtil } from '@/common/request';
import fs from 'node:fs'; import fs from 'node:fs';
import fsPromise from 'node:fs/promises'; import fsPromise from 'node:fs/promises';
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import { SysMessage, SysMessageType } from '@/core/proto/ProfileLike';
type RawToOb11Converters = { type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -185,7 +186,7 @@ export class OneBotMsgApi {
data: { data: {
file: 'marketface', file: 'marketface',
file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + _.key + ".jpg"), file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + _.key + ".jpg"),
path: elementWrapper.elementId, path: url,
url: url, url: url,
file_unique: _.key file_unique: _.key
}, },
@@ -460,6 +461,7 @@ export class OneBotMsgApi {
// File service // File service
[OB11MessageDataType.image]: async (sendMsg, context) => { [OB11MessageDataType.image]: async (sendMsg, context) => {
const sendPicElement = await this.core.apis.FileApi.createValidSendPicElement( const sendPicElement = await this.core.apis.FileApi.createValidSendPicElement(
context,
(await this.handleOb11FileLikeMessage(sendMsg, context)).path, (await this.handleOb11FileLikeMessage(sendMsg, context)).path,
sendMsg.data.summary, sendMsg.data.summary,
sendMsg.data.sub_type, sendMsg.data.sub_type,
@@ -470,7 +472,7 @@ export class OneBotMsgApi {
[OB11MessageDataType.file]: async (sendMsg, context) => { [OB11MessageDataType.file]: async (sendMsg, context) => {
const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context);
return await this.core.apis.FileApi.createValidSendFileElement(path, fileName); return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName);
}, },
[OB11MessageDataType.video]: async (sendMsg, context) => { [OB11MessageDataType.video]: async (sendMsg, context) => {
@@ -481,9 +483,7 @@ export class OneBotMsgApi {
const uri2LocalRes = await uri2local(this.core.NapCatTempPath, thumb); const uri2LocalRes = await uri2local(this.core.NapCatTempPath, thumb);
if (uri2LocalRes.success) thumb = uri2LocalRes.path; if (uri2LocalRes.success) thumb = uri2LocalRes.path;
} }
const videoEle = await this.core.apis.FileApi.createValidSendVideoElement(path, fileName, thumb); const videoEle = await this.core.apis.FileApi.createValidSendVideoElement(context, path, fileName, thumb);
context.deleteAfterSentFiles.push(videoEle.videoElement.filePath);
return videoEle; return videoEle;
}, },
@@ -805,7 +805,6 @@ export class OneBotMsgApi {
} }
const { const {
path, path,
isLocal,
fileName, fileName,
errMsg, errMsg,
success, success,
@@ -816,10 +815,42 @@ export class OneBotMsgApi {
throw Error('文件下载失败' + errMsg); throw Error('文件下载失败' + errMsg);
} }
if (!isLocal) { // 只删除http和base64转过来的文件 deleteAfterSentFiles.push(path);
deleteAfterSentFiles.push(path);
}
return { path, fileName: inputdata.name ?? fileName }; return { path, fileName: inputdata.name ?? fileName };
} }
async parseSysMessage(msg: number[]) {
const sysMsg = SysMessage.decode(Uint8Array.from(msg)) as unknown as SysMessageType;
if (sysMsg.msgSpec.length === 0) {
return;
}
const { msgType, subType, subSubType } = sysMsg.msgSpec[0];
if (msgType === 528 && subType === 39 && subSubType === 39) {
if (!sysMsg.bodyWrapper) return;
const event = await this.obContext.apis.UserApi.parseLikeEvent(sysMsg.bodyWrapper.wrappedBody);
return event;
}
/*
if (msgType === 732 && subType === 16 && subSubType === 16) {
const greyTip = GreyTipWrapper.fromBinary(Uint8Array.from(sysMsg.bodyWrapper!.wrappedBody.slice(7)));
if (greyTip.subTypeId === 36) {
const emojiLikeToOthers = EmojiLikeToOthersWrapper1
.fromBinary(greyTip.rest)
.wrapper!
.body!;
if (emojiLikeToOthers.attributes?.operation !== 1) { // Un-like
return;
}
const eventOrEmpty = await this.apis.GroupApi.createGroupEmojiLikeEvent(
greyTip.groupCode.toString(),
await this.core.apis.UserApi.getUinByUidV2(emojiLikeToOthers.attributes!.senderUid),
emojiLikeToOthers.msgSpec!.msgSeq.toString(),
emojiLikeToOthers.attributes!.emojiId,
);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
eventOrEmpty && await this.networkManager.emitEvent(eventOrEmpty);
}
}
*/
}
} }

View File

@@ -1,6 +1,8 @@
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { profileLikeTip, ProfileLikeTipType } from '@/core/proto/ProfileLike';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import { OB11ProfileLikeEvent } from '../event/notice/OB11ProfileLikeEvent';
export class OneBotUserApi { export class OneBotUserApi {
obContext: NapCatOneBot11Adapter; obContext: NapCatOneBot11Adapter;
@@ -10,4 +12,20 @@ export class OneBotUserApi {
this.obContext = obContext; this.obContext = obContext;
this.core = core; this.core = core;
} }
async parseLikeEvent(wrappedBody: Uint8Array): Promise<OB11ProfileLikeEvent | undefined> {
const likeTip = profileLikeTip.decode(Uint8Array.from(wrappedBody.slice(12))) as unknown as ProfileLikeTipType;
this.core.context.logger.logDebug("收到点赞通知消息");
const likeMsg = likeTip.msg;
if (!likeMsg) return;
const detail = likeMsg.detail;
if (!detail) return;
const times = detail.txt.match(/\d+/) ?? "0";
return new OB11ProfileLikeEvent(
this.core,
Number(detail.uin),
detail.nickname,
parseInt(times[0], 10),
likeMsg.time,
);
}
} }

View File

@@ -44,9 +44,7 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal
import { LRUCache } from '@/common/lru-cache'; import { LRUCache } from '@/common/lru-cache';
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener'; import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent'; import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent';
import { profileLikeTip, ProfileLikeTipType, SysMessage, SysMessageMsgSpecType, SysMessageType } from '@/core/proto/ProfileLike'; import { profileLikeTip, ProfileLikeTipType, SysMessage, SysMessageType } from '@/core/proto/ProfileLike';
import { Message } from 'protobufjs';
//OneBot实现类 //OneBot实现类
export class NapCatOneBot11Adapter { export class NapCatOneBot11Adapter {
readonly core: NapCatCore; readonly core: NapCatCore;
@@ -240,50 +238,10 @@ export class NapCatOneBot11Adapter {
private initMsgListener() { private initMsgListener() {
const msgListener = new NodeIKernelMsgListener(); const msgListener = new NodeIKernelMsgListener();
msgListener.onRecvSysMsg = async (msg) => { msgListener.onRecvSysMsg = (msg) => {
const sysMsg = SysMessage.decode(Uint8Array.from(msg)) as unknown as SysMessageType; this.apis.MsgApi.parseSysMessage(msg).then((event) => {
if (sysMsg.msgSpec.length === 0) { if (event) this.networkManager.emitEvent(event);
return; }).catch(e => this.context.logger.logError('constructSysMessage error: ', e));
}
const { msgType, subType, subSubType } = sysMsg.msgSpec[0];
if (msgType === 528 && subType === 39 && subSubType === 39) {
const likeTip = profileLikeTip.decode(Uint8Array.from(sysMsg.bodyWrapper.wrappedBody.slice(12))) as unknown as ProfileLikeTipType;
this.core.context.logger.logDebug("收到点赞通知消息");
const likeMsg = likeTip.msg;
if (!likeMsg) return;
const detail = likeMsg.detail;
if (!detail) return;
const times = detail.txt.match(/\d+/) ?? "0";
await this.networkManager.emitEvent(new OB11ProfileLikeEvent(
this.core,
Number(detail.uin),
detail.nickname,
parseInt(times[0], 10),
likeMsg.time,
)).catch(e => this.context.logger.logError('处理被点赞事件失败', e));
};
/*
if (msgType === 732 && subType === 16 && subSubType === 16) {
const greyTip = GreyTipWrapper.fromBinary(Uint8Array.from(sysMsg.bodyWrapper!.wrappedBody.slice(7)));
if (greyTip.subTypeId === 36) {
const emojiLikeToOthers = EmojiLikeToOthersWrapper1
.fromBinary(greyTip.rest)
.wrapper!
.body!;
if (emojiLikeToOthers.attributes?.operation !== 1) { // Un-like
return;
}
const eventOrEmpty = await this.apis.GroupApi.createGroupEmojiLikeEvent(
greyTip.groupCode.toString(),
await this.core.apis.UserApi.getUinByUidV2(emojiLikeToOthers.attributes!.senderUid),
emojiLikeToOthers.msgSpec!.msgSeq.toString(),
emojiLikeToOthers.attributes!.emojiId,
);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
eventOrEmpty && await this.networkManager.emitEvent(eventOrEmpty);
}
}
*/
}; };
msgListener.onInputStatusPush = async data => { msgListener.onInputStatusPush = async data => {

View File

@@ -47,7 +47,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
} }
//鉴权 //鉴权
this.authorize(token, wsClient, wsReq); this.authorize(token, wsClient, wsReq);
let paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url; const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
const isEventConnect = paramUrl === '/event' || paramUrl === '' || paramUrl === '/'; const isEventConnect = paramUrl === '/event' || paramUrl === '' || paramUrl === '/';
if (isEventConnect) { if (isEventConnect) {
this.connectEvent(core, wsClient); this.connectEvent(core, wsClient);

View File

@@ -208,3 +208,8 @@ export interface OB11PostSendMsg {
messages?: OB11MessageMixType; // 兼容 go-cqhttp messages?: OB11MessageMixType; // 兼容 go-cqhttp
auto_escape?: boolean | string auto_escape?: boolean | string
} }
export interface OB11PostContext {
message_type?: 'private' | 'group'
user_id?: string,
group_id?: string,
}

View File

@@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
undefined, undefined,
SettingButton('V2.4.5', 'napcat-update-button', 'secondary'), SettingButton('V2.4.7', 'napcat-update-button', 'secondary'),
), ),
]), ]),
SettingList([ SettingList([

View File

@@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
void 0, void 0,
SettingButton("V2.4.5", "napcat-update-button", "secondary") SettingButton("V2.4.7", "napcat-update-button", "secondary")
) )
]), ]),
SettingList([ SettingList([