mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
360 lines
14 KiB
TypeScript
360 lines
14 KiB
TypeScript
import {
|
||
NTQQFileApi,
|
||
NTQQFriendApi,
|
||
NTQQGroupApi,
|
||
NTQQMsgApi,
|
||
NTQQSystemApi,
|
||
NTQQUserApi,
|
||
NTQQWebApi,
|
||
} from '@/core/apis';
|
||
import { NTQQCollectionApi } from '@/core/apis/collection';
|
||
import {
|
||
NodeIQQNTWrapperSession,
|
||
NodeQQNTWrapperUtil,
|
||
PlatformType,
|
||
VendorType,
|
||
WrapperNodeApi,
|
||
WrapperSessionInitConfig,
|
||
} from '@/core/wrapper';
|
||
import { LogLevel, LogWrapper } from '@/common/log';
|
||
import { NodeIKernelLoginService } from '@/core/services';
|
||
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
|
||
import { NapCatPathWrapper } from '@/common/path';
|
||
import path from 'node:path';
|
||
import fs from 'node:fs';
|
||
import { hostname, systemName, systemVersion } from '@/common/system';
|
||
import { NTEventWrapper } from '@/common/event';
|
||
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
|
||
import { NapCatConfigLoader } from '@/core/helper/config';
|
||
import os from 'node:os';
|
||
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||
import { NTQQPacketApi } from './apis/packet';
|
||
export * from './wrapper';
|
||
export * from './types';
|
||
export * from './services';
|
||
export * from './listeners';
|
||
|
||
export enum NapCatCoreWorkingEnv {
|
||
Unknown = 0,
|
||
Shell = 1,
|
||
Framework = 2,
|
||
}
|
||
|
||
export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
|
||
let appPath;
|
||
if (os.platform() === 'darwin') {
|
||
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
||
} else if (os.platform() === 'linux') {
|
||
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
||
} else {
|
||
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
||
}
|
||
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
|
||
if (!fs.existsSync(wrapperNodePath)) {
|
||
wrapperNodePath = path.join(appPath, `./resources/app/wrapper.node`);
|
||
}
|
||
//老版本兼容 未来去掉
|
||
if (!fs.existsSync(wrapperNodePath)) {
|
||
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
|
||
}
|
||
const nativemodule: any = { exports: {} };
|
||
process.dlopen(nativemodule, wrapperNodePath);
|
||
return nativemodule.exports;
|
||
}
|
||
export function getMajorPath(QQVersion: string): string {
|
||
// major.node
|
||
let appPath;
|
||
if (os.platform() === 'darwin') {
|
||
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
||
} else if (os.platform() === 'linux') {
|
||
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
||
} else {
|
||
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
||
}
|
||
let majorPath = path.resolve(appPath, 'major.node');
|
||
if (!fs.existsSync(majorPath)) {
|
||
majorPath = path.join(appPath, `./resources/app/major.node`);
|
||
}
|
||
//老版本兼容 未来去掉
|
||
if (!fs.existsSync(majorPath)) {
|
||
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
|
||
}
|
||
return majorPath;
|
||
}
|
||
export class NapCatCore {
|
||
readonly context: InstanceContext;
|
||
readonly eventWrapper: NTEventWrapper;
|
||
NapCatDataPath: string = '';
|
||
NapCatTempPath: string = '';
|
||
apis: StableNTApiWrapper;
|
||
// runtime info, not readonly
|
||
selfInfo: SelfInfo;
|
||
util: NodeQQNTWrapperUtil;
|
||
configLoader: NapCatConfigLoader;
|
||
|
||
// 通过构造器递过去的 runtime info 应该尽量少
|
||
constructor(context: InstanceContext, selfInfo: SelfInfo) {
|
||
this.selfInfo = selfInfo;
|
||
this.context = context;
|
||
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
||
this.eventWrapper = new NTEventWrapper(context.session);
|
||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
|
||
this.apis = {
|
||
FileApi: new NTQQFileApi(this.context, this),
|
||
SystemApi: new NTQQSystemApi(this.context, this),
|
||
CollectionApi: new NTQQCollectionApi(this.context, this),
|
||
PacketApi: new NTQQPacketApi(this.context, this),
|
||
WebApi: new NTQQWebApi(this.context, this),
|
||
FriendApi: new NTQQFriendApi(this.context, this),
|
||
MsgApi: new NTQQMsgApi(this.context, this),
|
||
UserApi: new NTQQUserApi(this.context, this),
|
||
GroupApi: new NTQQGroupApi(this.context, this),
|
||
};
|
||
}
|
||
async initCore() {
|
||
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
|
||
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
|
||
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
|
||
// 创建临时目录
|
||
if (!fs.existsSync(this.NapCatTempPath)) {
|
||
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
||
}
|
||
//遍历this.apis[i].initApi 如果存在该函数进行async 调用
|
||
for (const apiKey in this.apis) {
|
||
const api = this.apis[apiKey as keyof StableNTApiWrapper];
|
||
if ('initApi' in api && typeof api.initApi === 'function') {
|
||
await api.initApi();
|
||
}
|
||
}
|
||
this.initNapCatCoreListeners().then().catch((e) => this.context.logger.logError(e));
|
||
|
||
this.context.logger.setFileLogEnabled(
|
||
this.configLoader.configData.fileLog,
|
||
);
|
||
this.context.logger.setConsoleLogEnabled(
|
||
this.configLoader.configData.consoleLog,
|
||
);
|
||
this.context.logger.setFileAndConsoleLogLevel(
|
||
this.configLoader.configData.fileLogLevel as LogLevel,
|
||
this.configLoader.configData.consoleLogLevel as LogLevel,
|
||
);
|
||
}
|
||
get dataPath(): string {
|
||
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
|
||
if (!result) {
|
||
result = path.resolve(os.homedir(), './.config/QQ');
|
||
fs.mkdirSync(result, { recursive: true });
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Renamed from 'InitDataListener'
|
||
async initNapCatCoreListeners() {
|
||
const msgListener = new NodeIKernelMsgListener();
|
||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||
// 下线通知
|
||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||
this.selfInfo.online = false;
|
||
};
|
||
msgListener.onRecvMsg = (msgs) => {
|
||
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
||
};
|
||
msgListener.onAddSendMsg = (msg) => {
|
||
this.context.logger.logMessage(msg, this.selfInfo);
|
||
};
|
||
//await sleep(2500);
|
||
this.context.session.getMsgService().addKernelMsgListener(
|
||
proxiedListenerOf(msgListener, this.context.logger),
|
||
);
|
||
|
||
const profileListener = new NodeIKernelProfileListener();
|
||
profileListener.onProfileDetailInfoChanged = (profile) => {
|
||
if (profile.uid === this.selfInfo.uid) {
|
||
Object.assign(this.selfInfo, profile);
|
||
}
|
||
};
|
||
profileListener.onSelfStatusChanged = (Info: SelfStatusInfo) => {
|
||
if (Info.status == 20) {
|
||
this.selfInfo.online = false;
|
||
this.context.logger.log("账号状态变更为离线");
|
||
} else {
|
||
this.selfInfo.online = true;
|
||
}
|
||
};
|
||
this.context.session.getProfileService().addKernelProfileListener(
|
||
proxiedListenerOf(profileListener, this.context.logger),
|
||
);
|
||
|
||
// 群相关
|
||
const groupListener = new NodeIKernelGroupListener();
|
||
groupListener.onGroupListUpdate = (updateType, groupList) => {
|
||
// console.log("onGroupListUpdate", updateType, groupList)
|
||
groupList.map(g => {
|
||
const existGroup = this.apis.GroupApi.groupCache.get(g.groupCode);
|
||
//群成员数量变化 应该刷新缓存
|
||
if (existGroup && g.memberCount === existGroup.memberCount) {
|
||
Object.assign(existGroup, g);
|
||
} else {
|
||
this.apis.GroupApi.groupCache.set(g.groupCode, g);
|
||
// 获取群成员
|
||
}
|
||
const sceneId = this.context.session.getGroupService().createMemberListScene(g.groupCode, 'groupMemberList_MainWindow');
|
||
this.context.session.getGroupService().getNextMemberList(sceneId, undefined, 3000).then( /* r => {
|
||
// console.log(`get group ${g.groupCode} members`, r);
|
||
// r.result.infos.forEach(member => {
|
||
// });
|
||
// groupMembers.set(g.groupCode, r.result.infos);
|
||
} */);
|
||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||
});
|
||
};
|
||
groupListener.onMemberListChange = (arg) => {
|
||
// TODO: 应该加一个内部自己维护的成员变动callback,用于判断成员变化通知
|
||
const groupCode = arg.sceneId.split('_')[0];
|
||
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
|
||
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
|
||
arg.infos.forEach((member, uid) => {
|
||
//console.log('onMemberListChange', member);
|
||
const existMember = existMembers.get(uid);
|
||
if (existMember) {
|
||
Object.assign(existMember, member);
|
||
} else {
|
||
existMembers.set(uid, member);
|
||
}
|
||
//移除成员
|
||
if (member.isDelete) {
|
||
existMembers.delete(uid);
|
||
}
|
||
});
|
||
} else {
|
||
this.apis.GroupApi.groupMemberCache.set(groupCode, arg.infos);
|
||
}
|
||
};
|
||
groupListener.onMemberInfoChange = (groupCode, dataSource, members) => {
|
||
if (dataSource === DataSource.LOCAL && members.get(this.selfInfo.uid)?.isDelete) {
|
||
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现
|
||
setTimeout(() => {
|
||
this.apis.GroupApi.groupCache.delete(groupCode);
|
||
}, 5000);
|
||
|
||
}
|
||
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode);
|
||
if (existMembers) {
|
||
members.forEach((member, uid) => {
|
||
const existMember = existMembers.get(uid);
|
||
if (existMember) {
|
||
// 检查管理变动
|
||
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
|
||
// 更新成员信息
|
||
Object.assign(existMember, member);
|
||
} else {
|
||
existMembers.set(uid, member);
|
||
}
|
||
//移除成员
|
||
if (member.isDelete) {
|
||
existMembers.delete(uid);
|
||
}
|
||
});
|
||
} else {
|
||
this.apis.GroupApi.groupMemberCache.set(groupCode, members);
|
||
}
|
||
};
|
||
this.context.session.getGroupService().addKernelGroupListener(
|
||
proxiedListenerOf(groupListener, this.context.logger),
|
||
);
|
||
}
|
||
|
||
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
|
||
if (memberNew.role !== memberOld?.role) {
|
||
this.context.logger.logDebug(`群 ${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export async function genSessionConfig(
|
||
guid: string,
|
||
QQVersionAppid: string,
|
||
QQVersion: string,
|
||
selfUin: string,
|
||
selfUid: string,
|
||
account_path: string
|
||
): Promise<WrapperSessionInitConfig> {
|
||
const downloadPath = path.join(account_path, 'NapCat', 'temp');
|
||
fs.mkdirSync(downloadPath, { recursive: true });
|
||
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
|
||
win32: PlatformType.KWINDOWS,
|
||
darwin: PlatformType.KMAC,
|
||
linux: PlatformType.KLINUX,
|
||
};
|
||
const systemPlatform = platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
|
||
return {
|
||
selfUin,
|
||
selfUid,
|
||
desktopPathConfig: {
|
||
account_path, // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取
|
||
},
|
||
clientVer: QQVersion,
|
||
a2: '',
|
||
d2: '',
|
||
d2Key: '',
|
||
machineId: '',
|
||
platform: systemPlatform, // 3是Windows?
|
||
platVer: systemVersion, // 系统版本号, 应该可以固定
|
||
appid: QQVersionAppid,
|
||
rdeliveryConfig: {
|
||
appKey: '',
|
||
systemId: 0,
|
||
appId: '',
|
||
logicEnvironment: '',
|
||
platform: systemPlatform,
|
||
language: '',
|
||
sdkVersion: '',
|
||
userId: '',
|
||
appVersion: '',
|
||
osVersion: '',
|
||
bundleId: '',
|
||
serverUrl: '',
|
||
fixedAfterHitKeys: [''],
|
||
},
|
||
defaultFileDownloadPath: downloadPath,
|
||
deviceInfo: {
|
||
guid,
|
||
buildVer: QQVersion,
|
||
localId: 2052,
|
||
devName: hostname,
|
||
devType: systemName,
|
||
vendorName: '',
|
||
osVer: systemVersion,
|
||
vendorOsName: systemName,
|
||
setMute: false,
|
||
vendorType: VendorType.KNOSETONIOS,
|
||
},
|
||
deviceConfig: '{"appearance":{"isSplitViewMode":true},"msg":{}}',
|
||
};
|
||
}
|
||
|
||
export interface InstanceContext {
|
||
readonly workingEnv: NapCatCoreWorkingEnv;
|
||
readonly wrapper: WrapperNodeApi;
|
||
readonly session: NodeIQQNTWrapperSession;
|
||
readonly logger: LogWrapper;
|
||
readonly loginService: NodeIKernelLoginService;
|
||
readonly basicInfoWrapper: QQBasicInfoWrapper;
|
||
readonly pathWrapper: NapCatPathWrapper;
|
||
}
|
||
|
||
export interface StableNTApiWrapper {
|
||
FileApi: NTQQFileApi,
|
||
SystemApi: NTQQSystemApi,
|
||
PacketApi: NTQQPacketApi,
|
||
CollectionApi: NTQQCollectionApi,
|
||
WebApi: NTQQWebApi,
|
||
FriendApi: NTQQFriendApi,
|
||
MsgApi: NTQQMsgApi,
|
||
UserApi: NTQQUserApi,
|
||
GroupApi: NTQQGroupApi
|
||
}
|