Compare commits

...

22 Commits

Author SHA1 Message Date
手瓜一十雪
7b82444338 fix 2024-12-30 16:17:29 +08:00
Mlikiowa
8108b9f565 release: v4.2.51 2024-12-30 08:15:48 +00:00
手瓜一十雪
c6ddd00cd9 Merge pull request #675 from NapNeko/test
fix: 重写远程遥测
2024-12-30 16:14:48 +08:00
手瓜一十雪
20c0c00fa0 fixfix 2024-12-30 16:14:32 +08:00
手瓜一十雪
1f90364ba6 fix: 重写远程遥测 2024-12-30 16:10:38 +08:00
手瓜一十雪
49ea4d31a5 fix 2024-12-30 13:23:33 +08:00
Mlikiowa
dc35f1456a release: v4.2.50 2024-12-30 04:33:08 +00:00
手瓜一十雪
0ebeb90804 fix 2024-12-30 12:31:43 +08:00
手瓜一十雪
3ef5436c98 Merge pull request #673 from NapNeko/fixseq
fix: #670
2024-12-30 12:28:29 +08:00
手瓜一十雪
de7996d789 refactor: 进群入群 2024-12-30 12:27:42 +08:00
手瓜一十雪
ac52d9bae2 fix: 算了能用就行 2024-12-30 11:59:16 +08:00
手瓜一十雪
cb02df3b76 Merge pull request #672 from clansty/feat/download_file-file-proto
feat: download_file 支持 file://
2024-12-29 22:45:27 +08:00
手瓜一十雪
5fc5a6f1a6 fix: #670 2024-12-29 22:44:25 +08:00
手瓜一十雪
726a0d0394 fix 2024-12-29 22:30:33 +08:00
Clansty
6edf5345a3 chore: remove unused import 2024-12-29 22:27:41 +08:00
Clansty
242bbfdb14 chore: uriToLocalFile 2024-12-29 22:03:04 +08:00
Clansty
89e7712676 feat: download_file 支持 file:// 2024-12-29 21:41:44 +08:00
手瓜一十雪
9525786929 fix 2024-12-29 16:20:41 +08:00
Mlikiowa
72088e41a8 release: v4.2.46 2024-12-29 07:47:38 +00:00
手瓜一十雪
a3ed9ff2ef Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-29 15:47:18 +08:00
手瓜一十雪
ff16dc73ec fix: typo 2024-12-29 15:47:16 +08:00
Mlikiowa
2da4ef5f0f release: v4.2.45 2024-12-29 07:46:08 +00:00
12 changed files with 214 additions and 68 deletions

View File

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

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.2.44",
"version": "4.2.51",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",

View File

@@ -175,10 +175,9 @@ export async function checkUriType(Uri: string) {
return { Uri: Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2LocalRes> {
export async function uriToLocalFile(dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filename = randomUUID();
const filePath = path.join(dir, filename);
switch (UriType) {
@@ -191,7 +190,7 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
}
case FileUriType.Remote: {
const buffer = await httpDownload(HandledUri);
const buffer = await httpDownload({ url: HandledUri, headers: headers });
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
@@ -206,4 +205,4 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
}
}

View File

@@ -1,40 +1,93 @@
import https from 'node:https';
import { napCatVersion } from './version';
import os from 'os';
export class UmamiTraceCore {
napcatVersion = napCatVersion;
qqversion = '1.0.0';
guid = 'default-user';
heartbeatInterval: NodeJS.Timeout | null = null;
website: string = '1fabb2b1-c3a3-4416-b1be-31e2cbdce978';
referrer: string = 'https://trace.napneko.icu/';
hostname: string = 'trace.napneko.icu';
ua: string = '';
export class umamiTrace {
static napcatVersion = napCatVersion;
static qqversion = '1.0.0';
static init(qqversion: string) {
init(qqversion: string, guid: string) {
this.qqversion = qqversion;
setInterval(() => {
this.trackEvent('heartbeat');
}, 5 * 60 * 1000);
}
static trackEvent(eventName: string, info?: string) {
const StatesData = {
type: 'event',
payload: {
'website': '596cbbb2-1740-4373-a807-cf3d0637bfa7',
'hostname': 'trace.napneko.icu',
'title': 'NapCat ' + umamiTrace.napcatVersion,
'url': '/' + umamiTrace.qqversion + '/' + umamiTrace.napcatVersion + '/' + eventName,
'referrer': 'https://napcat.onebot.napneko.icu/' + umamiTrace.qqversion + '/' + umamiTrace.napcatVersion + '/' + info,
}
let UaList = {
'linux': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/124.0.0.0 Safari/537.36 PTST/240508.140043',
'win32': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36',
'darwin': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36',
};
let request = https.request({
hostname: '104.19.42.72',// 固定 IP
try {
if (process.platform === 'win32') {
const ntVersion = os.release();
UaList.win32 = `Mozilla/5.0 (Windows NT ${ntVersion}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36`;
} else if (process.platform === 'darwin') {
const macVersion = os.release();
UaList.darwin = `Mozilla/5.0 (Macintosh; Intel Mac OS X ${macVersion}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36`;
}
} catch (error) {
this.ua = UaList.win32;
}
this.ua = UaList[process.platform as keyof typeof UaList] || UaList.win32;
this.identifyUser(guid);
this.startHeartbeat();
}
identifyUser(guid: string) {
this.guid = guid;
const data = {
napcat_version: this.napcatVersion,
qq_version: this.qqversion,
guid: guid
};
this.sendRequest({ website: this.website, ...data }, 'identify');
}
sendEvent(event: string, data?: object) {
const env = process.env;
const language = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES;
const payload = {
name: event,
hostname: this.hostname,
referrer: this.referrer,
website: this.website,
language: language || 'es-US',
napcat_version: this.napcatVersion,
qq_version: this.qqversion,
...data
};
this.sendRequest(payload);
}
sendTrace(eventName: string) {
const payload = {
website: this.website,
hostname: this.hostname,
title: 'NapCat ' + this.napcatVersion,
url: `/${this.qqversion}/${this.napcatVersion}/${eventName}`,
referrer: this.referrer,
};
this.sendRequest(payload);
}
sendRequest(payload: object, type = 'event') {
const options = {
hostname: '104.19.42.72', // 固定 IP 或者从 hostUrl 获取
port: 443,
path: '/api/send',
method: 'POST',
headers: {
"Host": "umami.napneko.icu",
"Content-Type": "application/json",
"User-Agent": `Mozilla/5.0 Umami/${process.version}`
"User-Agent": this.ua
}
}, (res) => {
};
const request = https.request(options, (res) => {
res.on('error', (error) => {
});
@@ -43,7 +96,31 @@ export class umamiTrace {
});
});
request.write(JSON.stringify(StatesData));
request.write(JSON.stringify({ type, payload }));
request.end();
}
}
startHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => {
this.sendEvent('heartbeat', {
title: 'NapCat ' + this.napcatVersion,
language: process.env.LANG || 'en-US',
url: `/${this.qqversion}/${this.napcatVersion}/heartbeat`,
version: this.napcatVersion,
qq_version: this.qqversion,
user_id: this.guid
});
}, 5 * 60 * 1000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
}
export const UmamiTrace = new UmamiTraceCore();

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.44';
export const napCatVersion = '4.2.51';

View File

@@ -30,7 +30,7 @@ import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { umamiTrace } from '@/common/umami';
import { UmamiTrace } from '@/common/umami';
export * from './wrapper';
export * from './types';
export * from './services';
@@ -156,9 +156,9 @@ export class NapCatCore {
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
if (this.context.workingEnv === NapCatCoreWorkingEnv.Framework) {
umamiTrace.trackEvent('framework/kickoff');
UmamiTrace.sendEvent('framework/kickoff');
} else {
umamiTrace.trackEvent('shell/kickoff');
UmamiTrace.sendEvent('shell/kickoff');
}
// 下线通知
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);

View File

@@ -56,6 +56,7 @@ export interface GrayTipElement {
aioOpGrayTipElement: TipAioOpGrayTipElement;
groupElement: TipGroupElement;
xmlElement: {
busiId: string;
content: string;
templId: string;
};

View File

@@ -9,7 +9,7 @@ import { NodeIKernelLoginService } from '@/core/services';
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
import { InitWebUi, WebUiConfig } from '@/webui';
import { NapCatOneBot11Adapter } from '@/onebot';
import { umamiTrace } from '@/common/umami';
import { UmamiTrace } from '@/common/umami';
//Framework ES入口文件
export async function getWebUiUrl() {
@@ -26,24 +26,22 @@ export async function NCoreInitFramework(
console.log('NapCat Framework App Loading...');
process.on('uncaughtException', (err) => {
umamiTrace.trackEvent('framework/error', err.message);
UmamiTrace.sendEvent('framework/error', { name: err.name });
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
});
process.on('unhandledRejection', (reason, promise) => {
console.log('[NapCat] [Error] unhandledRejection:', reason);
});
process.on('exit', (code: number) => {
umamiTrace.trackEvent('framework/exit', code.toString());
});
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
umamiTrace.init(basicInfoWrapper.getFullQQVesion());
umamiTrace.trackEvent('framework/login');
let guid = loginService.getMachineGuid();
UmamiTrace.init(basicInfoWrapper.getFullQQVesion(), guid);
UmamiTrace.sendTrace('framework/boot');
UmamiTrace.sendEvent('framework/login');
//直到登录成功后,执行下一步
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener();

View File

@@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import fs from 'fs';
import { join as joinPath } from 'node:path';
import { calculateFileMD5, httpDownload } from '@/common/file';
import { calculateFileMD5, uriToLocalFile } from '@/common/file';
import { randomUUID } from 'crypto';
import { Static, Type } from '@sinclair/typebox';
@@ -26,17 +26,20 @@ export default class GoCQHTTPDownloadFile extends OneBotAction<Payload, FileResp
async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name;
const name = payload.name || randomUUID();
const filePath = joinPath(this.core.NapCatTempPath, name);
let result: Awaited<ReturnType<typeof uriToLocalFile>>;
if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64');
result = await uriToLocalFile(this.core.NapCatTempPath, `base64://${payload.base64}`, name);
} else if (payload.url) {
const headers = this.getHeaders(payload.headers);
const buffer = await httpDownload({ url: payload.url, headers: headers });
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
result = await uriToLocalFile(this.core.NapCatTempPath, payload.url, name, headers);
} else {
throw new Error('不存在任何文件, 无法下载');
}
if (!result.success) {
throw new Error(result.errMsg);
}
const filePath = result.path;
if (fs.existsSync(filePath)) {
if (isRandomName) {

View File

@@ -24,6 +24,7 @@ import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNotic
import { OB11GroupNameEvent } from '../event/notice/OB11GroupNameEvent';
import { pathToFileURL } from 'node:url';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
export class OneBotGroupApi {
obContext: NapCatOneBot11Adapter;
@@ -219,15 +220,51 @@ export class OneBotGroupApi {
} else if (element.type === TipGroupElementType.KSHUTUP) {
let event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
return event;
} else if (element.type === TipGroupElementType.KMEMBERADD) {
// 自己的通知 协议推送为type->85 在这里实现为了避免邀请出现问题
if (element.memberUid == this.core.selfInfo.uid) {
this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),
+this.core.selfInfo.uin,
element.adminUid ? +await this.core.apis.UserApi.getUinByUidV2(element.adminUid) : 0,
'approve'
);
}
}
}
async parseSelfInviteEvent(msg: RawMessage, inviterUin: string, inviteeUin: string) {
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),
+inviteeUin,
+inviterUin,
'invite'
);
}
async parseGrayTipElement(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
// 解析群组事件 由sysmsg解析
return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
// 筛选出表情回应 事件
// 筛选自身入群情况
// if (grayTipElement.xmlElement.busiId === '10145') {
// const inviteData = new fastXmlParser.XMLParser({
// ignoreAttributes: false,
// attributeNamePrefix: '',
// }).parse(grayTipElement.xmlElement.content);
// const inviterUin: string = inviteData.gtip.qq[0].jp;
// const inviteeUin: string = inviteData.gtip.qq[1].jp;
// //刷新群缓存
// if (inviteeUin === this.core.selfInfo.uin) {
// this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
// return this.parseSelfInviteEvent(msg, inviterUin, inviteeUin);
// }
// } else
if (grayTipElement.xmlElement?.templId === '10382') {
return await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, grayTipElement);
} else {

View File

@@ -972,6 +972,7 @@ export class OneBotMsgApi {
return { path, fileName: inputdata.name ?? fileName };
}
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) {
case 130:
@@ -985,12 +986,41 @@ export class OneBotMsgApi {
}
}
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
let groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
let isAdminOrOwner = groupRole === 3 || groupRole === 4;
if (isAdminOrOwner && !operatorUid) {
let dataNotify: GroupNotify | undefined;
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
(doubt, notifies) => {
for (const notify of notifies) {
if (notify.group.groupCode === groupUin && notify.user1.uid === memberUid) {
dataNotify = notify;
return true;
}
}
return false;
}, 1, 1000).catch(undefined);
if (dataNotify) {
return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid;
}
}
return operatorUid;
}
async parseSysMessage(msg: number[]) {
const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg));
// 邀请需要解grayTipElement
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
const operatorUid = groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : '';
let operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
);
return new OB11GroupIncreaseEvent(
this.core,
groupChange.groupUin,
@@ -998,12 +1028,17 @@ export class OneBotMsgApi {
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0,
groupChange.decreaseType == 131 ? 'invite' : 'approve',
);
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
const operatorUid = groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString();
let operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString()
);
if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => {
this.core.apis.GroupApi.groupMemberCache.delete(groupChange.groupUin.toString());
@@ -1103,8 +1138,7 @@ export class OneBotMsgApi {
'',
request_seq
);
}
else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
} else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
}
}

View File

@@ -29,11 +29,11 @@ import { InitWebUi } from '@/webui';
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
import { napCatVersion } from '@/common/version';
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
import { umamiTrace } from '@/common/umami';
import { UmamiTrace } from '@/common/umami';
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions(logger: LogWrapper) {
process.on('uncaughtException', (err) => {
umamiTrace.trackEvent('framework/error', err.message);
UmamiTrace.sendEvent('framework/error', { name: err.name, message: err.message });
logger.logError('[NapCat] [Error] Unhandled Exception:', err.message);
});
process.on('unhandledRejection', (reason, promise) => {
@@ -153,7 +153,7 @@ async function handleLogin(
};
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
umamiTrace.trackEvent('shell/login/error/' + errType.toString() + '-' + errCode.toString(), errMsg);
UmamiTrace.sendEvent('shell/qrlogin/error', { errType, errCode, errMsg });
if (!isLogined) {
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
if (errType == 1 && errCode == 3) {
@@ -164,7 +164,7 @@ async function handleLogin(
};
loginListener.onLoginFailed = (args) => {
umamiTrace.trackEvent('shell/login/error/' + args.toString(), args.toString());
UmamiTrace.sendEvent('shell/login/error', { args });
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args.toString());
};
@@ -270,13 +270,9 @@ export async function NCoreInitShell() {
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
handleUncaughtExceptions(logger);
process.on('exit', (code: number) => {
umamiTrace.trackEvent('framework/exit', code.toString());
});
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
umamiTrace.init(basicInfoWrapper.getFullQQVesion());
umamiTrace.trackEvent('shell/boot');
const o3Service = wrapper.NodeIO3MiscService.get();
o3Service.addO3MiscListener(new NodeIO3MiscListener());
@@ -302,9 +298,10 @@ export async function NCoreInitShell() {
const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
UmamiTrace.init(basicInfoWrapper.getFullQQVesion(), loginService.getMachineGuid());
UmamiTrace.sendTrace('shell/boot');
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
umamiTrace.trackEvent('shell/login');
UmamiTrace.sendEvent('shell/login');
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));