Compare commits

...

15 Commits

Author SHA1 Message Date
linyuchen
4f9682289c feat: api /get_version_info
feat: api /can_send_image
feat: api /can_send_record
feat: ws heart & lifecycle
2024-02-16 21:32:37 +08:00
linyuchen
963aad1510 fix: some id(int and string) compatibility 2024-02-16 15:54:07 +08:00
linyuchen
0eeba1d29e fix: remove ws welcome 2024-02-16 10:34:56 +08:00
linyuchen
97200f427d docs: update readme 2024-02-16 00:52:19 +08:00
linyuchen
ef4443d080 feat: Websocket Server
feat: change port not need restart
2024-02-16 00:47:04 +08:00
linyuchen
a7d75f84cb fix: send voice msg 2024-02-15 18:43:29 +08:00
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
26 changed files with 616 additions and 221 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,11 @@
## 支持的API ## 支持的API
目前支持http协议POST方法不支持websocket事件上报也是http协议 目前支持协议
- [x] http调用api
- [x] http事件上报
- [x] 正向websocket
- [ ] 反向websocket
主要功能: 主要功能:
- [x] 发送好友消息 - [x] 发送好友消息
@@ -46,10 +52,14 @@
- [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
- [x] get_msg - [x] get_msg
- [x] get_version_info
- [x] can_send_image
- [x] can_send_record
## 示例 ## 示例
@@ -67,7 +77,7 @@
<details> <details>
<summary>调用接口报404</summary> <summary>调用接口报404</summary>
<br/> <br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口并且所有接口都只支持POST方法调用GET方法会报404 目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
</details> </details>
<br/> <br/>
@@ -94,11 +104,10 @@
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正向websocket
- [ ] 转发消息记录 - [ ] 转发消息记录
- [ ] 好友点赞api - [ ] 好友点赞api
- [ ] 支持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": "3.0.2", "version": "3.2.0",
"thumbnail": "./icon.png", "thumbnail": "./icon.png",
"authors": [{ "authors": [{
"name": "linyuchen", "name": "linyuchen",

34
package-lock.json generated
View File

@@ -18,11 +18,13 @@
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4",
"ws": "^8.16.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -2199,6 +2201,15 @@
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true "dev": true
}, },
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
"version": "1.11.6", "version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
@@ -4691,6 +4702,27 @@
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"dev": true "dev": true
}, },
"node_modules/ws": {
"version": "8.16.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@@ -26,10 +26,12 @@
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4",
"ws": "^8.16.0"
} }
} }

View File

@@ -1,28 +1,50 @@
import {Config} from "./types"; import { Config } from "./types";
const fs = require("fs") const fs = require("fs")
export class ConfigUtil{ export class ConfigUtil {
configPath: string; configPath: string;
constructor(configPath: string) { constructor(configPath: string) {
this.configPath = configPath; this.configPath = configPath;
} }
getConfig(): Config{ getConfig(): Config {
let defaultConfig: Config = {
port: 3000,
wsPort: 3001,
hosts: [],
token: "",
enableBase64: false,
debug: false,
log: false,
reportSelfMessage: false
}
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
return {port:3000, hosts: ["http://192.168.1.2:5000/"]} return defaultConfig
} else { } else {
const data = fs.readFileSync(this.configPath, "utf-8"); const data = fs.readFileSync(this.configPath, "utf-8");
let jsonData =JSON.parse(data); let jsonData: Config = defaultConfig;
if (!jsonData.hosts){ try {
jsonData = JSON.parse(data)
}
catch (e){
}
if (!jsonData.hosts) {
jsonData.hosts = [] jsonData.hosts = []
} }
if (!jsonData.wsPort){
jsonData.wsPort = 3001
}
if (!jsonData.token){
jsonData.token = ""
}
return jsonData; return jsonData;
} }
} }
setConfig(config: Config){ setConfig(config: Config) {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8") fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8")
} }
} }

View File

@@ -1,10 +1,31 @@
import { NTQQApi } from '../ntqqapi/ntcall'; import { NTQQApi } from '../ntqqapi/ntcall';
import { Friend, Group, GroupMember, 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){
@@ -65,4 +86,7 @@ export function getStrangerByUin(uin: string) {
return uidMaps[key]; return uidMaps[key];
} }
} }
} }
export const version = "v3.2.0"
export const heartInterval = 15000 // 毫秒

View File

@@ -1,6 +1,8 @@
export interface Config { export interface Config {
port: number port: number
wsPort: number
hosts: string[] hosts: string[]
token?: string
enableBase64?: boolean enableBase64?: boolean
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean

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, setToken, startHTTPServer, startWSServer } 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";
@@ -38,7 +37,17 @@ function onLoad() {
return getConfigUtil().getConfig() return getConfigUtil().getConfig()
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => { ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => {
let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(arg) getConfigUtil().setConfig(arg)
if (arg.port != oldConfig.port){
startHTTPServer(arg.port)
}
if (arg.wsPort != oldConfig.wsPort){
startWSServer(arg.wsPort)
}
if (arg.token != oldConfig.token){
setToken(arg.token);
}
}) })
ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => {
@@ -48,15 +57,20 @@ 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;
} }
if (msg.user_id == selfInfo.uin && !reportSelfMessage) { if (msg.user_id.toString() == selfInfo.uin && !reportSelfMessage) {
return return
} }
postMsg(msg); postMsg(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.toString()));
} }
} }
@@ -65,6 +79,7 @@ function onLoad() {
function start() { function start() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try { try {
// log("received msg length", payload.msgList.length);
postRawMsg(payload.msgList); postRawMsg(payload.msgList);
} catch (e) { } catch (e) {
log("report message error: ", e.toString()) log("report message error: ", e.toString())
@@ -76,7 +91,7 @@ function onLoad() {
if (!reportSelfMessage) { if (!reportSelfMessage) {
return return
} }
log("reportSelfMessage", payload) // log("reportSelfMessage", payload)
try { try {
postRawMsg([payload.msgRecord]); postRawMsg([payload.msgRecord]);
} catch (e) { } catch (e) {
@@ -84,10 +99,14 @@ function onLoad() {
} }
}) })
NTQQApi.getGroups(true).then() NTQQApi.getGroups(true).then()
startExpress(getConfigUtil().getConfig().port) const config = getConfigUtil().getConfig()
startHTTPServer(config.port)
startWSServer(config.wsPort)
setToken(config.token)
log("LLOneBot start")
} }
async function getSelfInfo() { const init = async () => {
try { try {
const _ = await NTQQApi.getSelfInfo() const _ = await NTQQApi.getSelfInfo()
Object.assign(selfInfo, _) Object.assign(selfInfo, _)
@@ -95,7 +114,6 @@ function onLoad() {
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 {
@@ -104,25 +122,19 @@ function onLoad() {
if (userInfo) { if (userInfo) {
selfInfo.nick = userInfo.nick selfInfo.nick = userInfo.nick
} else { } else {
return setTimeout(() => { return setTimeout(init, 1000)
getSelfInfo().then()
}, 100)
} }
} catch (e) { } catch (e) {
log("get self nickname failed", e.toString()) log("get self nickname failed", e.toString())
return setTimeout(() => { return setTimeout(init, 1000)
getSelfInfo().then()
}, 100)
} }
start(); start();
} else { }
setTimeout(() => { else{
getSelfInfo().then() setTimeout(init, 1000)
}, 100)
} }
} }
setTimeout(init, 1000)
getSelfInfo().then()
} }
@@ -131,7 +143,7 @@ function onBrowserWindowCreated(window: BrowserWindow) {
try { try {
hookNTQQApiReceive(window); hookNTQQApiReceive(window);
} catch (e) { } catch (e) {
log("llonebot hook error: ", e.toString()) log("LLOneBot hook error: ", e.toString())
} }
} }

View File

@@ -3,7 +3,7 @@ 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> = {}
@@ -131,21 +131,17 @@ registerReceiveHook<{
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
} else {
Object.assign(msgHistory[message.msgId], message)
}
} }
const msgIds = Object.keys(msgHistory); const msgIds = Object.keys(msgHistory);
if (msgIds.length > 3000) { if (msgIds.length > 30000) {
delete msgHistory[msgIds.sort()[0]] delete msgHistory[msgIds.sort()[0]]
} }
}) })

View File

@@ -78,8 +78,7 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
success = true success = true
resolve(r) resolve(r)
}; };
} } else {
else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result) log(`${methodName} callback`, result)
@@ -90,8 +89,7 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
success = true success = true
resolve(payload); resolve(payload);
}) })
} } else {
else {
success = true success = true
reject(`ntqq api call failed, ${result.errMsg}`); reject(`ntqq api call failed, ${result.errMsg}`);
} }
@@ -108,7 +106,7 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
ipcMain.emit( ipcMain.emit(
channel, channel,
{}, {},
{ type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1] }, {type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1]},
[methodName, ...args], [methodName, ...args],
) )
}) })
@@ -142,13 +140,17 @@ export class NTQQApi {
} }
static async getUserInfo(uid: string) { static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO, const result = await callNTQQApi<{
[{ force: true, uids: [uid] }, undefined], ReceiveCmd.USER_INFO) profiles: Map<string, User>
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO,
[{force: true, uids: [uid]}, undefined], 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<{ data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: Friend[] }[] }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.FRIENDS, [{ force_update: forced }, undefined], ReceiveCmd.FRIENDS) const data = await callNTQQApi<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: Friend[] }[]
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.FRIENDS, [{force_update: forced}, undefined], 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)
@@ -161,7 +163,10 @@ export class NTQQApi {
if (process.platform != "win32") { if (process.platform != "win32") {
cbCmd = ReceiveCmd.GROUPS_UNIX cbCmd = ReceiveCmd.GROUPS_UNIX
} }
const result = await callNTQQApi<{ updateType: number, groupList: Group[] }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUPS, [{ force_update: forced }, undefined], cbCmd) const result = await callNTQQApi<{
updateType: number,
groupList: Group[]
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUPS, [{force_update: forced}, undefined], cbCmd)
return result.groupList return result.groupList
} }
@@ -172,7 +177,9 @@ export class NTQQApi {
}]) }])
// log("get group member sceneId", sceneId); // log("get group member sceneId", sceneId);
try { try {
const result = await callNTQQApi<{result:{infos: any}}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBERS, const result = await callNTQQApi<{
result: { infos: any }
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBERS,
[{ [{
sceneId: sceneId, sceneId: sceneId,
num: num num: num
@@ -182,7 +189,7 @@ export class NTQQApi {
// 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()
values = Array.from(values) as GroupMember[] values = Array.from(values) as GroupMember[]
// log("members info", values); // log("members info", values);
return values return values
} catch (e) { } catch (e) {
@@ -192,7 +199,6 @@ export class NTQQApi {
} }
static getFileType(filePath: string) { static getFileType(filePath: string) {
return callNTQQApi<{ return callNTQQApi<{
ext: string ext: string
@@ -204,7 +210,10 @@ export class NTQQApi {
} }
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, [{ fromPath: filePath, toPath: destPath }]) return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_COPY, [{
fromPath: filePath,
toPath: destPath
}])
} }
static getImageSize(filePath: string) { static getImageSize(filePath: string) {
@@ -221,7 +230,14 @@ export class NTQQApi {
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string) { static async uploadFile(filePath: string) {
const md5 = await NTQQApi.getFileMd5(filePath); const md5 = await NTQQApi.getFileMd5(filePath);
const fileName = `${md5}.${(await NTQQApi.getFileType(filePath)).ext}`; let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) {
ext = "." + ext
}
else{
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>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.MEDIA_FILE_PATH, [{
path_info: { path_info: {
md5HexStr: md5, md5HexStr: md5,
@@ -245,9 +261,9 @@ export class NTQQApi {
} }
} }
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string){ static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (fs.existsSync(sourcePath)){ if (fs.existsSync(sourcePath)) {
return sourcePath return sourcePath
} }
const apiParams = [ const apiParams = [
@@ -267,8 +283,12 @@ export class NTQQApi {
await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.DOWNLOAD_MEDIA, apiParams) await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.DOWNLOAD_MEDIA, 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, [{ peer, msgIds }, null]) return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.RECALL_MSG, [{
peer,
msgIds
}, null])
} }
static sendMsg(peer: Peer, msgElements: SendMessageElement[]) { static sendMsg(peer: Peer, msgElements: SendMessageElement[]) {
@@ -297,8 +317,7 @@ export class NTQQApi {
// log("有正在发送的消息,等待中...") // log("有正在发送的消息,等待中...")
usingTime += 100; usingTime += 100;
setTimeout(checkLastSend, 100); setTimeout(checkLastSend, 100);
} } else {
else {
log("可以进行发送消息,设置发送成功回调", sendMessagePool) log("可以进行发送消息,设置发送成功回调", sendMessagePool)
sendMessagePool[peerUid] = (rawMessage: RawMessage) => { sendMessagePool[peerUid] = (rawMessage: RawMessage) => {
success = true; success = true;

View File

@@ -182,6 +182,7 @@ export interface PicElement {
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号

View File

@@ -0,0 +1,10 @@
import { ActionName } from "./types";
import CanSendRecord from "./CanSendRecord";
interface ReturnType{
yes: boolean
}
export default class CanSendImage extends CanSendRecord{
actionName = ActionName.CanSendImage
}

View File

@@ -0,0 +1,16 @@
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
interface ReturnType{
yes: boolean
}
export default class CanSendRecord extends BaseAction<any, ReturnType>{
actionName = ActionName.CanSendRecord
protected async _handle(payload): Promise<ReturnType>{
return {
yes: true
}
}
}

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

@@ -0,0 +1,12 @@
import BaseAction from "./BaseAction";
import {OB11Status} from "../types";
export default class GetStatus extends BaseAction<any, OB11Status> {
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: null,
good: true
}
}
}

View File

@@ -0,0 +1,15 @@
import BaseAction from "./BaseAction";
import { OB11Version } from "../types";
import {version} from "../../common/data";
import { ActionName } from "./types";
export default class GetVersionInfo extends BaseAction<any, OB11Version>{
actionName = ActionName.GetVersionInfo
protected async _handle(payload: any): Promise<OB11Version> {
return {
app_name: "LLOneBot",
protocol_version: "v11",
app_version: version
}
}
}

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";
@@ -13,7 +19,7 @@ import { ActionName } from "./types";
import * as fs from "fs"; 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> {
@@ -90,9 +96,9 @@ 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; } break;
@@ -119,8 +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)
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f=>fs.unlink(f, ()=>{})) deleteAfterSentFiles.map(f=>fs.unlink(f, ()=>{}))
return { message_id: returnMsg.msgId } return { message_id: returnMsg.msgShortId }
} catch (e) { } catch (e) {
throw(e.toString()) throw(e.toString())
} }

View File

@@ -9,6 +9,9 @@ import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg' import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg' import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg"; import DeleteMsg from "./DeleteMsg";
import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
export const actionHandlers = [ export const actionHandlers = [
new GetMsg(), new GetMsg(),
@@ -16,5 +19,8 @@ export const actionHandlers = [
new GetFriendList(), new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg() new DeleteMsg(),
new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage()
] ]

View File

@@ -1,3 +1,5 @@
import GetVersionInfo from "./GetVersionInfo";
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
export interface ValidCheckResult { export interface ValidCheckResult {
@@ -22,5 +24,9 @@ export enum ActionName{
SendMsg = "send_msg", SendMsg = "send_msg",
SendGroupMsg = "send_group_msg", SendGroupMsg = "send_group_msg",
SendPrivateMsg = "send_private_msg", SendPrivateMsg = "send_private_msg",
DeleteMsg = "delete_msg" DeleteMsg = "delete_msg",
GetVersionInfo = "get_version_info",
GetStatus = "get_status",
CanSendRecord = "can_send_record",
CanSendImage = "can_send_image",
} }

View File

@@ -1,18 +1,19 @@
import { OB11Return } from '../types'; import { OB11Return } from '../types';
export class OB11Response { export class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> { static res<T>(data: T, status: number = 0, message: string = "", echo=""): OB11Return<T> {
return { return {
status: status, status: status,
retcode: status, retcode: status,
data: data, data: data,
message: message message: message,
echo,
} }
} }
static ok<T>(data: T) { static ok<T>(data: T) {
return OB11Response.res<T>(data) return OB11Response.res<T>(data)
} }
static error(err: string) { static error(err: string, status=-1) {
return OB11Response.res(null, -1, err) return OB11Response.res(null, status, err)
} }
} }

View File

@@ -1,23 +1,31 @@
import {OB11MessageDataType, OB11GroupMemberRole, OB11Message, OB11MessageData, OB11Group, OB11GroupMember, OB11User} from "./types"; import {
OB11MessageDataType,
OB11GroupMemberRole,
OB11Message,
OB11Group,
OB11GroupMember,
OB11User, OB11LifeCycleEvent, OB11HeartEvent
} from "./types";
import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types'; import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types';
import { getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo } from '../common/data'; import { getFriend, getGroupMember, getHistoryMsgBySeq, heartInterval, msgHistory, selfInfo } from '../common/data';
import {file2base64, getConfigUtil, log} from "../common/utils"; import { file2base64, getConfigUtil, log } from "../common/utils";
import { NTQQApi } from "../ntqqapi/ntcall"; 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: parseInt(selfInfo.uin),
user_id: msg.senderUin, user_id: parseInt(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: {
user_id: msg.senderUin, user_id: parseInt(msg.senderUin),
nickname: msg.sendNickName, nickname: msg.sendNickName,
card: msg.sendMemberName || "", card: msg.sendMemberName || "",
}, },
@@ -28,10 +36,12 @@ export class OB11Constructor {
post_type: "message", post_type: "message",
} }
if (msg.chatType == ChatType.group) { if (msg.chatType == ChatType.group) {
resMsg.group_id = msg.peerUin resMsg.sub_type = "normal"
resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin); const member = await getGroupMember(msg.peerUin, msg.senderUin);
if (member) { if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role); resMsg.sender.role = OB11Constructor.groupMemberRole(member.role);
resMsg.sender.nickname = member.nick
} }
} else if (msg.chatType == ChatType.friend) { } else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = "friend" resMsg.sub_type = "friend"
@@ -56,13 +66,13 @@ export class OB11Constructor {
} else { } else {
let atUid = element.textElement.atNtUid let atUid = element.textElement.atNtUid
let atQQ = element.textElement.atUid let atQQ = element.textElement.atUid
if (!atQQ || atQQ === "0"){ if (!atQQ || atQQ === "0") {
const atMember = await getGroupMember(msg.peerUin, null, atUid) const atMember = await getGroupMember(msg.peerUin, null, atUid)
if (atMember){ if (atMember) {
atQQ = atMember.uin atQQ = atMember.uin
} }
} }
if (atQQ){ if (atQQ) {
message_data["data"]["mention"] = atQQ message_data["data"]["mention"] = atQQ
message_data["data"]["qq"] = atQQ message_data["data"]["qq"] = atQQ
} }
@@ -78,16 +88,15 @@ export class OB11Constructor {
try { try {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath) element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath)
}catch (e) { } catch (e) {
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl 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) {
@@ -104,10 +113,9 @@ 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.http_file){ if (message_data.data.http_file) {
message_data.data.file = message_data.data.http_file message_data.data.file = message_data.data.http_file
} } else if (message_data.data.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) {
@@ -125,24 +133,24 @@ export class OB11Constructor {
} }
return resMsg; return resMsg;
} }
static friend(friend: User): OB11User{ static friend(friend: User): OB11User {
return { return {
user_id: friend.uin, user_id: parseInt(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: parseInt(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)
} }
@@ -154,28 +162,52 @@ 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: parseInt(group_id),
user_id: member.uin, user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName card: member.cardName
} }
} }
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: parseInt(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)
} }
static lifeCycleEvent(): OB11LifeCycleEvent {
return {
time: Math.floor(Date.now() / 1000),
self_id: parseInt(selfInfo.uin),
post_type: "meta_event",
meta_event_type: "lifecycle",
sub_type: "connect"
}
}
static heartEvent(): OB11HeartEvent {
return {
time: Math.floor(Date.now() / 1000),
self_id: parseInt(selfInfo.uin),
post_type: "meta_event",
meta_event_type: "heartbeat",
status: {
online: true,
good: true
},
interval: heartInterval
}
}
} }

View File

@@ -1,73 +1,25 @@
import { getConfigUtil, log } from "../common/utils"; import * as http from "http";
import * as websocket from "ws";
const express = require("express"); import urlParse from "url";
import express from "express";
import { Request } from 'express'; import { Request } from 'express';
import { Response } from 'express'; import { Response } from 'express';
import { getConfigUtil, log } from "../common/utils";
const JSONbig = require('json-bigint')({ storeAsString: true }); import { heartInterval, selfInfo } from "../common/data";
import { selfInfo } from "../common/data"; import { OB11Message, OB11Return, OB11MessageData, OB11LifeCycleEvent, OB11MetaEvent } from './types';
import { OB11Message, OB11Return, OB11MessageData } from './types';
import { actionHandlers } from "./actions"; import { actionHandlers } from "./actions";
import { OB11Response } from "./actions/utils";
import { ActionName } from "./actions/types";
import BaseAction from "./actions/BaseAction";
import { OB11Constructor } from "./constructor";
let wsServer: websocket.Server = null;
let accessToken = ""
// @SiberianHusky 2021-08-15 const JSONbig = require('json-bigint')({storeAsString: true});
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}
// ==end==
class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: status,
data: data,
message: message
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data)
}
static error(err: string) {
return OB11Response.res(null, -1, err)
}
}
const expressAPP = express(); const expressAPP = express();
expressAPP.use(express.urlencoded({ extended: true, limit: "500mb" })); let httpServer: http.Server = null;
expressAPP.use(express.urlencoded({extended: true, limit: "500mb"}));
expressAPP.use((req, res, next) => { expressAPP.use((req, res, next) => {
let data = ''; let data = '';
@@ -86,29 +38,159 @@ expressAPP.use((req, res, next) => {
next(); next();
}); });
}); });
// expressAPP.use(express.json({
// limit: '500mb',
// verify: (req: any, res: any, buf: any, encoding: any) => {
// req.rawBody = buf;
// }
// }));
export function startExpress(port: number) { const expressAuthorize = (req: Request, res: Response, next: () => void) => {
let token = ""
const authHeader = req.get("authorization")
if (authHeader) {
token = authHeader.split("Bearer ").pop()
log("receive http header token", token)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
token = req.query.access_token[0].toString();
} else {
token = req.query.access_token.toString();
}
log("receive http url token", token)
}
if (accessToken) {
if (token != accessToken) {
return res.status(403).send(JSON.stringify({message: 'token verify failed!'}));
}
}
next();
};
export function setToken(token: string) {
accessToken = token
}
export function startHTTPServer(port: number) {
if (httpServer) {
httpServer.close();
}
expressAPP.get('/', (req: Request, res: Response) => { expressAPP.get('/', (req: Request, res: Response) => {
res.send('llonebot已启动'); res.send('LLOneBot已启动');
}) })
expressAPP.listen(port, "0.0.0.0", () => { httpServer = expressAPP.listen(port, "0.0.0.0", () => {
console.log(`llonebot started 0.0.0.0:${port}`); console.log(`LLOneBot http server started 0.0.0.0:${port}`);
}); });
} }
let wsEventClients: websocket.WebSocket[] = [];
type RouterHandler = (payload: any) => Promise<OB11Return<any>>
let routers: Record<string, RouterHandler> = {};
function wsReply(wsClient: websocket.WebSocket, data: OB11Return<any> | OB11Message | OB11MetaEvent) {
try {
wsClient.send(JSON.stringify(data))
log("ws 消息上报", data)
} catch (e) {
log("websocket 回复失败", e)
}
}
export function startWSServer(port: number) {
if (wsServer) {
wsServer.close((err) => {
log("ws server close failed!", err)
})
}
wsServer = new websocket.Server({port})
wsServer.on("connection", (ws, req) => {
const url = req.url;
log("received ws connect", url)
let token: string = ""
const authHeader = req.headers['authorization'];
if (authHeader) {
token = authHeader.split("Bearer ").pop()
log("receive ws header token", token);
} else {
const parsedUrl = urlParse.parse(url, true);
const urlToken = parsedUrl.query.access_token;
if (urlToken) {
if (Array.isArray(urlToken)) {
token = urlToken[0]
} else {
token = urlToken
}
log("receive ws url token", token);
}
}
if (accessToken) {
if (token != accessToken) {
ws.send(JSON.stringify(OB11Response.res(null, 1403, "token验证失败")))
return ws.close()
}
}
// const queryParams = querystring.parse(parsedUrl.query);
// let token = req
// ws.send('Welcome to the LLOneBot WebSocket server! url:' + url);
if (url == "/api" || url == "/api/" || url == "/") {
ws.on("message", async (msg) => {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}}
let echo = ""
log("收到ws消息", msg.toString())
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
} catch (e) {
return wsReply(ws, {...OB11Response.error("json解析失败请检查数据格式"), echo})
}
const handle: RouterHandler | undefined = routers[receiveData.action]
if (!handle) {
let handleResult = OB11Response.error("不支持的api " + receiveData.action, 1404)
handleResult.echo = echo
return wsReply(ws, handleResult)
}
try {
let handleResult = await handle(receiveData.params)
if (echo){
handleResult.echo = echo
}
wsReply(ws, handleResult)
} catch (e) {
wsReply(ws, OB11Response.error(`api处理出错:${e}`))
}
})
}
if (url == "/event" || url == "/event/" || url == "/") {
log("event上报ws客户端已连接")
wsEventClients.push(ws)
try {
wsReply(ws, OB11Constructor.lifeCycleEvent())
}catch (e){
log("发送生命周期失败", e)
}
// 心跳
let wsHeart = setInterval(()=>{
if (wsEventClients.find(c => c == ws)){
wsReply(ws, OB11Constructor.heartEvent())
}
}, heartInterval)
ws.on("close", () => {
clearInterval(wsHeart);
log("event上报ws客户端已断开")
wsEventClients = wsEventClients.filter((c) => c != ws)
})
}
})
}
export function postMsg(msg: OB11Message) { export function postMsg(msg: OB11Message) {
const { reportSelfMessage } = getConfigUtil().getConfig() const {reportSelfMessage} = getConfigUtil().getConfig()
if (!reportSelfMessage) { if (!reportSelfMessage) {
if (msg.user_id == selfInfo.uin) { if (msg.user_id.toString() == selfInfo.uin) {
return return
} }
} }
@@ -121,41 +203,46 @@ export function postMsg(msg: OB11Message) {
}, },
body: JSON.stringify(msg) body: JSON.stringify(msg)
}).then((res: any) => { }).then((res: any) => {
log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg)); log(`新消息事件HTTP上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => { }, (err: any) => {
log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg)); log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg));
}); });
} }
for (const wsClient of wsEventClients) {
log("新消息事件ws上报", msg)
new Promise((resolve, reject) => {
wsReply(wsClient, msg);
}).then();
}
} }
let routers: Record<string, (payload: any) => Promise<OB11Return<any>>> = {};
function registerRouter(action: string, handle: (payload: any) => Promise<any>) { function registerRouter(action: string, handle: (payload: any) => Promise<any>) {
let url = action.toString() let url = action.toString()
if (!action.startsWith("/")) { if (!action.startsWith("/")) {
url = "/" + action url = "/" + action
} }
async function _handle(res: Response, payload: any) { async function _handle(res: Response, payload: any) {
log("receive post data", url, payload) log("receive post data", url, payload)
try { try {
const result = await handle(payload) const result = await handle(payload)
res.send(result) res.send(result)
} } catch (e) {
catch (e) {
log(e.stack); log(e.stack);
res.send(OB11Response.error(e.stack.toString())) res.send(OB11Response.error(e.stack.toString()))
} }
} }
expressAPP.post(url, (req: Request, res: Response) => { expressAPP.post(url, expressAuthorize, (req: Request, res: Response) => {
_handle(res, req.body).then() _handle(res, req.body).then()
}); });
expressAPP.get(url, (req: Request, res: Response) => { expressAPP.get(url, expressAuthorize, (req: Request, res: Response) => {
_handle(res, req.query as any).then() _handle(res, req.query as any).then()
}); });
routers[url] = handle routers[action] = handle
} }
for (const action of actionHandlers) { for (const action of actionHandlers) {
registerRouter(action.actionName, (payload) => action.handle(payload)) registerRouter(action.actionName, (payload) => action.handle(payload))
} }

View File

@@ -2,7 +2,7 @@ import { AtType } from "../ntqqapi/types";
import { RawMessage } from "../ntqqapi/types"; import { RawMessage } from "../ntqqapi/types";
export interface OB11User{ export interface OB11User{
user_id: string; user_id: number;
nickname: string; nickname: string;
remark?: string remark?: string
} }
@@ -20,8 +20,8 @@ export enum OB11GroupMemberRole{
} }
export interface OB11GroupMember { export interface OB11GroupMember {
group_id: string group_id: number
user_id: string user_id: number
nickname: string nickname: string
card?: string card?: string
sex?: OB11UserSex sex?: OB11UserSex
@@ -34,14 +34,14 @@ export interface OB11GroupMember {
} }
export interface OB11Group{ export interface OB11Group{
group_id: string group_id: number
group_name: string group_name: string
member_count?: number member_count?: number
max_member_count?: number max_member_count?: number
} }
interface OB11Sender { interface OB11Sender {
user_id: string, user_id: number,
nickname: string, nickname: string,
sex?: OB11UserSex, sex?: OB11UserSex,
age?: number, age?: number,
@@ -56,14 +56,14 @@ export enum OB11MessageType {
} }
export interface OB11Message { export interface OB11Message {
self_id?: string, self_id?: number,
time: number, time: number,
message_id: string, message_id: number,
real_id: string, real_id: string,
user_id: string, user_id: number,
group_id?: string, group_id?: number,
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,
@@ -89,7 +89,8 @@ export interface OB11Return<DataType> {
status: number status: number
retcode: number retcode: number
data: DataType data: DataType
message: string message: string,
echo?: string
} }
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{}
@@ -139,4 +140,34 @@ export interface OB11PostSendMsg {
user_id: string, user_id: string,
group_id?: string, group_id?: string,
message: OB11MessageData[] | string | OB11MessageData; message: OB11MessageData[] | string | OB11MessageData;
}
export interface OB11Version {
app_name: "LLOneBot"
app_version: string
protocol_version: "v11"
}
export interface OB11MetaEvent {
time: number
self_id: number
post_type: "meta_event"
meta_event_type: "lifecycle" | "heartbeat"
}
export interface OB11LifeCycleEvent extends OB11MetaEvent{
meta_event_type: "lifecycle"
sub_type: "enable" | "disable" | "connect"
}
export interface OB11Status {
online: boolean | null,
good: boolean
}
export interface OB11HeartEvent extends OB11MetaEvent{
meta_event_type: "heartbeat"
status: OB11Status
interval: number
} }

View File

@@ -1,6 +1,7 @@
import { CONFIG_DIR, isGIF } from "../common/utils"; import { CONFIG_DIR, isGIF } from "../common/utils";
import * as path from 'path'; import * as path from 'path';
import { NTQQApi } from '../ntqqapi/ntcall'; import { NTQQApi } from '../ntqqapi/ntcall';
import { OB11MessageData } from "./types";
const fs = require("fs").promises; const fs = require("fs").promises;
export async function uri2local(fileName: string, uri: string){ export async function uri2local(fileName: string, uri: string){
@@ -39,11 +40,12 @@ export async function uri2local(fileName: string, uri: string){
} }
} else if (url.protocol === "file:"){ } else if (url.protocol === "file:"){
// await fs.copyFile(url.pathname, filePath); // await fs.copyFile(url.pathname, filePath);
let pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32"){ if (process.platform === "win32"){
filePath = url.pathname.slice(1) filePath = pathname.slice(1)
} }
else{ else{
filePath = url.pathname filePath = pathname
} }
res.isLocal = true res.isLocal = true
} }
@@ -58,4 +60,40 @@ export async function uri2local(fileName: string, uri: string){
res.success = true res.success = true
res.path = filePath res.path = filePath
return res return res
} }
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}

View File

@@ -29,16 +29,24 @@ async function onSettingWindowCreated(view: Element) {
<setting-panel> <setting-panel>
<setting-list class="wrap"> <setting-list class="wrap">
<setting-item class="vertical-list-item" data-direction="row"> <setting-item class="vertical-list-item" data-direction="row">
<setting-text>监听端口</setting-text> <setting-text>HTTP监听端口</setting-text>
<input id="port" type="number" value="${config.port}"/> <input id="port" type="number" value="${config.port}"/>
</setting-item> </setting-item>
<setting-item class="vertical-list-item" data-direction="row">
<setting-text>正向ws监听端口</setting-text>
<input id="wsPort" type="number" value="${config.wsPort}"/>
</setting-item>
<setting-item class="vertical-list-item" data-direction="row">
<setting-text>Access Token</setting-text>
<input id="token" type="text" placeholder="可为空" value="${config.token}"/>
</setting-item>
<div> <div>
<button id="addHost" class="q-button">添加上报地址</button> <button id="addHost" class="q-button">添加HTTP上报地址</button>
</div> </div>
<div id="hostItems"> <div id="hostItems">
${hostsEleStr} ${hostsEleStr}
</div> </div>
<button id="save" class="q-button">保存(监听端口重启QQ后生效)</button> <button id="save" class="q-button">保存</button>
</setting-list> </setting-list>
</setting-panel> </setting-panel>
<setting-panel> <setting-panel>
@@ -124,19 +132,26 @@ async function onSettingWindowCreated(view: Element) {
doc.getElementById("save")?.addEventListener("click", doc.getElementById("save")?.addEventListener("click",
() => { () => {
const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement
const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement
const hostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>; const hostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>;
const tokenEle = document.getElementById("token") as HTMLInputElement;
// const port = doc.querySelector("input[type=number]")?.value // const port = doc.querySelector("input[type=number]")?.value
// const host = doc.querySelector("input[type=text]")?.value // const host = doc.querySelector("input[type=text]")?.value
// 获取端口和host // 获取端口和host
const port = portEle.value const port = portEle.value
const wsPort = wsPortEle.value
const token = tokenEle.value
let hosts: string[] = []; let hosts: string[] = [];
for (const hostEle of hostEles) { for (const hostEle of hostEles) {
if (hostEle.value) { if (hostEle.value) {
hosts.push(hostEle.value); hosts.push(hostEle.value.trim());
} }
} }
config.port = parseInt(port); config.port = parseInt(port);
config.wsPort = parseInt(wsPort);
config.hosts = hosts; config.hosts = hosts;
config.token = token.trim();
window.llonebot.setConfig(config); window.llonebot.setConfig(config);
alert("保存成功"); alert("保存成功");
}) })