mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
84382caebc | ||
![]() |
662530e507 | ||
![]() |
edf81d0a2e | ||
![]() |
7cbae86941 | ||
![]() |
8ff7420a5e | ||
![]() |
7ae59b1419 | ||
![]() |
41036f8ee8 | ||
![]() |
380777ca04 | ||
![]() |
c658cd1096 | ||
![]() |
c7b9946d2f | ||
![]() |
0caca473d6 | ||
![]() |
3e5d35957d | ||
![]() |
6b8b14aba2 | ||
![]() |
5db7a90a24 | ||
![]() |
88b86611a3 | ||
![]() |
886fe2052e | ||
![]() |
e4dd194d4a | ||
![]() |
a47af60f58 | ||
![]() |
35f24eb806 | ||
![]() |
36e3119d34 | ||
![]() |
8ff3ad824e | ||
![]() |
556000c002 | ||
![]() |
fda050d3fe | ||
![]() |
b1047309c9 | ||
![]() |
d766c4945e |
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.7.31",
|
||||
"version": "4.7.36",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -55,6 +55,7 @@
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"echarts": "^5.5.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"framer-motion": "^12.0.6",
|
||||
@@ -88,6 +89,7 @@
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@react-types/shared": "^3.26.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/fabric": "^5.3.9",
|
||||
"@types/node": "^22.12.0",
|
||||
|
@@ -3,7 +3,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||
import { LogLevel } from '@/const/enum'
|
||||
|
||||
import { serverRequest } from '@/utils/request'
|
||||
|
||||
import CryptoJS from "crypto-js";
|
||||
export interface Log {
|
||||
level: LogLevel
|
||||
message: string
|
||||
@@ -17,9 +17,10 @@ export default class WebUIManager {
|
||||
}
|
||||
|
||||
public static async loginWithToken(token: string) {
|
||||
const sha256 = CryptoJS.SHA256(token + '.napcat').toString();
|
||||
const { data } = await serverRequest.post<ServerResponse<AuthResponse>>(
|
||||
'/auth/login',
|
||||
{ token }
|
||||
{ hash: sha256 }
|
||||
)
|
||||
return data.data.Credential
|
||||
}
|
||||
|
@@ -47,6 +47,22 @@ export default function WebLoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理全局键盘事件
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !isLoading) {
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [tokenValue, isLoading]) // 依赖项包含用于登录的状态
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
onSubmit()
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.7.31",
|
||||
"version": "4.7.36",
|
||||
"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",
|
||||
|
@@ -115,7 +115,7 @@ async function tryDownload(options: string | HttpDownloadOptions, useReferer: bo
|
||||
if (useReferer && !headers['Referer']) {
|
||||
headers['Referer'] = url;
|
||||
}
|
||||
const fetchRes = await fetch(url, { headers }).catch((err) => {
|
||||
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
|
||||
if (err.cause) {
|
||||
throw err.cause;
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.7.31';
|
||||
export const napCatVersion = '4.7.36';
|
||||
|
@@ -86,4 +86,31 @@ export class NTQQFriendApi {
|
||||
accept,
|
||||
});
|
||||
}
|
||||
async handleDoubtFriendRequest(friendUid: string, str1: string = '', str2: string = '') {
|
||||
this.context.session.getBuddyService().approvalDoubtBuddyReq(friendUid, str1, str2);
|
||||
}
|
||||
async getDoubtFriendRequest(count: number) {
|
||||
let date = Date.now().toString();
|
||||
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelBuddyService/getDoubtBuddyReq',
|
||||
'NodeIKernelBuddyListener/onDoubtBuddyReqChange',
|
||||
[date, count, ''],
|
||||
() => true,
|
||||
(data) => data.reqId === date
|
||||
);
|
||||
let requests = Promise.all(ret.doubtList.map(async (item) => {
|
||||
return {
|
||||
flag: item.uid, //注意强制String 非isNumeric 不遵守则不符合设计
|
||||
uin: await this.core.apis.UserApi.getUinByUidV2(item.uid) ?? 0,// 信息字段
|
||||
nick: item.nick, // 信息字段 这个不是nickname 可能是来源的群内的昵称
|
||||
source: item.source, // 信息字段
|
||||
reason: item.reason, // 信息字段
|
||||
msg: item.msg, // 信息字段
|
||||
group_code: item.groupCode, // 信息字段
|
||||
time: item.reqTime, // 信息字段
|
||||
type: 'doubt' //保留字段
|
||||
};
|
||||
}))
|
||||
return requests;
|
||||
}
|
||||
}
|
||||
|
16
src/core/external/appid.json
vendored
16
src/core/external/appid.json
vendored
@@ -258,5 +258,21 @@
|
||||
"3.2.17-34467": {
|
||||
"appid": 537282292,
|
||||
"qua": "V1_LNX_NQ_3.2.17_34467_GW_B"
|
||||
},
|
||||
"9.9.19-34566": {
|
||||
"appid": 537282307,
|
||||
"qua": "V1_WIN_NQ_9.9.19_34566_GW_B"
|
||||
},
|
||||
"3.2.17-34566": {
|
||||
"appid": 537282343,
|
||||
"qua": "V1_LNX_NQ_3.2.17_34566_GW_B"
|
||||
},
|
||||
"3.2.17-34606": {
|
||||
"appid": 537282343,
|
||||
"qua": "V1_LNX_NQ_3.2.17_34606_GW_B"
|
||||
},
|
||||
"9.9.19-34606": {
|
||||
"appid": 537282307,
|
||||
"qua": "V1_WIN_NQ_9.9.19_34606_GW_B"
|
||||
}
|
||||
}
|
18
src/core/external/offset.json
vendored
18
src/core/external/offset.json
vendored
@@ -327,12 +327,28 @@
|
||||
"send": "770CDC0",
|
||||
"recv": "77106F0"
|
||||
},
|
||||
"9.9.19-34362-x64":{
|
||||
"9.9.19-34362-x64": {
|
||||
"send": "3BD80D0",
|
||||
"recv": "3BDC8D0"
|
||||
},
|
||||
"9.9.19-34467-x64": {
|
||||
"send": "3BD8690",
|
||||
"recv": "3BDCE90"
|
||||
},
|
||||
"9.9.19-34566-x64": {
|
||||
"send": "3BDA110",
|
||||
"recv": "3BDE910"
|
||||
},
|
||||
"9.9.19-34606-x64": {
|
||||
"send": "3BDA110",
|
||||
"recv": "3BDE910"
|
||||
},
|
||||
"3.2.17-34566-x64": {
|
||||
"send": "AD7DC60",
|
||||
"recv": "AD81680"
|
||||
},
|
||||
"3.2.17-34606-arm64": {
|
||||
"send": "7711270",
|
||||
"recv": "7714BA0"
|
||||
}
|
||||
}
|
@@ -40,12 +40,30 @@ export class NodeIKernelBuddyListener {
|
||||
}
|
||||
|
||||
onDelBatchBuddyInfos(arg: unknown): any {
|
||||
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
|
||||
}
|
||||
|
||||
onDoubtBuddyReqChange(arg: unknown): any {
|
||||
onDoubtBuddyReqChange(_arg:
|
||||
{
|
||||
reqId: string;
|
||||
cookie: string;
|
||||
doubtList: Array<{
|
||||
uid: string;
|
||||
nick: string;
|
||||
age: number,
|
||||
sex: number;
|
||||
commFriendNum: number;
|
||||
reqTime: string;
|
||||
msg: string;
|
||||
source: string;
|
||||
reason: string;
|
||||
groupCode: string;
|
||||
nameMore?: null;
|
||||
}>;
|
||||
}): void | Promise<void> {
|
||||
}
|
||||
|
||||
onDoubtBuddyReqUnreadNumChange(arg: unknown): any {
|
||||
onDoubtBuddyReqUnreadNumChange(_num: number): void | Promise<void> {
|
||||
}
|
||||
|
||||
onNickUpdated(arg: unknown): any {
|
||||
|
@@ -106,15 +106,15 @@ export interface NodeIKernelBuddyService {
|
||||
|
||||
getAddMeSetting(): unknown;
|
||||
|
||||
getDoubtBuddyReq(): unknown;
|
||||
getDoubtBuddyReq(reqId: string, num: number,uk:string): Promise<GeneralCallResult>;
|
||||
|
||||
getDoubtBuddyUnreadNum(): number;
|
||||
|
||||
approvalDoubtBuddyReq(uid: number, isAgree: boolean): void;
|
||||
approvalDoubtBuddyReq(uid: string, str1: string, str2: string): void;
|
||||
|
||||
delDoubtBuddyReq(uid: number): void;
|
||||
|
||||
delAllDoubtBuddyReq(): void;
|
||||
delAllDoubtBuddyReq(): Promise<GeneralCallResult>;
|
||||
|
||||
reportDoubtBuddyReqUnread(): void;
|
||||
|
||||
|
@@ -38,13 +38,15 @@ export async function NCoreInitFramework(
|
||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
||||
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
|
||||
if (reset && path) {
|
||||
FFmpegService.setFfmpegPath(path,logger);
|
||||
}
|
||||
}).catch(e => {
|
||||
logger.logError('[Ffmpeg] Error:', e);
|
||||
});
|
||||
if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
|
||||
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
|
||||
if (reset && path) {
|
||||
FFmpegService.setFfmpegPath(path, logger);
|
||||
}
|
||||
}).catch(e => {
|
||||
logger.logError('[Ffmpeg] Error:', e);
|
||||
});
|
||||
}
|
||||
//直到登录成功后,执行下一步
|
||||
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
|
||||
const loginListener = new NodeIKernelLoginListener();
|
||||
|
@@ -38,6 +38,7 @@ export default class GoCQHTTPUploadGroupFile extends OneBotAction<Payload, null>
|
||||
deleteAfterSentFiles: []
|
||||
};
|
||||
const sendFileEle = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, payload.folder ?? payload.folder_id);
|
||||
msgContext.deleteAfterSentFiles.push(downloadResult.path);
|
||||
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], msgContext.deleteAfterSentFiles);
|
||||
return null;
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction<Payload, nul
|
||||
if (payload.user_id) {
|
||||
const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
if (!peerUid) {
|
||||
throw new Error( `私聊${payload.user_id}不存在`);
|
||||
throw new Error(`私聊${payload.user_id}不存在`);
|
||||
}
|
||||
const isBuddy = await this.core.apis.FriendApi.isBuddy(peerUid);
|
||||
return { chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid };
|
||||
@@ -48,6 +48,7 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction<Payload, nul
|
||||
deleteAfterSentFiles: []
|
||||
};
|
||||
const sendFileEle: SendFileElement = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name);
|
||||
msgContext.deleteAfterSentFiles.push(downloadResult.path);
|
||||
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], msgContext.deleteAfterSentFiles);
|
||||
return null;
|
||||
}
|
||||
|
@@ -115,10 +115,16 @@ import { RenameGroupFile } from './extends/RenameGroupFile';
|
||||
import { GetRkeyServer } from './packet/GetRkeyServer';
|
||||
import { GetRkeyEx } from './packet/GetRkeyEx';
|
||||
import { CleanCache } from './system/CleanCache';
|
||||
import SetFriendRemark from './user/SetFriendRemark';
|
||||
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
|
||||
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
|
||||
|
||||
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||
|
||||
const actionHandlers = [
|
||||
new SetDoubtFriendsAddRequest(obContext, core),
|
||||
new GetDoubtFriendsAddRequest(obContext, core),
|
||||
new SetFriendRemark(obContext, core),
|
||||
new GetRkeyEx(obContext, core),
|
||||
new GetRkeyServer(obContext, core),
|
||||
new SetGroupRemark(obContext, core),
|
||||
|
@@ -38,7 +38,7 @@ export function normalize(message: OB11MessageMixType, autoEscape = false): OB11
|
||||
|
||||
export async function createContext(core: NapCatCore, payload: OB11PostContext | undefined, contextMode: ContextMode = ContextMode.Normal): Promise<Peer> {
|
||||
if (!payload) {
|
||||
throw new Error('请指定 group_id 或 user_id');
|
||||
throw new Error('请传递请求内容');
|
||||
}
|
||||
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
|
||||
return {
|
||||
@@ -48,7 +48,16 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext |
|
||||
}
|
||||
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
|
||||
const Uid = await core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
if (!Uid) throw new Error('无法获取用户信息');
|
||||
if (!Uid) {
|
||||
if (payload.group_id) {
|
||||
return {
|
||||
chatType: ChatType.KCHATTYPEGROUP,
|
||||
peerUid: payload.group_id.toString(),
|
||||
guildId: ''
|
||||
}
|
||||
}
|
||||
throw new Error('无法获取用户信息');
|
||||
}
|
||||
const isBuddy = await core.apis.FriendApi.isBuddy(Uid);
|
||||
if (!isBuddy) {
|
||||
const ret = await core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, Uid);
|
||||
@@ -78,7 +87,13 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext |
|
||||
guildId: '',
|
||||
};
|
||||
}
|
||||
throw new Error('请指定 group_id 或 user_id');
|
||||
if (contextMode === ContextMode.Private && payload.group_id) {
|
||||
throw new Error('当前私聊发送,请指定 user_id 而不是 group_id');
|
||||
}
|
||||
if (contextMode === ContextMode.Group && payload.user_id) {
|
||||
throw new Error('当前群聊发送,请指定 group_id 而不是 user_id');
|
||||
}
|
||||
throw new Error('请指定正确的 group_id 或 user_id');
|
||||
}
|
||||
|
||||
function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
|
||||
|
18
src/onebot/action/new/GetDoubtFriendsAddRequest.ts
Normal file
18
src/onebot/action/new/GetDoubtFriendsAddRequest.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
count: Type.Number({ default: 50 }),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class GetDoubtFriendsAddRequest extends OneBotAction<Payload, unknown> {
|
||||
override actionName = ActionName.GetDoubtFriendsAddRequest;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
return await this.core.apis.FriendApi.getDoubtFriendRequest(payload.count);
|
||||
}
|
||||
}
|
21
src/onebot/action/new/SetDoubtFriendsAddRequest.ts
Normal file
21
src/onebot/action/new/SetDoubtFriendsAddRequest.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
flag: Type.String(),
|
||||
//注意强制String 非isNumeric 不遵守则不符合设计
|
||||
approve: Type.Boolean({ default: true }),
|
||||
//该字段没有语义 仅做保留 强制为True
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class SetDoubtFriendsAddRequest extends OneBotAction<Payload, unknown> {
|
||||
override actionName = ActionName.SetDoubtFriendsAddRequest;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
return await this.core.apis.FriendApi.handleDoubtFriendRequest(payload.flag);
|
||||
}
|
||||
}
|
@@ -10,6 +10,10 @@ export interface InvalidCheckResult {
|
||||
}
|
||||
|
||||
export const ActionName = {
|
||||
// new extends 完全差异OneBot类别
|
||||
GetDoubtFriendsAddRequest: 'get_doubt_friends_add_request',
|
||||
SetDoubtFriendsAddRequest: 'set_doubt_friends_add_request',
|
||||
// napcat
|
||||
GetRkeyEx: 'get_rkey',
|
||||
GetRkeyServer: 'get_rkey_server',
|
||||
SetGroupRemark: 'set_group_remark',
|
||||
@@ -35,6 +39,7 @@ export const ActionName = {
|
||||
SetGroupLeave: 'set_group_leave',
|
||||
SetSpecialTitle: 'set_group_special_title',
|
||||
SetFriendAddRequest: 'set_friend_add_request',
|
||||
SetFriendRemark: 'set_friend_remark',
|
||||
SetGroupAddRequest: 'set_group_add_request',
|
||||
GetLoginInfo: 'get_login_info',
|
||||
GoCQHTTP_GetStrangerInfo: 'get_stranger_info',
|
||||
|
25
src/onebot/action/user/SetFriendRemark.ts
Normal file
25
src/onebot/action/user/SetFriendRemark.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
user_id: Type.String(),
|
||||
remark: Type.String()
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export default class SetFriendRemark extends OneBotAction<Payload, null> {
|
||||
override actionName = ActionName.SetFriendRemark;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload): Promise<null> {
|
||||
let friendUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id);
|
||||
let is_friend = await this.core.apis.FriendApi.isBuddy(friendUid);
|
||||
if (!is_friend) {
|
||||
throw new Error(`用户 ${payload.user_id} 不是好友`);
|
||||
}
|
||||
await this.core.apis.FriendApi.setBuddyRemark(friendUid, payload.remark);
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -250,7 +250,34 @@ export class OneBotGroupApi {
|
||||
'invite'
|
||||
);
|
||||
}
|
||||
|
||||
async parse51TypeEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
// 神经腾讯 没了妈妈想出来的
|
||||
// Warn 下面存在高并发危险
|
||||
if (grayTipElement.jsonGrayTipElement.jsonStr) {
|
||||
const json: {
|
||||
align: string,
|
||||
items: Array<{ txt: string, type: string }>
|
||||
} = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
if (json.items.length === 1 && json.items[0]?.txt.endsWith('加入群')) {
|
||||
let old_members = structuredClone(this.core.apis.GroupApi.groupMemberCache.get(msg.peerUid));
|
||||
if (!old_members) return;
|
||||
let new_members_map = await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
|
||||
if (!new_members_map) return;
|
||||
let new_members = Array.from(new_members_map.values());
|
||||
// 对比members查找新成员
|
||||
let new_member = new_members.find((member) => old_members.get(member.uid) == undefined);
|
||||
if (!new_member) return;
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
+msg.peerUid,
|
||||
+new_member.uin,
|
||||
0,
|
||||
'invite',
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
async parseGrayTipElement(msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
|
||||
// 解析群组事件 由sysmsg解析
|
||||
@@ -282,6 +309,9 @@ export class OneBotGroupApi {
|
||||
return await this.parsePaiYiPai(msg, grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
} else if (grayTipElement.jsonGrayTipElement.busiId == JsonGrayBusiId.AIO_GROUP_ESSENCE_MSG_TIP) {
|
||||
return await this.parseEssenceMsg(msg, grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
} else if (+(grayTipElement.jsonGrayTipElement.busiId ?? 0) == 51) {
|
||||
// 51是什么?{"align":"center","items":[{"txt":"下一秒起床通过王者荣耀加入群","type":"nor"}]
|
||||
return await this.parse51TypeEvent(msg, grayTipElement);
|
||||
} else {
|
||||
return await this.parseOtherJsonEvent(msg, grayTipElement.jsonGrayTipElement.jsonStr, this.core.context);
|
||||
}
|
||||
|
@@ -907,10 +907,10 @@ export class OneBotMsgApi {
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
|
||||
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode);
|
||||
resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话';
|
||||
resMsg.temp_source = resMsg.group_id;
|
||||
resMsg.temp_source = 0;
|
||||
} else {
|
||||
resMsg.group_id = 284840486;
|
||||
resMsg.temp_source = resMsg.group_id;
|
||||
resMsg.temp_source = 0;
|
||||
resMsg.sender.nickname = '临时会话';
|
||||
}
|
||||
}
|
||||
@@ -1105,6 +1105,8 @@ export class OneBotMsgApi {
|
||||
return 'kick';
|
||||
case 3:
|
||||
return 'kick_me';
|
||||
case 129:
|
||||
return 'disband';
|
||||
default:
|
||||
return 'kick';
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
import { NapCatCore } from '@/core';
|
||||
|
||||
export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me';
|
||||
export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me' | 'disband';
|
||||
|
||||
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_decrease';
|
||||
@@ -11,7 +11,7 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
|
||||
constructor(core: NapCatCore, groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
|
||||
super(core, groupId, userId);
|
||||
this.group_id = groupId;
|
||||
this.operator_id = operatorId;
|
||||
this.operator_id = operatorId;
|
||||
this.user_id = userId;
|
||||
this.sub_type = subType;
|
||||
}
|
||||
|
@@ -314,13 +314,15 @@ export async function NCoreInitShell() {
|
||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||
handleUncaughtExceptions(logger);
|
||||
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
|
||||
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
|
||||
if (reset && path) {
|
||||
FFmpegService.setFfmpegPath(path, logger);
|
||||
}
|
||||
}).catch(e => {
|
||||
logger.logError('[Ffmpeg] Error:', e);
|
||||
});
|
||||
if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
|
||||
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
|
||||
if (reset && path) {
|
||||
FFmpegService.setFfmpegPath(path, logger);
|
||||
}
|
||||
}).catch(e => {
|
||||
logger.logError('[Ffmpeg] Error:', e);
|
||||
});
|
||||
}
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { createServer as createHttpsServer } from 'https';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { NapCatPathWrapper } from '@/common/path';
|
||||
import { WebUiConfigWrapper } from '@webapi/helper/config';
|
||||
@@ -13,11 +14,10 @@ import { createUrl } from '@webapi/utils/url';
|
||||
import { sendError } from '@webapi/utils/response';
|
||||
import { join } from 'node:path';
|
||||
import { terminalManager } from '@webapi/terminal/terminal_manager';
|
||||
import multer from 'multer'; // 新增:引入multer用于错误捕获
|
||||
import multer from 'multer'; // 引入multer用于错误捕获
|
||||
|
||||
// 实例化Express
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
/**
|
||||
* 初始化并启动WebUI服务。
|
||||
* 该函数配置了Express服务器以支持JSON解析和静态文件服务,并监听6099端口。
|
||||
@@ -29,6 +29,7 @@ export let webUiPathWrapper: NapCatPathWrapper;
|
||||
const MAX_PORT_TRY = 100;
|
||||
import * as net from 'node:net';
|
||||
import { WebUiDataRuntime } from './src/helper/Data';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
export let webUiRuntimePort = 6099;
|
||||
export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number, string]> {
|
||||
try {
|
||||
@@ -40,7 +41,23 @@ export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string,
|
||||
return ['', 0, ''];
|
||||
}
|
||||
}
|
||||
async function checkCertificates(logger: LogWrapper): Promise<{ key: string, cert: string } | null> {
|
||||
try {
|
||||
const certPath = join(webUiPathWrapper.configPath, 'cert.pem');
|
||||
const keyPath = join(webUiPathWrapper.configPath, 'key.pem');
|
||||
|
||||
if (existsSync(certPath) && existsSync(keyPath)) {
|
||||
const cert = readFileSync(certPath, 'utf8');
|
||||
const key = readFileSync(keyPath, 'utf8');
|
||||
logger.log('[NapCat] [WebUi] 找到SSL证书,将启用HTTPS模式');
|
||||
return { cert, key };
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.log('[NapCat] [WebUi] 检查SSL证书时出错: ' + error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) {
|
||||
webUiPathWrapper = pathWrapper;
|
||||
WebUiConfig = new WebUiConfigWrapper();
|
||||
@@ -107,6 +124,9 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
// 挂载静态路由(前端),路径为 /webui
|
||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
||||
// 初始化WebSocket服务器
|
||||
const sslCerts = await checkCertificates(logger);
|
||||
const isHttps = !!sslCerts;
|
||||
let server = isHttps && sslCerts ? createHttpsServer(sslCerts, app) : createServer(app);
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
terminalManager.initialize(request, socket, head, logger);
|
||||
});
|
||||
|
@@ -20,25 +20,26 @@ export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => {
|
||||
export const LoginHandler: RequestHandler = async (req, res) => {
|
||||
// 获取WebUI配置
|
||||
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
|
||||
// 获取请求体中的token
|
||||
const { token } = req.body;
|
||||
// 获取请求体中的hash
|
||||
const { hash } = req.body;
|
||||
// 获取客户端IP
|
||||
const clientIP = req.ip || req.socket.remoteAddress || '';
|
||||
|
||||
// 如果token为空,返回错误信息
|
||||
if (isEmpty(token)) {
|
||||
if (isEmpty(hash)) {
|
||||
return sendError(res, 'token is empty');
|
||||
}
|
||||
// 检查登录频率
|
||||
if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) {
|
||||
return sendError(res, 'login rate limit');
|
||||
}
|
||||
//验证config.token是否等于token
|
||||
if (WebUiConfigData.token !== token) {
|
||||
//验证config.token hash是否等于token hash
|
||||
if (!AuthHelper.comparePasswordHash(WebUiConfigData.token, hash)) {
|
||||
return sendError(res, 'token is invalid');
|
||||
}
|
||||
|
||||
// 签发凭证
|
||||
const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(WebUiConfigData.token))).toString(
|
||||
const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(hash))).toString(
|
||||
'base64'
|
||||
);
|
||||
// 返回成功信息
|
||||
|
@@ -5,13 +5,13 @@ export class AuthHelper {
|
||||
|
||||
/**
|
||||
* 签名凭证方法。
|
||||
* @param token 待签名的凭证字符串。
|
||||
* @param hash 待签名的凭证字符串。
|
||||
* @returns 签名后的凭证对象。
|
||||
*/
|
||||
public static signCredential(token: string): WebUiCredentialJson {
|
||||
public static signCredential(hash: string): WebUiCredentialJson {
|
||||
const innerJson: WebUiCredentialInnerJson = {
|
||||
CreatedTime: Date.now(),
|
||||
TokenEncoded: token,
|
||||
HashEncoded: hash,
|
||||
};
|
||||
const jsonString = JSON.stringify(innerJson);
|
||||
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
|
||||
@@ -57,8 +57,7 @@ export class AuthHelper {
|
||||
const currentTime = Date.now() / 1000;
|
||||
const createdTime = credentialJson.Data.CreatedTime;
|
||||
const timeDifference = currentTime - createdTime;
|
||||
|
||||
return timeDifference <= 3600 && credentialJson.Data.TokenEncoded === token;
|
||||
return timeDifference <= 3600 && credentialJson.Data.HashEncoded === AuthHelper.generatePasswordHash(token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,4 +84,23 @@ export class AuthHelper {
|
||||
|
||||
return store.exists(`revoked:${hmac}`) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密码Hash
|
||||
* @param password 密码
|
||||
* @returns 生成的Hash值
|
||||
*/
|
||||
public static generatePasswordHash(password: string): string {
|
||||
return crypto.createHash('sha256').update(password + '.napcat').digest().toString('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比密码和Hash值
|
||||
* @param password 密码
|
||||
* @param hash Hash值
|
||||
* @returns 布尔值,表示密码是否匹配Hash值
|
||||
*/
|
||||
public static comparePasswordHash(password: string, hash: string): boolean {
|
||||
return this.generatePasswordHash(password) === hash;
|
||||
}
|
||||
}
|
||||
|
@@ -21,17 +21,18 @@ export async function auth(req: Request, res: Response, next: NextFunction) {
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
||||
// 获取token
|
||||
const token = authorization[1];
|
||||
const hash = authorization[1];
|
||||
if(!hash) return sendError(res, 'Unauthorized');
|
||||
// 解析token
|
||||
let Credential: WebUiCredentialJson;
|
||||
try {
|
||||
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
||||
Credential = JSON.parse(Buffer.from(hash, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
||||
// 获取配置
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
// 验证凭证在1小时内有效且token与原始token相同
|
||||
// 验证凭证在1小时内有效
|
||||
const credentialJson = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
||||
if (credentialJson) {
|
||||
// 通过验证
|
||||
|
2
src/webui/src/types/sign_token.d.ts
vendored
2
src/webui/src/types/sign_token.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
interface WebUiCredentialInnerJson {
|
||||
CreatedTime: number;
|
||||
TokenEncoded: string;
|
||||
HashEncoded: string;
|
||||
}
|
||||
|
||||
interface WebUiCredentialJson {
|
||||
|
Reference in New Issue
Block a user