Compare commits

...

47 Commits

Author SHA1 Message Date
linyuchen
c636af0b0e chore: ver 3.9.0 2024-02-27 04:10:08 +08:00
linyuchen
b8af582749 docs: update readme 2024-02-27 03:38:03 +08:00
linyuchen
8e09a9e0fd fix: receive video and file 2024-02-27 03:37:52 +08:00
linyuchen
001dfc4db2 docs: update readme 2024-02-27 03:15:25 +08:00
linyuchen
a164884b76 refactor: video and file only support local file uri 2024-02-27 03:15:13 +08:00
linyuchen
58f0a99d0b Merge branch 'main' into dev 2024-02-27 02:47:32 +08:00
linyuchen
528c6061e2 feat: 群管理功能 2024-02-27 02:46:57 +08:00
linyuchen
f5ac499861 feat: 发送视频和文件 2024-02-27 01:28:42 +08:00
linyuchen
621d9df450 docs: update todo list 2024-02-26 23:59:58 +08:00
linyuchen
1f657f3e84 Merge branch 'dev' 2024-02-26 23:24:03 +08:00
linyuchen
329dc433fb refactor: ffmpeg setting ui 2024-02-26 23:22:34 +08:00
linyuchen
90f64ab04e docs: update readme thanks 2024-02-26 22:30:27 +08:00
linyuchen
1583a36c2e Merge remote-tracking branch 'origin/main' 2024-02-26 22:26:38 +08:00
linyuchen
d70e95a451 chore: ver 3.8 2024-02-26 22:25:21 +08:00
linyuchen
c6256abcb2 fix: report image url filed 2024-02-26 22:21:20 +08:00
linyuchen
d57c14a8b9 feat: convert wav by ffmpeg 2024-02-26 22:19:37 +08:00
linyuchen
82268c619c Merge pull request #82 from disymayufei/patch-1 2024-02-26 17:37:53 +08:00
Disy
befdf8571a chore: Update README.md
补全README的一些信息
2024-02-26 16:04:57 +08:00
linyuchen
730294236c feat: convert wav by ffmpeg 2024-02-25 12:46:37 +08:00
linyuchen
d9d7e9e830 feat: auto delete receive file 2024-02-25 02:17:18 +08:00
linyuchen
6170307241 Merge remote-tracking branch 'origin/dev' into dev 2024-02-25 01:28:44 +08:00
linyuchen
138614cc4a feat: 好友请求时间,处理好友请求api 2024-02-25 01:28:15 +08:00
linyuchen
62870576a1 feat: 好友请求时间,处理还有请求api 2024-02-25 01:27:25 +08:00
linyuchen
cfb066971f feat: 上报支持CQCode 2024-02-24 18:27:49 +08:00
linyuchen
4941f0071a Merge remote-tracking branch 'origin/dev' into dev 2024-02-24 17:53:35 +08:00
linyuchen
6e61621f44 Merge pull request #75 from MisaLiu/feat_msg_format
增加对 `event.message_format` 和 CQ 码(仅接收)的支持
2024-02-24 17:47:44 +08:00
linyuchen
eb1a867a0e refactor: senderShowName of forward message 2024-02-24 17:24:30 +08:00
Misa Liu
f9ec7eddf2 feat: Support CQCode message format 2024-02-24 01:06:41 +08:00
Misa Liu
ffdec86209 feat: Add setting section of messagePostFormat 2024-02-24 00:40:45 +08:00
Misa Liu
66de0076d4 feat: Add message_format to message event 2024-02-23 21:48:36 +08:00
linyuchen
2eb0ad589a chore: ver 3.7.0 2024-02-23 19:57:20 +08:00
linyuchen
829aba18f8 feat: 管理员变动事件
feat: 加群事件
feat: 加群请求处理api
feat: 退群api
fix: 回复消息id改为string
2024-02-23 19:56:20 +08:00
linyuchen
67dfd7c22f Merge branch 'main' into dev 2024-02-23 14:06:36 +08:00
linyuchen
27745087ad Merge pull request #69 from MisaLiu/fix_app_version 2024-02-23 11:50:37 +08:00
linyuchen
4ba333b6f5 Merge pull request #68 from MisaLiu/fix_echo 2024-02-23 11:50:28 +08:00
Misa Liu
f4fe26fbe1 fix: Fix app_version in get_version_info 2024-02-23 10:33:15 +08:00
Misa Liu
30e488aeaf fix: Fix var type of echo 2024-02-23 10:22:42 +08:00
linyuchen
1f0dad786c feat: group admin change notice 2024-02-23 04:08:20 +08:00
linyuchen
8dfc71ab6d fix: message id int32 2024-02-22 23:05:07 +08:00
linyuchen
12d1f87ad5 fix: message id int32 2024-02-22 23:02:23 +08:00
linyuchen
b27dadbbca temp save 2024-02-22 22:55:52 +08:00
linyuchen
688624500f docs: tg 2024-02-22 17:35:14 +08:00
linyuchen
eefb919f0f docs: update readme 2024-02-21 22:21:32 +08:00
linyuchen
5044d24ee1 feat: go-cqhttp api get_stranger_info
feat: api send_like
fix: some image of message use base64 instead of http url
2024-02-21 22:19:02 +08:00
linyuchen
7664e746b4 feat: some go-cqhttp feature 2024-02-21 17:17:15 +08:00
linyuchen
ebea755731 perf: log long string 2024-02-21 16:43:10 +08:00
linyuchen
e4508ea5c7 feat: go-cqhttp api send_private_forward_msg & send_group_forward_msg 2024-02-21 16:36:40 +08:00
52 changed files with 2505 additions and 316 deletions

View File

@@ -17,7 +17,7 @@ jobs:
node-version: 18 node-version: 18
- name: install dependenies - name: install dependenies
run: npm install run: export ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install
- name: build - name: build
run: npm run build run: npm run build

112
README.md
View File

@@ -1,7 +1,9 @@
# LLOneBot API
# LLOneBot API
LiteLoaderQQNT的OneBot11协议插件 LiteLoaderQQNT的OneBot11协议插件
TG群<https://t.me/+nLZEnpne-pQ1OWFl>
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档* *注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI* *V3之后不再需要LLAPI*
@@ -16,6 +18,17 @@ LiteLoaderQQNT的OneBot11协议插件
*插件目录:`LiteLoaderQQNT/plugins`* *插件目录:`LiteLoaderQQNT/plugins`*
安装后的目录结构如下
```
├── plugins
│ ├── LLOneBot
│ │ └── main.js
│ │ └── preload.js
│ │ └── renderer.js
│ │ └── manifest.json
│ │ └── node_modules/...
```
## 支持的API ## 支持的API
目前支持的协议 目前支持的协议
@@ -31,11 +44,23 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] 获取群列表 - [x] 获取群列表
- [x] 获取群成员列表 - [x] 获取群成员列表
- [x] 撤回消息 - [x] 撤回消息
- [x] 处理添加好友请求
- [x] 处理加群请求
- [x] 退群
- [x] 上报好友消息 - [x] 上报好友消息
- [x] 上报添加好友请求
- [x] 上报群消息 - [x] 上报群消息
- [x] 上报好友、群消息撤回 - [x] 上报好友、群消息撤回
- [x] 上报加群请求
- [x] 上报群员人数变动(尚不支持识别群员人数变动原因)
- [x] 设置群管理员
- [x] 群禁言/全体禁言
- [x] 群踢人
- [x] 群改群成员名片
- [x] 修改群名
消息格式支持: 消息格式支持:
- [x] cq码
- [x] 文字 - [x] 文字
- [x] 表情 - [x] 表情
- [x] 图片 - [x] 图片
@@ -44,25 +69,19 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] 语音(支持mp3、wav等多种音频格式直接发送) - [x] 语音(支持mp3、wav等多种音频格式直接发送)
- [x] json消息(只上报) - [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收) - [x] 转发消息记录(目前只能发不能收)
- [ ] 红包 - [x] 视频(上报时暂时只有个空的file)
- [ ] xml - [x] 文件(上报时暂时只有个空的file), type为file, data为{file: uri}, 发送时uri支持http://, file://, base64://
```
支持的api: {
- [x] get_login_info "type": "file",
- [x] send_msg "data": {
- [x] send_group_msg "file": "file:///D:/1.txt"
- [x] send_private_msg }
- [x] delete_msg }
- [x] get_group_list ```
- [x] get_group_info - [ ] 发送音乐卡片
- [x] get_group_member_list - [ ] 红包(没有计划支持)
- [x] get_group_member_info - [ ] xml (没有计划支持)
- [x] get_friend_list
- [x] get_msg
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
## 示例 ## 示例
@@ -81,6 +100,7 @@ LiteLoaderQQNT的OneBot11协议插件
<summary>调用接口报404</summary> <summary>调用接口报404</summary>
<br/> <br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口 目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
-
</details> </details>
<br/> <br/>
@@ -91,13 +111,6 @@ LiteLoaderQQNT的OneBot11协议插件
</details> </details>
<br/> <br/>
<details>
<summary>不支持cq码</summary>
<br/>
cq码已经过时了没有支持的打算(主要是我不用这玩意儿,加上我懒)
</details>
<br/>
<details> <details>
<summary>QQ变得很卡</summary> <summary>QQ变得很卡</summary>
<br/> <br/>
@@ -105,12 +118,55 @@ LiteLoaderQQNT的OneBot11协议插件
</details> </details>
<br/> <br/>
## 支持的onebot v11 api:
- [x] get_login_info
- [x] send_msg
- [x] send_group_msg
- [x] send_private_msg
- [x] delete_msg
- [x] get_group_list
- [x] get_group_info
- [x] get_group_member_list
- [x] get_group_member_info
- [x] get_friend_list
- [x] set_friend_add_request
- [x] get_msg
- [x] send_like
- [x] set_group_add_request
- [x] set_group_leave
- [x] set_group_kick
- [x] set_group_ban
- [x] set_group_whole_ban
- [x] set_group_kick
- [x] set_group_admin
- [x] set_group_card
- [x] set_group_name
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
### 支持的go-cqhtp api:
- [x] send_private_forward_msg
- [x] send_group_forward_msg
- [x] get_stranger_info
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR - [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录 - [x] 转发消息记录
- [ ] 好友点赞api - [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [ ] 音乐卡片
- [ ] 无头模式
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>
## 鸣谢
* [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
* chronocat
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)

View File

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

883
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "cross-env ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save",
"build": "npm run build-main && npm run build-preload && npm run build-renderer", "build": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js", "build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js", "build-preload": "webpack --config webpack.preload.config.js",
@@ -19,21 +18,24 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"fluent-ffmpeg": "^2.1.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"music-metadata": "^8.1.4", "music-metadata": "^8.1.4",
"silk-wasm": "^3.2.3", "silk-wasm": "^3.2.3",
"utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3", "electron": "^29.0.1",
"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",

View File

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

View File

@@ -2,6 +2,8 @@ import fs from "fs";
import {Config, OB11Config} from "./types"; import {Config, OB11Config} from "./types";
import {mergeNewProperties} from "./utils"; import {mergeNewProperties} from "./utils";
export const HOOK_LOG= false;
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string; private readonly configPath: string;
private config: Config | null = null; private config: Config | null = null;
@@ -27,7 +29,8 @@ export class ConfigUtil {
enableHttp: true, enableHttp: true,
enableHttpPost: true, enableHttpPost: true,
enableWs: true, enableWs: true,
enableWsReverse: false enableWsReverse: false,
messagePostFormat: "array",
} }
let defaultConfig: Config = { let defaultConfig: Config = {
ob11: ob11Default, ob11: ob11Default,
@@ -36,7 +39,8 @@ export class ConfigUtil {
enableLocalFile2Url: false, enableLocalFile2Url: false,
debug: false, debug: false,
log: false, log: false,
reportSelfMessage: false reportSelfMessage: false,
autoDeleteFile: false,
}; };
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {

View File

@@ -1,11 +1,18 @@
import {NTQQApi} from '../ntqqapi/ntcall'; import {NTQQApi} from '../ntqqapi/ntcall';
import {Friend, Group, GroupMember, RawMessage, SelfInfo} from "../ntqqapi/types"; import {Friend, FriendRequest, Group, GroupMember, GroupNotify, RawMessage, SelfInfo} from "../ntqqapi/types";
import {LLOneBotError} from "./types";
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
export const version = "3.9.0"
let globalMsgId = Date.now() export let groupNotifies: Map<string, GroupNotify> = new Map<string, GroupNotify>();
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>();
export let llonebotError: LLOneBotError = {
ffmpegError: "",
otherError: ""
}
let globalMsgId = Math.floor(Date.now() / 1000);
export function addHistoryMsg(msg: RawMessage): boolean { export function addHistoryMsg(msg: RawMessage): boolean {
let existMsg = msgHistory[msg.msgId] let existMsg = msgHistory[msg.msgId]
@@ -43,14 +50,17 @@ export async function getGroup(qq: string): Promise<Group | undefined> {
return group return group
} }
export async function getGroupMember(groupQQ: string, memberQQ: string=null, memberUid: string=null) { export async function getGroupMember(groupQQ: string | number, memberQQ: string | number, memberUid: string = null) {
groupQQ = groupQQ.toString();
if (memberQQ){
memberQQ = memberQQ.toString();
}
const group = await getGroup(groupQQ) const group = await getGroup(groupQQ)
if (group) { if (group) {
let filterFunc: (member: GroupMember) => boolean let filterFunc: (member: GroupMember) => boolean
if (memberQQ) { if (memberQQ) {
filterFunc = member => member.uin === memberQQ filterFunc = member => member.uin === memberQQ
} } else if (memberUid) {
else if (memberUid){
filterFunc = member => member.uid === memberUid filterFunc = member => member.uid === memberUid
} }
let member = group.members?.find(filterFunc) let member = group.members?.find(filterFunc)
@@ -87,4 +97,3 @@ export function getUidByUin(uin: string) {
} }
} }
export const version = "v3.4.0"

View File

@@ -91,6 +91,7 @@ export abstract class HttpServerBase {
if (method == "get"){ if (method == "get"){
payload = req.query payload = req.query
} }
log("收到http请求", url, payload);
try{ try{
res.send(await handler(res, payload)) res.send(await handler(res, payload))
}catch (e) { }catch (e) {

View File

@@ -7,6 +7,7 @@ export interface OB11Config {
enableHttpPost?: boolean enableHttpPost?: boolean
enableWs?: boolean enableWs?: boolean
enableWsReverse?: boolean enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string'
} }
export interface Config { export interface Config {
@@ -17,4 +18,11 @@ export interface Config {
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean
log?: boolean log?: boolean
autoDeleteFile?: boolean
ffmpeg?: string // ffmpeg路径
}
export type LLOneBotError = {
ffmpegError?: string
otherError?: string
} }

View File

@@ -5,6 +5,8 @@ import util from "util";
import {encode, getDuration} from "silk-wasm"; import {encode, getDuration} from "silk-wasm";
import fs from 'fs'; import fs from 'fs';
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {exec} from "node:child_process";
import ffmpeg from "fluent-ffmpeg"
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
@@ -13,6 +15,23 @@ export function getConfigUtil() {
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function log(...msg: any[]) { export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) { if (!getConfigUtil().getConfig().log) {
return return
@@ -28,7 +47,8 @@ export function log(...msg: any[]) {
for (let msgItem of msg) { for (let msgItem of msg) {
// 判断是否是对象 // 判断是否是对象
if (typeof msgItem === "object") { if (typeof msgItem === "object") {
logMsg += JSON.stringify(msgItem) + " "; let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue; continue;
} }
logMsg += msgItem + " "; logMsg += msgItem + " ";
@@ -118,7 +138,23 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
}); });
} }
export function checkFFMPEG(newPath: string=null): Promise<boolean> {
return new Promise((resolve, reject) => {
const ffmpegPath = newPath || 'ffmpeg'
exec(ffmpegPath + ' -version', (error, stdout, stderr) => {
if (error) {
log('ffmpeg is not installed or not found in PATH:', error);
resolve(false)
}
log('ffmpeg is installed. Version info:', stdout);
resolve(true);
});
});
}
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
const fsp = require("fs").promises
function getFileHeader(filePath: string) { function getFileHeader(filePath: string) {
// 定义要读取的字节数 // 定义要读取的字节数
const bytesToRead = 7; const bytesToRead = 7;
@@ -136,6 +172,35 @@ export async function encodeSilk(filePath: string) {
} }
} }
function isWavFile(filePath: string) {
return new Promise((resolve, reject) => {
fs.open(filePath, 'r', (err, fd) => {
if (err) {
reject(err);
return;
}
// 读取前12个字节
const buffer = Buffer.alloc(12);
fs.read(fd, buffer, 0, 12, 0, (err, bytesRead, buffer) => {
if (err) {
reject(err);
return;
}
fs.close(fd, (err) => {
if (err) {
reject(err);
return;
}
// 检查RIFF头和WAVE格式标识
const isRIFF = buffer.toString('utf8', 0, 4) === 'RIFF';
const isWAVE = buffer.toString('utf8', 8, 12) === 'WAVE';
resolve(isRIFF && isWAVE);
});
});
});
});
}
async function getAudioSampleRate(filePath: string) { async function getAudioSampleRate(filePath: string) {
try { try {
const mm = await import('music-metadata'); const mm = await import('music-metadata');
@@ -150,20 +215,45 @@ export async function encodeSilk(filePath: string) {
try { try {
const fileName = path.basename(filePath); const fileName = path.basename(filePath);
const pcm = fs.readFileSync(filePath);
const pttPath = path.join(CONFIG_DIR, uuidv4()); const pttPath = path.join(CONFIG_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") { if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换`) log(`语音文件${filePath}需要转换`)
const isWav = await isWavFile(filePath);
if (!isWav) {
log(`语音文件${filePath}正在转换成wav`)
// let voiceData = await fsp.readFile(filePath)
const wavPath = pttPath + ".wav"
await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath){
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav").on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", ()=>{
filePath = wavPath
resolve(wavPath);
});
})
const sampleRate = await getAudioSampleRate(filePath) || 44100; const sampleRate = await getAudioSampleRate(filePath) || 44100;
const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, sampleRate); const silk = await encode(pcm, sampleRate);
fs.writeFileSync(pttPath, silk.data); fs.writeFileSync(pttPath, silk.data);
log(`语音文件${filePath}转换成功!`) fs.unlink(wavPath, (err) => {});
log(`语音文件${filePath}转换成功!`, pttPath)
return { return {
converted: true, converted: true,
path: pttPath, path: pttPath,
duration: silk.duration, duration: silk.duration,
}; };
} else { } else {
const pcm = fs.readFileSync(filePath);
const duration = getDuration(pcm); const duration = getDuration(pcm);
return { return {
converted: false, converted: false,
@@ -171,8 +261,13 @@ export async function encodeSilk(filePath: string) {
duration: duration, duration: duration,
}; };
} }
}
} catch (error) { } catch (error) {
log("convert silk failed", error.stack); log("convert silk failed", error.stack);
return {}; return {};
} }
} }
export function isNull(value: any) {
return value === undefined || value === null;
}

2
src/global.d.ts vendored
View File

@@ -4,7 +4,7 @@ import {LLOneBot} from "./preload";
declare global { declare global {
interface Window { interface Window {
llonebot: typeof llonebot; llonebot: LLOneBot;
LiteLoader: any; LiteLoader: any;
} }
} }

View File

@@ -1,21 +1,48 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import {BrowserWindow, ipcMain} from 'electron'; import {BrowserWindow, dialog, ipcMain} from 'electron';
import fs from 'fs'; import fs from 'fs';
import {Config} from "../common/types"; import {Config} from "../common/types";
import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "../common/channels"; import {
CHANNEL_ERROR,
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG,
} from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {CONFIG_DIR, getConfigUtil, log} from "../common/utils"; import {checkFFMPEG, CONFIG_DIR, getConfigUtil, log} from "../common/utils";
import {addHistoryMsg, getGroupMember, msgHistory, selfInfo} from "../common/data"; import {
addHistoryMsg,
friendRequests,
getGroup,
getGroupMember,
groupNotifies,
llonebotError,
msgHistory,
selfInfo
} from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook"; import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor"; import {OB11Constructor} from "../onebot11/constructor";
import {NTQQApi} from "../ntqqapi/ntcall"; import {NTQQApi} from "../ntqqapi/ntcall";
import {ChatType, RawMessage} from "../ntqqapi/types"; import {
ChatType,
FriendRequestNotify,
GroupMember,
GroupNotifies,
GroupNotifyTypes,
RawMessage
} from "../ntqqapi/types";
import {ob11HTTPServer} from "../onebot11/server/http"; import {ob11HTTPServer} from "../onebot11/server/http";
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
import {postEvent} from "../onebot11/server/postevent"; import {postOB11Event} from "../onebot11/server/postOB11Event";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest";
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest";
import * as path from "node:path";
let running = false; let running = false;
@@ -24,13 +51,48 @@ let running = false;
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log("llonebot main onLoad"); log("llonebot main onLoad");
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
const selectPath = new Promise<string>((resolve, reject) => {
dialog
.showOpenDialog({
title: "请选择ffmpeg",
properties: ["openFile"],
buttonLabel: "确定",
})
.then((result) => {
log("选择文件", result);
if (!result.canceled) {
const _selectPath = path.join(result.filePaths[0]);
resolve(_selectPath);
// let config = getConfigUtil().getConfig()
// config.ffmpeg = path.join(result.filePaths[0]);
// getConfigUtil().setConfig(config);
}
resolve("")
})
.catch((err) => {
reject(err);
});
})
try {
return await selectPath;
} catch (e) {
log("选择文件出错", e)
return ""
}
})
if (!fs.existsSync(CONFIG_DIR)) { if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true}); fs.mkdirSync(CONFIG_DIR, {recursive: true});
} }
ipcMain.handle(CHANNEL_GET_CONFIG, (event: any, arg: any) => { ipcMain.handle(CHANNEL_ERROR, (event, arg) => {
return getConfigUtil().getConfig() return llonebotError;
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => { ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
const config = getConfigUtil().getConfig()
return config;
})
ipcMain.on(CHANNEL_SET_CONFIG, (event, arg: Config) => {
let oldConfig = getConfigUtil().getConfig(); let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(arg) getConfigUtil().setConfig(arg)
if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) { if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) {
@@ -75,13 +137,20 @@ function onLoad() {
} }
} }
} }
// 检查ffmpeg
if (arg.ffmpeg) {
checkFFMPEG(arg.ffmpeg).then(success => {
llonebotError.ffmpegError = ''
})
}
}) })
ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { ipcMain.on(CHANNEL_LOG, (event, arg) => {
log(arg); log(arg);
}) })
function postReceiveMsg(msgList: RawMessage[]) { function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
@@ -94,17 +163,17 @@ function onLoad() {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
} }
if (msg.user_id.toString() == selfInfo.uin && !reportSelfMessage) { const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return return
} }
postEvent(msg); postOB11Event(msg);
// log("post msg", msg) // log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.toString()));
} }
} }
async function startReceiveHook() {
async function start() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try { try {
postReceiveMsg(payload.msgList); postReceiveMsg(payload.msgList);
@@ -123,7 +192,7 @@ function onLoad() {
} }
if (message.chatType == ChatType.friend) { if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId); const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postEvent(friendRecallEvent); postOB11Event(friendRecallEvent);
} else if (message.chatType == ChatType.group) { } else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin let operatorId = message.senderUin
for (const element of message.elements) { for (const element of message.elements) {
@@ -138,7 +207,7 @@ function onLoad() {
oriMessage.msgShortId oriMessage.msgShortId
) )
postEvent(groupRecallEvent); postOB11Event(groupRecallEvent);
} }
continue continue
} }
@@ -157,8 +226,109 @@ function onLoad() {
log("report self message error: ", e.toString()); log("report self message error: ", e.toString());
} }
}) })
registerReceiveHook<{
"doubt": boolean,
"oldestUnreadSeq": string,
"unreadCount": number
}>(ReceiveCmd.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
log("开始获取群通知详情")
let notify: GroupNotifies;
try {
notify = await NTQQApi.getGroupNotifies();
} catch (e) {
// log("获取群通知详情失败", e);
return
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
log("获取群通知详情完成", notifies, payload);
try {
for (const notify of notifies) {
const notifyTime = parseInt(notify.seq) / 1000
log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
if (notifyTime < startTime) {
continue;
}
const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid);
let member2: GroupMember;
if (notify.user2.uid) {
member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
}
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
log("有管理员变动通知");
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
log("开始获取变动的管理员")
if (member1) {
log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set";
postOB11Event(groupAdminNoticeEvent, true);
} else {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
}
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT) {
log("有成员退出通知");
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin))
// postEvent(groupDecreaseEvent, true);
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log("有加群请求");
groupNotifies[notify.seq] = notify;
let groupRequestEvent = new OB11GroupRequestEvent();
groupRequestEvent.group_id = parseInt(notify.group.groupCode);
let requestQQ = ""
try {
requestQQ = (await NTQQApi.getUserDetailInfo(notify.user1.uid)).uin;
} catch (e) {
log("获取加群人QQ号失败", e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0;
groupRequestEvent.sub_type = "add"
groupRequestEvent.comment = notify.postscript;
groupRequestEvent.flag = notify.seq;
postOB11Event(groupRequestEvent);
}
}
} catch (e) {
log("解析群通知失败", e.stack);
}
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmd.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
if (req.isUnread && !friendRequests[req.sourceId] && (parseInt(req.reqTime) > startTime / 1000)) {
friendRequests[req.sourceId] = req;
log("有新的好友请求", req);
let friendRequestEvent = new OB11FriendRequestEvent();
try {
let requester = await NTQQApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin);
} catch (e) {
log("获取加好友者QQ号失败", e);
}
friendRequestEvent.flag = req.sourceId.toString();
friendRequestEvent.comment = req.extWords;
postOB11Event(friendRequestEvent);
}
}
})
}
let startTime = 0;
async function start() {
startTime = Date.now();
startReceiveHook().then();
NTQQApi.getGroups(true).then() NTQQApi.getGroups(true).then()
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
// 检查ffmpeg
checkFFMPEG(config.ffmpeg).then(exist => {
if (!exist) {
llonebotError.ffmpegError = `没有找到ffmpeg,音频只能发送wav和silk`
}
})
if (config.ob11.enableHttp) { if (config.ob11.enableHttp) {
try { try {
ob11HTTPServer.start(config.ob11.httpPort) ob11HTTPServer.start(config.ob11.httpPort)
@@ -176,6 +346,7 @@ function onLoad() {
log("LLOneBot start") log("LLOneBot start")
} }
let getSelfNickCount = 0;
const init = async () => { const init = async () => {
try { try {
const _ = await NTQQApi.getSelfInfo(); const _ = await NTQQApi.getSelfInfo();
@@ -192,8 +363,11 @@ function onLoad() {
if (userInfo) { if (userInfo) {
selfInfo.nick = userInfo.nick; selfInfo.nick = userInfo.nick;
} else { } else {
getSelfNickCount++;
if (getSelfNickCount < 10) {
return setTimeout(init, 1000); return setTimeout(init, 1000);
} }
}
} catch (e) { } catch (e) {
log("get self nickname failed", e.toString()); log("get self nickname failed", e.toString());
return setTimeout(init, 1000); return setTimeout(init, 1000);

View File

@@ -2,6 +2,7 @@ import {
AtType, AtType,
ElementType, ElementType,
SendFaceElement, SendFaceElement,
SendFileElement,
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
@@ -80,12 +81,34 @@ export class SendMsgElementConstructor {
}; };
} }
static async file(filePath: string, isVideo:boolean = false): Promise<SendFileElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(filePath);
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: "",
fileElement: {
fileName,
"filePath": path,
"fileSize": (fileSize).toString(),
}
}
if (isVideo){
element.fileElement.picHeight = 1024;
element.fileElement.picWidth = 768;
}
return element;
}
static video(filePath: string): Promise<SendFileElement> {
return SendMsgElementConstructor.file(filePath, true);
}
static async ptt(pttPath: string): Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath); const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration); // log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
if (converted) { if (converted) {
fs.unlink(silkPath, ()=>{}); fs.unlink(silkPath, () => {
});
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,

View File

@@ -1,12 +1,14 @@
import {BrowserWindow} from 'electron'; import {BrowserWindow} from 'electron';
import {log, sleep} from "../common/utils"; import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {Group, 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 {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent"; import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {v4 as uuidv4} from "uuid" import {v4 as uuidv4} from "uuid"
import {postEvent} from "../onebot11/server/postevent"; import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from "../common/config";
import fs from "fs";
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -15,9 +17,14 @@ export enum ReceiveCmd {
NEW_MSG = "nodeIKernelMsgListener/onRecvMsg", NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg", SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged", USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged",
GROUPS = "nodeIKernelGroupListener/onGroupListUpdate", GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = "onGroupListUpdate", GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = "onBuddyListChange" FRIENDS = "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies",
FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange"
} }
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> { interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
@@ -43,7 +50,7 @@ let receiveHooks: Array<{
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send; const originalSend = window.webContents.send;
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// log(`received ntqq api message: ${channel}`, JSON.stringify(args)) HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args))
if (args?.[1] instanceof Array) { if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) { for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName; const ntQQApiMethodName = receiveData.cmdName;
@@ -87,15 +94,15 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcMsg = new Proxy(ipc_message_proxy, { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
log("call NTQQ api", thisArg, args); HOOK_LOG && log("call NTQQ api", thisArg, args);
return target.apply(thisArg, args); return target.apply(thisArg, args);
}, },
}); });
// if (webContents._events["-ipc-message"]?.[0]) { if (webContents._events["-ipc-message"]?.[0]) {
// webContents._events["-ipc-message"][0] = proxyIpcMsg; webContents._events["-ipc-message"][0] = proxyIpcMsg;
// } else { } else {
// webContents._events["-ipc-message"] = proxyIpcMsg; webContents._events["-ipc-message"] = proxyIpcMsg;
// } }
} }
export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string { export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
@@ -118,8 +125,7 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
let existGroup = groups.find(g => g.groupCode == group.groupCode); let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
Object.assign(existGroup, group); Object.assign(existGroup, group);
} } else {
else {
groups.push(group); groups.push(group);
existGroup = group; existGroup = group;
} }
@@ -155,13 +161,12 @@ async function processGroupEvent(payload) {
for (const member of oldMembers) { for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) { if (!newMembersSet.has(member.uin)) {
postEvent(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))); postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break; break;
} }
} }
} } else if (existGroup.memberCount < group.memberCount) {
else if (existGroup.memberCount < group.memberCount) {
const oldMembers = existGroup.members; const oldMembers = existGroup.members;
const oldMembersSet = new Set<string>(); const oldMembersSet = new Set<string>();
for (const member of oldMembers) { for (const member of oldMembers) {
@@ -174,7 +179,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)) {
postEvent(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin))); postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)));
break; break;
} }
} }
@@ -183,8 +188,7 @@ async function processGroupEvent(payload) {
} }
updateGroups(newGroupList, false).then(); updateGroups(newGroupList, false).then();
} } catch (e) {
catch (e) {
updateGroups(payload.groupList).then(); updateGroups(payload.groupList).then();
console.log(e); console.log(e);
} }
@@ -193,8 +197,7 @@ async function processGroupEvent(payload) {
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => { registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => {
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then(); updateGroups(payload.groupList).then();
} } else {
else {
if (process.platform == "win32") { if (process.platform == "win32") {
processGroupEvent(payload).then(); processGroupEvent(payload).then();
} }
@@ -203,13 +206,13 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUP
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => { registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => {
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then(); updateGroups(payload.groupList).then();
} } else {
else {
if (process.platform != "win32") { if (process.platform != "win32") {
processGroupEvent(payload).then(); processGroupEvent(payload).then();
} }
} }
}) })
registerReceiveHook<{ registerReceiveHook<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => { }>(ReceiveCmd.FRIENDS, payload => {
@@ -227,9 +230,32 @@ registerReceiveHook<{
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile} = getConfigUtil().getConfig();
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message) // log("收到新消息push到历史记录", message)
addHistoryMsg(message) addHistoryMsg(message)
// 清理文件
if (!autoDeleteFile) {
continue
}
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath;
const pttPath = msgElement.pttElement?.filePath;
const pathList = [picPath, pttPath];
if (msgElement.picElement){
pathList.push(...Object.values(msgElement.picElement.thumbPath));
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log("删除文件成功", path)
});
}
}
}, 60 * 1000)
}
} }
const msgIds = Object.keys(msgHistory); const msgIds = Object.keys(msgHistory);
if (msgIds.length > 30000) { if (msgIds.length > 30000) {
@@ -250,3 +276,4 @@ registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRe
} }
} }
}) })

View File

@@ -1,9 +1,23 @@
import {ipcMain} from "electron"; import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook"; import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log} from "../common/utils"; import {log} from "../common/utils";
import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types"; import {
ChatType,
ElementType,
Friend,
FriendRequest,
Group,
GroupMember, GroupMemberRole,
GroupNotifies,
GroupNotify,
GroupRequestOperateTypes,
RawMessage,
SelfInfo,
SendMessageElement,
User
} from "./types";
import * as fs from "fs"; import * as fs from "fs";
import {addHistoryMsg, msgHistory, selfInfo, uidMaps} from "../common/data"; import {addHistoryMsg, friendRequests, groupNotifies, msgHistory, selfInfo} from "../common/data";
import {v4 as uuidv4} from "uuid" import {v4 as uuidv4} from "uuid"
interface IPCReceiveEvent { interface IPCReceiveEvent {
@@ -32,6 +46,7 @@ export enum NTQQApiMethod {
GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene", GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList", GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo", USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
FILE_TYPE = "getFileType", FILE_TYPE = "getFileType",
FILE_MD5 = "getFileMd5", FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile", FILE_COPY = "copyFile",
@@ -41,7 +56,19 @@ export enum NTQQApiMethod {
RECALL_MSG = "nodeIKernelMsgService/recallMsg", RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg", SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia", DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment" // 合并转发 MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = "nodeIKernelGroupService/kickMember",
MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp",
MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp",
SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName",
SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
} }
enum NTQQApiChannel { enum NTQQApiChannel {
@@ -56,35 +83,40 @@ export interface Peer {
guildId?: "" guildId?: ""
} }
enum CallBackType {
UUID,
METHOD
}
interface NTQQApiParams { interface NTQQApiParams {
methodName: NTQQApiMethod, methodName: NTQQApiMethod | string,
className?: NTQQApiClass, className?: NTQQApiClass,
channel?: NTQQApiChannel, channel?: NTQQApiChannel,
classNameIsRegister?: boolean
args?: unknown[], args?: unknown[],
cbCmd?: ReceiveCmd | null cbCmd?: ReceiveCmd | null,
cmdCB?: (payload: any) => boolean;
afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number, timeoutSecond?: number,
} }
function callNTQQApi<ReturnType>(params: NTQQApiParams) { function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let { let {
className, methodName, channel, args, className, methodName, channel, args,
cbCmd, timeoutSecond: timeout cbCmd, timeoutSecond: timeout,
classNameIsRegister, cmdCB, afterFirstCmd
} = params; } = params;
className = className ?? NTQQApiClass.NT_API; className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2; channel = channel ?? NTQQApiChannel.IPC_UP_2;
args = args ?? []; args = args ?? [];
timeout = timeout ?? 5; timeout = timeout ?? 5;
afterFirstCmd = afterFirstCmd ?? true;
const uuid = uuidv4(); const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid) // log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) // log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000 const _timeout = timeout * 1000
let success = false let success = false
let eventName = className + "-" + channel[channel.length - 1];
if (classNameIsRegister) {
eventName += "-register";
}
const apiArgs = [methodName, ...args]
if (!cbCmd) { if (!cbCmd) {
// QQ后端会返回结果并且可以插根据uuid识别 // QQ后端会返回结果并且可以插根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
@@ -93,15 +125,27 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
}; };
} else { } else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { const secondCallback = () => {
log(`${methodName} callback`, result)
if (result.result == 0) {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
log(methodName, "second callback", cbCmd, payload); // log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId); removeReceiveHook(hookId);
success = true success = true
resolve(payload); resolve(payload);
}
} else {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
}) })
}
!afterFirstCmd && secondCallback();
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback();
} else { } else {
success = true success = true
reject(`ntqq api call failed, ${result.errMsg}`); reject(`ntqq api call failed, ${result.errMsg}`);
@@ -111,12 +155,11 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
setTimeout(() => { setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName) // log("ntqq api timeout", success, channel, className, methodName)
if (!success) { if (!success) {
log(`ntqq api timeout ${channel}, ${className}, ${methodName}`) log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs);
reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`) reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
} }
}, _timeout) }, _timeout)
const eventName = className + "-" + channel[channel.length - 1];
const apiArgs = [methodName, ...args]
ipcMain.emit( ipcMain.emit(
channel, channel,
{}, {},
@@ -138,7 +181,7 @@ interface GeneralCallResult {
export class NTQQApi { export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND) // static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static likeFriend(uid: string, count = 1) { static likeFriend(uid: string, count = 1) {
return callNTQQApi({ return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND, methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{ args: [{
doLikeUserInfo: { doLikeUserInfo: {
@@ -167,6 +210,26 @@ export class NTQQApi {
return result.profiles.get(uid) return result.profiles.get(uid)
} }
static async getUserDetailInfo(uid: string) {
const result = await callNTQQApi<{ info: User }>({
methodName: NTQQApiMethod.USER_DETAIL_INFO,
cbCmd: ReceiveCmd.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid
},
null
]
})
return result.info
}
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { data: {
@@ -226,10 +289,11 @@ export class NTQQApi {
let members = Array.from(values) as GroupMember[] let members = Array.from(values) as GroupMember[]
for (const member of members) { for (const member of members) {
uidMaps[member.uid] = member.uin; // uidMaps[member.uid] = member.uin;
} }
log(uidMaps); // log(uidMaps);
// log("members info", values); // log("members info", values);
log(`get group ${groupQQ} members success`)
return members return members
} catch (e) { } catch (e) {
log(`get group ${groupQQ} members failed`, e) log(`get group ${groupQQ} members failed`, e)
@@ -274,7 +338,7 @@ export class NTQQApi {
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) {
const md5 = await NTQQApi.getFileMd5(filePath); const md5 = await NTQQApi.getFileMd5(filePath);
let ext = (await NTQQApi.getFileType(filePath))?.ext let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
@@ -289,7 +353,7 @@ export class NTQQApi {
path_info: { path_info: {
md5HexStr: md5, md5HexStr: md5,
fileName: fileName, fileName: fileName,
elementType: 2, elementType: elementType,
elementSubType: 0, elementSubType: 0,
thumbSize: 0, thumbSize: 0,
needCreate: true, needCreate: true,
@@ -328,7 +392,16 @@ export class NTQQApi {
}, },
undefined, undefined,
] ]
await callNTQQApi({methodName: NTQQApiMethod.DOWNLOAD_MEDIA, args: apiParams}) // log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath;
}
})
return sourcePath return sourcePath
} }
@@ -341,8 +414,8 @@ export class NTQQApi {
}) })
} }
static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false) { static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false, timeout = 10000) {
const sendTimeout = 10 * 1000 const sendTimeout = timeout
return new Promise<RawMessage>((resolve, reject) => { return new Promise<RawMessage>((resolve, reject) => {
const peerUid = peer.peerUid; const peerUid = peer.peerUid;
@@ -379,6 +452,7 @@ export class NTQQApi {
return reject("发送超时") return reject("发送超时")
} }
if (msgHistory[rawMessage.msgId]?.sendStatus == 2) { if (msgHistory[rawMessage.msgId]?.sendStatus == 2) {
log(`${peerUid}发送消息成功`)
success = true; success = true;
resolve(rawMessage); resolve(rawMessage);
} else { } else {
@@ -389,6 +463,7 @@ export class NTQQApi {
checkSendComplete(); checkSendComplete();
} else { } else {
success = true; success = true;
log(`${peerUid}发送消息成功`)
resolve(rawMessage); resolve(rawMessage);
} }
} }
@@ -408,7 +483,7 @@ export class NTQQApi {
static multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
let msgInfos = msgIds.map(id => { let msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: "LLOneBot"} return {msgId: id, senderShowName: selfInfo.nick}
}) })
const apiArgs = [ const apiArgs = [
{ {
@@ -458,4 +533,162 @@ export class NTQQApi {
}) })
}) })
} }
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmd.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
{"doubt": false, "startSeq": "", "number": 14},
null
]
});
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq];
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
});
}
static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{"groupCode": groupQQ},
null
]
})
}
static async handleFriendRequest(sourceId: number, accept: boolean,) {
const request: FriendRequest = friendRequests[sourceId]
if (!request) {
throw `sourceId ${sourceId}, 对应的好友请求不存在`
}
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[sourceId];
return result;
}
static kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = "") {
return callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
}
]
}
)
}
static banMember(groupQQ: string, memList: { uid: string, timeStamp: number }[]) {
// timeStamp为秒数, 0为解除禁言
return callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static banGroup(groupQQ: string, shutUp: boolean){
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static setGroupName(groupQQ: string, groupName: string){
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args:[
{
groupCode: groupQQ,
groupName
},null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
}
} }

View File

@@ -45,6 +45,12 @@ export interface Group {
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
} }
export enum GroupMemberRole {
normal = 2,
admin = 3,
owner = 4
}
export interface GroupMember { export interface GroupMember {
avatarPath: string; avatarPath: string;
cardName: string; cardName: string;
@@ -53,7 +59,7 @@ export interface GroupMember {
nick: string; nick: string;
qid: string; qid: string;
remark: string; remark: string;
role: number; // 群主:4, 管理员:3群员:2 role: GroupMemberRole; // 群主:4, 管理员:3群员:2
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚 shutUpTime: number; // 禁言时间,单位是什么暂时不清楚
uid: string; // 加密的字符串 uid: string; // 加密的字符串
uin: string; // QQ号 uin: string; // QQ号
@@ -62,6 +68,7 @@ export interface GroupMember {
export enum ElementType { export enum ElementType {
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3,
PTT = 4, PTT = 4,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
@@ -136,7 +143,30 @@ export interface SendFaceElement {
faceElement: FaceElement faceElement: FaceElement
} }
export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement | SendFaceElement export interface FileElement {
"fileMd5"?: "",
"fileName": string,
"filePath": string,
"fileSize": string,
"picHeight"?: number,
"picWidth"?: number,
"picThumbPath"?: {},
"file10MMd5"?: "",
"fileSha"?: "",
"fileSha3"?: "",
"fileUuid"?: "",
"fileSubId"?: "",
"thumbFileSize"?: number
}
export interface SendFileElement {
"elementType": ElementType.FILE,
"elementId": "",
"fileElement": FileElement
}
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
@@ -206,6 +236,31 @@ export interface FaceElement {
faceType: 1 faceType: 1
} }
export interface VideoElement {
"filePath": string,
"fileName": string,
"videoMd5": string,
"thumbMd5": string
"fileTime": 87, // second
"thumbSize": 314235, // byte
"fileFormat": 2, // 2表示mp4
"fileSize": string, // byte
"thumbWidth": number,
"thumbHeight": number,
"busiType": 0, // 未知
"subBusiType": 0, // 未知
"thumbPath": {},
"transferStatus": 0, // 未知
"progress": 0, // 下载进度?
"invalidState": 0, // 未知
"fileUuid": string, // 可以用于下载链接?
"fileSubId": "",
"fileBizId": null,
"originVideoMd5": "",
"import_rich_media_context": null,
"sourceVideoCodecFormat": 0
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string;
msgShortId?: number; // 自己维护的消息id msgShortId?: number; // 自己维护的消息id
@@ -239,5 +294,63 @@ export interface RawMessage {
arkElement: ArkElement; arkElement: ArkElement;
grayTipElement: GrayTipElement; grayTipElement: GrayTipElement;
faceElement: FaceElement; faceElement: FaceElement;
videoElement: VideoElement;
fileElement: FileElement;
}[]; }[];
} }
export enum GroupNotifyTypes {
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
ADMIN_UNSET = 12,
MEMBER_EXIT = 11, // 主动退出?
}
export interface GroupNotifies {
doubt: boolean,
nextStartSeq: string,
notifies: GroupNotify[],
}
export interface GroupNotify {
seq: string, // 转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes,
status: 0, // 未知
group: { groupCode: string, groupName: string },
user1: { uid: string, nickName: string }, // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者
actionUser: { uid: string, nickName: string }, //未知
actionTime: string,
invitationExt: {
srcType: number, // 0?未知
groupCode: string, waitStatus: number
},
postscript: string, // 加群用户填写的验证信息
repeatSeqs: [],
warningTips: string
}
export enum GroupRequestOperateTypes {
approve = 1,
reject = 2
}
export interface FriendRequest {
friendUid: string,
reqTime: string, // 时间戳,秒
extWords: string, // 申请人填写的验证消息
isUnread: boolean,
friendNick: string,
sourceId: number,
groupCode: string
}
export interface FriendRequestNotify {
data: {
unreadNums: number,
buddyReqs: FriendRequest[]
}
}

View File

@@ -1,6 +1,6 @@
import {ActionName, BaseCheckResult} from "./types" import {ActionName, BaseCheckResult} from "./types"
import {OB11Response, OB11WebsocketResponse} from "./utils" import {OB11Response} from "./utils"
import {OB11Return, OB11WebsocketReturn} from "../types"; import {OB11Return} from "../types";
class BaseAction<PayloadType, ReturnDataType> { class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName actionName: ActionName
@@ -23,16 +23,16 @@ class BaseAction<PayloadType, ReturnDataType> {
} }
} }
public async websocketHandle(payload: PayloadType, echo: string): Promise<OB11WebsocketReturn<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload) const result = await this.check(payload)
if (!result.valid) { if (!result.valid) {
return OB11WebsocketResponse.error(result.message, 1400) return OB11Response.error(result.message, 1400)
} }
try { try {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11WebsocketResponse.ok(resData, echo); return OB11Response.ok(resData, echo);
} catch (e) { } catch (e) {
return OB11WebsocketResponse.error(e.toString(), 1200) return OB11Response.error(e.toString(), 1200, echo)
} }
} }

View File

@@ -0,0 +1,28 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {friends} from "../../common/data";
import {ActionName} from "./types";
import {log} from "../../common/utils";
interface Payload{
method: string,
args: any[],
}
export default class Debug extends BaseAction<Payload, any>{
actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> {
log("debug call ntqq api", payload);
const method = NTQQApi[payload.method]
if (!method){
throw `${method} 不存在`
}
const result = method(...payload.args);
if (method.constructor.name === "AsyncFunction"){
return await result
}
return result
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

@@ -0,0 +1,9 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
export default class GetGuildList extends BaseAction<null, null>{
actionName = ActionName.GetGuildList
protected async _handle(payload: null): Promise<null> {
return null;
}
}

View File

@@ -0,0 +1,31 @@
import BaseAction from "./BaseAction";
import {getFriend} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import { log } from "../../common/utils";
interface Payload {
user_id: number,
times: number
}
export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> {
const qq = payload.user_id.toString();
const friend = await getFriend(qq)
if (!friend) {
throw (`点赞失败,${qq}不是好友`)
}
try {
let result = await NTQQApi.likeFriend(friend.uid, parseInt(payload.times.toString()) || 1);
if (result.result !== 0){
throw result.errMsg
}
} catch (e) {
throw `点赞失败 ${e}`
}
return null
}
}

View File

@@ -9,6 +9,8 @@ import {ActionName, BaseCheckResult} from "./types";
import * as fs from "fs"; import * as fs from "fs";
import {log} from "../../common/utils"; import {log} from "../../common/utils";
import {v4 as uuidv4} from "uuid" import {v4 as uuidv4} from "uuid"
import {decodeCQCode} from "../cqcode";
import {Send} from "express";
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@@ -49,7 +51,7 @@ export interface ReturnDataType {
message_id: number message_id: number
} }
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
@@ -115,14 +117,15 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
private convertMessage2List(message: OB11MessageMixType) { protected convertMessage2List(message: OB11MessageMixType) {
if (typeof message === "string") { if (typeof message === "string") {
message = [{ // message = [{
type: OB11MessageDataType.text, // type: OB11MessageDataType.text,
data: { // data: {
text: message // text: message
} // }
}] as OB11MessageData[] // }] as OB11MessageData[]
message = decodeCQCode(message.toString())
} else if (!Array.isArray(message)) { } else if (!Array.isArray(message)) {
message = [message] message = [message]
} }
@@ -145,22 +148,25 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let nodeIds: string[] = [] let nodeIds: string[] = []
for (const messageNode of messageNodes) { for (const messageNode of messageNodes) {
// 一个node表示一个人的消息 // 一个node表示一个人的消息
let nodeId = messageNode.data.id; let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片 // 有nodeId表示一个子转发消息卡片
if (nodeId) { if (nodeId) {
nodeIds.push(nodeId) let nodeMsg = getHistoryMsgByShortId(nodeId);
if (nodeMsg) {
nodeIds.push(nodeMsg.msgId);
}
} else { } else {
// 自定义的消息 // 自定义的消息
// 提取消息段发给自己生成消息id // 提取消息段发给自己生成消息id
try {
const { const {
sendElements, sendElements,
deleteAfterSentFiles deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group) } = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
try {
log("开始生成转发节点", sendElements); log("开始生成转发节点", sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true); const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
nodeIds.push(nodeMsg.msgId) nodeIds.push(nodeMsg.msgId)
log("转发节点生成成功", nodeMsg.msgId);
} catch (e) { } catch (e) {
log("生效转发消息节点失败", e) log("生效转发消息节点失败", e)
} }
@@ -225,6 +231,8 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
break; break;
case OB11MessageDataType.image: case OB11MessageDataType.image:
case OB11MessageDataType.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const file = sendMsg.data?.file const file = sendMsg.data?.file
if (file) { if (file) {
@@ -233,14 +241,17 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (!isLocal) { // 只删除http和base64转过来的文件 if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path) deleteAfterSentFiles.push(path)
} }
if (sendMsg.type === OB11MessageDataType.image) { const constructorMap = {
sendElements.push(await SendMsgElementConstructor.pic(path)) [OB11MessageDataType.image]: SendMsgElementConstructor.pic,
} else { [OB11MessageDataType.voice]: SendMsgElementConstructor.ptt,
sendElements.push(await SendMsgElementConstructor.ptt(path)) [OB11MessageDataType.video]: SendMsgElementConstructor.video,
[OB11MessageDataType.file]: SendMsgElementConstructor.file,
}
sendElements.push(await constructorMap[sendMsg.type](path));
} }
} }
} }
} break; break;
} }
} }
@@ -255,7 +266,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (!sendElements.length) { if (!sendElements.length) {
throw ("消息体无法解析") throw ("消息体无法解析")
} }
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete) const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000);
addHistoryMsg(returnMsg) addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => { deleteAfterSentFiles.map(f => fs.unlink(f, () => {
})) }))

View File

@@ -0,0 +1,18 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
interface Payload {
flag: string,
approve: boolean,
remark?: string,
}
export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest;
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.handleFriendRequest(parseInt(payload.flag), payload.approve)
return null;
}
}

View File

@@ -0,0 +1,30 @@
import BaseAction from "./BaseAction";
import {groupNotifies} from "../../common/data";
import {GroupNotify, GroupRequestOperateTypes} from "../../ntqqapi/types";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
interface Payload{
flag: string,
// sub_type: "add" | "invite",
// type: "add" | "invite"
approve: boolean,
reason: string
}
export default class SetGroupAddRequest extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupAddRequest
protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString();
const notify: GroupNotify = groupNotifies[seq]
try{
await NTQQApi.handleGroupRequest(seq,
payload.approve ? GroupRequestOperateTypes.approve: GroupRequestOperateTypes.reject,
payload.reason
)
}catch (e) {
throw e
}
return null
}
}

View File

@@ -0,0 +1,23 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {GroupMemberRole} from "../../ntqqapi/types";
import {ActionName} from "./types";
interface Payload{
group_id: number,
user_id: number,
enable: boolean
}
export default class SetGroupAdmin extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupAdmin
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.setMemberRole(payload.group_id.toString(), member.uid, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal)
return null
}
}

View File

@@ -0,0 +1,23 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
interface Payload{
group_id: number,
user_id: number,
duration: number
}
export default class SetGroupBan extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupBan
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.banMember(payload.group_id.toString(),
[{uid:member.uid, timeStamp: parseInt(payload.duration.toString())}])
return null
}
}

View File

@@ -0,0 +1,23 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {GroupMemberRole} from "../../ntqqapi/types";
import {ActionName} from "./types";
interface Payload{
group_id: number,
user_id: number,
card: string
}
export default class SetGroupCard extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupCard
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || "")
return null
}
}

View File

@@ -0,0 +1,22 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
interface Payload{
group_id: number,
user_id: number,
reject_add_request: boolean
}
export default class SetGroupKick extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupKick
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request);
return null
}
}

View File

@@ -0,0 +1,22 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {log} from "../../common/utils";
import {ActionName} from "./types";
interface Payload{
group_id: number,
is_dismiss: boolean
}
export default class SetGroupLeave extends BaseAction<Payload, any>{
actionName = ActionName.SetGroupLeave
protected async _handle(payload: Payload): Promise<any> {
try{
await NTQQApi.quitGroup(payload.group_id.toString())
}
catch (e) {
log("退群失败", e)
throw e
}
}
}

View File

@@ -0,0 +1,18 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
interface Payload {
group_id: number,
group_name: string
}
export default class SetGroupName extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupName
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.setGroupName(payload.group_id.toString(), payload.group_name)
return null
}
}

View File

@@ -0,0 +1,18 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
interface Payload {
group_id: number,
enable: boolean
}
export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.banGroup(payload.group_id.toString(), !!payload.enable)
return null
}
}

View File

@@ -0,0 +1,24 @@
import BaseAction from "../BaseAction";
import {OB11GroupMember, OB11User} from "../../types";
import {friends, getFriend, getGroupMember, groups} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types";
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{user_id: number}, OB11User>{
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString()
const friend = await getFriend(user_id)
if (friend){
return OB11Constructor.friend(friend);
}
for(const group of groups){
const member = await getGroupMember(group.groupCode, user_id)
if (member){
return OB11Constructor.groupMember(group.groupCode, member) as OB11User
}
}
throw ("查无此人")
}
}

View File

@@ -0,0 +1,15 @@
import SendMsg, {ReturnDataType} from "../SendMsg";
import {OB11MessageMixType, OB11PostSendMsg} from "../../types";
import {ActionName, BaseCheckResult} from "../types";
export class GoCQHTTPSendGroupForwardMsg extends SendMsg{
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
protected async check(payload: OB11PostSendMsg){
payload.message = this.convertMessage2List(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendGroupForwardMsg{
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
}

View File

@@ -14,18 +14,50 @@ import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord"; import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage"; import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus"; import GetStatus from "./GetStatus";
import {GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg";
import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo";
import SendLike from "./SendLike";
import SetGroupAddRequest from "./SetGroupAddRequest";
import SetGroupLeave from "./SetGroupLeave";
import GetGuildList from "./GetGuildList";
import Debug from "./Debug";
import SetFriendAddRequest from "./SetFriendAddRequest";
import SetGroupWholeBan from "./SetGroupWholeBan";
import SetGroupName from "./SetGroupName";
import SetGroupBan from "./SetGroupBan";
import SetGroupKick from "./SetGroupKick";
import SetGroupAdmin from "./SetGroupAdmin";
import SetGroupCard from "./SetGroupCard";
export const actionHandlers = [ export const actionHandlers = [
new Debug(),
new SendLike(),
new GetMsg(), new GetMsg(),
new GetLoginInfo(), new GetLoginInfo(),
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 SetGroupAddRequest(),
new SetFriendAddRequest(),
new SetGroupLeave(),
new GetVersionInfo(), new GetVersionInfo(),
new CanSendRecord(), new CanSendRecord(),
new CanSendImage(), new CanSendImage(),
new GetStatus() new GetStatus(),
new SetGroupWholeBan(),
new SetGroupBan(),
new SetGroupKick(),
new SetGroupAdmin(),
new SetGroupName(),
new SetGroupCard(),
//以下为go-cqhttp api
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GetGuildList()
] ]
function initActionMap() { function initActionMap() {

View File

@@ -1,3 +1,5 @@
import GetGuildList from "./GetGuildList";
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
export interface ValidCheckResult { export interface ValidCheckResult {
@@ -12,7 +14,8 @@ export interface InvalidCheckResult {
} }
export enum ActionName { export enum ActionName {
TestForwardMsg = "test_forward_msg", Debug = "llonebot_debug",
SendLike = "send_like",
GetLoginInfo = "get_login_info", GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list", GetFriendList = "get_friend_list",
GetGroupInfo = "get_group_info", GetGroupInfo = "get_group_info",
@@ -24,8 +27,22 @@ export enum ActionName {
SendGroupMsg = "send_group_msg", SendGroupMsg = "send_group_msg",
SendPrivateMsg = "send_private_msg", SendPrivateMsg = "send_private_msg",
DeleteMsg = "delete_msg", DeleteMsg = "delete_msg",
SetGroupAddRequest = "set_group_add_request",
SetFriendAddRequest = "set_friend_add_request",
SetGroupLeave = "set_group_leave",
GetVersionInfo = "get_version_info", GetVersionInfo = "get_version_info",
GetStatus = "get_status", GetStatus = "get_status",
CanSendRecord = "can_send_record", CanSendRecord = "can_send_record",
CanSendImage = "can_send_image", CanSendImage = "can_send_image",
SetGroupKick = "set_group_kick",
SetGroupBan = "set_group_ban",
SetGroupWholeBan = "set_group_whole_ban",
SetGroupAdmin = "set_group_admin",
SetGroupCard = "set_group_card",
SetGroupName = "set_group_name",
// 以下为go-cqhttp api
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg",
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",
GoCQHTTP_GetStrangerInfo = "get_stranger_info",
GetGuildList = "get_guild_list",
} }

View File

@@ -1,4 +1,5 @@
import {OB11Return, OB11WebsocketReturn} from '../types'; import {OB11Return} from '../types';
import {isNull} from '../../common/utils';
export class OB11Response { export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> { static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {
@@ -6,31 +7,25 @@ export class OB11Response {
status: status, status: status,
retcode: retcode, retcode: retcode,
data: data, data: data,
message: message message: message,
} wording: message,
} echo: null
static ok<T>(data: T) {
return OB11Response.res<T>(data, "ok", 0)
}
static error(err: string, retcode: number) {
return OB11Response.res(null, "failed", retcode, err)
} }
} }
export class OB11WebsocketResponse { static ok<T>(data: T, echo: any = null) {
static res<T>(data: T, status: string, retcode: number, echo: string, message: string = ""): OB11WebsocketReturn<T> { let res = OB11Response.res<T>(data, "ok", 0)
return { if (!isNull(echo)) {
status: status, res.echo = echo;
retcode: retcode, }
data: data, return res;
echo: echo, }
message: message
} static error(err: string, retcode: number, echo: any = null) {
} let res = OB11Response.res(null, "failed", retcode, err)
static ok<T>(data: T, echo: string = "") { if (!isNull(echo)) {
return OB11WebsocketResponse.res<T>(data, "ok", 0, echo) res.echo = echo;
} }
static error(err: string, retcode: number, echo: string = "") { return res;
return OB11WebsocketResponse.res(null, "failed", retcode, echo, err)
} }
} }

View File

@@ -11,12 +11,14 @@ import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfI
import {getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo} from '../common/data'; import {getFriend, getGroupMember, getHistoryMsgBySeq, 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";
import {EventType} from "./event/OB11BaseEvent";
import {encodeCQCode} from "./cqcode";
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
const {enableLocalFile2Url} = getConfigUtil().getConfig() const {enableLocalFile2Url, ob11: {messagePostFormat}} = 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: parseInt(selfInfo.uin), self_id: parseInt(selfInfo.uin),
@@ -33,11 +35,12 @@ export class OB11Constructor {
raw_message: "", raw_message: "",
font: 14, font: 14,
sub_type: "friend", sub_type: "friend",
message: [], message: messagePostFormat === 'string' ? '' : [],
post_type: "message", message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
} }
if (msg.chatType == ChatType.group) { if (msg.chatType == ChatType.group) {
resMsg.sub_type = "normal" resMsg.sub_type = "normal" // 这里go-cqhttp是group而onebot11标准是normal, 蛋疼
resMsg.group_id = parseInt(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) {
@@ -80,27 +83,43 @@ export class OB11Constructor {
} }
} else if (element.textElement) { } else if (element.textElement) {
message_data["type"] = "text" message_data["type"] = "text"
resMsg.raw_message += message_data["data"]["text"] = element.textElement.content let text = element.textElement.content
if (!text.trim()) {
continue;
}
message_data["data"]["text"] = text
} else if (element.replyElement) {
message_data["type"] = "reply"
const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq)
if (replyMsg) {
message_data["data"]["id"] = replyMsg.msgShortId.toString()
} else {
continue
}
} else if (element.picElement) { } else if (element.picElement) {
message_data["type"] = "image" message_data["type"] = "image"
message_data["data"]["file_id"] = element.picElement.fileUuid message_data["data"]["file_id"] = element.picElement.fileUuid
message_data["data"]["path"] = element.picElement.sourcePath message_data["data"]["path"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.sourcePath message_data["data"]["file"] = element.picElement.sourcePath
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl message_data["data"]["url"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl
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) {
} }
} else if (element.replyElement) { } else if (element.videoElement) {
message_data["type"] = "reply" message_data["type"] = OB11MessageDataType.video;
const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq) message_data["data"]["file"] = element.videoElement.filePath
if (replyMsg) { message_data["data"]["file_id"] = element.videoElement.fileUuid
message_data["data"]["id"] = replyMsg.msgShortId // 怎么拿到url呢
} else { } else if (element.fileElement) {
continue message_data["type"] = OB11MessageDataType.file;
message_data["data"]["file"] = element.fileElement.filePath
message_data["data"]["file_id"] = element.fileElement.fileUuid
message_data["data"]["file_size"] = element.fileElement.fileSize
// 怎么拿到url呢
} }
} else if (element.pttElement) { else if (element.pttElement) {
message_data["type"] = OB11MessageDataType.voice; message_data["type"] = OB11MessageDataType.voice;
message_data["data"]["file"] = element.pttElement.filePath message_data["data"]["file"] = element.pttElement.filePath
message_data["data"]["file_id"] = element.pttElement.fileUuid message_data["data"]["file_id"] = element.pttElement.fileUuid
@@ -118,13 +137,16 @@ export class OB11Constructor {
message_data["type"] = OB11MessageDataType.face; message_data["type"] = OB11MessageDataType.face;
message_data["data"]["id"] = element.faceElement.faceIndex.toString(); message_data["data"]["id"] = element.faceElement.faceIndex.toString();
} }
if (message_data.data.file) { if (message_data.data.file) {
let filePath: string = message_data.data.file; let filePath: string = message_data.data.file;
if (!enableLocalFile2Url) { if (!enableLocalFile2Url) {
message_data.data.file = "file://" + filePath message_data.data.file = "file://" + filePath
} else { // 不使用本地路径 } else { // 不使用本地路径
if (message_data.data.http_file) { const ignoreTypes = [OB11MessageDataType.file, OB11MessageDataType.video]
message_data.data.file = message_data.data.http_file if (!ignoreTypes.includes(message_data.type)) {
if (message_data.data.url && !message_data.data.url.startsWith(IMAGE_HTTP_HOST + "/download")) {
message_data.data.file = message_data.data.url
} else { } else {
let {err, data} = await file2base64(filePath); let {err, data} = await file2base64(filePath);
if (err) { if (err) {
@@ -133,12 +155,21 @@ export class OB11Constructor {
message_data.data.file = "base64://" + data message_data.data.file = "base64://" + data
} }
} }
}else{
message_data.data.file = "file://" + filePath
} }
} }
}
if (message_data.type !== "unknown" && message_data.data) { if (message_data.type !== "unknown" && message_data.data) {
resMsg.message.push(message_data); if (messagePostFormat === 'string') {
const cqCode = encodeCQCode(message_data);
(resMsg.message as string) += cqCode;
resMsg.raw_message += cqCode;
} else (resMsg.message as OB11MessageData[]).push(message_data);
} }
} }
resMsg.raw_message = resMsg.raw_message.trim();
return resMsg; return resMsg;
} }
@@ -175,7 +206,8 @@ export class OB11Constructor {
group_id: parseInt(group_id), group_id: parseInt(group_id),
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName card: member.cardName,
role: OB11Constructor.groupMemberRole(member.role),
} }
} }

71
src/onebot11/cqcode.ts Normal file
View File

@@ -0,0 +1,71 @@
import {OB11MessageData} from "./types";
const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/
function unescape(source: string) {
return String(source)
.replace(/&#91;/g, '[')
.replace(/&#93;/g, ']')
.replace(/&#44;/g, ',')
.replace(/&amp;/g, '&')
}
function from(source: string) {
const capture = pattern.exec(source)
if (!capture) return null
const [, type, attrs] = capture
const data: Record<string, any> = {}
attrs && attrs.slice(1).split(',').forEach((str) => {
const index = str.indexOf('=')
data[str.slice(0, index)] = unescape(str.slice(index + 1))
})
return {type, data, capture}
}
function h(type: string, data: any) {
return {
type,
data,
}
}
export function decodeCQCode(source: string): OB11MessageData[] {
const elements: any[] = []
let result: ReturnType<typeof from>
while ((result = from(source))) {
const {type, data, capture} = result
if (capture.index) {
elements.push(h('text', {text: unescape(source.slice(0, capture.index))}))
}
elements.push(h(type, data))
source = source.slice(capture.index + capture[0].length)
}
if (source) elements.push(h('text', {text: unescape(source)}))
return elements
}
export function encodeCQCode(data: OB11MessageData) {
const CQCodeEscape = (text: string) => {
return text.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;')
.replace(/\&/g, '&amp;')
.replace(/,/g, '&#44;');
};
if (data.type === 'text') {
return CQCodeEscape(data.data.text);
}
let result = '[CQ:' + data.type;
for (const name in data.data) {
const value = data.data[name];
result += `,${name}=${CQCodeEscape(value)}`;
}
result += ']';
return result;
}
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好")
// console.log(JSON.stringify(result))

View File

@@ -4,7 +4,8 @@ export enum EventType {
META = "meta_event", META = "meta_event",
REQUEST = "request", REQUEST = "request",
NOTICE = "notice", NOTICE = "notice",
MESSAGE = "message" MESSAGE = "message",
MESSAGE_SENT = "message_sent",
} }

View File

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

View File

@@ -2,7 +2,7 @@ import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent { export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = "group_decrease"; notice_type = "group_decrease";
sub_type = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me") sub_type: "leave" | "kick" | "kick_me" = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operate_id: number; operate_id: number;
constructor(groupId: number, userId: number) { constructor(groupId: number, userId: number) {

View File

@@ -0,0 +1,11 @@
import {OB11BaseNoticeEvent} from "../notice/OB11BaseNoticeEvent";
import {EventType} from "../OB11BaseEvent";
export class OB11FriendRequestEvent extends OB11BaseNoticeEvent {
// post_type = EventType.REQUEST
user_id: number;
request_type: "friend" = "friend";
comment: string;
flag: string;
}

View File

@@ -0,0 +1,9 @@
import {OB11GroupNoticeEvent} from "../notice/OB11GroupNoticeEvent";
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent{
request_type: "group" = "group";
sub_type: "add" | "invite" = "add";
comment: string;
flag: string;
}

View File

@@ -29,10 +29,10 @@ export function postWsEvent(event: PostEventType) {
} }
} }
export function postEvent(msg: PostEventType) { export function postOB11Event(msg: PostEventType, reportSelf=false) {
const config = getConfigUtil().getConfig(); const config = getConfigUtil().getConfig();
// 判断msg是否是event // 判断msg是否是event
if (!config.reportSelfMessage) { if (!config.reportSelfMessage && !reportSelf) {
if ((msg as OB11Message).user_id.toString() == selfInfo.uin) { if ((msg as OB11Message).user_id.toString() == selfInfo.uin) {
return return
} }

View File

@@ -4,10 +4,10 @@ import * as WebSocket from "ws";
import {selfInfo} from "../../../common/data"; import {selfInfo} from "../../../common/data";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent"; import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
import {ActionName} from "../../action/types"; import {ActionName} from "../../action/types";
import {OB11WebsocketResponse} from "../../action/utils"; import {OB11Response} from "../../action/utils";
import BaseAction from "../../action/BaseAction"; import BaseAction from "../../action/BaseAction";
import {actionMap} from "../../action"; import {actionMap} from "../../action";
import {registerWsEventSender, unregisterWsEventSender} from "../postevent"; import {registerWsEventSender, unregisterWsEventSender} from "../postOB11Event";
import {wsReply} from "./reply"; import {wsReply} from "./reply";
export let rwsList: ReverseWebsocket[] = []; export let rwsList: ReverseWebsocket[] = [];
@@ -33,24 +33,24 @@ export class ReverseWebsocket {
} }
public async onmessage(msg: string) { public async onmessage(msg: string) {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}} let receiveData: { action: ActionName, params: any, echo?: any } = {action: null, params: {}}
let echo = "" let echo = null
log("收到反向Websocket消息", msg.toString())
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
echo = receiveData.echo echo = receiveData.echo
log("收到反向Websocket消息", receiveData)
} catch (e) { } catch (e) {
return wsReply(this.websocket, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo)) return wsReply(this.websocket, OB11Response.error("json解析失败请检查数据格式", 1400, echo))
} }
const action: BaseAction<any, any> = actionMap.get(receiveData.action); const action: BaseAction<any, any> = actionMap.get(receiveData.action);
if (!action) { if (!action) {
return wsReply(this.websocket, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo)) return wsReply(this.websocket, OB11Response.error("不支持的api " + receiveData.action, 1404, echo))
} }
try { try {
let handleResult = await action.websocketHandle(receiveData.params, echo); let handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(this.websocket, handleResult) wsReply(this.websocket, handleResult)
} catch (e) { } catch (e) {
wsReply(this.websocket, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo)) wsReply(this.websocket, OB11Response.error(`api处理出错:${e}`, 1200, echo))
} }
} }

View File

@@ -1,8 +1,8 @@
import {WebSocket} from "ws"; import {WebSocket} from "ws";
import {getConfigUtil, log} from "../../../common/utils"; import {getConfigUtil, log} from "../../../common/utils";
import {actionMap} from "../../action"; import {actionMap} from "../../action";
import {OB11WebsocketResponse} from "../../action/utils"; import {OB11Response} from "../../action/utils";
import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postevent"; import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postOB11Event";
import {ActionName} from "../../action/types"; import {ActionName} from "../../action/types";
import BaseAction from "../../action/BaseAction"; import BaseAction from "../../action/BaseAction";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent"; import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
@@ -15,33 +15,33 @@ let heartbeatRunning = false;
class OB11WebsocketServer extends WebsocketServerBase { class OB11WebsocketServer extends WebsocketServerBase {
authorizeFailed(wsClient: WebSocket) { authorizeFailed(wsClient: WebSocket) {
wsClient.send(JSON.stringify(OB11WebsocketResponse.res(null, "failed", 1403, "token验证失败"))) wsClient.send(JSON.stringify(OB11Response.res(null, "failed", 1403, "token验证失败")))
} }
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: string) { async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> = actionMap.get(actionName); const action: BaseAction<any, any> = actionMap.get(actionName);
if (!action) { if (!action) {
return wsReply(wsClient, OB11WebsocketResponse.error("不支持的api " + actionName, 1404, echo)) return wsReply(wsClient, OB11Response.error("不支持的api " + actionName, 1404, echo))
} }
try { try {
let handleResult = await action.websocketHandle(params, echo); let handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult) wsReply(wsClient, handleResult)
} catch (e) { } catch (e) {
wsReply(wsClient, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo)) wsReply(wsClient, OB11Response.error(`api处理出错:${e}`, 1200, echo))
} }
} }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
if (url == "/api" || url == "/api/" || url == "/") { if (url == "/api" || url == "/api/" || url == "/") {
wsClient.on("message", async (msg) => { wsClient.on("message", async (msg) => {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}} let receiveData: { action: ActionName, params: any, echo?: any } = {action: null, params: {}}
let echo = "" let echo = null
log("收到正向Websocket消息", msg.toString())
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
echo = receiveData.echo echo = receiveData.echo
log("收到正向Websocket消息", receiveData);
} catch (e) { } catch (e) {
return wsReply(wsClient, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo)) return wsReply(wsClient, OB11Response.error("json解析失败请检查数据格式", 1400, echo))
} }
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then() this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then()
}) })

View File

@@ -1,17 +1,17 @@
import * as websocket from "ws"; import * as websocket from "ws";
import {OB11WebsocketResponse} from "../../action/utils"; import {OB11Response} from "../../action/utils";
import {PostEventType} from "../postevent"; import {PostEventType} from "../postOB11Event";
import {log} from "../../../common/utils"; import {isNull, log} from "../../../common/utils";
export function wsReply(wsClient: websocket.WebSocket, data: OB11WebsocketResponse | PostEventType) { export function wsReply(wsClient: websocket.WebSocket, data: OB11Response | PostEventType) {
try { try {
let packet = Object.assign({ let packet = Object.assign({
}, data); }, data);
if (!packet["echo"]){ if (isNull(packet["echo"])){
delete packet["echo"]; delete packet["echo"];
} }
wsClient.send(JSON.stringify(packet)) wsClient.send(JSON.stringify(packet))
log("ws 消息上报", wsClient.url, data) log("ws 消息上报", wsClient.url || "", data)
} catch (e) { } catch (e) {
log("websocket 回复失败", e) log("websocket 回复失败", e)
} }

View File

@@ -1,4 +1,5 @@
import {AtType, RawMessage} from "../ntqqapi/types"; import {RawMessage} from "../ntqqapi/types";
import {EventType} from "./event/OB11BaseEvent";
export interface OB11User { export interface OB11User {
user_id: number; user_id: number;
@@ -64,10 +65,11 @@ export interface OB11Message {
message_type: "private" | "group", message_type: "private" | "group",
sub_type?: "friend" | "group" | "normal", sub_type?: "friend" | "group" | "normal",
sender: OB11Sender, sender: OB11Sender,
message: OB11MessageData[], message: OB11MessageData[] | string,
message_format: 'array' | 'string',
raw_message: string, raw_message: string,
font: number, font: number,
post_type?: "message", post_type?: EventType,
raw?: RawMessage raw?: RawMessage
} }
@@ -76,16 +78,16 @@ export interface OB11Return<DataType> {
retcode: number retcode: number
data: DataType data: DataType
message: string, message: string,
} echo?: any, // ws调用api才有此字段
wording?: string, // go-cqhttp字段错误信息
export interface OB11WebsocketReturn<DataType> extends OB11Return<DataType>{
echo: string
} }
export enum OB11MessageDataType { export enum OB11MessageDataType {
text = "text", text = "text",
image = "image", image = "image",
video = "video",
voice = "record", voice = "record",
file = "file",
at = "at", at = "at",
reply = "reply", reply = "reply",
json = "json", json = "json",
@@ -103,7 +105,7 @@ export interface OB11MessageText {
interface OB11MessageFileBase { interface OB11MessageFileBase {
data: { data: {
file: string, file: string,
http_file?: string; url?: string;
} }
} }
@@ -115,6 +117,14 @@ export interface OB11MessageRecord extends OB11MessageFileBase {
type: OB11MessageDataType.voice type: OB11MessageDataType.voice
} }
export interface OB11MessageFile extends OB11MessageFileBase {
type: OB11MessageDataType.file
}
export interface OB11MessageVideo extends OB11MessageFileBase {
type: OB11MessageDataType.video
}
export interface OB11MessageAt { export interface OB11MessageAt {
type: OB11MessageDataType.at type: OB11MessageDataType.at
data: { data: {
@@ -152,7 +162,7 @@ export type OB11MessageData =
OB11MessageText | OB11MessageText |
OB11MessageFace | OB11MessageFace |
OB11MessageAt | OB11MessageReply | OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode OB11MessageNode
export interface OB11PostSendMsg { export interface OB11PostSendMsg {
@@ -160,6 +170,7 @@ export interface OB11PostSendMsg {
user_id: string, user_id: string,
group_id?: string, group_id?: string,
message: OB11MessageMixType; message: OB11MessageMixType;
messages?: OB11MessageMixType; // 兼容 go-cqhttp
} }
export interface OB11Version { export interface OB11Version {

View File

@@ -61,39 +61,3 @@ 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,7 +1,14 @@
// Electron 主进程 与 渲染进程 交互的桥梁 // Electron 主进程 与 渲染进程 交互的桥梁
import {Config} from "./common/types"; import {Config, LLOneBotError} from "./common/types";
import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "./common/channels"; import {
CHANNEL_ERROR,
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG,
} from "./common/channels";
const {contextBridge} = require("electron"); const {contextBridge} = require("electron");
const {ipcRenderer} = require('electron'); const {ipcRenderer} = require('electron');
@@ -12,9 +19,15 @@ const llonebot = {
setConfig: (config: Config) => { setConfig: (config: Config) => {
ipcRenderer.send(CHANNEL_SET_CONFIG, config); ipcRenderer.send(CHANNEL_SET_CONFIG, config);
}, },
getConfig: async () => { getConfig: async (): Promise<Config> => {
return ipcRenderer.invoke(CHANNEL_GET_CONFIG); return ipcRenderer.invoke(CHANNEL_GET_CONFIG);
}, },
getError: async (): Promise<LLOneBotError> => {
return ipcRenderer.invoke(CHANNEL_ERROR);
},
selectFile: (): Promise<string> => {
return ipcRenderer.invoke(CHANNEL_SELECT_FILE);
}
} }
export type LLOneBot = typeof llonebot; export type LLOneBot = typeof llonebot;

View File

@@ -2,14 +2,17 @@
// 打开设置界面时触发 // 打开设置界面时触发
async function onSettingWindowCreated(view: Element) { async function onSettingWindowCreated(view: Element) {
window.llonebot.log("setting window created"); window.llonebot.log("setting window created");
const isEmpty = (value: any) => value === undefined || value === null || value === '';
let config = await window.llonebot.getConfig() let config = await window.llonebot.getConfig()
const httpClass = "http"; const httpClass = "http";
const httpPostClass = "http-post"; const httpPostClass = "http-post";
const wsClass = "ws"; const wsClass = "ws";
const reverseWSClass = "reverse-ws"; const reverseWSClass = "reverse-ws";
const llonebotError = await window.llonebot.getError();
window.llonebot.log("获取error" + JSON.stringify(llonebotError));
function createHttpHostEleStr(host: string) { function createHttpHostEleStr(host: string) {
let eleStr = ` let eleStr = `
<setting-item data-direction="row" class="hostItem vertical-list-item ${httpPostClass}"> <setting-item data-direction="row" class="hostItem vertical-list-item ${httpPostClass}">
@@ -47,6 +50,18 @@ async function onSettingWindowCreated(view: Element) {
let html = ` let html = `
<div class="config_view llonebot"> <div class="config_view llonebot">
<setting-section> <setting-section>
<setting-panel id="llonebotError" style="display:${llonebotError.ffmpegError || llonebotError.otherError ? '' : 'none'}">
<setting-item id="ffmpegError" data-direction="row"
style="diplay:${llonebotError.ffmpegError ? '' : 'none'}"
class="hostItem vertical-list-item">
<setting-text data-type="secondary" class="err-content">${llonebotError.ffmpegError}</setting-text>
</setting-item>
<setting-item id="otherError" data-direction="row"
style="diplay:${llonebotError.otherError ? '' : 'none'}"
class="hostItem vertical-list-item">
<setting-text data-type="secondary" class="err-content">${llonebotError.otherError}</setting-text>
</setting-item>
</setting-panel>
<setting-panel> <setting-panel>
<setting-list class="wrap"> <setting-list class="wrap">
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item">
@@ -102,38 +117,65 @@ async function onSettingWindowCreated(view: Element) {
<setting-text>Access Token</setting-text> <setting-text>Access Token</setting-text>
<input id="token" type="text" placeholder="可为空" value="${config.token}"/> <input id="token" type="text" placeholder="可为空" value="${config.token}"/>
</setting-item> </setting-item>
<setting-item data-direction="row" class="vertical-list-item">
<setting-item data-direction="row" class="vertical-list-item" style="width: 80%">
<setting-text>ffmpeg路径</setting-text>
<input id="ffmpegPath" class="input-text" type="text"
style="width:80%;padding: 5px"
value="${config.ffmpeg || ''}"/>
</setting-item>
<button id="selectFFMPEG" class="q-button q-button--small q-button--secondary">选择ffmpeg</button>
</setting-item>
<button id="save" class="q-button">保存</button> <button id="save" class="q-button">保存</button>
</setting-list> </setting-list>
</setting-panel> </setting-panel>
<setting-panel> <setting-panel>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<setting-item data-direction="row" class="vertical-list-item">
<div>
<setting-text>消息上报数据类型</setting-text>
<setting-text data-type="secondary">如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 <a href="javascript:LiteLoader.api.openExternal('https://github.com/botuniverse/onebot-11/tree/master/message#readme');">OneBot v11 文档</a></setting-text>
</div>
<setting-select id="messagePostFormat">
<setting-option data-value="array" ${config.ob11.messagePostFormat !== "string" ? "is-selected" : ""}>消息段</setting-option>
<setting-option data-value="string" ${config.ob11.messagePostFormat === "string" ? "is-selected" : ""}>CQ码</setting-option>
</setting-select>
</setting-item>
<setting-item data-direction="row" class="vertical-list-item">
<div> <div>
<div>上报文件不采用本地路径</div> <div>上报文件不采用本地路径</div>
<div class="tips">开启后,上报图片为http连接语音为base64编码</div> <div class="tips">开启后,上报文件(图片语音等)为http链接或base64编码</div>
</div> </div>
<setting-switch id="switchFileUrl" ${config.enableLocalFile2Url ? "is-active" : ""}></setting-switch> <setting-switch id="switchFileUrl" ${config.enableLocalFile2Url ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="vertical-list-item">
<div> <div>
<div>debug模式</div> <div>debug模式</div>
<div class="tips">开启后上报消息添加raw字段附带原始消息</div> <div class="tips">开启后上报消息添加raw字段附带原始消息</div>
</div> </div>
<setting-switch id="debug" ${config.debug ? "is-active" : ""}></setting-switch> <setting-switch id="debug" ${config.debug ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="vertical-list-item">
<div> <div>
<div>上报自身消息</div> <div>上报自身消息</div>
<div class="tips">慎用,不然会自己和自己聊个不停</div> <div class="tips">慎用,不然会自己和自己聊个不停</div>
</div> </div>
<setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch> <setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="vertical-list-item">
<div> <div>
<div>日志</div> <div>日志</div>
<div class="tips">目录:${window.LiteLoader.plugins["LLOneBot"].path.data}</div> <div class="tips">目录:${window.LiteLoader.plugins["LLOneBot"].path.data}</div>
</div> </div>
<setting-switch id="log" ${config.log ? "is-active" : ""}></setting-switch> <setting-switch id="log" ${config.log ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
<setting-item data-direction="row" class="vertical-list-item">
<div>
<div>自动删除收到的文件</div>
<div class="tips">一分钟后会删除收到的图片语音</div>
</div>
<setting-switch id="autoDeleteFile" ${config.autoDeleteFile ? "is-active" : ""}></setting-switch>
</setting-item>
</setting-panel> </setting-panel>
</setting-section> </setting-section>
</div> </div>
@@ -155,6 +197,36 @@ async function onSettingWindowCreated(view: Element) {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html"); const doc = parser.parseFromString(html, "text/html");
const getError = async ()=> {
const llonebotError = await window.llonebot.getError();
console.log(llonebotError);
const llonebotErrorEle = document.getElementById("llonebotError");
const ffmpegErrorEle = document.getElementById("ffmpegError");
const otherErrorEle = document.getElementById("otherError");
if (llonebotError.otherError || llonebotError.ffmpegError){
llonebotErrorEle.style.display = ''
}
else{
llonebotErrorEle.style.display = 'none'
}
if (llonebotError.ffmpegError) {
const errContentEle = doc.querySelector("#ffmpegError .err-content")
// const errContent = ffmpegErrorEle.getElementsByClassName("err-content")[0];
errContentEle.textContent = llonebotError.ffmpegError;
(ffmpegErrorEle as HTMLElement).style.display = ''
}
else{
ffmpegErrorEle.style.display = ''
}
if (llonebotError.otherError) {
const errContentEle = doc.querySelector("#otherError .err-content")
errContentEle.textContent = llonebotError.otherError;
otherErrorEle.style.display = ''
}
else{
otherErrorEle.style.display = 'none'
}
}
function addHostEle(type: string, initValue: string = "") { function addHostEle(type: string, initValue: string = "") {
let addressEle, hostItemsEle; let addressEle, hostItemsEle;
@@ -174,6 +246,10 @@ async function onSettingWindowCreated(view: Element) {
doc.getElementById("addHttpHost").addEventListener("click", () => addHostEle("http")) doc.getElementById("addHttpHost").addEventListener("click", () => addHostEle("http"))
doc.getElementById("addWsHost").addEventListener("click", () => addHostEle("ws")) doc.getElementById("addWsHost").addEventListener("click", () => addHostEle("ws"))
doc.getElementById("messagePostFormat").addEventListener("selected", (e: CustomEvent) => {
config.ob11.messagePostFormat = e.detail && !isEmpty(e.detail.value) ? e.detail.value : 'array';
window.llonebot.setConfig(config);
})
function switchClick(eleId: string, configKey: string, _config = null) { function switchClick(eleId: string, configKey: string, _config = null) {
if (!_config) { if (!_config) {
@@ -211,6 +287,7 @@ async function onSettingWindowCreated(view: Element) {
switchClick("switchFileUrl", "enableLocalFile2Url"); switchClick("switchFileUrl", "enableLocalFile2Url");
switchClick("reportSelfMessage", "reportSelfMessage"); switchClick("reportSelfMessage", "reportSelfMessage");
switchClick("log", "log"); switchClick("log", "log");
switchClick("autoDeleteFile", "autoDeleteFile");
doc.getElementById("save")?.addEventListener("click", doc.getElementById("save")?.addEventListener("click",
() => { () => {
@@ -219,6 +296,7 @@ async function onSettingWindowCreated(view: Element) {
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; const tokenEle = document.getElementById("token") as HTMLInputElement;
const ffmpegPathEle = document.getElementById("ffmpegPath") as HTMLInputElement;
// 获取端口和host // 获取端口和host
const httpPort = httpPortEle.value const httpPort = httpPortEle.value
@@ -243,15 +321,26 @@ async function onSettingWindowCreated(view: Element) {
config.ob11.wsPort = parseInt(wsPort); config.ob11.wsPort = parseInt(wsPort);
config.ob11.wsHosts = wsHosts; config.ob11.wsHosts = wsHosts;
config.token = token; config.token = token;
config.ffmpeg = ffmpegPathEle.value.trim();
window.llonebot.setConfig(config); window.llonebot.setConfig(config);
setTimeout(()=>{
getError().then();
}, 1000);
alert("保存成功"); alert("保存成功");
}) })
doc.getElementById("selectFFMPEG")?.addEventListener("click", ()=>{
window.llonebot.selectFile().then(selectPath=>{
if (selectPath){
config.ffmpeg = (document.getElementById("ffmpegPath") as HTMLInputElement).value = selectPath;
// window.llonebot.setConfig(config);
}
});
})
doc.body.childNodes.forEach(node => { doc.body.childNodes.forEach(node => {
view.appendChild(node); view.appendChild(node);
}); });
} }

View File

@@ -1,4 +1,5 @@
// import path from "path"; // import path from "path";
const webpack = require('webpack');
const path = require('path'); const path = require('path');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin');
@@ -70,6 +71,9 @@ let config = {
} }
}) })
}), }),
new webpack.DefinePlugin({
'process.env.FLUENTFFMPEG_COV': false,
}),
], // devtool: 'source-map', ], // devtool: 'source-map',
} }