fix: send multi forward msg

This commit is contained in:
linyuchen 2024-02-19 21:48:50 +08:00
parent 9b8b9a203c
commit 1938eef746
16 changed files with 500 additions and 203 deletions

View File

@ -1,3 +1,5 @@
import {Peer} from "../ntqqapi/ntcall";
export const CHANNEL_GET_CONFIG = "llonebot_get_config" export const CHANNEL_GET_CONFIG = "llonebot_get_config"
export const CHANNEL_SET_CONFIG = "llonebot_set_config" export const CHANNEL_SET_CONFIG = "llonebot_set_config"
export const CHANNEL_LOG = "llonebot_log" export const CHANNEL_LOG = "llonebot_log"

View File

@ -2,8 +2,6 @@ import * as path from "path";
import {selfInfo} from "./data"; import {selfInfo} from "./data";
import {ConfigUtil} from "./config"; import {ConfigUtil} from "./config";
import util from "util"; import util from "util";
import { sendLog } from '../main/ipcsend';
const fs = require('fs'); const fs = require('fs');
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
@ -94,3 +92,6 @@ export async function file2base64(path: string){
} }
return result; return result;
} }
export const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms))

9
src/global.d.ts vendored
View File

@ -1,15 +1,10 @@
import { Config } from "./common/types"; import {LLOneBot} from "./preload";
declare var llonebot: {
log(data: any): void,
setConfig(config: Config):void;
getConfig():Promise<Config>;
};
declare global { declare global {
interface Window { interface Window {
llonebot: typeof llonebot; llonebot: LLOneBot;
LiteLoader: any; LiteLoader: any;
} }
} }

View File

@ -1,18 +1,12 @@
import {webContents} from 'electron'; import {webContents} from 'electron';
import { CHANNEL_LOG } from '../common/channels';
function sendIPCMsg(channel: string, data: any) {
function sendIPCMsg(channel: string, ...data: any) {
let contents = webContents.getAllWebContents(); let contents = webContents.getAllWebContents();
for (const content of contents) { for (const content of contents) {
try { try {
content.send(channel, ...data) content.send(channel, data)
} catch (e) { } catch (e) {
console.log("llonebot send ipc msg to render error:", e) console.log("llonebot send ipc msg to render error:", e)
} }
} }
} }
export function sendLog(...args){
sendIPCMsg(CHANNEL_LOG, ...args)
}

View File

@ -7,7 +7,7 @@ import { CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG, } from "../common/
import { postMsg, setToken, startHTTPServer, startWSServer } from "../onebot11/server"; import { postMsg, setToken, startHTTPServer, startWSServer } from "../onebot11/server";
import { CONFIG_DIR, getConfigUtil, log } from "../common/utils"; import { CONFIG_DIR, getConfigUtil, log } from "../common/utils";
import { addHistoryMsg, getGroupMember, msgHistory, selfInfo, uidMaps } from "../common/data"; import { addHistoryMsg, getGroupMember, msgHistory, selfInfo, uidMaps } from "../common/data";
import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook";
import { OB11Constructor } from "../onebot11/constructor"; import { OB11Constructor } from "../onebot11/constructor";
import { NTQQApi } from "../ntqqapi/ntcall"; import { NTQQApi } from "../ntqqapi/ntcall";
import { ChatType, RawMessage } from "../ntqqapi/types"; import { ChatType, RawMessage } from "../ntqqapi/types";
@ -20,10 +20,6 @@ let running = false;
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log("llonebot main onLoad"); log("llonebot main onLoad");
// const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data;
if (!fs.existsSync(CONFIG_DIR)) { if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true}); fs.mkdirSync(CONFIG_DIR, {recursive: true});
} }
@ -52,7 +48,7 @@ function onLoad() {
function postRawMsg(msgList: RawMessage[]) { function postRawMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
log("收到新消息", message) // log("收到新消息", message)
message.msgShortId = msgHistory[message.msgId]?.msgShortId message.msgShortId = msgHistory[message.msgId]?.msgShortId
if (!message.msgShortId) { if (!message.msgShortId) {
addHistoryMsg(message) addHistoryMsg(message)
@ -82,8 +78,8 @@ function onLoad() {
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("message update", message, message.sendStatus) // log("message update", message.sendStatus, message)
if (message.sendStatus === 2) { if (message.recallTime != "0") {
// 撤回消息上报 // 撤回消息上报
const oriMessage = msgHistory[message.msgId] const oriMessage = msgHistory[message.msgId]
if (!oriMessage) { if (!oriMessage) {
@ -166,6 +162,7 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
try { try {
hookNTQQApiCall(window);
hookNTQQApiReceive(window); hookNTQQApiReceive(window);
} catch (e) { } catch (e) {
log("LLOneBot hook error: ", e.toString()) log("LLOneBot hook error: ", e.toString())

View File

@ -1,8 +1,7 @@
import { BrowserWindow } from 'electron'; import { BrowserWindow } from 'electron';
import { getConfigUtil, log } from "../common/utils"; import { log } from "../common/utils";
import { NTQQApi, NTQQApiClass, sendMessagePool } from "./ntcall"; import { NTQQApi, NTQQApiClass, sendMessagePool } from "./ntcall";
import { Group, User } from "./types"; import { Group, RawMessage, User } from "./types";
import { RawMessage } from "./types";
import { addHistoryMsg, friends, groups, msgHistory } from "../common/data"; import { addHistoryMsg, friends, groups, msgHistory } from "../common/data";
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -51,7 +50,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
let _ = hook.hookFunc(receiveData.payload) let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction"){ if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then() (_ as Promise<void>).then()
} }
} catch (e) { } catch (e) {
@ -78,6 +77,24 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
window.webContents.send = patchSend; window.webContents.send = patchSend;
} }
export function hookNTQQApiCall(window: BrowserWindow) {
// 监听调用NTQQApi
let webContents = window.webContents as any;
const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"];
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
log("call NTQQ api", thisArg, args);
return target.apply(thisArg, args);
},
});
// if (webContents._events["-ipc-message"]?.[0]) {
// webContents._events["-ipc-message"][0] = proxyIpcMsg;
// } else {
// webContents._events["-ipc-message"] = proxyIpcMsg;
// }
}
export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string { export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
const id = uuidv4() const id = uuidv4()
receiveHooks.push({ receiveHooks.push({
@ -133,7 +150,6 @@ registerReceiveHook<{
// }) // })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message) // log("收到新消息push到历史记录", message)

View File

@ -1,13 +1,10 @@
import { ipcMain } from "electron"; import {ipcMain} from "electron";
import { v4 as uuidv4 } from "uuid"; import {v4 as uuidv4} from "uuid";
import { ReceiveCmd, hookApiCallbacks, registerReceiveHook, removeReceiveHook } from "./hook"; import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import { log } from "../common/utils"; import {log} from "../common/utils";
import { ChatType, Friend, PicElement, SelfInfo, User } from "./types"; import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types";
import { Group } from "./types";
import { GroupMember } from "./types";
import { RawMessage } from "./types";
import { SendMessageElement } from "./types";
import * as fs from "fs"; import * as fs from "fs";
import {addHistoryMsg, msgHistory, selfInfo} from "../common/data";
interface IPCReceiveEvent { interface IPCReceiveEvent {
eventName: string eventName: string
@ -29,7 +26,6 @@ export enum NTQQApiClass {
export enum NTQQApiMethod { export enum NTQQApiMethod {
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
SELF_INFO = "fetchAuthData", SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList", FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUPS = "nodeIKernelGroupService/getGroupList", GROUPS = "nodeIKernelGroupService/getGroupList",
@ -44,7 +40,8 @@ export enum NTQQApiMethod {
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild", MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg", RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg", SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia" DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment" // 合并转发
} }
enum NTQQApiChannel { enum NTQQApiChannel {
@ -64,8 +61,24 @@ enum CallBackType {
METHOD METHOD
} }
interface NTQQApiParams {
methodName: NTQQApiMethod,
className?: NTQQApiClass,
channel?: NTQQApiChannel,
args?: unknown[],
cbCmd?: ReceiveCmd | null
timeoutSecond?: number,
}
function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClass, methodName: NTQQApiMethod, args: unknown[] = [], cbCmd: ReceiveCmd | null = null, timeout = 5) { function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let {
className, methodName, channel, args,
cbCmd, timeoutSecond: timeout
} = params;
className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2;
args = args ?? [];
timeout = timeout ?? 5;
const uuid = uuidv4(); const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid) // log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
@ -102,16 +115,18 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`) reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`)
} }
}, _timeout) }, _timeout)
const eventName = className + "-" + channel[channel.length - 1];
const apiArgs = [methodName, ...args]
ipcMain.emit( ipcMain.emit(
channel, channel,
{}, {},
{type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1]}, {type: 'request', callbackId: uuid, eventName},
[methodName, ...args], apiArgs
) )
}) })
} }
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult { interface GeneralCallResult {
@ -123,34 +138,49 @@ interface GeneralCallResult {
export class NTQQApi { export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND) // static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static likeFriend(uid: string, count = 1) { static likeFriend(uid: string, count = 1) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND, [{ return callNTQQApi({
doLikeUserInfo: { methodName: NTQQApiMethod.LIKE_FRIEND,
friendUid: uid, args: [{
sourceId: 71, doLikeUserInfo: {
doLikeCount: count, friendUid: uid,
doLikeTollCount: 0 sourceId: 71,
} doLikeCount: count,
}, doLikeTollCount: 0
null]) }
}, null]
})
} }
static getSelfInfo() { static getSelfInfo() {
return callNTQQApi<SelfInfo>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.GLOBAL_DATA, NTQQApiMethod.SELF_INFO, [], null, 2) return callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
} }
static async getUserInfo(uid: string) { static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ const result = await callNTQQApi<{ profiles: Map<string, User> }>({
profiles: Map<string, User> methodName: NTQQApiMethod.USER_INFO,
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO, args: [{force: true, uids: [uid]}, undefined],
[{force: true, uids: [uid]}, undefined], ReceiveCmd.USER_INFO) cbCmd: ReceiveCmd.USER_INFO
})
return result.profiles.get(uid) return result.profiles.get(uid)
} }
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: Friend[] }[] data: {
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.FRIENDS, [{force_update: forced}, undefined], ReceiveCmd.FRIENDS) categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS
})
let _friends: Friend[] = []; let _friends: Friend[] = [];
for (const fData of data.data) { for (const fData of data.data) {
_friends.push(...fData.buddyList) _friends.push(...fData.buddyList)
@ -166,26 +196,31 @@ export class NTQQApi {
const result = await callNTQQApi<{ const result = await callNTQQApi<{
updateType: number, updateType: number,
groupList: Group[] groupList: Group[]
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUPS, [{force_update: forced}, undefined], cbCmd) }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList return result.groupList
} }
static async getGroupMembers(groupQQ: string, num = 3000) { static async getGroupMembers(groupQQ: string, num = 3000) {
const sceneId = await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBER_SCENE, [{ const sceneId = await callNTQQApi({
groupCode: groupQQ, methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
scene: "groupMemberList_MainWindow" args: [{
}]) groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId); // log("get group member sceneId", sceneId);
try { try {
const result = await callNTQQApi<{ const result = await callNTQQApi<{
result: { infos: any } result: { infos: any }
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBERS, }>({
[{ methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId, sceneId: sceneId,
num: num num: num
}, },
null null
]) ]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos)) // log("members info", typeof result.result.infos, Object.keys(result.result.infos))
let values = result.result.infos.values() let values = result.result.infos.values()
@ -200,31 +235,38 @@ export class NTQQApi {
static getFileType(filePath: string) { static getFileType(filePath: string) {
return callNTQQApi<{ return callNTQQApi<{ ext: string }>({
ext: string className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath]
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_TYPE, [filePath]) })
} }
static getFileMd5(filePath: string) { static getFileMd5(filePath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_MD5, [filePath]) return callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath]
})
} }
static copyFile(filePath: string, destPath: string) { static copyFile(filePath: string, destPath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_COPY, [{ return callNTQQApi<string>({
fromPath: filePath, className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_COPY, args: [{
toPath: destPath fromPath: filePath,
}]) toPath: destPath
}]
})
} }
static getImageSize(filePath: string) { static getImageSize(filePath: string) {
return callNTQQApi<{ return callNTQQApi<{ width: number, height: number }>({
width: number, className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath]
height: number })
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.IMAGE_SIZE, [filePath])
} }
static getFileSize(filePath: string) { static getFileSize(filePath: string) {
return callNTQQApi<number>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_SIZE, [filePath]) return callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath]
})
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
@ -233,23 +275,25 @@ export class NTQQApi {
let ext = (await NTQQApi.getFileType(filePath))?.ext let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
ext = "." + ext ext = "." + ext
} } else {
else{
ext = "" ext = ""
} }
const fileName = `${md5}${ext}`; const fileName = `${md5}${ext}`;
const mediaPath = await callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.MEDIA_FILE_PATH, [{ const mediaPath = await callNTQQApi<string>({
path_info: { methodName: NTQQApiMethod.MEDIA_FILE_PATH,
md5HexStr: md5, args: [{
fileName: fileName, path_info: {
elementType: 2, md5HexStr: md5,
elementSubType: 0, fileName: fileName,
thumbSize: 0, elementType: 2,
needCreate: true, elementSubType: 0,
downloadType: 1, thumbSize: 0,
file_uuid: "" needCreate: true,
} downloadType: 1,
}]) file_uuid: ""
}
}]
})
log("media path", mediaPath) log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath); await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath); const fileSize = await NTQQApi.getFileSize(filePath);
@ -280,28 +324,32 @@ export class NTQQApi {
}, },
undefined, undefined,
] ]
await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.DOWNLOAD_MEDIA, apiParams) await callNTQQApi({methodName: NTQQApiMethod.DOWNLOAD_MEDIA, args: apiParams})
return sourcePath return sourcePath
} }
static recallMsg(peer: Peer, msgIds: string[]) { static recallMsg(peer: Peer, msgIds: string[]) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.RECALL_MSG, [{ return callNTQQApi({
peer, methodName: NTQQApiMethod.RECALL_MSG, args: [{
msgIds peer,
}, null]) msgIds
}, null]
})
} }
static sendMsg(peer: Peer, msgElements: SendMessageElement[]) { static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false) {
const sendTimeout = 10 * 1000 const sendTimeout = 10 * 1000
return new Promise<RawMessage>((resolve, reject) => { return new Promise<RawMessage>((resolve, reject) => {
const peerUid = peer.peerUid; const peerUid = peer.peerUid;
let usingTime = 0; let usingTime = 0;
let success = false; let success = false;
let isTimeout = false;
const checkSuccess = () => { const checkSuccess = () => {
if (!success) { if (!success) {
sendMessagePool[peerUid] = null; sendMessagePool[peerUid] = null;
isTimeout = true;
reject("发送超时") reject("发送超时")
} }
} }
@ -311,29 +359,99 @@ export class NTQQApi {
let lastSending = sendMessagePool[peerUid] let lastSending = sendMessagePool[peerUid]
if (sendTimeout < usingTime) { if (sendTimeout < usingTime) {
sendMessagePool[peerUid] = null; sendMessagePool[peerUid] = null;
isTimeout = true;
reject("发送超时") reject("发送超时")
} }
if (!!lastSending) { if (!!lastSending) {
// log("有正在发送的消息,等待中...") // log("有正在发送的消息,等待中...")
usingTime += 100; usingTime += 500;
setTimeout(checkLastSend, 100); setTimeout(checkLastSend, 500);
} else { } else {
log("可以进行发送消息,设置发送成功回调", sendMessagePool) log("可以进行发送消息,设置发送成功回调", sendMessagePool)
sendMessagePool[peerUid] = (rawMessage: RawMessage) => { sendMessagePool[peerUid] = (rawMessage: RawMessage) => {
success = true;
sendMessagePool[peerUid] = null; sendMessagePool[peerUid] = null;
resolve(rawMessage); const checkSendComplete = () => {
if (isTimeout) {
return reject("发送超时")
}
if (msgHistory[rawMessage.msgId]?.sendStatus == 2) {
success = true;
resolve(rawMessage);
} else {
setTimeout(checkSendComplete, 500)
}
}
if (waitComplete) {
checkSendComplete();
} else {
success = true;
resolve(rawMessage);
}
} }
} }
} }
checkLastSend() checkLastSend()
callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.SEND_MSG, [{ callNTQQApi({
msgId: "0", methodName: NTQQApiMethod.SEND_MSG,
peer, msgElements, args: [{
msgAttributeInfos: new Map(), msgId: "0",
}, null]).then() peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
}) })
} }
static multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
let msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: "LLOneBot"}
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
return new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject("转发消息超时");
}
}, 5000)
registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord;
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData);
if (forwardData.app != "com.tencent.multimsg") {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true;
addHistoryMsg(msg)
resolve(msg);
log("收到转发消息:", payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
}
} }

View File

@ -211,13 +211,15 @@ export interface RawMessage {
msgShortId?: number; // 自己维护的消息id msgShortId?: number; // 自己维护的消息id
msgTime: string; msgTime: string;
msgSeq: string; msgSeq: string;
senderUin: string; // 发送者QQ号 senderUid: string;
senderUin?: string; // 发送者QQ号
peerUid: string; // 群号 或者 QQ uid peerUid: string; // 群号 或者 QQ uid
peerUin: string; // 群号 或者 发送者QQ号 peerUin: string; // 群号 或者 发送者QQ号
sendNickName: string; sendNickName: string;
sendMemberName?: string; // 发送者群名片 sendMemberName?: string; // 发送者群名片
chatType: ChatType; chatType: ChatType;
sendStatus?: number; // 消息状态2是已撤回 sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string; // 撤回时间, "0"是没有撤回
elements: { elements: {
elementId: string, elementId: string,
replyElement: { replyElement: {

View File

@ -1,13 +1,14 @@
import { AtType, ChatType, Group, SendMessageElement } from "../../ntqqapi/types"; import {AtType, ChatType, Group, SendMessageElement} from "../../ntqqapi/types";
import { addHistoryMsg, friends, getGroup, getHistoryMsgByShortId, getStrangerByUin, } from "../../common/data"; import {addHistoryMsg, friends, getGroup, getHistoryMsgByShortId, getStrangerByUin, selfInfo,} from "../../common/data";
import { OB11MessageData, OB11MessageDataType, OB11PostSendMsg } from '../types'; import {OB11MessageData, OB11MessageDataType, OB11MessageNode, OB11PostSendMsg} from '../types';
import { NTQQApi, Peer } from "../../ntqqapi/ntcall"; import {NTQQApi, Peer} from "../../ntqqapi/ntcall";
import { SendMsgElementConstructor } from "../../ntqqapi/constructor"; import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
import { uri2local } from "../utils"; import {uri2local} from "../utils";
import { v4 as uuid4 } from 'uuid'; import {v4 as uuid4} from 'uuid';
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName, BaseCheckResult} from "./types";
import * as fs from "fs"; import * as fs from "fs";
import {log, sleep} from "../../common/utils";
export interface ReturnDataType { export interface ReturnDataType {
message_id: number message_id: number
@ -16,12 +17,26 @@ export interface ReturnDataType {
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload);
const fmNum = this.forwardMsgNum(payload)
if ( fmNum && fmNum != messages.length) {
return {
valid: false,
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
}
}
return {
valid: true,
}
}
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = { const peer: Peer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: "" peerUid: ""
} }
let deleteAfterSentFiles: string[] = []
let group: Group | undefined = undefined; let group: Group | undefined = undefined;
if (payload?.group_id) { if (payload?.group_id) {
group = await getGroup(payload.group_id.toString()) group = await getGroup(payload.group_id.toString())
@ -46,6 +61,26 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
peer.peerUid = tempUser.uid peer.peerUid = tempUser.uid
} }
} }
const messages = this.convertMessage2List(payload);
if (this.forwardMsgNum(payload)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw ("发送转发消息失败 " + e.toString())
}
}
// log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group)
try {
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw (e.toString())
}
}
private convertMessage2List(payload: OB11PostSendMsg) {
if (typeof payload.message === "string") { if (typeof payload.message === "string") {
payload.message = [{ payload.message = [{
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
@ -56,8 +91,62 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} else if (!Array.isArray(payload.message)) { } else if (!Array.isArray(payload.message)) {
payload.message = [payload.message] payload.message = [payload.message]
} }
const sendElements: SendMessageElement[] = [] return payload.message;
for (let sendMsg of payload.message) { }
private forwardMsgNum(payload: OB11PostSendMsg): number {
if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == OB11MessageDataType.node).length
}
return 0
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) {
const selfPeer: Peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
let nodeIds: string[] = []
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
nodeIds.push(nodeId)
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(messageNode.data.content, group)
try {
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
nodeIds.push(nodeMsg.msgId)
} catch (e) {
log("生效转发消息节点失败")
}
}
}
// 开发转发
try {
return await NTQQApi.multiForwardMsg(selfPeer, destPeer, nodeIds)
} catch (e) {
log("forward failed", e)
return null;
}
}
private async createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) {
let sendElements: SendMessageElement[] = []
let deleteAfterSentFiles: string[] = []
for (let sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue
}
switch (sendMsg.type) { switch (sendMsg.type) {
case OB11MessageDataType.text: { case OB11MessageDataType.text: {
const text = sendMsg.data?.text; const text = sendMsg.data?.text;
@ -116,19 +205,36 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
} }
break;
// case OB11MessageDataType.node: {
// try {
// await this.handleForwardNode(peer, sendMsg, group);
// } catch (e) {
// log("forward msg crash", e.stack)
// }
// }
} }
} }
// log("send msg:", peer, sendElements)
try { return {
const returnMsg = await NTQQApi.sendMsg(peer, sendElements) sendElements,
addHistoryMsg(returnMsg) deleteAfterSentFiles
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw (e.toString())
} }
} }
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete=false) {
if (!sendElements.length) {
throw ("消息体无法解析")
}
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete)
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return returnMsg
}
} }
export default SendMsg export default SendMsg

View File

@ -0,0 +1,34 @@
import { OB11Message } from '../types';
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import { NTQQApi, Peer } from "../../ntqqapi/ntcall";
import { ChatType } from "../../ntqqapi/types";
import { selfInfo } from "../../common/data";
import { SendMsgElementConstructor } from "../../ntqqapi/constructor";
import {sleep} from "../../common/utils";
export interface PayloadType {
message: string,
group_id: string
}
export default class TestForwardMsg extends BaseAction<PayloadType, OB11Message> {
actionName = ActionName.TestForwardMsg
protected async _handle(payload: PayloadType) {
// log("history msg ids", Object.keys(msgHistory));
const selfPeer: Peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
const sendMsg = await NTQQApi.sendMsg(selfPeer, [SendMsgElementConstructor.text(payload.message)])
const sendMsg2 = await NTQQApi.sendMsg(selfPeer, [SendMsgElementConstructor.text(payload.message)])
await NTQQApi.multiForwardMsg(
selfPeer,
{chatType: ChatType.group, peerUid: payload.group_id, guildId: ""},
[sendMsg.msgId, sendMsg2.msgId]
)
return null
}
}

View File

@ -13,8 +13,10 @@ import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord"; import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage"; import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus"; import GetStatus from "./GetStatus";
import TestForwardMsg from "./TestForwdMsg";
export const actionHandlers = [ export const actionHandlers = [
new TestForwardMsg(),
new GetMsg(), new GetMsg(),
new GetLoginInfo(), new GetLoginInfo(),
new GetFriendList(), new GetFriendList(),

View File

@ -14,6 +14,7 @@ export interface InvalidCheckResult {
} }
export enum ActionName{ export enum ActionName{
TestForwardMsg = "test_forward_msg",
GetLoginInfo = "get_login_info", GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list", GetFriendList = "get_friend_list",
GetGroupInfo = "get_group_info", GetGroupInfo = "get_group_info",

View File

@ -41,24 +41,28 @@ expressAPP.use((req, res, next) => {
}); });
const expressAuthorize = (req: Request, res: Response, next: () => void) => { const expressAuthorize = (req: Request, res: Response, next: () => void) => {
let token = "" try {
const authHeader = req.get("authorization") let token = ""
if (authHeader) { const authHeader = req.get("authorization")
token = authHeader.split("Bearer ").pop() if (authHeader) {
log("receive http header token", token) token = authHeader.split("Bearer ").pop()
} else if (req.query.access_token) { log("receive http header token", token)
if (Array.isArray(req.query.access_token)) { } else if (req.query.access_token) {
token = req.query.access_token[0].toString(); if (Array.isArray(req.query.access_token)) {
} else { token = req.query.access_token[0].toString();
token = req.query.access_token.toString(); } else {
token = req.query.access_token.toString();
}
log("receive http url token", token)
} }
log("receive http url token", token)
}
if (accessToken) { if (accessToken) {
if (token != accessToken) { if (token != accessToken) {
return res.status(403).send(JSON.stringify({message: 'token verify failed!'})); return res.status(403).send(JSON.stringify({message: 'token verify failed!'}));
}
} }
}catch (e) {
log("receive http failed", e.stack)
} }
next(); next();

View File

@ -71,19 +71,6 @@ export interface OB11Message {
raw?: RawMessage raw?: RawMessage
} }
export type OB11ApiName =
"send_msg"
| "send_private_msg"
| "send_group_msg"
| "get_group_list"
| "get_group_info"
| "get_friend_list"
| "delete_msg"
| "get_login_info"
| "get_group_member_list"
| "get_group_member_info"
| "get_msg"
export interface OB11Return<DataType> { export interface OB11Return<DataType> {
status: number status: number
retcode: number retcode: number
@ -102,45 +89,69 @@ export enum OB11MessageDataType {
at = "at", at = "at",
reply = "reply", reply = "reply",
json = "json", json = "json",
face = "face" face = "face",
node = "node" // 合并转发消息
} }
export type OB11MessageData = { export interface OB11MessageText {
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
content: string, data: {
data?: {
text: string, // 纯文本 text: string, // 纯文本
} }
} | { }
type: "image" | "voice" | "record",
file: string, // 本地路径 interface OB11MessageFileBase {
data?: {
file: string // 本地路径
}
} | {
type: OB11MessageDataType.at,
atType?: AtType,
content?: string,
atUid?: string,
atNtUid?: string,
data?: {
qq: string // at的qq号
}
} | {
type: OB11MessageDataType.reply,
msgId: string,
msgSeq: string,
senderUin: string,
data: { data: {
id: string, file: string
} }
} | { }
type: OB11MessageDataType.face,
export interface OB11MessageImage extends OB11MessageFileBase {
type: OB11MessageDataType.image
}
export interface OB11MessageRecord extends OB11MessageFileBase {
type: OB11MessageDataType.voice
}
export interface OB11MessageAt {
type: OB11MessageDataType.at
data: {
qq: string | "all"
}
}
export interface OB11MessageReply {
type: OB11MessageDataType.reply
data: { data: {
id: string id: string
} }
} }
export interface OB11MessageFace {
type: OB11MessageDataType.face
data: {
id: string
}
}
export interface OB11MessageNode {
type: OB11MessageDataType.node
data: {
id?: string
user_id?: number
nickname: string
content: OB11MessageData[]
}
}
export type OB11MessageData =
OB11MessageText |
OB11MessageFace |
OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord |
OB11MessageNode
export interface OB11PostSendMsg { export interface OB11PostSendMsg {
message_type?: "private" | "group" message_type?: "private" | "group"
user_id: string, user_id: string,

View File

@ -1,25 +1,24 @@
// Electron 主进程 与 渲染进程 交互的桥梁 // Electron 主进程 与 渲染进程 交互的桥梁
import {Config} from "./common/types"; import {Config} from "./common/types";
import { import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "./common/channels";
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SET_CONFIG,
} from "./common/channels";
const {contextBridge} = require("electron"); const {contextBridge} = require("electron");
const {ipcRenderer} = require('electron'); const {ipcRenderer} = require('electron');
// 在window对象下导出只读对象 const llonebot = {
contextBridge.exposeInMainWorld("llonebot", {
log: (data: any) => { log: (data: any) => {
ipcRenderer.send(CHANNEL_LOG, data); ipcRenderer.send(CHANNEL_LOG, data);
}, },
setConfig: (config: Config)=>{ setConfig: (config: Config) => {
ipcRenderer.send(CHANNEL_SET_CONFIG, config); ipcRenderer.send(CHANNEL_SET_CONFIG, config);
}, },
getConfig: async () => { getConfig: async () => {
return ipcRenderer.invoke(CHANNEL_GET_CONFIG); return ipcRenderer.invoke(CHANNEL_GET_CONFIG);
}, },
}); }
export type LLOneBot = typeof llonebot;
// 在window对象下导出只读对象
contextBridge.exposeInMainWorld("llonebot", llonebot);
;

View File

@ -67,7 +67,7 @@ async function onSettingWindowCreated(view: Element) {
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item">
<div> <div>
<div></div> <div></div>
<div class="tips"></div> <div class="tips"></div>
</div> </div>
<setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch> <setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
@ -165,6 +165,21 @@ async function onSettingWindowCreated(view: Element) {
} }
function init() {
let hash = location.hash;
if (hash === "#/blank") {
return;
}
}
if (location.hash === "#/blank") {
(window as any).navigation.addEventListener("navigatesuccess", init, {once: true});
} else {
init();
}
export { export {
onSettingWindowCreated onSettingWindowCreated
} }