Compare commits

..

11 Commits

Author SHA1 Message Date
linyuchen
9bb69058c2 fix: group msg subtype: normal 2024-02-14 12:58:29 +08:00
linyuchen
89971dd2e4 docs: update README 2024-02-14 01:38:46 +08:00
linyuchen
aea67db27c fix: report self sent message_id 2024-02-14 01:35:48 +08:00
linyuchen
c4b45f8298 ver: 3.0.5 2024-02-14 01:02:48 +08:00
linyuchen
1a77abfc62 fix: 发送回复消息多了个@符号 2024-02-14 01:01:54 +08:00
linyuchen
eb32ecb79b perf: 去掉多余日志 2024-02-14 00:37:53 +08:00
linyuchen
ccf91f4a94 fix: 消息重复上报 2024-02-14 00:35:36 +08:00
linyuchen
282b2a0da0 fix: message_id过长导致koishi对接失败
perf: 初始化卡顿优化
2024-02-13 21:17:16 +08:00
linyuchen
b28b812396 fix: file://中有中文无法正确解析 2024-02-13 19:56:02 +08:00
linyuchen
1936671cb3 fix: self nickname
fix: @member msg report
fix: send file:// on Windows
ver: 3.0.2
2024-02-13 18:37:01 +08:00
linyuchen
6a8d67a8ae fix: self nickname
fix: auto download receive image
fix: @member msg report
ver: 3.0.1
2024-02-13 13:12:16 +08:00
13 changed files with 295 additions and 215 deletions

View File

@@ -1,9 +1,11 @@
# LLOneBot API # LLOneBot API
将NTQQLiteLoaderAPI封装成OneBot11标准的API LiteLoaderQQNT的OneBot11协议插件
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档* *注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI*
## 安装方法 ## 安装方法
1.安装[LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) 1.安装[LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
@@ -16,7 +18,7 @@
## 支持的API ## 支持的API
目前只支持http协议POST方法不支持websocket事件上报也是http协议 目前只支持http协议不支持websocket事件上报也是http协议
主要功能: 主要功能:
- [x] 发送好友消息 - [x] 发送好友消息
@@ -46,6 +48,7 @@
- [x] send_private_msg - [x] send_private_msg
- [x] delete_msg - [x] delete_msg
- [x] get_group_list - [x] get_group_list
- [x] get_group_info
- [x] get_group_member_list - [x] get_group_member_list
- [x] get_group_member_info - [x] get_group_member_info
- [x] get_friend_list - [x] get_friend_list
@@ -67,7 +70,7 @@
<details> <details>
<summary>调用接口报404</summary> <summary>调用接口报404</summary>
<br/> <br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口并且所有接口都只支持POST方法调用GET方法会报404 目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
</details> </details>
<br/> <br/>
@@ -94,11 +97,10 @@
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [ ] 转发消息记录 - [ ] 转发消息记录
- [ ] 好友点赞api - [ ] 好友点赞api
- [ ] 支持websocket等个有缘人提PR实现 - [ ] 支持websocket等个有缘人提PR实现
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi", "description": "LiteLoaderQQNT的OneBotApi",
"version": "2.5.0", "version": "3.0.7",
"thumbnail": "./icon.png", "thumbnail": "./icon.png",
"authors": [{ "authors": [{
"name": "linyuchen", "name": "linyuchen",

View File

@@ -1,10 +1,31 @@
import { NTQQApi } from '../ntqqapi/ntcall'; import { NTQQApi } from '../ntqqapi/ntcall';
import { Friend, Group, RawMessage, SelfInfo } from "../ntqqapi/types"; import { Friend, Group, GroupMember, RawMessage, SelfInfo } from "../ntqqapi/types";
import { log } from "./utils";
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export let msgHistory: Record<string, RawMessage> = {} // msgId: RawMessage export let msgHistory: Record<string, RawMessage> = {} // msgId: RawMessage
let globalMsgId = Date.now()
export function addHistoryMsg(msg: RawMessage): boolean{
let existMsg = msgHistory[msg.msgId]
if (existMsg){
Object.assign(existMsg, msg)
msg.msgShortId = existMsg.msgShortId;
return false
}
msg.msgShortId = ++globalMsgId
msgHistory[msg.msgId] = msg
return true
}
export function getHistoryMsgByShortId(shortId: number | string){
// log("getHistoryMsgByShortId", shortId, Object.values(msgHistory).map(m=>m.msgShortId))
return Object.values(msgHistory).find(msg => msg.msgShortId.toString() == shortId.toString())
}
export async function getFriend(qq: string): Promise<Friend | undefined> { export async function getFriend(qq: string): Promise<Friend | undefined> {
let friend = friends.find(friend => friend.uin === qq) let friend = friends.find(friend => friend.uin === qq)
// if (!friend){ // if (!friend){
@@ -23,16 +44,23 @@ export async function getGroup(qq: string): Promise<Group | undefined> {
return group return group
} }
export async function getGroupMember(groupQQ: string, memberQQ: string) { export async function getGroupMember(groupQQ: string, memberQQ: string=null, memberUid: string=null) {
const group = await getGroup(groupQQ) const group = await getGroup(groupQQ)
if (group) { if (group) {
let member = group.members?.find(member => member.uin === memberQQ) let filterFunc: (member: GroupMember) => boolean
if (memberQQ){
filterFunc = member => member.uin === memberQQ
}
else if (memberUid){
filterFunc = member => member.uid === memberUid
}
let member = group.members?.find(filterFunc)
if (!member){ if (!member){
const _members = await NTQQApi.getGroupMembers(groupQQ) const _members = await NTQQApi.getGroupMembers(groupQQ)
if (_members.length){ if (_members.length){
group.members = _members group.members = _members
} }
member = group.members?.find(member => member.uin === memberQQ) member = group.members?.find(filterFunc)
} }
return member return member
} }

View File

@@ -10,10 +10,9 @@ import {
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
} from "../common/channels"; } from "../common/channels";
import { ConfigUtil } from "../common/config";
import { postMsg, startExpress } from "../onebot11/server"; import { postMsg, startExpress } from "../onebot11/server";
import { CONFIG_DIR, getConfigUtil, log } from "../common/utils"; import { CONFIG_DIR, getConfigUtil, log } from "../common/utils";
import { friends, groups, msgHistory, selfInfo } from "../common/data"; import { addHistoryMsg, msgHistory, selfInfo } from "../common/data";
import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; import { 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";
@@ -32,7 +31,7 @@ function onLoad() {
if (!fs.existsSync(CONFIG_DIR)) { if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true }); fs.mkdirSync(CONFIG_DIR, {recursive: true});
} }
ipcMain.handle(CHANNEL_GET_CONFIG, (event: any, arg: any) => { ipcMain.handle(CHANNEL_GET_CONFIG, (event: any, arg: any) => {
return getConfigUtil().getConfig() return getConfigUtil().getConfig()
@@ -47,8 +46,12 @@ function onLoad() {
function postRawMsg(msgList: RawMessage[]) { function postRawMsg(msgList: RawMessage[]) {
const { debug, reportSelfMessage } = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (const message of msgList) { for (let message of msgList) {
message.msgShortId = msgHistory[message.msgId]?.msgShortId
if (!message.msgShortId) {
addHistoryMsg(message)
}
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
@@ -57,82 +60,68 @@ function onLoad() {
return return
} }
postMsg(msg); postMsg(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.toString()));
} }
} }
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try {
postRawMsg(payload.msgList);
} catch (e) {
log("report message error: ", e.toString())
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => { function start() {
const { reportSelfMessage } = getConfigUtil().getConfig() log("llonebot start")
if (!reportSelfMessage) { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
return try {
} // log("received msg length", payload.msgList.length);
log("reportSelfMessage", payload) postRawMsg(payload.msgList);
try { } catch (e) {
postRawMsg([payload.msgRecord]); log("report message error: ", e.toString())
} catch (e) { }
log("report self message error: ", e.toString()) })
}
})
async function getSelfInfo() { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => {
try{ const {reportSelfMessage} = getConfigUtil().getConfig()
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
postRawMsg([payload.msgRecord]);
} catch (e) {
log("report self message error: ", e.toString())
}
})
NTQQApi.getGroups(true).then()
startExpress(getConfigUtil().getConfig().port)
}
const init = async () => {
try {
const _ = await NTQQApi.getSelfInfo() const _ = await NTQQApi.getSelfInfo()
Object.assign(selfInfo, _) Object.assign(selfInfo, _)
selfInfo.nick = selfInfo.uin selfInfo.nick = selfInfo.uin
log("get self simple info", _) log("get self simple info", _)
}catch(e){ } catch (e) {
log("retry get self info") log("retry get self info")
} }
if (selfInfo.uin) { if (selfInfo.uin) {
try { try {
const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid)) const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid))
log("self info", userInfo);
if (userInfo) { if (userInfo) {
selfInfo.nick = userInfo.nick selfInfo.nick = userInfo.nick
} else {
return setTimeout(init, 1000)
} }
} } catch (e) {
catch (e) {
log("get self nickname failed", e.toString()) log("get self nickname failed", e.toString())
return setTimeout(init, 1000)
} }
// try { start();
// friends.push(...(await NTQQApi.getFriends(true)))
// log("get friends", friends)
// let _groups: Group[] = []
// for(let i=0; i++; i<3){
// try{
// _groups = await NTQQApi.getGroups(true)
// log("get groups sucess", _groups)
// break
// } catch(e) {
// log("get groups failed", e)
// }
// }
// for (let g of _groups) {
// g.members = (await NTQQApi.getGroupMembers(g.groupCode))
// log("group members", g.members)
// groups.push(g)
// }
// } catch (e) {
// log("!!!初始化失败", e.stack.toString())
// }
startExpress(getConfigUtil().getConfig().port)
} }
else{ else{
setTimeout(() => { setTimeout(init, 1000)
getSelfInfo().then()
}, 100)
} }
} }
getSelfInfo().then() setTimeout(init, 1000)
} }

View File

@@ -1,24 +1,24 @@
import {BrowserWindow} from 'electron'; import { BrowserWindow } from 'electron';
import {getConfigUtil, log} from "../common/utils"; import { getConfigUtil, log } from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; import { NTQQApi, NTQQApiClass, sendMessagePool } from "./ntcall";
import { Group, User } from "./types"; import { Group, User } from "./types";
import { RawMessage } from "./types"; import { RawMessage } from "./types";
import {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';
export let hookApiCallbacks: Record<string, (apiReturn: any)=>void>={} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export enum ReceiveCmd { export enum ReceiveCmd {
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate", UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = "nodeIKernelMsgListener/onRecvMsg", NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg", SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged", USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
GROUPS = "nodeIKernelGroupListener/onGroupListUpdate", GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = "onGroupListUpdate", GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = "onBuddyListChange" FRIENDS = "onBuddyListChange"
} }
interface NTQQApiReturnData<PayloadType=unknown> extends Array<any> { interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: { 0: {
"type": "request", "type": "request",
"eventName": NTQQApiClass, "eventName": NTQQApiClass,
@@ -51,7 +51,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
hook.hookFunc(receiveData.payload); hook.hookFunc(receiveData.payload);
}catch (e) { } catch (e) {
log("hook error", e, receiveData.payload) log("hook error", e, receiveData.payload)
} }
}).then() }).then()
@@ -59,10 +59,10 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
} }
} }
} }
if (args[0]?.callbackId){ if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args) // log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId; const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]){ if (hookApiCallbacks[callbackId]) {
// log("callback found") // log("callback found")
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]); hookApiCallbacks[callbackId](args[1]);
@@ -85,36 +85,43 @@ export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (
return id; return id;
} }
export function removeReceiveHook(id: string){ export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex(h=>h.id === id) const index = receiveHooks.findIndex(h => h.id === id)
receiveHooks.splice(index, 1); receiveHooks.splice(index, 1);
} }
async function updateGroups(_groups: Group[]){ async function updateGroups(_groups: Group[]) {
for(let group of _groups){ for (let group of _groups) {
let existGroup = groups.find(g=>g.groupCode == group.groupCode) let existGroup = groups.find(g => g.groupCode == group.groupCode)
if (!existGroup){ if (!existGroup) {
// log("update group") NTQQApi.getGroupMembers(group.groupCode).then(members => {
let _membeers = await NTQQApi.getGroupMembers(group.groupCode) if (members) {
if (_membeers){ group.members = members
group.members = _membeers }
} })
groups.push(group)
log("update group members", group.members) log("update group members", group.members)
} } else {
else{ Object.assign(existGroup, group)
group.members = [...existGroup.members]
} }
} }
groups.length = 0;
groups.push(..._groups)
} }
registerReceiveHook<{groupList: Group[]}>(ReceiveCmd.GROUPS, (payload)=>updateGroups(payload.groupList).then()) registerReceiveHook<{ groupList: Group[] }>(ReceiveCmd.GROUPS, (payload) => updateGroups(payload.groupList).then())
registerReceiveHook<{groupList: Group[]}>(ReceiveCmd.GROUPS_UNIX, (payload)=>updateGroups(payload.groupList).then()) registerReceiveHook<{ groupList: Group[] }>(ReceiveCmd.GROUPS_UNIX, (payload) => updateGroups(payload.groupList).then())
registerReceiveHook<{data:{categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[]}[]}>(ReceiveCmd.FRIENDS, payload=>{ registerReceiveHook<{
friends.length = 0 data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => {
for (const fData of payload.data) { for (const fData of payload.data) {
friends.push(...fData.buddyList) const _friends = fData.buddyList;
for (let friend of _friends) {
let existFriend = friends.find(f => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
} else {
Object.assign(existFriend, friend)
}
}
} }
}) })
@@ -124,31 +131,30 @@ registerReceiveHook<{data:{categoryId: number, categroyName: string, categroyMbC
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
msgHistory[message.msgId] = message; addHistoryMsg(message)
} }
}) })
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)
if (!msgHistory[message.msgId]){ addHistoryMsg(message)
msgHistory[message.msgId] = message }
} const msgIds = Object.keys(msgHistory);
else{ if (msgIds.length > 30000) {
Object.assign(msgHistory[message.msgId], message) delete msgHistory[msgIds.sort()[0]]
}
} }
}) })
registerReceiveHook<{msgRecord: RawMessage}>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord})=>{ registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => {
const message = msgRecord; const message = msgRecord;
const peerUid = message.peerUid; const peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
const sendCallback = sendMessagePool[peerUid]; const sendCallback = sendMessagePool[peerUid];
if (sendCallback){ if (sendCallback) {
try{ try {
sendCallback(message); sendCallback(message);
}catch(e){ } catch (e) {
log("receive self msg error", e.stack) log("receive self msg error", e.stack)
} }
} }

View File

@@ -2,11 +2,12 @@ import { ipcMain } from "electron";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { ReceiveCmd, hookApiCallbacks, registerReceiveHook, removeReceiveHook } from "./hook"; import { ReceiveCmd, hookApiCallbacks, registerReceiveHook, removeReceiveHook } from "./hook";
import { log } from "../common/utils"; import { log } from "../common/utils";
import { ChatType, Friend, SelfInfo, User } from "./types"; import { ChatType, Friend, PicElement, SelfInfo, User } from "./types";
import { Group } from "./types"; import { Group } from "./types";
import { GroupMember } from "./types"; import { GroupMember } from "./types";
import { RawMessage } from "./types"; import { RawMessage } from "./types";
import { SendMessageElement } from "./types"; import { SendMessageElement } from "./types";
import * as fs from "fs";
interface IPCReceiveEvent { interface IPCReceiveEvent {
eventName: string eventName: string
@@ -43,6 +44,7 @@ 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"
} }
enum NTQQApiChannel { enum NTQQApiChannel {
@@ -140,10 +142,9 @@ export class NTQQApi {
} }
static async getUserInfo(uid: string) { static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ info: User }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO, const result = await callNTQQApi<{ profiles: Map<string, User> }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO,
[{ force: true, uids: [uid] }, undefined], ReceiveCmd.USER_INFO) [{ force: true, uids: [uid] }, undefined], ReceiveCmd.USER_INFO)
return result.info return result.profiles.get(uid)
} }
static async getFriends(forced = false) { static async getFriends(forced = false) {
@@ -244,6 +245,28 @@ export class NTQQApi {
} }
} }
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string){
// 用于下载收到的消息中的图片等
if (fs.existsSync(sourcePath)){
return sourcePath
}
const apiParams = [
{
getReq: {
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
undefined,
]
await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.DOWNLOAD_MEDIA, apiParams)
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, [{ peer, msgIds }, null]) return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.RECALL_MSG, [{ peer, msgIds }, null])
} }
@@ -293,4 +316,5 @@ export class NTQQApi {
}) })
} }
} }

View File

@@ -167,8 +167,22 @@ export interface ArkElement {
bytesData: string; bytesData: string;
} }
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
export interface PicElement {
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/
sourcePath: string; // 图片本地路径
thumbPath: Map<number, string>;
picWidth: number;
picHeight: number;
fileSize: number;
fileName: string;
fileUuid: string;
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string;
msgShortId?: number; // 自己维护的消息id
msgTime: string; msgTime: string;
msgSeq: string; msgSeq: string;
senderUin: string; // 发送者QQ号 senderUin: string; // 发送者QQ号
@@ -178,6 +192,7 @@ export interface RawMessage {
sendMemberName?: string; // 发送者群名片 sendMemberName?: string; // 发送者群名片
chatType: ChatType; chatType: ChatType;
elements: { elements: {
elementId: string,
replyElement: { replyElement: {
senderUid: string; // 原消息发送者QQ号 senderUid: string; // 原消息发送者QQ号
sourceMsgIsIncPic: boolean; // 原消息是否有图片 sourceMsgIsIncPic: boolean; // 原消息是否有图片
@@ -186,18 +201,11 @@ export interface RawMessage {
}; };
textElement: { textElement: {
atType: AtType; atType: AtType;
atUid: string; atUid: string; // QQ号
content: string; content: string;
atNtUid: string; atNtUid: string; // uid号
};
picElement: {
sourcePath: string; // 图片本地路径
picWidth: number;
picHeight: number;
fileSize: number;
fileName: string;
fileUuid: string;
}; };
picElement: PicElement;
pttElement: PttElement; pttElement: PttElement;
arkElement: ArkElement; arkElement: ArkElement;
}[]; }[];

View File

@@ -1,21 +1,21 @@
import { ActionName } from "./types"; import { ActionName } from "./types";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { NTQQApi } from "../../ntqqapi/ntcall"; import { NTQQApi } from "../../ntqqapi/ntcall";
import { msgHistory } from "../../common/data"; import { getHistoryMsgByShortId, msgHistory } from "../../common/data";
interface Payload { interface Payload {
message_id: string message_id: number
} }
class DeleteMsg extends BaseAction<Payload, void> { class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg actionName = ActionName.DeleteMsg
protected async _handle(payload:Payload){ protected async _handle(payload:Payload){
let msg = msgHistory[payload.message_id] let msg = getHistoryMsgByShortId(payload.message_id)
await NTQQApi.recallMsg({ await NTQQApi.recallMsg({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid peerUid: msg.peerUid
}, [payload.message_id]) }, [msg.msgId])
} }
} }

View File

@@ -1,4 +1,4 @@
import { msgHistory } from "../../common/data"; import { getHistoryMsgByShortId, msgHistory } from "../../common/data";
import { OB11Message } from '../types'; import { OB11Message } from '../types';
import { OB11Constructor } from "../constructor"; import { OB11Constructor } from "../constructor";
import { log } from "../../common/utils"; import { log } from "../../common/utils";
@@ -7,7 +7,7 @@ import { ActionName } from "./types";
export interface PayloadType { export interface PayloadType {
message_id: string message_id: number
} }
export type ReturnDataType = OB11Message export type ReturnDataType = OB11Message
@@ -17,7 +17,7 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
protected async _handle(payload: PayloadType){ protected async _handle(payload: PayloadType){
// log("history msg ids", Object.keys(msgHistory)); // log("history msg ids", Object.keys(msgHistory));
const msg = msgHistory[payload.message_id.toString()] const msg = getHistoryMsgByShortId(payload.message_id)
if (msg) { if (msg) {
const msgData = await OB11Constructor.message(msg); const msgData = await OB11Constructor.message(msg);
return msgData return msgData

View File

@@ -1,5 +1,11 @@
import { AtType, ChatType, Group } from "../../ntqqapi/types"; import { AtType, ChatType, Group } from "../../ntqqapi/types";
import { friends, getGroup, getStrangerByUin, msgHistory } from "../../common/data"; import {
addHistoryMsg,
friends,
getGroup,
getHistoryMsgByShortId,
getStrangerByUin,
} from "../../common/data";
import { OB11MessageData, OB11MessageDataType, OB11PostSendMsg } from '../types'; import { OB11MessageData, OB11MessageDataType, OB11PostSendMsg } from '../types';
import { NTQQApi } from "../../ntqqapi/ntcall"; import { NTQQApi } from "../../ntqqapi/ntcall";
import { Peer } from "../../ntqqapi/ntcall"; import { Peer } from "../../ntqqapi/ntcall";
@@ -10,9 +16,10 @@ import { v4 as uuid4 } from 'uuid';
import { log } from "../../common/utils"; import { log } from "../../common/utils";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import { ActionName } from "./types";
import * as fs from "fs";
export interface ReturnDataType { export interface ReturnDataType {
message_id: string message_id: number
} }
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
@@ -23,6 +30,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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())
@@ -88,27 +96,27 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let replyMsgId = sendMsg.data.id; let replyMsgId = sendMsg.data.id;
if (replyMsgId) { if (replyMsgId) {
replyMsgId = replyMsgId.toString() replyMsgId = replyMsgId.toString()
const replyMsg = msgHistory[replyMsgId] const replyMsg = getHistoryMsgByShortId(replyMsgId)
if (replyMsg) { if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsgId, replyMsg.senderUin, replyMsg.senderUin)) sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin))
}
}
} break;
case OB11MessageDataType.image: {
const file = sendMsg.data?.file
if (file) {
const picPath = await (await uri2local(uuid4(), file)).path
if (picPath) {
sendElements.push(await SendMsgElementConstructor.pic(picPath))
} }
} }
} break; } break;
case OB11MessageDataType.image:
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const file = sendMsg.data?.file const file = sendMsg.data?.file
if (file) { if (file) {
const voicePath = await (await uri2local(uuid4(), file)).path const {path, isLocal} = (await uri2local(uuid4(), file))
if (voicePath) { if (path) {
sendElements.push(await SendMsgElementConstructor.ptt(voicePath)) if (!isLocal){ // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.image){
sendElements.push(await SendMsgElementConstructor.pic(path))
}
else {
sendElements.push(await SendMsgElementConstructor.ptt(path))
}
} }
} }
} }
@@ -117,7 +125,9 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// log("send msg:", peer, sendElements) // log("send msg:", peer, sendElements)
try { try {
const returnMsg = await NTQQApi.sendMsg(peer, sendElements) const returnMsg = await NTQQApi.sendMsg(peer, sendElements)
return { message_id: returnMsg.msgId } addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f=>fs.unlink(f, ()=>{}))
return { message_id: returnMsg.msgShortId }
} catch (e) { } catch (e) {
throw(e.toString()) throw(e.toString())
} }

View File

@@ -1,18 +1,27 @@
import {OB11MessageDataType, OB11GroupMemberRole, OB11Message, OB11MessageData, OB11Group, OB11GroupMember, OB11User} from "./types"; import {
import { AtType, ChatType, Group, GroupMember, RawMessage, SelfInfo, User } from '../ntqqapi/types'; OB11MessageDataType,
import { getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo } from '../common/data'; OB11GroupMemberRole,
import {file2base64, getConfigUtil, log} from "../common/utils"; OB11Message,
OB11Group,
OB11GroupMember,
OB11User
} from "./types";
import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types';
import { getFriend, getGroupMember, getHistoryMsgBySeq, msgHistory, selfInfo } from '../common/data';
import { file2base64, getConfigUtil, log } from "../common/utils";
import { NTQQApi } from "../ntqqapi/ntcall";
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
const {enableBase64} = getConfigUtil().getConfig() const {enableBase64} = getConfigUtil().getConfig()
const message_type = msg.chatType == ChatType.group ? "group" : "private"; const message_type = msg.chatType == ChatType.group ? "group" : "private";
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: selfInfo.uin, self_id: selfInfo.uin,
user_id: msg.senderUin, user_id: msg.senderUin,
time: parseInt(msg.msgTime) || 0, time: parseInt(msg.msgTime) || 0,
message_id: msg.msgId, message_id: msg.msgShortId,
real_id: msg.msgId, real_id: msg.msgId,
message_type: msg.chatType == ChatType.group ? "group" : "private", message_type: msg.chatType == ChatType.group ? "group" : "private",
sender: { sender: {
@@ -27,6 +36,7 @@ export class OB11Constructor {
post_type: "message", post_type: "message",
} }
if (msg.chatType == ChatType.group) { if (msg.chatType == ChatType.group) {
resMsg.sub_type = "normal"
resMsg.group_id = msg.peerUin resMsg.group_id = msg.peerUin
const member = await getGroupMember(msg.peerUin, msg.senderUin); const member = await getGroupMember(msg.peerUin, msg.senderUin);
if (member) { if (member) {
@@ -53,10 +63,18 @@ export class OB11Constructor {
message_data["data"]["mention"] = "all" message_data["data"]["mention"] = "all"
message_data["data"]["qq"] = "all" message_data["data"]["qq"] = "all"
} else { } else {
let uid = element.textElement.atUid let atUid = element.textElement.atNtUid
let atMember = await getGroupMember(msg.peerUin, uid) let atQQ = element.textElement.atUid
message_data["data"]["mention"] = atMember?.uin if (!atQQ || atQQ === "0") {
message_data["data"]["qq"] = atMember?.uin const atMember = await getGroupMember(msg.peerUin, null, atUid)
if (atMember) {
atQQ = atMember.uin
}
}
if (atQQ) {
message_data["data"]["mention"] = atQQ
message_data["data"]["qq"] = atQQ
}
} }
} else if (element.textElement) { } else if (element.textElement) {
message_data["type"] = "text" message_data["type"] = "text"
@@ -66,13 +84,18 @@ export class OB11Constructor {
message_data["data"]["file_id"] = element.picElement.fileUuid message_data["data"]["file_id"] = element.picElement.fileUuid
message_data["data"]["path"] = element.picElement.sourcePath message_data["data"]["path"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.sourcePath message_data["data"]["file"] = element.picElement.sourcePath
try {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath)
} catch (e) {
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl
}
} else if (element.replyElement) { } else if (element.replyElement) {
message_data["type"] = "reply" message_data["type"] = "reply"
const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq) const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq)
if (replyMsg) { if (replyMsg) {
message_data["data"]["id"] = replyMsg.msgId message_data["data"]["id"] = replyMsg.msgShortId
} } else {
else{
continue continue
} }
} else if (element.pttElement) { } else if (element.pttElement) {
@@ -89,11 +112,12 @@ export class OB11Constructor {
message_data["type"] = OB11MessageDataType.json; message_data["type"] = OB11MessageDataType.json;
message_data["data"]["data"] = element.arkElement.bytesData; message_data["data"]["data"] = element.arkElement.bytesData;
} }
if (message_data.data.file) { if (message_data.data.http_file) {
message_data.data.file = message_data.data.http_file
} else if (message_data.data.file) {
let filePath: string = message_data.data.file; let filePath: string = message_data.data.file;
message_data.data.file = "file://" + filePath message_data.data.file = "file://" + filePath
if (enableBase64) { if (enableBase64) {
// filePath = filePath.replace("\\Ori\\", "\\Thumb\\")
let {err, data} = await file2base64(filePath); let {err, data} = await file2base64(filePath);
if (err) { if (err) {
console.log("文件转base64失败", err) console.log("文件转base64失败", err)
@@ -106,36 +130,26 @@ export class OB11Constructor {
resMsg.message.push(message_data); resMsg.message.push(message_data);
} }
} }
// if (msgHistory.length > 10000) {
// msgHistory.splice(0, 100)
// }
// msgHistory.push(message)
// if (!reportSelfMessage && onebot_message_data["user_id"] == self_qq) {
// console.log("开启了不上传自己发送的消息,进行拦截 ", onebot_message_data);
// } else {
// console.log("发送上传消息给ipc main", onebot_message_data);
// window.llonebot.postData(onebot_message_data);
// }
return resMsg; return resMsg;
} }
static friend(friend: User): OB11User{ static friend(friend: User): OB11User {
return { return {
user_id: friend.uin, user_id: friend.uin,
nickname: friend.nick, nickname: friend.nick,
remark: friend.remark remark: friend.remark
} }
} }
static selfInfo(selfInfo: SelfInfo): OB11User{ static selfInfo(selfInfo: SelfInfo): OB11User {
return { return {
user_id: selfInfo.uin, user_id: selfInfo.uin,
nickname: selfInfo.nick nickname: selfInfo.nick
} }
} }
static friends(friends: User[]): OB11User[]{ static friends(friends: User[]): OB11User[] {
return friends.map(OB11Constructor.friend) return friends.map(OB11Constructor.friend)
} }
@@ -147,7 +161,7 @@ export class OB11Constructor {
}[role] }[role]
} }
static groupMember(group_id: string, member: GroupMember): OB11GroupMember{ static groupMember(group_id: string, member: GroupMember): OB11GroupMember {
return { return {
group_id, group_id,
user_id: member.uin, user_id: member.uin,
@@ -156,19 +170,19 @@ export class OB11Constructor {
} }
} }
static groupMembers(group: Group): OB11GroupMember[]{ static groupMembers(group: Group): OB11GroupMember[] {
log("construct ob11 group members", group) log("construct ob11 group members", group)
return group.members.map(m=>OB11Constructor.groupMember(group.groupCode, m)) return group.members.map(m => OB11Constructor.groupMember(group.groupCode, m))
} }
static group(group: Group): OB11Group{ static group(group: Group): OB11Group {
return { return {
group_id: group.groupCode, group_id: group.groupCode,
group_name: group.groupName group_name: group.groupName
} }
} }
static groups(groups: Group[]): OB11Group[]{ static groups(groups: Group[]): OB11Group[] {
return groups.map(OB11Constructor.group) return groups.map(OB11Constructor.group)
} }
} }

View File

@@ -58,12 +58,12 @@ export enum OB11MessageType {
export interface OB11Message { export interface OB11Message {
self_id?: string, self_id?: string,
time: number, time: number,
message_id: string, message_id: number,
real_id: string, real_id: string,
user_id: string, user_id: string,
group_id?: string, group_id?: string,
message_type: "private" | "group", message_type: "private" | "group",
sub_type?: "friend" | "group" | "other", sub_type?: "friend" | "group" | "normal",
sender: OB11Sender, sender: OB11Sender,
message: OB11MessageData[], message: OB11MessageData[],
raw_message: string, raw_message: string,

View File

@@ -6,6 +6,12 @@ const fs = require("fs").promises;
export async function uri2local(fileName: string, uri: string){ export async function uri2local(fileName: string, uri: string){
let filePath = path.join(CONFIG_DIR, fileName) let filePath = path.join(CONFIG_DIR, fileName)
let url = new URL(uri); let url = new URL(uri);
let res = {
success: false,
errMsg: "",
path: "",
isLocal: false
}
if (url.protocol == "base64:") { if (url.protocol == "base64:") {
// base64转成文件 // base64转成文件
let base64Data = uri.split("base64://")[1] let base64Data = uri.split("base64://")[1]
@@ -13,51 +19,44 @@ export async function uri2local(fileName: string, uri: string){
const buffer = Buffer.from(base64Data, 'base64'); const buffer = Buffer.from(base64Data, 'base64');
await fs.writeFile(filePath, buffer); await fs.writeFile(filePath, buffer);
} catch (e: any) { } catch (e: any) {
return { res.errMsg = `base64文件下载失败,` + e.toString()
success: false, return res
errMsg: `base64文件下载失败,` + e.toString(),
path: ""
}
} }
} else if (url.protocol == "http:" || url.protocol == "https:") { } else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件 // 下载文件
let res = await fetch(url) let fetchRes = await fetch(url)
if (!res.ok) { if (!fetchRes.ok) {
return { res.errMsg = `${url}下载失败,` + fetchRes.statusText
success: false, return res
errMsg: `${url}下载失败,` + res.statusText,
path: ""
}
} }
let blob = await res.blob(); let blob = await fetchRes.blob();
let buffer = await blob.arrayBuffer(); let buffer = await blob.arrayBuffer();
try { try {
await fs.writeFile(filePath, Buffer.from(buffer)); await fs.writeFile(filePath, Buffer.from(buffer));
} catch (e: any) { } catch (e: any) {
return { res.errMsg = `${url}下载失败,` + e.toString()
success: false, return res
errMsg: `${url}下载失败,` + e.toString(),
path: ""
}
} }
} else if (url.protocol === "file:"){ } else if (url.protocol === "file:"){
await fs.copyFile(url.pathname, filePath); // await fs.copyFile(url.pathname, filePath);
// filePath = (await NTQQApi.uploadFile(url.pathname)).path; let pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32"){
filePath = pathname.slice(1)
}
else{
filePath = pathname
}
res.isLocal = true
} }
else{ else{
return { res.errMsg = `不支持的file协议,` + url.protocol
success: false, return res
errMsg: `不支持的file协议,` + url.protocol,
path: ""
}
} }
if (isGIF(filePath)) { if (isGIF(filePath) && !res.isLocal) {
await fs.rename(filePath, filePath + ".gif"); await fs.rename(filePath, filePath + ".gif");
filePath += ".gif"; filePath += ".gif";
} }
return { res.success = true
success: true, res.path = filePath
errMsg: "", return res
path: filePath
};
} }