Merge pull request #1 from disymayufei/dev-1

合并开发分支
This commit is contained in:
Disy 2024-02-19 22:56:26 +08:00 committed by GitHub
commit 5cf9a6e942
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 835 additions and 480 deletions

View File

@ -18,7 +18,11 @@ LiteLoaderQQNT的OneBot11协议插件
## 支持的API ## 支持的API
目前只支持http协议不支持websocket事件上报也是http协议 目前支持的协议
- [x] http调用api
- [x] http事件上报
- [x] 正向websocket
- [ ] 反向websocket
主要功能: 主要功能:
- [x] 发送好友消息 - [x] 发送好友消息
@ -29,9 +33,11 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] 撤回消息 - [x] 撤回消息
- [x] 上报好友消息 - [x] 上报好友消息
- [x] 上报群消息 - [x] 上报群消息
- [x] 上报好友、群消息撤回
消息格式支持: 消息格式支持:
- [x] 文字 - [x] 文字
- [x] 表情
- [x] 图片 - [x] 图片
- [x] 引用消息 - [x] 引用消息
- [x] @群成员 - [x] @群成员
@ -53,6 +59,10 @@ LiteLoaderQQNT的OneBot11协议插件
- [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] get_status
- [x] can_send_image
- [x] can_send_record
## 示例 ## 示例
@ -98,9 +108,9 @@ LiteLoaderQQNT的OneBot11协议插件
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正向websocket
- [ ] 转发消息记录 - [ ] 转发消息记录
- [ ] 好友点赞api - [ ] 好友点赞api
- [ ] 支持websocket等个有缘人提PR实现
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>

View File

@ -1,31 +1,33 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi", "description": "LiteLoaderQQNT的OneBotApi",
"version": "3.0.8", "version": "3.4.0",
"thumbnail": "./icon.png", "thumbnail": "./icon.png",
"authors": [{ "authors": [
"name": "linyuchen", {
"link": "https://github.com/linyuchen" "name": "linyuchen",
}], "link": "https://github.com/linyuchen"
"repository": { }
],
"repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi", "repo": "linyuchen/LiteLoaderQQNT-OneBotApi",
"branch": "main", "branch": "main",
"release": { "release": {
"tag": "latest", "tag": "latest",
"name": "LLOneBot.zip" "name": "LLOneBot.zip"
}
},
"platform": [
"win32",
"linux",
"darwin"
],
"injects": {
"renderer": "./renderer.js",
"main": "./main.js",
"preload": "./preload.js"
} }
},
"platform": [
"win32",
"linux",
"darwin"
],
"injects": {
"renderer": "./renderer.js",
"main": "./main.js",
"preload": "./preload.js"
}
} }

8
package-lock.json generated
View File

@ -25,7 +25,8 @@
"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": {
@ -2204,7 +2205,7 @@
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.10", "version": "8.5.10",
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.10.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
@ -4739,8 +4740,9 @@
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.16.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"dev": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },

View File

@ -33,6 +33,7 @@
"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

@ -2,7 +2,7 @@ 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) {
@ -10,24 +10,46 @@ export class ConfigUtil{
} }
getConfig(): Config { getConfig(): Config {
let defaultConfig: Config = {
httpPort: 3000,
httpHosts: [],
wsPort: 3001,
wsHosts: [],
token: "",
enableBase64: false,
debug: false,
log: false,
reportSelfMessage: false
}
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
return { return defaultConfig
httpPort: 3000,
httpHosts: ["http://127.0.0.1:5000/"],
wsPort: 3001,
wsHosts: ["ws://127.0.0.1:3002/"]
}
} 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.hosts = []; jsonData = JSON.parse(data)
}
catch (e) {}
if (!jsonData.httpHosts) {
jsonData.httpHosts = []
}
if (!jsonData.wsHosts) {
jsonData.wsHosts = []
}
if (!jsonData.wsPort) {
jsonData.wsPort = 3001
}
if (!jsonData.httpPort) {
jsonData.httpPort = 3000
}
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,6 +1,5 @@
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[] = []
@ -87,3 +86,6 @@ export function getStrangerByUin(uin: string) {
} }
} }
} }
export const version = "v3.4.0"
export const heartInterval = 15000 // 毫秒

View File

@ -7,6 +7,7 @@ export interface Config {
enableHttpPost?: boolean enableHttpPost?: boolean
enableWs?: boolean enableWs?: boolean
enableWsReverse?: boolean enableWsReverse?: boolean
token?: string
enableBase64?: boolean enableBase64?: boolean
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean

View File

@ -2,7 +2,6 @@ import * as path from "path";
import {selfInfo} from "./data"; import {selfInfo} from "./data";
import {ConfigUtil} from "./config"; import {ConfigUtil} from "./config";
import util from "util"; import util from "util";
import { sendLog } from '../main/ipcsend';
const fs = require('fs'); const fs = require('fs');
@ -33,7 +32,7 @@ export function log(...msg: any[]) {
} }
logMsg += msgItem + " "; logMsg += msgItem + " ";
} }
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n` logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg); // sendLog(...msg);
// console.log(msg) // console.log(msg)
fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), logMsg, (err: any) => { fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), logMsg, (err: any) => {

2
src/global.d.ts vendored
View File

@ -1,4 +1,4 @@
import { Config } from "./common/types"; import {Config} from "./common/types";
declare var llonebot: { declare var llonebot: {

View File

@ -1,5 +1,5 @@
import {webContents} from 'electron'; import {webContents} from 'electron';
import { CHANNEL_LOG } from '../common/channels'; import {CHANNEL_LOG} from '../common/channels';
function sendIPCMsg(channel: string, ...data: any) { function sendIPCMsg(channel: string, ...data: any) {

View File

@ -1,22 +1,18 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import * as path from "path"; import {BrowserWindow, ipcMain} from 'electron';
import { BrowserWindow, ipcMain } from 'electron';
import * as util from 'util';
import { Config } from "../common/types"; import {Config} from "../common/types";
import { import {postMsg, setToken, startHTTPServer, initWebsocket} from "../onebot11/server";
CHANNEL_GET_CONFIG, import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "../common/channels";
CHANNEL_LOG, import {CONFIG_DIR, getConfigUtil, log} from "../common/utils";
CHANNEL_SET_CONFIG, import {addHistoryMsg, getGroupMember, msgHistory, selfInfo} from "../common/data";
} from "../common/channels"; import {hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {initWebsocket, postMsg, startExpress, startWebsocketServer} from "../onebot11/server"; import {OB11Constructor} from "../onebot11/constructor";
import { CONFIG_DIR, getConfigUtil, log } from "../common/utils"; import {NTQQApi} from "../ntqqapi/ntcall";
import { addHistoryMsg, msgHistory, selfInfo } from "../common/data"; import {ChatType, RawMessage} from "../ntqqapi/types";
import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import { OB11Constructor } from "../onebot11/constructor"; import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
import { NTQQApi } from "../ntqqapi/ntcall";
import { Group, RawMessage, SelfInfo } from "../ntqqapi/types";
const fs = require('fs'); const fs = require('fs');
@ -36,7 +32,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) => {
getConfigUtil().setConfig(arg); let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(arg)
if (arg.httpPort != oldConfig.httpPort) {
startHTTPServer(arg.httpPort)
}
if (arg.wsPort != oldConfig.wsPort) {
initWebsocket(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) => {
@ -47,7 +53,8 @@ function onLoad() {
function postRawMsg(msgList: RawMessage[]) { function postRawMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
message.msgShortId = msgHistory[message.msgId]?.msgShortId; log("收到新消息", message)
message.msgShortId = msgHistory[message.msgId]?.msgShortId
if (!message.msgShortId) { if (!message.msgShortId) {
addHistoryMsg(message); addHistoryMsg(message);
} }
@ -55,8 +62,8 @@ function onLoad() {
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);
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.toString()));
@ -64,8 +71,7 @@ function onLoad() {
} }
function start() { async function start() {
log("llonebot start");
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try { try {
postRawMsg(payload.msgList); postRawMsg(payload.msgList);
@ -73,7 +79,39 @@ function onLoad() {
log("report message error: ", e.toString()); log("report message error: ", e.toString());
} }
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
for (const message of payload.msgList) {
// log("message update", message, message.sendStatus)
if (message.sendStatus === 2) {
// 撤回消息上报
const oriMessage = msgHistory[message.msgId]
if (!oriMessage) {
continue
}
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postMsg(friendRecallEvent);
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, null, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId
)
postMsg(groupRecallEvent);
}
continue
}
addHistoryMsg(message)
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig(); const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) { if (!reportSelfMessage) {
@ -86,12 +124,13 @@ function onLoad() {
log("report self message error: ", e.toString()); log("report self message error: ", e.toString());
} }
}) })
NTQQApi.getGroups(true).then(); NTQQApi.getGroups(true).then()
const config = getConfigUtil().getConfig(); const config = getConfigUtil().getConfig()
startExpress(config.httpPort); startHTTPServer(config.httpPort)
startWebsocketServer(config.wsPort); initWebsocket(config.wsPort);
initWebsocket(); setToken(config.token)
log("LLOneBot start")
} }
const init = async () => { const init = async () => {
@ -116,10 +155,9 @@ function onLoad() {
log("get self nickname failed", e.toString()); log("get self nickname failed", e.toString());
return setTimeout(init, 1000); return setTimeout(init, 1000);
} }
start(); start().then();
} } else {
else{ setTimeout(init, 1000)
setTimeout(init, 1000);
} }
} }
setTimeout(init, 1000); setTimeout(init, 1000);
@ -131,7 +169,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

@ -1,4 +1,12 @@
import {ElementType, SendPicElement, SendPttElement, SendReplyElement, SendTextElement, AtType} from "./types"; import {
AtType,
ElementType,
SendFaceElement,
SendPicElement,
SendPttElement,
SendReplyElement,
SendTextElement
} from "./types";
import {NTQQApi} from "./ntcall"; import {NTQQApi} from "./ntcall";
@ -44,7 +52,7 @@ export class SendMsgElementConstructor {
} }
} }
static async pic(picPath: string): Promise<SendPicElement>{ static async pic(picPath: string): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath);
const imageSize = await NTQQApi.getImageSize(picPath); const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = { const picElement = {
@ -70,7 +78,7 @@ export class SendMsgElementConstructor {
}; };
} }
static async ptt(pttPath: string):Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(pttPath); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(pttPath);
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
@ -94,4 +102,15 @@ export class SendMsgElementConstructor {
} }
}; };
} }
static face(faceId: number): SendFaceElement {
return {
elementType: ElementType.FACE,
elementId: "",
faceElement: {
faceIndex: faceId,
faceType: 1
}
}
}
} }

View File

@ -1,15 +1,12 @@
import {BrowserWindow} from 'electron'; import {BrowserWindow} from 'electron';
import {log, sleep} from "../common/utils"; import {log, sleep} from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {Group, GroupMember, RawMessage, User} from "./types"; import {Group, RawMessage, User} from "./types";
import {addHistoryMsg, friends, groups, msgHistory} from "../common/data"; import {addHistoryMsg, friends, groups, msgHistory} from "../common/data";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import {callEvent, EventType} from "../onebot11/event/manager"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11Message} from "../onebot11/types"; import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {OB11Constructor} from "../onebot11/constructor"; import {postMsg} from "../onebot11/server";
import BaseMessageEvent from "../onebot11/event/BaseMessageEvent";
import GroupDecreaseEvent from "../onebot11/event/GroupDecreaseEvent";
import GroupIncreaseEvent from "../onebot11/event/GroupIncreaseEvent";
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@ -39,7 +36,7 @@ interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
let receiveHooks: Array<{ let receiveHooks: Array<{
method: ReceiveCmd, method: ReceiveCmd,
hookFunc: (payload: any) => void, hookFunc: ((payload: any) => void | Promise<void>)
id: string id: string
}> = [] }> = []
@ -55,7 +52,10 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
if (hook.method === ntQQApiMethodName) { if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
hook.hookFunc(receiveData.payload); let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction"){
(_ as Promise<void>).then()
}
} catch (e) { } catch (e) {
log("hook error", e, receiveData.payload) log("hook error", e, receiveData.payload)
} }
@ -137,7 +137,7 @@ async function processGroupEvent(payload) {
for (const member of oldMembers) { for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) { if (!newMembersSet.has(member.uin)) {
callEvent(new GroupDecreaseEvent(group.groupCode, parseInt(member.uin))); postMsg(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break; break;
} }
} }
@ -156,7 +156,7 @@ async function processGroupEvent(payload) {
group.members = newMembers; group.members = newMembers;
for (const member of newMembers) { for (const member of newMembers) {
if (!oldMembersSet.has(member.uin)) { if (!oldMembersSet.has(member.uin)) {
callEvent(new GroupIncreaseEvent(group.groupCode, parseInt(member.uin))); postMsg(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)));
break; break;
} }
} }
@ -208,22 +208,10 @@ registerReceiveHook<{
} }
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, (payload) => {
for (const message of payload.msgList) {
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)
addHistoryMsg(message) addHistoryMsg(message)
OB11Constructor.message(message).then(
function (message) {
callEvent<OB11Message>(new BaseMessageEvent(), message);
}
);
} }
const msgIds = Object.keys(msgHistory); const msgIds = Object.keys(msgHistory);
if (msgIds.length > 30000) { if (msgIds.length > 30000) {

View File

@ -1,12 +1,8 @@
import { ipcMain } from "electron"; import {ipcMain} from "electron";
import { v4 as uuidv4 } from "uuid"; import {v4 as uuidv4} from "uuid";
import { ReceiveCmd, hookApiCallbacks, registerReceiveHook, removeReceiveHook } from "./hook"; import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import { log } from "../common/utils"; import {log} from "../common/utils";
import { ChatType, Friend, PicElement, SelfInfo, User } from "./types"; import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types";
import { Group } from "./types";
import { GroupMember } from "./types";
import { RawMessage } from "./types";
import { SendMessageElement } from "./types";
import * as fs from "fs"; import * as fs from "fs";
interface IPCReceiveEvent { interface IPCReceiveEvent {

View File

@ -7,13 +7,14 @@ export interface User {
remark?: string remark?: string
} }
export interface SelfInfo extends User{ export interface SelfInfo extends User {
} }
export interface Friend extends User{} export interface Friend extends User {
}
export interface Group{ export interface Group {
groupCode: string, groupCode: string,
maxMember: number, maxMember: number,
memberCount: number, memberCount: number,
@ -62,6 +63,7 @@ export enum ElementType {
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
PTT = 4, PTT = 4,
FACE = 6,
REPLY = 7, REPLY = 7,
} }
@ -76,6 +78,7 @@ export interface SendTextElement {
atNtUid: string, atNtUid: string,
} }
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT, elementType: ElementType.PTT,
elementId: "", elementId: "",
@ -127,7 +130,13 @@ export interface SendReplyElement {
} }
} }
export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement export interface SendFaceElement {
elementType: ElementType.FACE,
elementId: "",
faceElement: FaceElement
}
export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement | SendFaceElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
@ -140,6 +149,7 @@ export enum ChatType {
group = 2, group = 2,
temp = 100 temp = 100
} }
export interface PttElement { export interface PttElement {
canConvert2Text: boolean; canConvert2Text: boolean;
duration: number; // 秒数 duration: number; // 秒数
@ -180,6 +190,22 @@ export interface PicElement {
fileUuid: string; fileUuid: string;
} }
export interface GrayTipElement {
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
}
export interface FaceElement {
faceIndex: number,
faceType: 1
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string;
msgShortId?: number; // 自己维护的消息id msgShortId?: number; // 自己维护的消息id
@ -191,6 +217,7 @@ export interface RawMessage {
sendNickName: string; sendNickName: string;
sendMemberName?: string; // 发送者群名片 sendMemberName?: string; // 发送者群名片
chatType: ChatType; chatType: ChatType;
sendStatus?: number; // 消息状态2是已撤回
elements: { elements: {
elementId: string, elementId: string,
replyElement: { replyElement: {
@ -208,17 +235,7 @@ export interface RawMessage {
picElement: PicElement; picElement: PicElement;
pttElement: PttElement; pttElement: PttElement;
arkElement: ArkElement; arkElement: ArkElement;
grayTipElement: GrayTipElement;
faceElement: FaceElement;
}[]; }[];
} }
export interface MessageElement {
raw: RawMessage;
peer: any;
sender: {
uid: string; // 一串加密的字符串
memberName: string;
nickname: string;
};
}

View File

@ -1,6 +1,8 @@
import {log} from "../common/utils";
const WebSocket = require("ws"); const WebSocket = require("ws");
class ReconnectingWebsocket { export class ReconnectingWebsocket {
private websocket; private websocket;
private readonly url: string; private readonly url: string;
@ -15,14 +17,6 @@ class ReconnectingWebsocket {
public onclose = function () {} public onclose = function () {}
private heartbeat() {
clearTimeout(this.websocket.pingTimeout);
this.websocket.pingTimeout = setTimeout(() => {
this.websocket.terminate();
}, 3000);
}
public send(msg) { public send(msg) {
if (this.websocket && this.websocket.readyState == WebSocket.OPEN) { if (this.websocket && this.websocket.readyState == WebSocket.OPEN) {
this.websocket.send(msg); this.websocket.send(msg);
@ -50,15 +44,13 @@ class ReconnectingWebsocket {
this.websocket.on("error", console.error); this.websocket.on("error", console.error);
this.websocket.on("ping", this.heartbeat);
this.websocket.on("close", function close() { this.websocket.on("close", function close() {
console.log("The websocket connection: " + instance.url + " closed, trying reconnecting..."); console.log("The websocket connection: " + instance.url + " closed, trying reconnecting...");
instance.onclose(); instance.onclose();
setTimeout(instance.reconnect, 3000); setTimeout(() => {
instance.reconnect();
}, 3000); // TODO: 重连间隔在配置文件中实现
}); });
} }
} }
export default ReconnectingWebsocket;

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,7 +1,7 @@
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 { getHistoryMsgByShortId, msgHistory } from "../../common/data"; import {getHistoryMsgByShortId} from "../../common/data";
interface Payload { interface Payload {
message_id: number message_id: number

View File

@ -1,8 +1,8 @@
import { OB11User } from '../types'; import {OB11User} from '../types';
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import { friends } from "../../common/data"; import {friends} from "../../common/data";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
class GetFriendList extends BaseAction<null, OB11User[]> { class GetFriendList extends BaseAction<null, OB11User[]> {

View File

@ -1,20 +1,20 @@
import { OB11Group } from '../types'; import {OB11Group} from '../types';
import { getGroup, groups } from "../../common/data"; import {getGroup} from "../../common/data";
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
interface PayloadType { interface PayloadType {
group_id: number group_id: number
} }
class GetGroupInfo extends BaseAction<PayloadType, OB11Group[]> { class GetGroupInfo extends BaseAction<PayloadType, OB11Group> {
actionName = ActionName.GetGroupInfo actionName = ActionName.GetGroupInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString()) const group = await getGroup(payload.group_id.toString())
if (group) { if (group) {
return OB11Constructor.groups(groups) return OB11Constructor.group(group)
} else { } else {
throw `${payload.group_id}不存在` throw `${payload.group_id}不存在`
} }

View File

@ -1,9 +1,8 @@
import { OB11Group } from '../types'; import {OB11Group} from '../types';
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import { groups } from "../../common/data"; import {groups} from "../../common/data";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
class GetGroupList extends BaseAction<null, OB11Group[]> { class GetGroupList extends BaseAction<null, OB11Group[]> {

View File

@ -1,8 +1,8 @@
import { OB11GroupMember } from '../types'; import {OB11GroupMember} from '../types';
import { getGroupMember } from "../../common/data"; import {getGroupMember} from "../../common/data";
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
export interface PayloadType { export interface PayloadType {

View File

@ -1,9 +1,9 @@
import { OB11GroupMember } from '../types'; import {OB11GroupMember} from '../types';
import { getGroup } from "../../common/data"; import {getGroup} from "../../common/data";
import { NTQQApi } from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
export interface PayloadType { export interface PayloadType {
group_id: number group_id: number

View File

@ -1,8 +1,8 @@
import { OB11User } from '../types'; import {OB11User} from '../types';
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import { selfInfo } from "../../common/data"; import {selfInfo} from "../../common/data";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
class GetLoginInfo extends BaseAction<null, OB11User> { class GetLoginInfo extends BaseAction<null, OB11User> {

View File

@ -1,8 +1,8 @@
import { getHistoryMsgByShortId, msgHistory } from "../../common/data"; import {getHistoryMsgByShortId} from "../../common/data";
import { OB11Message } from '../types'; import {OB11Message} from '../types';
import { OB11Constructor } from "../constructor"; import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
export interface PayloadType { export interface PayloadType {
@ -16,6 +16,9 @@ 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));
if (!payload.message_id){
throw("参数message_id不能为空")
}
const msg = getHistoryMsgByShortId(payload.message_id) const msg = getHistoryMsgByShortId(payload.message_id)
if (msg) { if (msg) {
const msgData = await OB11Constructor.message(msg); const msgData = await OB11Constructor.message(msg);

View File

@ -0,0 +1,14 @@
import BaseAction from "./BaseAction";
import {OB11Status} from "../types";
import {ActionName} from "./types";
export default class GetStatus extends BaseAction<any, OB11Status> {
actionName = ActionName.GetStatus
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,5 @@
import SendMsg from "./SendMsg"; import SendMsg from "./SendMsg";
import { ActionName } from "./types"; import {ActionName} from "./types";
class SendGroupMsg extends SendMsg{ class SendGroupMsg extends SendMsg{

View File

@ -1,20 +1,12 @@
import { AtType, ChatType, Group } from "../../ntqqapi/types"; import {AtType, ChatType, Group, SendMessageElement} from "../../ntqqapi/types";
import { import {addHistoryMsg, friends, getGroup, getHistoryMsgByShortId, getStrangerByUin,} from "../../common/data";
addHistoryMsg, import {OB11MessageData, OB11MessageDataType, OB11PostSendMsg} from '../types';
friends, import {NTQQApi, Peer} from "../../ntqqapi/ntcall";
getGroup, import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
getHistoryMsgByShortId, import {uri2local} from "../utils";
getStrangerByUin, import {v4 as uuid4} from 'uuid';
} from "../../common/data";
import { OB11MessageData, OB11MessageDataType, OB11PostSendMsg } from '../types';
import { NTQQApi } from "../../ntqqapi/ntcall";
import { Peer } from "../../ntqqapi/ntcall";
import { SendMessageElement } from "../../ntqqapi/types";
import { SendMsgElementConstructor } from "../../ntqqapi/constructor";
import { uri2local } from "../utils";
import { v4 as uuid4 } from 'uuid';
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { ActionName } from "./types"; import {ActionName} from "./types";
import * as fs from "fs"; import * as fs from "fs";
export interface ReturnDataType { export interface ReturnDataType {
@ -24,7 +16,7 @@ export interface ReturnDataType {
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
protected async _handle(payload: OB11PostSendMsg){ protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = { const peer: Peer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: "" peerUid: ""
@ -39,18 +31,16 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
peer.chatType = ChatType.group peer.chatType = ChatType.group
// peer.name = group.name // peer.name = group.name
peer.peerUid = group.groupCode peer.peerUid = group.groupCode
} } else if (payload?.user_id) {
else if (payload?.user_id) {
const friend = friends.find(f => f.uin == payload.user_id.toString()) const friend = friends.find(f => f.uin == payload.user_id.toString())
if (friend) { if (friend) {
// peer.name = friend.nickName // peer.name = friend.nickName
peer.peerUid = friend.uid peer.peerUid = friend.uid
} } else {
else {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
const tempUser = getStrangerByUin(payload.user_id.toString()) const tempUser = getStrangerByUin(payload.user_id.toString())
if (!tempUser) { if (!tempUser) {
throw(`找不到私聊对象${payload.user_id}`) throw (`找不到私聊对象${payload.user_id}`)
} }
// peer.name = tempUser.nickName // peer.name = tempUser.nickName
peer.peerUid = tempUser.uid peer.peerUid = tempUser.uid
@ -63,8 +53,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
text: payload.message text: payload.message
} }
}] as OB11MessageData[] }] as OB11MessageData[]
} } else if (!Array.isArray(payload.message)) {
else if (!Array.isArray(payload.message)) {
payload.message = [payload.message] payload.message = [payload.message]
} }
const sendElements: SendMessageElement[] = [] const sendElements: SendMessageElement[] = []
@ -75,22 +64,23 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (text) { if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text)) sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
} }
} break; }
break;
case OB11MessageDataType.at: { case OB11MessageDataType.at: {
let atQQ = sendMsg.data?.qq; let atQQ = sendMsg.data?.qq;
if (atQQ) { if (atQQ) {
atQQ = atQQ.toString() atQQ = atQQ.toString()
if (atQQ === "all") { if (atQQ === "all") {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员")) sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员"))
} } else {
else {
const atMember = group?.members.find(m => m.uin == atQQ) const atMember = group?.members.find(m => m.uin == atQQ)
if (atMember) { if (atMember) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick)) sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick))
} }
} }
} }
} break; }
break;
case OB11MessageDataType.reply: { case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id; let replyMsgId = sendMsg.data.id;
if (replyMsgId) { if (replyMsgId) {
@ -100,20 +90,27 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin)) sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin))
} }
} }
} break; }
break;
case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)))
}
}
break;
case OB11MessageDataType.image: case OB11MessageDataType.image:
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const file = sendMsg.data?.file const file = sendMsg.data?.file
if (file) { if (file) {
const {path, isLocal} = (await uri2local(uuid4(), file)) const {path, isLocal} = (await uri2local(uuid4(), file))
if (path) { if (path) {
if (!isLocal){ // 只删除http和base64转过来的文件 if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path) deleteAfterSentFiles.push(path)
} }
if (sendMsg.type === OB11MessageDataType.image){ if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(await SendMsgElementConstructor.pic(path)) sendElements.push(await SendMsgElementConstructor.pic(path))
} } else {
else {
sendElements.push(await SendMsgElementConstructor.ptt(path)) sendElements.push(await SendMsgElementConstructor.ptt(path))
} }
} }
@ -125,10 +122,11 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
try { try {
const returnMsg = await NTQQApi.sendMsg(peer, sendElements) const returnMsg = await NTQQApi.sendMsg(peer, sendElements)
addHistoryMsg(returnMsg) addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f=>fs.unlink(f, ()=>{})) deleteAfterSentFiles.map(f => fs.unlink(f, () => {
return { message_id: returnMsg.msgShortId } }))
return {message_id: returnMsg.msgShortId}
} catch (e) { } catch (e) {
throw(e.toString()) throw (e.toString())
} }
} }
} }

View File

@ -1,5 +1,5 @@
import SendMsg from "./SendMsg"; import SendMsg from "./SendMsg";
import { ActionName } from "./types"; import {ActionName} from "./types";
class SendPrivateMsg extends SendMsg { class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg actionName = ActionName.SendPrivateMsg

View File

@ -10,6 +10,10 @@ import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg' import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg"; import DeleteMsg from "./DeleteMsg";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus";
export const actionHandlers = [ export const actionHandlers = [
new GetMsg(), new GetMsg(),
@ -17,7 +21,11 @@ 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(),
new GetStatus()
] ]
function initActionMap() { function initActionMap() {

View File

@ -11,7 +11,7 @@ export interface InvalidCheckResult {
[k: string | number]: any [k: string | number]: any
} }
export enum ActionName{ export enum ActionName {
GetLoginInfo = "get_login_info", GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list", GetFriendList = "get_friend_list",
GetGroupInfo = "get_group_info", GetGroupInfo = "get_group_info",
@ -22,5 +22,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,15 +1,8 @@
import { import {OB11Group, OB11GroupMember, OB11GroupMemberRole, OB11Message, OB11MessageDataType, OB11User} from "./types";
OB11MessageDataType, import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types';
OB11GroupMemberRole, import {getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo} from '../common/data';
OB11Message, import {file2base64, getConfigUtil, log} from "../common/utils";
OB11Group, import {NTQQApi} from "../ntqqapi/ntcall";
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 {
@ -18,14 +11,14 @@ export class OB11Constructor {
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.msgShortId, 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 || "",
}, },
@ -37,10 +30,11 @@ export class OB11Constructor {
} }
if (msg.chatType == ChatType.group) { if (msg.chatType == ChatType.group) {
resMsg.sub_type = "normal" resMsg.sub_type = "normal"
resMsg.group_id = msg.peerUin 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"
@ -111,6 +105,9 @@ export class OB11Constructor {
} else if (element.arkElement) { } else if (element.arkElement) {
message_data["type"] = OB11MessageDataType.json; message_data["type"] = OB11MessageDataType.json;
message_data["data"]["data"] = element.arkElement.bytesData; message_data["data"]["data"] = element.arkElement.bytesData;
} else if (element.faceElement){
message_data["type"] = OB11MessageDataType.face;
message_data["data"]["id"] = element.faceElement.faceIndex.toString();
} }
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
@ -135,7 +132,7 @@ export class OB11Constructor {
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
} }
@ -144,7 +141,7 @@ export class OB11Constructor {
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
} }
} }
@ -163,8 +160,8 @@ export class OB11Constructor {
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
} }
@ -177,8 +174,10 @@ export class OB11Constructor {
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,
member_count: group.memberCount,
max_member_count: group.maxMember
} }
} }

View File

@ -1,10 +0,0 @@
import {selfInfo} from "../../common/data";
import {EventType} from "./manager";
class BaseEvent {
time = new Date().getTime();
self_id = selfInfo.uin;
post_type: EventType;
}
export default BaseEvent;

View File

@ -1,8 +0,0 @@
import BaseEvent from "./BaseEvent";
import {EventType} from "./manager";
class BaseMessageEvent extends BaseEvent {
post_type = EventType.MESSAGE;
}
export default BaseMessageEvent

View File

@ -0,0 +1,15 @@
import {selfInfo} from "../../common/data";
export enum EventType {
META = "meta_event",
REQUEST = "request",
NOTICE = "notice",
MESSAGE = "message"
}
export abstract class OB11BaseEvent {
time = new Date().getTime();
self_id = selfInfo.uin;
post_type: EventType;
}

View File

@ -1,32 +1,24 @@
import {selfInfo} from "../../common/data"; import * as websocket from "ws";
import BaseEvent from "./BaseEvent"; import {PostMsgType, wsReply} from "../server";
import {ReconnectingWebsocket} from "../ReconnectingWebsocket";
const websocketList = []; const websocketList = [];
export enum EventType { export function registerEventSender(ws: websocket.WebSocket | ReconnectingWebsocket) {
META = "meta_event",
REQUEST = "request",
NOTICE = "notice",
MESSAGE = "message"
}
export function registerEventSender(ws) {
websocketList.push(ws); websocketList.push(ws);
} }
export function unregisterEventSender(ws) { export function unregisterEventSender(ws: websocket.WebSocket | ReconnectingWebsocket) {
let index = websocketList.indexOf(ws); let index = websocketList.indexOf(ws);
if (index !== -1) { if (index !== -1) {
websocketList.splice(index, 1); websocketList.splice(index, 1);
} }
} }
export function callEvent<DataType>(event: BaseEvent, data: DataType = null) { export function callEvent(event: PostMsgType) {
new Promise(() => {
const assignedEvent = (data == null ? event : Object.assign(event, data)); for (const ws of websocketList) {
for (const ws of websocketList) { wsReply(ws, event);
ws.send( }
JSON.stringify(assignedEvent) }).then()
);
}
} }

View File

@ -0,0 +1,5 @@
import {EventType, OB11BaseEvent} from "../OB11BaseEvent";
export abstract class OB11BaseMessageEvent extends OB11BaseEvent {
post_type = EventType.MESSAGE;
}

View File

@ -0,0 +1,6 @@
import {EventType, OB11BaseEvent} from "../OB11BaseEvent";
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META;
meta_event_type: string;
}

View File

@ -0,0 +1,21 @@
import {OB11BaseMetaEvent} from "./OB11BaseMetaEvent";
interface HeartbeatStatus {
online: boolean | null,
good: boolean
}
export class OB11HeartbeatEvent extends OB11BaseMetaEvent {
meta_event_type = "heartbeat";
status: HeartbeatStatus;
interval: number;
public constructor(isOnline: boolean | null, isGood: boolean, interval: number) {
super();
this.interval = interval;
this.status = {
online: isOnline,
good: isGood
}
}
}

View File

@ -0,0 +1,17 @@
import {OB11BaseMetaEvent} from "./OB11BaseMetaEvent";
export enum LifeCycleSubType {
ENABLE = "enable",
DISABLE = "disable",
CONNECT = "connect"
}
export class OB11LifeCycleEvent extends OB11BaseMetaEvent {
meta_event_type = "lifecycle";
sub_type: LifeCycleSubType;
public constructor(subType: LifeCycleSubType) {
super();
this.sub_type = subType;
}
}

View File

@ -0,0 +1,5 @@
import {EventType, OB11BaseEvent} from "../OB11BaseEvent";
export abstract class OB11BaseNoticeEvent extends OB11BaseEvent {
post_type = EventType.NOTICE;
}

View File

@ -0,0 +1,13 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
export class OB11FriendRecallNoticeEvent extends OB11BaseNoticeEvent {
notice_type = "friend_recall"
user_id: number
message_id: number
public constructor(userId: number, messageId: number) {
super();
this.user_id = userId;
this.message_id = messageId;
}
}

View File

@ -0,0 +1,6 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
export class OB11GroupAdminNoticeEvent extends OB11BaseNoticeEvent {
notice_type = "group_admin"
sub_type: string // "set" | "unset"
}

View File

@ -1,13 +1,9 @@
import BaseEvent from "./BaseEvent"; import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
import {EventType} from "./manager";
class GroupDecreaseEvent extends BaseEvent { export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
post_type = EventType.NOTICE;
notice_type = "group_decrease"; notice_type = "group_decrease";
subtype = "leave"; // TODO: 实现其他几种子类型的识别 sub_type = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
group_id: number;
operate_id: number; operate_id: number;
user_id: number;
constructor(groupId: number, userId: number) { constructor(groupId: number, userId: number) {
super(); super();
@ -16,5 +12,3 @@ class GroupDecreaseEvent extends BaseEvent {
this.user_id = userId; this.user_id = userId;
} }
} }
export default GroupDecreaseEvent

View File

@ -1,13 +1,9 @@
import BaseEvent from "./BaseEvent"; import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
import {EventType} from "./manager";
class GroupIncreaseEvent extends BaseEvent { export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
post_type = EventType.NOTICE;
notice_type = "group_increase"; notice_type = "group_increase";
subtype = "approve"; // TODO: 实现其他几种子类型的识别 sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite")
group_id: number;
operate_id: number; operate_id: number;
user_id: number;
constructor(groupId: number, userId: number) { constructor(groupId: number, userId: number) {
super(); super();
@ -16,6 +12,3 @@ class GroupIncreaseEvent extends BaseEvent {
this.user_id = userId; this.user_id = userId;
} }
} }
export default GroupIncreaseEvent

View File

@ -0,0 +1,6 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number;
user_id: number;
}

View File

@ -0,0 +1,16 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent {
notice_type = "group_recall"
operator_id: number
message_id: number
constructor(groupId: number, userId: number, operatorId: number, messageId: number) {
super();
this.group_id = groupId;
this.user_id = userId;
this.operator_id = operatorId;
this.message_id = messageId;
}
}

View File

@ -1,26 +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";
const expressWs = require("express-ws"); import express, {Request, Response} from "express";
import {getConfigUtil, log} from "../common/utils";
import { Request } from 'express'; import {heartInterval, selfInfo} from "../common/data";
import { Response } from 'express'; import {OB11Message, OB11MessageData, OB11Return} from './types';
const JSONbig = require('json-bigint')({ storeAsString: true });
import { selfInfo } from "../common/data";
import { OB11Message, OB11Return, OB11MessageData } from './types';
import {actionHandlers, actionMap} from "./actions"; import {actionHandlers, actionMap} from "./actions";
import {OB11Response, OB11WebsocketResponse} from "./actions/utils"; import {OB11Response, OB11WebsocketResponse} from "./actions/utils";
import {registerEventSender, unregisterEventSender} from "./event/manager"; import {callEvent, registerEventSender, unregisterEventSender} from "./event/manager";
import ReconnectingWebsocket from "./ReconnectingWebsocket"; import {ReconnectingWebsocket} from "./ReconnectingWebsocket";
import {ActionName} from "./actions/types";
import {OB11BaseMetaEvent} from "./event/meta/OB11BaseMetaEvent";
import {OB11BaseNoticeEvent} from "./event/notice/OB11BaseNoticeEvent";
import BaseAction from "./actions/BaseAction";
import {LifeCycleSubType, OB11LifeCycleEvent} from "./event/meta/OB11LifeCycleEvent";
import {OB11HeartbeatEvent} from "./event/meta/OB11HeartbeatEvent";
let accessToken = "";
let heartbeatRunning = false;
// @SiberianHusky 2021-08-15 // @SiberianHusky 2021-08-15
enum WebsocketType {
API,
EVENT,
ALL
}
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@ -59,11 +58,14 @@ function checkSendMessage(sendMsgList: OB11MessageData[]) {
// ==end== // ==end==
const expressAPP = express(); const JSONbig = require('json-bigint')({storeAsString: true});
expressAPP.use(express.urlencoded({ extended: true, limit: "500mb" }));
const expressWsApp = express(); const expressAPP = express();
const websocketClientConnections = []; expressAPP.use(express.urlencoded({extended: true, limit: "500mb"}));
let httpServer: http.Server = null;
let websocketServer = null;
expressAPP.use((req, res, next) => { expressAPP.use((req, res, next) => {
let data = ''; let data = '';
@ -83,185 +85,258 @@ expressAPP.use((req, res, next) => {
}); });
}); });
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已启动');
}) })
if (getConfigUtil().getConfig().enableHttp) { if (getConfigUtil().getConfig().enableHttp) {
expressAPP.listen(port, "0.0.0.0", () => { httpServer = expressAPP.listen(port, "0.0.0.0", () => {
console.log(`llonebot http service started 0.0.0.0:${port}`); console.log(`llonebot http service started 0.0.0.0:${port}`);
}); });
} }
} }
export function startWebsocketServer(port: number) { export function initWebsocket(port: number) {
const config = getConfigUtil().getConfig(); if (!heartbeatRunning) {
if (config.enableWs) { setInterval(() => {
try { callEvent(new OB11HeartbeatEvent(true, true, heartInterval));
expressWs(expressWsApp) }, heartInterval); // 心跳包
expressWsApp.listen(getConfigUtil().getConfig().wsPort, function () {
console.log(`llonebot websocket service started 0.0.0.0:${port}`); heartbeatRunning = true;
}); }
}
catch (e) {
console.log(e);
}
}
}
export function initWebsocket() {
if (getConfigUtil().getConfig().enableWs) { if (getConfigUtil().getConfig().enableWs) {
expressWsApp.ws("/api", (ws, req) => { if (websocketServer) {
initWebsocketServer(ws, req, WebsocketType.API); websocketServer.close((err) => {
}); log("ws server close failed!", err)
expressWsApp.ws("/event", (ws, req) => { })
initWebsocketServer(ws, req, WebsocketType.EVENT); }
});
expressWsApp.ws("/", (ws, req) => { websocketServer = new websocket.Server({port});
initWebsocketServer(ws, req, WebsocketType.ALL); console.log(`llonebot websocket service started 0.0.0.0:${port}`);
});
websocketServer.on("connection", (ws, req) => {
const url = req.url.split("?").shift();
log("receive 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(req.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(OB11WebsocketResponse.res(null, "failed", 1403, "token验证失败")))
return ws.close()
}
}
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("收到正向Websocket消息", msg.toString())
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
} catch (e) {
return wsReply(ws, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo))
}
const action: BaseAction<any, any> = actionMap.get(receiveData.action);
if (!action) {
return wsReply(ws, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(ws, handleResult)
} catch (e) {
wsReply(ws, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo))
}
})
}
if (url == "/event" || url == "/event/" || url == "/") {
registerEventSender(ws);
log("event上报ws客户端已连接")
try {
wsReply(ws, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e){
log("发送生命周期失败", e)
}
ws.on("close", () => {
log("event上报ws客户端已断开")
unregisterEventSender(ws);
})
}
})
} }
initReverseWebsocket(); initReverseWebsocket();
} }
function initReverseWebsocket() { function initReverseWebsocket() {
const config = getConfigUtil().getConfig(); const config = getConfigUtil().getConfig();
if (config.enableWsReverse) { if (config.enableWsReverse) {
console.log("Prepare to connect all reverse websockets...");
for (const url of config.wsHosts) { for (const url of config.wsHosts) {
try { new Promise(() => {
let wsClient = new ReconnectingWebsocket(url); try {
websocketClientConnections.push(wsClient); let wsClient = new ReconnectingWebsocket(url);
registerEventSender(wsClient); registerEventSender(wsClient);
wsClient.onclose = function () { wsClient.onopen = function () {
console.log("The websocket connection: " + url + " closed, trying reconnecting..."); wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT));
unregisterEventSender(wsClient);
let index = websocketClientConnections.indexOf(wsClient);
if (index !== -1) {
websocketClientConnections.splice(index, 1);
} }
}
wsClient.onmessage = async function (message) { wsClient.onclose = function () {
console.log(message); unregisterEventSender(wsClient);
if (typeof message === "string") { }
wsClient.onmessage = async function (msg) {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}}
let echo = ""
log("收到正向Websocket消息", msg.toString())
try { try {
let recv = JSON.parse(message); receiveData = JSON.parse(msg.toString())
let echo = recv.echo ?? ""; echo = receiveData.echo
if (actionMap.has(recv.action)) {
let action = actionMap.get(recv.action);
const result = await action.websocketHandle(recv.params, echo);
wsClient.send(JSON.stringify(result));
}
else {
wsClient.send(JSON.stringify(OB11WebsocketResponse.error("Bad Request", 1400, echo)));
}
} catch (e) { } catch (e) {
log(e.stack); return wsReply(wsClient, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo))
wsClient.send(JSON.stringify(OB11WebsocketResponse.error(e.stack.toString(), 1200))); }
const action: BaseAction<any, any> = actionMap.get(receiveData.action);
if (!action) {
return wsReply(wsClient, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(wsClient, handleResult)
} catch (e) {
wsReply(wsClient, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo))
} }
} }
} }
} catch (e) {
catch (e) { log(e.stack);
console.log(e); }
} }).then();
} }
} }
} }
function initWebsocketServer(ws, req, type: WebsocketType) { export function wsReply(wsClient: websocket.WebSocket | ReconnectingWebsocket, data: OB11WebsocketResponse | PostMsgType) {
if (type == WebsocketType.EVENT || type == WebsocketType.ALL) { try {
registerEventSender(ws); let packet = Object.assign({
echo: ""
}, data);
if (!packet.echo) {
packet.echo = "";
}
wsClient.send(JSON.stringify(packet))
log("ws 消息上报", data)
} catch (e) {
log("websocket 回复失败", e)
} }
ws.on("message", async function (message) {
if (type == WebsocketType.API || type == WebsocketType.ALL) {
try {
let recv = JSON.parse(message);
let echo = recv.echo ?? "";
if (actionMap.has(recv.action)) {
let action = actionMap.get(recv.action)
const result = await action.websocketHandle(recv.params, echo);
ws.send(JSON.stringify(result));
}
else {
ws.send(JSON.stringify(OB11WebsocketResponse.error("Bad Request", 1400, echo)));
}
} catch (e) {
log(e.stack);
ws.send(JSON.stringify(OB11WebsocketResponse.error(e.stack.toString(), 1200)));
}
}
});
ws.on("close", function (ev) {
if (type == WebsocketType.EVENT || type == WebsocketType.ALL) {
unregisterEventSender(ws);
}
});
} }
export type PostMsgType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
export function postMsg(msg: OB11Message) { export function postMsg(msg: PostMsgType) {
const config = getConfigUtil().getConfig(); const config = getConfigUtil().getConfig();
if (config.enableHttpPost) { // 判断msg是否是event
if (!config.reportSelfMessage) { if (!config.reportSelfMessage) {
if (msg.user_id == selfInfo.uin) { if ((msg as OB11Message).user_id.toString() == selfInfo.uin) {
return return
}
}
for (const host of config.httpHosts) {
fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => {
log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg));
});
} }
} }
for (const host of config.httpHosts) {
fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => {
log(`新消息事件HTTP上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg));
});
}
log("新消息事件ws上报", msg);
callEvent(msg);
} }
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(), 200)) res.send(OB11Response.error(e.stack.toString(), 200))
} }
} }
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
} }
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

@ -1,8 +1,7 @@
import { AtType } from "../ntqqapi/types"; import {AtType, 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 +19,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 +33,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,12 +55,12 @@ export enum OB11MessageType {
} }
export interface OB11Message { export interface OB11Message {
self_id?: string, self_id?: number,
time: number, time: number,
message_id: number, 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" | "normal", sub_type?: "friend" | "group" | "normal",
sender: OB11Sender, sender: OB11Sender,
@ -89,26 +88,21 @@ export interface OB11Return<DataType> {
status: string status: string
retcode: number retcode: number
data: DataType data: DataType
message: string message: string,
} }
export interface OB11WebsocketReturn<DataType> { export interface OB11WebsocketReturn<DataType> extends OB11Return<DataType>{
status: string
retcode: number
data: DataType
echo: string echo: string
message: string
} }
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{}
export enum OB11MessageDataType { export enum OB11MessageDataType {
text = "text", text = "text",
image = "image", image = "image",
voice = "record", voice = "record",
at = "at", at = "at",
reply = "reply", reply = "reply",
json = "json" json = "json",
face = "face"
} }
export type OB11MessageData = { export type OB11MessageData = {
@ -140,6 +134,11 @@ export type OB11MessageData = {
data: { data: {
id: string, id: string,
} }
} | {
type: OB11MessageDataType.face,
data: {
id: string
}
} }
export interface OB11PostSendMsg { export interface OB11PostSendMsg {
@ -148,3 +147,16 @@ export interface OB11PostSendMsg {
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 OB11Status {
online: boolean | null,
good: boolean
}

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 {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){
@ -60,3 +61,39 @@ export async function uri2local(fileName: string, uri: string){
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

@ -1,11 +1,7 @@
// Electron 主进程 与 渲染进程 交互的桥梁 // Electron 主进程 与 渲染进程 交互的桥梁
import {Config} from "./common/types"; import {Config} from "./common/types";
import { import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "./common/channels";
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SET_CONFIG,
} from "./common/channels";
const {contextBridge} = require("electron"); const {contextBridge} = require("electron");

View File

@ -62,13 +62,17 @@ async function onSettingWindowCreated(view: Element) {
<setting-text>Websocket监听端口</setting-text> <setting-text>Websocket监听端口</setting-text>
<input id="wsPort" type="number" value="${config.wsPort}"/> <input id="wsPort" type="number" value="${config.wsPort}"/>
</setting-item> </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="addWsHost" class="q-button">Websocket上报地址</button> <button id="addWsHost" class="q-button">Websocket上报地址</button>
</div> </div>
<div id="wsHostItems"> <div id="wsHostItems">
${wsHostsEleStr} ${wsHostsEleStr}
</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>
@ -151,13 +155,15 @@ async function onSettingWindowCreated(view: Element) {
function addHostEle(type: string, initValue: string = "") { function addHostEle(type: string, initValue: string = "") {
let addressDoc = parser.parseFromString(createHttpHostEleStr(initValue), "text/html"); let addressEle, hostItemsEle;
let addressEle = addressDoc.querySelector("setting-item")
let hostItemsEle;
if (type === "ws") { if (type === "ws") {
let addressDoc = parser.parseFromString(createWsHostEleStr(initValue), "text/html");
addressEle = addressDoc.querySelector("setting-item")
hostItemsEle = document.getElementById("wsHostItems"); hostItemsEle = document.getElementById("wsHostItems");
} }
else { else {
let addressDoc = parser.parseFromString(createHttpHostEleStr(initValue), "text/html");
addressEle = addressDoc.querySelector("setting-item")
hostItemsEle = document.getElementById("httpHostItems"); hostItemsEle = document.getElementById("httpHostItems");
} }
@ -197,6 +203,7 @@ async function onSettingWindowCreated(view: Element) {
const httpHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("httpHost") as HTMLCollectionOf<HTMLInputElement>; const httpHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("httpHost") as HTMLCollectionOf<HTMLInputElement>;
const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement; const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement;
const wsHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("wsHost") as HTMLCollectionOf<HTMLInputElement>; const wsHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("wsHost") as HTMLCollectionOf<HTMLInputElement>;
const tokenEle = document.getElementById("token") as HTMLInputElement;
// 获取端口和host // 获取端口和host
const httpPort = httpPortEle.value const httpPort = httpPortEle.value
@ -209,6 +216,7 @@ async function onSettingWindowCreated(view: Element) {
} }
const wsPort = wsPortEle.value const wsPort = wsPortEle.value
const token = tokenEle.value
let wsHosts: string[] = []; let wsHosts: string[] = [];
for (const hostEle of wsHostEles) { for (const hostEle of wsHostEles) {
@ -222,6 +230,7 @@ async function onSettingWindowCreated(view: Element) {
config.httpHosts = httpHosts; config.httpHosts = httpHosts;
config.wsPort = parseInt(wsPort); config.wsPort = parseInt(wsPort);
config.wsHosts = wsHosts; config.wsHosts = wsHosts;
config.token = token.trim();
window.llonebot.setConfig(config); window.llonebot.setConfig(config);
alert("保存成功"); alert("保存成功");
}) })