Compare commits

...

47 Commits

Author SHA1 Message Date
linyuchen
6e71cd6064 feat: group whole ban event 2024-03-11 11:55:10 +08:00
linyuchen
83dc1abd4a refactor: optimize json parser 2024-03-11 11:44:17 +08:00
linyuchen
4cabb9696e refactor: custom json parser 2024-03-11 10:46:58 +08:00
linyuchen
75883e9cae refactor: refactor new group member event
feat: group ban event
2024-03-11 09:55:50 +08:00
linyuchen
eeadaa12e9 fix: group notify db cache 2024-03-10 22:44:33 +08:00
linyuchen
192736c8be docs: add friend link 2024-03-10 10:05:13 +08:00
linyuchen
586fbb6518 refactor: host input component add attribute parameter 2024-03-10 10:04:09 +08:00
linyuchen
0a42e2df5b Merge remote-tracking branch 'origin/main' 2024-03-10 00:04:37 +08:00
linyuchen
97a637f0c6 chore: ver 3.13.10 2024-03-10 00:04:09 +08:00
linyuchen
3f10b7a002 fix: message real_id use int32 2024-03-10 00:03:37 +08:00
linyuchen
f638e48260 fix: 消息重复入库导致message_id每次都是+2 2024-03-10 00:03:18 +08:00
linyuchen
354ee389bc feat: clean cache api 2024-03-09 22:58:21 +08:00
linyuchen
7188946d7a refactor: try to refactor forward msg 2024-03-09 22:30:16 +08:00
linyuchen
53055e9eab Merge pull request #124 from super1207/main
fix cqcode encode
2024-03-09 22:24:24 +08:00
super1207
7bdb84b11b fix cqcode format 2024-03-09 15:04:09 +08:00
linyuchen
c906bcf7ea docs: update todo list 2024-03-09 12:53:16 +08:00
linyuchen
cdc82562a3 docs: update readme 2024-03-09 12:49:35 +08:00
linyuchen
c34ce8ce0c docs: update readme 2024-03-09 12:40:52 +08:00
linyuchen
d1c94754ee fix: send forward msg too fast 2024-03-09 09:59:39 +08:00
linyuchen
2626555c51 fix: check pic and ptt size 2024-03-09 02:43:58 +08:00
linyuchen
5ff6ceec6d chore: ver 3.13.8 2024-03-09 02:38:05 +08:00
linyuchen
17af156451 fix: update msg seqId 2024-03-09 02:34:21 +08:00
linyuchen
c3c9e74832 fix: image cache url 2024-03-08 23:52:35 +08:00
linyuchen
0480208738 feat: file cache db 2024-03-08 23:27:20 +08:00
linyuchen
62eefbdb69 fix: sent pic message url 2024-03-08 22:16:05 +08:00
linyuchen
566537cbe3 fix: 构建转发消息的文件没有自动删除 2024-03-07 20:19:00 +08:00
linyuchen
ed831ae4cd fix: 网络下载文件大小异常提示 2024-03-07 19:07:00 +08:00
linyuchen
501031b39b fix: 网络视频后缀识别
fix: 网络文件下载失败的错误提示
2024-03-07 18:43:47 +08:00
linyuchen
7bfb3f2003 fix: 私聊带@报错,现已过滤私聊的at消息 2024-03-07 17:37:57 +08:00
linyuchen
ba8ed36c6a fix: can not send reply msg
fix: send like return error
2024-03-07 15:34:09 +08:00
Misa Liu
66ca936148 style: Finishing code 2024-02-28 00:16:04 +08:00
Misa Liu
5088112864 feat: Now clean_cache API can delete cache files 2024-02-28 00:16:04 +08:00
Misa Liu
91075e192b feat: Add getFileCacheInfo to NTQQApi 2024-02-28 00:16:03 +08:00
Misa Liu
11108bc13f fix: Fix type 2024-02-28 00:16:03 +08:00
Misa Liu
3ec1134204 feat: Add clean_cache API to OneBot adapter 2024-02-28 00:16:02 +08:00
Misa Liu
de41dab846 fix: Fix a typo 2024-02-28 00:16:02 +08:00
Misa Liu
ededfe0f8c fix: Delete specific IPC channel for cache related APIs 2024-02-28 00:16:02 +08:00
Misa Liu
6548876c74 fix: Use a specific IPC channel for cache related API 2024-02-28 00:16:01 +08:00
Misa Liu
839fd7f1ab feat: Add addCacheScannedPaths to NTQQApi 2024-02-28 00:16:01 +08:00
Misa Liu
2f9cd8ba19 feat: Add getDesktopTmpPath to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
85d648622d feat: Add getHotUpdateCachePath to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
f08c816286 feat: Add clearCache to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
6c267044f0 fix: Use a specific IPC channel for cache related API 2024-02-28 00:15:59 +08:00
Misa Liu
b548fd3f0e feat: Add getCacheSessingPathList to NTQQApi 2024-02-28 00:15:59 +08:00
Misa Liu
f110c2d3df style: Fix typo 2024-02-28 00:15:58 +08:00
Misa Liu
f521873ba7 feat: Add scanCache to NTQQApi 2024-02-28 00:15:58 +08:00
Misa Liu
afe0ff89a7 feat: Add chat cache scan & clear to NTQQApi 2024-02-28 00:15:58 +08:00
30 changed files with 941 additions and 574 deletions

172
README.md
View File

@@ -1,175 +1,25 @@
# LLOneBot API # LLOneBot API
LiteLoaderQQNTOneBot11协议插件 LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG群<https://t.me/+nLZEnpne-pQ1OWFl>
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI*
## 安装方法 ## 安装方法
### 通用手动安装方法 <https://llonebot.github.io/zh-CN/guide/getting-started>
1.安装[LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) ## 设置界面
2.安装本项目插件[LLOneBot](https://github.com/linyuchen/LiteLoaderQQNT-OneBotApi/releases/), 注意本插件2.0以下的版本不支持LiteLoader 1.0.0及以上版本 <img src="./doc/image/setting.png" width="500px" alt="图片名称"/>
*关于插件的安装方法: 下载后解压复制到插件目录* ## HTTP 调用示例
*插件目录:`LiteLoaderQQNT/plugins`*
安装后的目录结构如下
```
├── plugins
│ ├── LLOneBot
│ │ └── main/
│ │ └── preload/
│ │ └── renderer/
│ │ └── manifest.json
│ │ └── node_modules/...
```
### Linux 容器化快速安装
执行以下任意脚本按照提示设置NoVnc密码即可运行脚本问题与异常参考 [llonebot-docker](https://github.com/MliKiowa/llonebot-docker) 项目。
```bash
curl https://cdn.jsdelivr.net/gh/LLOneBot/llonebot-docker/fastboot.sh -o fastboot.sh & chmod +x fastboot.sh & sudo sh fastboot.sh
```
```bash
wget -O fastboot.sh https://cdn.jsdelivr.net/gh/LLOneBot/llonebot-docker/fastboot.sh & chmod +x fastboot.sh & sudo sh fastboot.sh
```
### 使用termux安装
具体安装过程与教程,参考 [llonebot-termux](https://github.com/LLOneBot/llonebot-termux) 项目。
## 支持的功能
目前支持的协议
- [x] http调用api
- [x] http事件上报不支持快捷回复等快捷操作
- [x] 正向websocket
- [x] 反向websocket
主要功能:
- [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] 语音(支持mp3、wav等多种音频格式直接发送)
- [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收)
- [x] 视频(上报时暂时只有个空的file)
- [x] 文件(上报时暂时只有个空的file), type为file, data为{file: uri}, 发送时uri支持http://, file://, base64://
```
{
"type": "file",
"data": {
"file": "file:///D:/1.txt",
"name": "自定义显示的文件名" // 此字段不是必须的
}
}
```
- [ ] 发送音乐卡片
- [ ] 红包(没有计划支持)
- [ ] xml (没有计划支持)
## 示例
![](doc/image/example.jpg) ![](doc/image/example.jpg)
## 一些坑 ## 支持的 api 和功能详情
<details> <https://llonebot.github.io/zh-CN/develop/api>
<summary>下载了插件但是没有看到在NTQQ中生效</summary>
<br/>
检查是否下载的是插件release的版本如果是源码的话需要自行编译。依然不生效请查阅<a href="https://liteloaderqqnt.github.io/guide/plugins.html">LiteLoaderQQNT的文档</a>
</details>
<br/>
<details>
<summary>调用接口报404</summary>
<br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
-
</details>
<br/>
<details>
<summary>发送不了图片和语音</summary>
<br/>
检查当前操作用户是否有LiteLoaderQQNT/data/LLOneBot的写入权限如Windows把QQ上安装到C盘有可能会出现无权限导致发送失败
</details>
<br/>
<details>
<summary>QQ变得很卡</summary>
<br/>
这是你的群特别多导致的,因为启动后会批量获取群成员列表,获取完之后就正常了
</details>
<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
- [x] get_image
- [x] get_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进程调用
@@ -179,8 +29,11 @@ wget -O fastboot.sh https://cdn.jsdelivr.net/gh/LLOneBot/llonebot-docker/fastboo
- [x] 群管理功能,禁言、踢人,改群名片等 - [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息 - [x] 视频消息
- [x] 文件消息 - [x] 文件消息
- [ ] 音乐卡片
- [ ] 无头模式 - [ ] 无头模式
- [ ] 群禁言事件上报
- [ ] 优化加群成功事件上报
- [ ] 清理缓存api
- [ ] 框架对接文档
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>
@@ -190,3 +43,6 @@ wget -O fastboot.sh https://cdn.jsdelivr.net/gh/LLOneBot/llonebot-docker/fastboo
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) * [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
* chronocat * chronocat
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) * [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
## 友链
* [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)(一款用C#实现的NTQQ纯协议跨平台QQ机器人框架)

BIN
doc/image/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,11 +1,11 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.13.4", "name": "LLOneBot v3.14.0",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi", "description": "LiteLoaderQQNT的OneBotApi",
"version": "3.13.4", "version": "3.14.0",
"thumbnail": "./icon.png", "icon": "./icon.png",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",

17
scripts/test/test_db.ts Normal file
View File

@@ -0,0 +1,17 @@
import {Level} from "level"
const db = new Level(process.env["level_db_path"], {valueEncoding: 'json'});
async function getGroupNotify() {
let keys = await db.keys().all();
let result = []
for (const key of keys) {
// console.log(key)
if (key.startsWith("group_notify_")) {
result.push(key)
}
}
return result
}
getGroupNotify().then(console.log)

View File

@@ -21,15 +21,12 @@ export const selfInfo: SelfInfo = {
} }
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export let groupNotifies: Map<string, GroupNotify> = new Map<string, GroupNotify>()
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>() export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
otherError: '' otherError: ''
} }
export const fileCache = new Map<string, FileCache>()
export async function getFriend(qq: string, uid: string = ""): Promise<Friend | undefined> { export async function getFriend(qq: string, uid: string = ""): Promise<Friend | undefined> {
let filterKey = uid ? "uid" : "uin" let filterKey = uid ? "uid" : "uin"

View File

@@ -1,17 +1,18 @@
// import {DATA_DIR} from "./utils";
import {Level} from "level"; import {Level} from "level";
import {RawMessage} from "../ntqqapi/types"; import {type GroupNotify, RawMessage} from "../ntqqapi/types";
import {DATA_DIR, log} from "./utils"; import {DATA_DIR, log} from "./utils";
import {selfInfo} from "./data"; import {selfInfo} from "./data";
import * as wasi from "wasi"; import {FileCache} from "./types";
class DBUtil { class DBUtil {
private readonly DB_KEY_PREFIX_MSG_ID = "msg_id_"; public readonly DB_KEY_PREFIX_MSG_ID = "msg_id_";
private readonly DB_KEY_PREFIX_MSG_SHORT_ID = "msg_short_id_"; public readonly DB_KEY_PREFIX_MSG_SHORT_ID = "msg_short_id_";
private readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_"; public readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_";
private db: Level; public readonly DB_KEY_PREFIX_FILE = "file_";
private cache: Record<string, RawMessage> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_";
public db: Level;
public cache: Record<string, RawMessage | string | FileCache | GroupNotify> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number; private currentShortId: number;
/* /*
@@ -19,6 +20,7 @@ class DBUtil {
* msg_id_101231230999: {} // 长id: RawMessage * msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id * msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id * msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */ * */
constructor() { constructor() {
@@ -61,7 +63,7 @@ class DBUtil {
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> { async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId; const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId;
if (this.cache[shortMsgIdKey]) { if (this.cache[shortMsgIdKey]) {
return this.cache[shortMsgIdKey] return this.cache[shortMsgIdKey] as RawMessage
} }
const longId = await this.db.get(shortMsgIdKey); const longId = await this.db.get(shortMsgIdKey);
const msg = await this.getMsgByLongId(longId) const msg = await this.getMsgByLongId(longId)
@@ -72,7 +74,7 @@ class DBUtil {
async getMsgByLongId(longId: string): Promise<RawMessage> { async getMsgByLongId(longId: string): Promise<RawMessage> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId; const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId;
if (this.cache[longIdKey]) { if (this.cache[longIdKey]) {
return this.cache[longIdKey] return this.cache[longIdKey] as RawMessage
} }
const data = await this.db.get(longIdKey) const data = await this.db.get(longIdKey)
const msg = JSON.parse(data) const msg = JSON.parse(data)
@@ -83,7 +85,7 @@ class DBUtil {
async getMsgBySeqId(seqId: string): Promise<RawMessage> { async getMsgBySeqId(seqId: string): Promise<RawMessage> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId; const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId;
if (this.cache[seqIdKey]) { if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] return this.cache[seqIdKey] as RawMessage
} }
const longId = await this.db.get(seqIdKey); const longId = await this.db.get(seqIdKey);
const msg = await this.getMsgByLongId(longId) const msg = await this.getMsgByLongId(longId)
@@ -95,12 +97,12 @@ class DBUtil {
// 有则更新,无则添加 // 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId); // log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] let existMsg = this.cache[longIdKey] as RawMessage
if (!existMsg) { if (!existMsg) {
try { try {
existMsg = await this.getMsgByLongId(msg.msgId) existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) { } catch (e) {
// log("addMsg getMsgByLongId error", e.stack.toString())
} }
} }
if (existMsg) { if (existMsg) {
@@ -108,7 +110,8 @@ class DBUtil {
this.updateMsg(msg).then() this.updateMsg(msg).then()
return existMsg.msgShortId return existMsg.msgShortId
} }
this.addCache(msg);
// log("新增消息记录", msg.msgId)
const shortMsgId = await this.genMsgShortId(); const shortMsgId = await this.genMsgShortId();
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId; const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId;
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq; const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq;
@@ -117,13 +120,11 @@ class DBUtil {
this.db.put(shortIdKey, msg.msgId).then(); this.db.put(shortIdKey, msg.msgId).then();
this.db.put(longIdKey, JSON.stringify(msg)).then(); this.db.put(longIdKey, JSON.stringify(msg)).then();
try { try {
if (!(await this.db.get(seqIdKey))) { await this.db.get(seqIdKey)
this.db.put(seqIdKey, msg.msgId).then();
}
} catch (e) { } catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then();
} }
this.cache[shortIdKey] = this.cache[longIdKey] = msg;
if (!this.cache[seqIdKey]) { if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg; this.cache[seqIdKey] = msg;
} }
@@ -136,12 +137,12 @@ class DBUtil {
async updateMsg(msg: RawMessage) { async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] let existMsg = this.cache[longIdKey] as RawMessage
if (!existMsg) { if (!existMsg) {
try { try {
existMsg = await this.getMsgByLongId(msg.msgId) existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) { } catch (e) {
return existMsg = msg
} }
} }
@@ -154,11 +155,10 @@ class DBUtil {
} }
this.db.put(shortIdKey, msg.msgId).then(); this.db.put(shortIdKey, msg.msgId).then();
try { try {
if (!(await this.db.get(seqIdKey))) { await this.db.get(seqIdKey)
this.db.put(seqIdKey, msg.msgId).then();
}
} catch (e) { } catch (e) {
this.db.put(seqIdKey, msg.msgId).then();
// log("更新seqId error", e.stack, seqIdKey);
} }
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId); // log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
} }
@@ -179,6 +179,54 @@ class DBUtil {
await this.db.put(key, this.currentShortId.toString()); await this.db.put(key, this.currentShortId.toString());
return this.currentShortId; return this.currentShortId;
} }
async addFileCache(fileName: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileName;
if (this.cache[key]) {
return
}
let cacheDBData = {...data}
delete cacheDBData['downloadFunc']
this.cache[fileName] = data;
await this.db.put(key, JSON.stringify(cacheDBData));
}
async getFileCache(fileName: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileName;
if (this.cache[key]) {
return this.cache[key] as FileCache
}
try {
let data = await this.db.get(key);
return JSON.parse(data);
} catch (e) {
}
}
async addGroupNotify(notify: GroupNotify) {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq;
let existNotify = this.cache[key] as GroupNotify
if (existNotify){
return
}
this.cache[key] = notify;
this.db.put(key, JSON.stringify(notify)).then();
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq;
if (this.cache[key]) {
return this.cache[key] as GroupNotify
}
try {
let data = await this.db.get(key);
return JSON.parse(data);
} catch (e) {
}
}
} }
export const dbUtil = new DBUtil(); export const dbUtil = new DBUtil();

View File

@@ -11,8 +11,21 @@ export abstract class HttpServerBase {
constructor() { constructor() {
this.expressAPP = express(); this.expressAPP = express();
this.expressAPP.use(express.urlencoded({extended: true, limit: "500mb"})); this.expressAPP.use(express.urlencoded({extended: true, limit: "5000mb"}));
this.expressAPP.use(json({limit: "500mb"})); this.expressAPP.use((req, res, next) => {
// 兼容处理没有带content-type的请求
// log("req.headers['content-type']", req.headers['content-type'])
req.headers['content-type'] = 'application/json';
const originalJson = express.json({limit: "5000mb"});
// 调用原始的express.json()处理器
originalJson(req, res, (err) => {
if (err) {
log("Error parsing JSON:", err);
return res.status(400).send("Invalid JSON");
}
next();
});
});
} }
authorize(req: Request, res: Response, next: () => void) { authorize(req: Request, res: Response, next: () => void) {

View File

@@ -16,7 +16,6 @@ import {
friendRequests, getFriend, friendRequests, getFriend,
getGroup, getGroup,
getGroupMember, getGroupMember,
groupNotifies,
llonebotError, refreshGroupMembers, llonebotError, refreshGroupMembers,
selfInfo selfInfo
} from "../common/data"; } from "../common/data";
@@ -155,8 +154,10 @@ function onLoad() {
async function postReceiveMsg(msgList: RawMessage[]) { async function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
// log("收到新消息", message.msgSeq) // log("收到新消息", message.msgId, message.msgSeq)
message.msgShortId = await dbUtil.addMsg(message) // if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message);
// }
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
@@ -169,6 +170,12 @@ function onLoad() {
postOB11Event(msg); postOB11Event(msg);
// log("post msg", msg) // log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.stack.toString())); }).catch(e => log("constructMessage error: ", e.stack.toString()));
OB11Constructor.GroupEvent(message).then(groupEvent=>{
if (groupEvent){
// log("post group event", groupEvent);
postOB11Event(groupEvent);
}
})
} }
} }
@@ -182,7 +189,7 @@ function onLoad() {
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("message update", message.sendStatus, message) // log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != "0") { if (message.recallTime != "0") {
// 撤回消息上报 // 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId) const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
@@ -249,19 +256,17 @@ function onLoad() {
for (const notify of notifies) { for (const notify of notifies) {
try { try {
notify.time = Date.now(); notify.time = Date.now();
const notifyTime = parseInt(notify.seq) / 1000 // const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); // log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) { // if (notifyTime < startTime) {
// continue; // continue;
// } // }
let existNotify = groupNotifies[notify.seq]; let existNotify = await dbUtil.getGroupNotify(notify.seq);
if (existNotify) { if (existNotify) {
if (Date.now() - existNotify.time < 3000) { continue
continue
}
} }
log("收到群通知", notify); log("收到群通知", notify);
groupNotifies[notify.seq] = notify; await dbUtil.addGroupNotify(notify);
// let member2: GroupMember; // let member2: GroupMember;
// if (notify.user2.uid) { // if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid); // member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);

View File

@@ -57,6 +57,9 @@ export class SendMsgElementConstructor {
static async pic(picPath: string): Promise<SendPicElement> { static async pic(picPath: string): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC);
if (fileSize === 0){
throw "文件异常大小为0";
}
const imageSize = await NTQQApi.getImageSize(picPath); const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = { const picElement = {
md5HexStr: md5, md5HexStr: md5,
@@ -89,6 +92,9 @@ export class SendMsgElementConstructor {
picWidth = 768; picWidth = 768;
} }
const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE); const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE);
if (fileSize === 0){
throw "文件异常大小为0";
}
let element: SendFileElement = { let element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: "", elementId: "",
@@ -112,6 +118,9 @@ export class SendMsgElementConstructor {
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, ElementType.PTT); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
if (fileSize === 0){
throw "文件异常大小为0";
}
if (converted) { if (converted) {
fs.unlink(silkPath, () => { fs.unlink(silkPath, () => {
}); });

View File

@@ -1,72 +1,73 @@
import {type BrowserWindow} from 'electron' import {BrowserWindow} from 'electron';
import {getConfigUtil, log, sleep} from '../common/utils' import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi, type NTQQApiClass, sendMessagePool} from './ntcall' import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {type Group, type RawMessage, type User} from './types' import {Group, RawMessage, User} from "./types";
import {friends, groups, selfInfo, tempGroupCodeMap} from '../common/data' import {friends, groups, selfInfo, tempGroupCodeMap} 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 {postOB11Event} from '../onebot11/server/postOB11Event' import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from '../common/config' import {HOOK_LOG} from "../common/config";
import fs from 'fs' import fs from "fs";
import {dbUtil} from "../common/db"; import {dbUtil} from "../common/db";
export const hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export enum ReceiveCmd { export enum ReceiveCmd {
UPDATE_MSG = 'nodeIKernelMsgListener/onMsgInfoListUpdate', UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = 'nodeIKernelMsgListener/onRecvMsg', NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = 'nodeIKernelMsgListener/onAddSendMsg', SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = 'nodeIKernelProfileListener/onProfileSimpleChanged', USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO = 'nodeIKernelProfileListener/onProfileDetailInfoChanged', 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', MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated', UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupSingleScreenNotifies', GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies",
FRIEND_REQUEST = 'nodeIKernelBuddyListener/onBuddyReqChange', FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange",
SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH = "nodeIKernelStorageCleanListener/onFinishScan",
} }
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> { interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: { 0: {
'type': 'request' "type": "request",
'eventName': NTQQApiClass "eventName": NTQQApiClass,
'callbackId'?: string "callbackId"?: string
} },
1: 1:
Array<{ {
cmdName: ReceiveCmd cmdName: ReceiveCmd,
cmdType: 'event' cmdType: "event",
payload: PayloadType payload: PayloadType
}> }[]
} }
const receiveHooks: Array<{ let receiveHooks: Array<{
method: ReceiveCmd method: ReceiveCmd,
hookFunc: ((payload: any) => void | Promise<void>) hookFunc: ((payload: any) => void | Promise<void>)
id: string id: string
}> = [] }> = []
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) => {
HOOK_LOG && 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 (const receiveData of args?.[1]) { for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (const hook of receiveHooks) { for (let hook of receiveHooks) {
if (hook.method === ntQQApiMethodName) { if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
const _ = hook.hookFunc(receiveData.payload) let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') { if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then() (_ as Promise<void>).then()
} }
} catch (e) { } catch (e) {
log('hook error', e, receiveData.payload) log("hook error", e, receiveData.payload)
} }
}).then() }).then()
} }
@@ -75,35 +76,35 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
} }
if (args[0]?.callbackId) { if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args) // log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) { if (hookApiCallbacks[callbackId]) {
// log("callback found") // log("callback found")
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]) hookApiCallbacks[callbackId](args[1]);
}).then() }).then()
delete hookApiCallbacks[callbackId] delete hookApiCallbacks[callbackId];
} }
} }
return originalSend.call(window.webContents, channel, ...args) return originalSend.call(window.webContents, channel, ...args);
} }
window.webContents.send = patchSend window.webContents.send = patchSend;
} }
export function hookNTQQApiCall(window: BrowserWindow) { export function hookNTQQApiCall(window: BrowserWindow) {
// 监听调用NTQQApi // 监听调用NTQQApi
const webContents = window.webContents as any let webContents = window.webContents as any;
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message'] const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"];
const proxyIpcMsg = new Proxy(ipc_message_proxy, { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
HOOK_LOG && 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;
} }
} }
@@ -114,29 +115,29 @@ export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (
hookFunc, hookFunc,
id id
}) })
return id return id;
} }
export function removeReceiveHook(id: string) { export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex(h => h.id === id) const index = receiveHooks.findIndex(h => h.id === id)
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1);
} }
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (const group of _groups) { for (let group of _groups) {
let existGroup = groups.find(g => g.groupCode == group.groupCode) let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
Object.assign(existGroup, group) Object.assign(existGroup, group);
} else { } else {
groups.push(group) groups.push(group);
existGroup = group existGroup = group;
} }
if (needUpdate) { if (needUpdate) {
const members = await NTQQApi.getGroupMembers(group.groupCode) const members = await NTQQApi.getGroupMembers(group.groupCode);
if (members) { if (members) {
existGroup.members = members existGroup.members = members;
} }
} }
} }
@@ -144,83 +145,66 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
async function processGroupEvent(payload) { async function processGroupEvent(payload) {
try { try {
const newGroupList = payload.groupList const newGroupList = payload.groupList;
for (const group of newGroupList) { for (const group of newGroupList) {
const existGroup = groups.find(g => g.groupCode == group.groupCode) let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
if (existGroup.memberCount > group.memberCount) { if (existGroup.memberCount > group.memberCount) {
const oldMembers = existGroup.members const oldMembers = existGroup.members;
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时 await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQApi.getGroupMembers(group.groupCode) const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers group.members = newMembers;
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度 const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
for (const member of newMembers) { for (const member of newMembers) {
newMembersSet.add(member.uin) newMembersSet.add(member.uin);
} }
for (const member of oldMembers) { for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) { if (!newMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))) postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break break;
}
}
} else if (existGroup.memberCount < group.memberCount) {
const oldMembers = existGroup.members
const oldMembersSet = new Set<string>()
for (const member of oldMembers) {
oldMembersSet.add(member.uin)
}
await sleep(200)
const newMembers = await NTQQApi.getGroupMembers(group.groupCode)
group.members = newMembers
for (const member of newMembers) {
if (!oldMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)))
break
} }
} }
} }
} }
} }
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);
} }
} }
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();
} }
} }
}) })
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: Array<{ categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }> data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => { }>(ReceiveCmd.FRIENDS, payload => {
for (const fData of payload.data) { for (const fData of payload.data) {
const _friends = fData.buddyList const _friends = fData.buddyList;
for (const friend of _friends) { for (let friend of _friends) {
const existFriend = friends.find(f => f.uin == friend.uin) let existFriend = friends.find(f => f.uin == friend.uin)
if (!existFriend) { if (!existFriend) {
friends.push(friend) friends.push(friend)
} else { } else {
@@ -230,11 +214,11 @@ registerReceiveHook<{
} }
}) })
registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile, autoDeleteFileSecond} = getConfigUtil().getConfig() const {autoDeleteFile} = getConfigUtil().getConfig();
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgSeq) // log("收到新消息push到历史记录", message.msgId)
dbUtil.addMsg(message).then() // dbUtil.addMsg(message).then()
// 清理文件 // 清理文件
if (!autoDeleteFile) { if (!autoDeleteFile) {
continue continue
@@ -255,27 +239,27 @@ registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) =>
for (const path of pathList) { for (const path of pathList) {
if (path) { if (path) {
fs.unlink(picPath, () => { fs.unlink(picPath, () => {
log('删除文件成功', path) log("删除文件成功", path)
}) });
} }
} }
}, autoDeleteFileSecond * 1000) }, 60 * 1000)
} }
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => {
const message = msgRecord const message = msgRecord;
const peerUid = message.peerUid const peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgSeq); // log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then() dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid] const sendCallback = sendMessagePool[peerUid]
if (sendCallback) { if (sendCallback) {
try { try {
sendCallback(message) sendCallback(message);
} catch (e) { } catch (e) {
log('receive self msg error', e.stack) log("receive self msg error", e.stack)
} }
} }
}) })

View File

@@ -1,25 +1,29 @@
import {ipcMain} from 'electron' import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from './hook' import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log, sleep} from '../common/utils' import {log, sleep} from "../common/utils";
import { import {
type ChatType, ChatType,
ElementType, ElementType,
type Friend, Friend,
type FriendRequest, FriendRequest,
type Group, GroupMember, Group,
type GroupMemberRole, GroupMember,
type GroupNotifies, GroupMemberRole,
type GroupNotify, GroupNotifies,
type GroupRequestOperateTypes, GroupNotify,
type RawMessage, GroupRequestOperateTypes,
type SelfInfo, RawMessage,
type SendMessageElement, SelfInfo,
type User SendMessageElement,
} from './types' User,
import * as fs from 'node:fs' CacheScanResult,
import {friendRequests, groupNotifies, selfInfo, uidMaps} from '../common/data' ChatCacheList, ChatCacheListItemBasic,
import {v4 as uuidv4} from 'uuid' CacheFileList, CacheFileListItem, CacheFileType,
import path from 'path' } from "./types";
import * as fs from "fs";
import {friendRequests, selfInfo, uidMaps} from "../common/data";
import {v4 as uuidv4} from "uuid"
import path from "path";
import {dbUtil} from "../common/db"; import {dbUtil} from "../common/db";
interface IPCReceiveEvent { interface IPCReceiveEvent {
@@ -35,89 +39,104 @@ export type IPCReceiveDetail = [
] ]
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = 'ns-ntApi', NT_API = "ns-ntApi",
FS_API = 'ns-FsApi', FS_API = "ns-FsApi",
GLOBAL_DATA = 'ns-GlobalDataApi' OS_API = "ns-OsApi",
HOTUPDATE_API = "ns-HotUpdateApi",
BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi"
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike', LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
SELF_INFO = 'fetchAuthData', SELF_INFO = "fetchAuthData",
FRIENDS = 'nodeIKernelBuddyService/getBuddyList', FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUPS = 'nodeIKernelGroupService/getGroupList', GROUPS = "nodeIKernelGroupService/getGroupList",
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', USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
FILE_TYPE = 'getFileType', FILE_TYPE = "getFileType",
FILE_MD5 = 'getFileMd5', FILE_MD5 = "getFileMd5",
FILE_COPY = 'copyFile', FILE_COPY = "copyFile",
IMAGE_SIZE = 'getImageSizeFromPath', IMAGE_SIZE = "getImageSizeFromPath",
FILE_SIZE = 'getFileSize', FILE_SIZE = "getFileSize",
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = 'nodeIKernelMsgService/sendMsg', SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia', DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment", // 逐条转发 FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发 MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies', GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange" // READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest', HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = 'nodeIKernelGroupService/kickMember', KICK_MEMBER = "nodeIKernelGroupService/kickMember",
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp', MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp",
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp', MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp",
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName', SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName",
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole', SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin', PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName', SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
} }
enum NTQQApiChannel { enum NTQQApiChannel {
IPC_UP_2 = 'IPC_UP_2', IPC_UP_2 = "IPC_UP_2",
IPC_UP_3 = 'IPC_UP_3', IPC_UP_3 = "IPC_UP_3",
IPC_UP_1 = 'IPC_UP_1', IPC_UP_1 = "IPC_UP_1",
} }
export interface Peer { export interface Peer {
chatType: ChatType chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串 peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: '' guildId?: ""
} }
interface NTQQApiParams { interface NTQQApiParams {
methodName: NTQQApiMethod | string methodName: NTQQApiMethod | string,
className?: NTQQApiClass className?: NTQQApiClass,
channel?: NTQQApiChannel channel?: NTQQApiChannel,
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] args?: unknown[],
cbCmd?: ReceiveCmd | null cbCmd?: ReceiveCmd | null,
cmdCB?: (payload: any) => boolean cmdCB?: (payload: any) => boolean;
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeoutSecond?: number,
} }
async 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 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 afterFirstCmd = afterFirstCmd ?? true;
const uuid = uuidv4() const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid) // log("callNTQQApi", channel, className, methodName, args, uuid)
return await 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] let eventName = className + "-" + channel[channel.length - 1];
if (classNameIsRegister) { if (classNameIsRegister) {
eventName += '-register' eventName += "-register";
} }
const apiArgs = [methodName, ...args] const apiArgs = [methodName, ...args]
if (!cbCmd) { if (!cbCmd) {
@@ -125,40 +144,40 @@ async function callNTQQApi<ReturnType>(params: NTQQApiParams) {
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true success = true
resolve(r) resolve(r)
} };
} else { } else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB); // log(methodName, "second callback", cbCmd, payload, cmdCB);
if (cmdCB) { if (!!cmdCB) {
if (cmdCB(payload)) { if (cmdCB(payload)) {
removeReceiveHook(hookId) removeReceiveHook(hookId);
success = true success = true
resolve(payload) resolve(payload);
} }
} else { } else {
removeReceiveHook(hookId) removeReceiveHook(hookId);
success = true success = true
resolve(payload) resolve(payload);
} }
}) })
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback();
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result) log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) { if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback();
} else { } else {
success = true success = true
reject(`ntqq api call failed, ${result.errMsg}`) reject(`ntqq api call failed, ${result.errMsg}`);
} }
} }
} }
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}, ${eventName}, ${methodName}`, apiArgs) log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs);
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
} }
}, _timeout) }, _timeout)
@@ -172,13 +191,15 @@ async function callNTQQApi<ReturnType>(params: NTQQApiParams) {
}) })
} }
export const sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult { interface GeneralCallResult {
result: number // 0: success result: number, // 0: success
errMsg: string errMsg: string
} }
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 async likeFriend(uid: string, count = 1) { static async likeFriend(uid: string, count = 1) {
@@ -198,9 +219,7 @@ export class NTQQApi {
static async getSelfInfo() { static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({ return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA, className: NTQQApiClass.GLOBAL_DATA,
// channel: NTQQApiChannel.IPC_UP_3, methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
methodName: NTQQApiMethod.SELF_INFO,
timeoutSecond: 2
}) })
} }
@@ -235,19 +254,19 @@ export class NTQQApi {
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: Array<{ data: {
categoryId: number categoryId: number,
categroyName: string categroyName: string,
categroyMbCount: number categroyMbCount: number,
buddyList: Friend[] buddyList: Friend[]
}> }[]
}>( }>(
{ {
methodName: NTQQApiMethod.FRIENDS, methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined], args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS cbCmd: ReceiveCmd.FRIENDS
}) })
const _friends: Friend[] = [] let _friends: Friend[] = [];
for (const fData of data.data) { for (const fData of data.data) {
_friends.push(...fData.buddyList) _friends.push(...fData.buddyList)
} }
@@ -256,11 +275,11 @@ export class NTQQApi {
static async getGroups(forced = false) { static async getGroups(forced = false) {
let cbCmd = ReceiveCmd.GROUPS let cbCmd = ReceiveCmd.GROUPS
if (process.platform != 'win32') { if (process.platform != "win32") {
cbCmd = ReceiveCmd.GROUPS_UNIX cbCmd = ReceiveCmd.GROUPS_UNIX
} }
const result = await callNTQQApi<{ const result = await callNTQQApi<{
updateType: number updateType: number,
groupList: Group[] groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList return result.groupList
@@ -271,7 +290,7 @@ export class NTQQApi {
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{ args: [{
groupCode: groupQQ, groupCode: groupQQ,
scene: 'groupMemberList_MainWindow' scene: "groupMemberList_MainWindow"
}] }]
}) })
// log("get group member sceneId", sceneId); // log("get group member sceneId", sceneId);
@@ -281,8 +300,8 @@ export class NTQQApi {
}>({ }>({
methodName: NTQQApiMethod.GROUP_MEMBERS, methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{ args: [{
sceneId, sceneId: sceneId,
num num: num
}, },
null null
] ]
@@ -343,35 +362,35 @@ export class NTQQApi {
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { 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) {
ext = '.' + ext ext = "." + ext
} else { } else {
ext = '' ext = ""
} }
let fileName = `${path.basename(filePath)}` let fileName = `${path.basename(filePath)}`;
if (!fileName.includes('.')) { if (fileName.indexOf(".") === -1) {
fileName += ext fileName += ext;
} }
const mediaPath = await callNTQQApi<string>({ const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH, methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{ args: [{
path_info: { path_info: {
md5HexStr: md5, md5HexStr: md5,
fileName, fileName: fileName,
elementType, elementType: elementType,
elementSubType: 0, elementSubType: 0,
thumbSize: 0, thumbSize: 0,
needCreate: true, needCreate: true,
downloadType: 1, downloadType: 1,
file_uuid: '' file_uuid: ""
} }
}] }]
}) })
log('media path', mediaPath) log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath) await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath) const fileSize = await NTQQApi.getFileSize(filePath);
return { return {
md5, md5,
fileName, fileName,
@@ -388,16 +407,16 @@ export class NTQQApi {
const apiParams = [ const apiParams = [
{ {
getReq: { getReq: {
msgId, msgId: msgId,
chatType, chatType: chatType,
peerUid, peerUid: peerUid,
elementId, elementId: elementId,
thumbSize: 0, thumbSize: 0,
downloadType: 1, downloadType: 1,
filePath: thumbPath filePath: thumbPath,
} },
}, },
undefined undefined,
] ]
// log("需要下载media", sourcePath); // log("需要下载media", sourcePath);
await callNTQQApi({ await callNTQQApi({
@@ -406,7 +425,7 @@ export class NTQQApi {
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => { cmdCB: (payload: { notifyInfo: { filePath: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); // log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath return payload.notifyInfo.filePath == sourcePath;
} }
}) })
return sourcePath return sourcePath
@@ -426,30 +445,30 @@ export class NTQQApi {
const peerUid = peer.peerUid const peerUid = peer.peerUid
// 等待上一个相同的peer发送完 // 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0 let checkLastSendUsingTime = 0;
const waitLastSend = async () => { const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) { if (checkLastSendUsingTime > timeout) {
throw ('发送超时') throw ("发送超时")
} }
const lastSending = sendMessagePool[peer.peerUid] let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) { if (lastSending) {
// log("有正在发送的消息,等待中...") // log("有正在发送的消息,等待中...")
await sleep(500) await sleep(500);
checkLastSendUsingTime += 500 checkLastSendUsingTime += 500;
return await waitLastSend() return await waitLastSend();
} else { } else {
return;
} }
} }
await waitLastSend() await waitLastSend();
let sentMessage: RawMessage = null let sentMessage: RawMessage = null;
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid] delete sendMessagePool[peerUid];
sentMessage = rawMessage sentMessage = rawMessage;
} }
let checkSendCompleteUsingTime = 0 let checkSendCompleteUsingTime = 0;
const checkSendComplete = async (): Promise<RawMessage> => { const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) { if (sentMessage) {
if (waitComplete) { if (waitComplete) {
@@ -469,14 +488,13 @@ export class NTQQApi {
await sleep(500) await sleep(500)
return await checkSendComplete() return await checkSendComplete()
} }
log("开始发送消息", peer, msgElements)
callNTQQApi({ callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG, methodName: NTQQApiMethod.SEND_MSG,
args: [{ args: [{
msgId: '0', msgId: "0",
peer, peer, msgElements,
msgElements, msgAttributeInfos: new Map(),
msgAttributeInfos: new Map()
}, null] }, null]
}).then() }).then()
return await checkSendComplete() return await checkSendComplete()
@@ -512,13 +530,13 @@ export class NTQQApi {
commentElements: [], commentElements: [],
msgAttributeInfos: new Map() msgAttributeInfos: new Map()
}, },
null null,
] ]
return await new Promise<RawMessage>((resolve, reject) => { return await new Promise<RawMessage>((resolve, reject) => {
let complete = false let complete = false
setTimeout(() => { setTimeout(() => {
if (!complete) { if (!complete) {
reject('转发消息超时') reject("转发消息超时");
} }
}, 5000) }, 5000)
registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
@@ -544,10 +562,10 @@ export class NTQQApi {
methodName: NTQQApiMethod.MULTI_FORWARD_MSG, methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs args: apiArgs
}).then(result => { }).then(result => {
log('转发消息结果:', result, apiArgs) log("转发消息结果:", result, apiArgs)
if (result.result !== 0) { if (result.result !== 0) {
complete = true complete = true;
reject('转发消息失败,' + JSON.stringify(result)) reject("转发消息失败," + JSON.stringify(result));
} }
}) })
}) })
@@ -558,56 +576,56 @@ export class NTQQApi {
// 加群通知,退出通知,需要管理员权限 // 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({ callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY, methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true classNameIsRegister: true,
}).then() }).then()
return await callNTQQApi<GroupNotifies>({ return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE, methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmd.GROUP_NOTIFY, cbCmd: ReceiveCmd.GROUP_NOTIFY,
afterFirstCmd: false, afterFirstCmd: false,
args: [ args: [
{doubt: false, startSeq: '', number: 14}, {"doubt": false, "startSeq": "", "number": 14},
null null
] ]
}) });
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq] const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) { if (!notify) {
throw `${seq}对应的加群通知不存在` throw `${seq}对应的加群通知不存在`
} }
delete groupNotifies[seq] // delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [ args: [
{ {
doubt: false, "doubt": false,
operateMsg: { "operateMsg": {
operateType, // 2 拒绝 "operateType": operateType, // 2 拒绝
targetMsg: { "targetMsg": {
seq, // 通知序列号 "seq": seq, // 通知序列号
type: notify.type, "type": notify.type,
groupCode: notify.group.groupCode, "groupCode": notify.group.groupCode,
postscript: reason "postscript": reason
} }
} }
}, },
null null
] ]
}) });
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({ await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP, methodName: NTQQApiMethod.QUIT_GROUP,
args: [ args: [
{groupCode: groupQQ}, {"groupCode": groupQQ},
null null
] ]
}) })
} }
static async handleFriendRequest(sourceId: number, accept: boolean) { static async handleFriendRequest(sourceId: number, accept: boolean,) {
const request: FriendRequest = friendRequests[sourceId] const request: FriendRequest = friendRequests[sourceId]
if (!request) { if (!request) {
throw `sourceId ${sourceId}, 对应的好友请求不存在` throw `sourceId ${sourceId}, 对应的好友请求不存在`
@@ -616,16 +634,16 @@ export class NTQQApi {
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [ args: [
{ {
approvalInfo: { "approvalInfo": {
friendUid: request.friendUid, "friendUid": request.friendUid,
reqTime: request.reqTime, "reqTime": request.reqTime,
accept accept
} }
} }
] ]
}) })
delete friendRequests[sourceId] delete friendRequests[sourceId];
return result return result;
} }
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
@@ -637,7 +655,7 @@ export class NTQQApi {
groupCode: groupQQ, groupCode: groupQQ,
kickUids, kickUids,
refuseForever, refuseForever,
kickReason kickReason,
} }
] ]
} }
@@ -652,7 +670,7 @@ export class NTQQApi {
args: [ args: [
{ {
groupCode: groupQQ, groupCode: groupQQ,
memList memList,
} }
] ]
} }
@@ -712,4 +730,107 @@ export class NTQQApi {
static publishGroupBulletin(groupQQ: string, title: string, content: string) { static publishGroupBulletin(groupQQ: string, title: string, content: string) {
} }
}
static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{
isSilent
}, null]
});
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: {...pathMap},
}, null]
});
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then();
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
});
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE
});
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP
});
}
static getCacheSessionPathList() {
return callNTQQApi<{
key: string,
value: string
}[]>({
className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION,
});
}
static clearCache(cacheKeys: Array<string> = [ 'tmp', 'hotUpdate' ]) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys
}, null]
});
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [{
chatType: type,
pageSize,
order: 1,
pageIndex
}, null]
}).then(list => res(list))
.catch(e => rej(e));
});
}
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType };
return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET,
args: [{
fileType: fileType,
restart: true,
pageSize: pageSize,
order: 1,
lastRecord: _lastRecord,
}, null]
})
}
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys
}, null]
});
}
}

View File

@@ -112,6 +112,7 @@ export enum PicType {
gif = 2000, gif = 2000,
jpg = 1000 jpg = 1000
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: "", elementId: "",
@@ -218,8 +219,8 @@ export interface PttElement {
export interface ArkElement { export interface ArkElement {
bytesData: string; bytesData: string;
linkInfo:null, linkInfo: null,
subElementType:null subElementType: null
} }
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn" export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
@@ -233,6 +234,7 @@ export interface PicElement {
fileSize: number; fileSize: number;
fileName: string; fileName: string;
fileUuid: string; fileUuid: string;
md5HexStr?: string;
} }
export interface GrayTipElement { export interface GrayTipElement {
@@ -244,7 +246,8 @@ export interface GrayTipElement {
operatorMemRemark?: string; operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语 wording: string; // 自定义的撤回提示语
} }
aioOpGrayTipElement: TipAioOpGrayTipElement aioOpGrayTipElement: TipAioOpGrayTipElement,
groupElement: TipGroupElement
} }
export interface FaceElement { export interface FaceElement {
@@ -277,15 +280,20 @@ export interface VideoElement {
"sourceVideoCodecFormat": 0 "sourceVideoCodecFormat": 0
} }
export interface TipAioOpGrayTipElement{ export interface TipAioOpGrayTipElement { // 这是什么提示来着?
operateType: number, operateType: number,
peerUid: string, peerUid: string,
fromGrpCodeOfTmpChat: string, fromGrpCodeOfTmpChat: string,
} }
export enum TipGroupElementType {
memberIncrease = 1,
ban = 8
}
export interface TipGroupElement { export interface TipGroupElement {
"type": 1, // 1是表示有人加入群, 自己加入群也会收到这个 "type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, "role": 0, // 暂时不知
"groupName": string, // 暂时获取不到 "groupName": string, // 暂时获取不到
"memberUid": string, "memberUid": string,
"memberNick": string, "memberNick": string,
@@ -294,7 +302,7 @@ export interface TipGroupElement {
"adminNick": string, "adminNick": string,
"adminRemark": string, "adminRemark": string,
"createGroup": null, "createGroup": null,
"memberAdd": { "memberAdd"?: {
"showType": 1, "showType": 1,
"otherAdd": null, "otherAdd": null,
"otherAddByOtherQRCode": null, "otherAddByOtherQRCode": null,
@@ -304,7 +312,22 @@ export interface TipGroupElement {
"otherInviteYou": null, "otherInviteYou": null,
"youInviteOther": null "youInviteOther": null
}, },
"shutUp": null "shutUp"?: {
"curTime": string,
"duration": string, // 禁言时间,秒
"admin": {
"uid": string,
"card": string,
"name": string,
"role": GroupMemberRole
},
"member": {
"uid": string
"card": string,
"name": string,
"role": GroupMemberRole
}
}
} }
@@ -365,7 +388,7 @@ export interface GroupNotifies {
export interface GroupNotify { export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 转成数字再除以1000应该就是时间戳 seq: string, // 唯一标识符,转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes, type: GroupNotifyTypes,
status: 0, // 未知 status: 0, // 未知
group: { groupCode: string, groupName: string }, group: { groupCode: string, groupName: string },
@@ -402,4 +425,68 @@ export interface FriendRequestNotify {
unreadNums: number, unreadNums: number,
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }
export interface CacheScanResult {
result: number,
size: [ // 单位为字节
string, // 系统总存储空间
string, // 系统可用存储空间
string, // 系统已用存储空间
string, // QQ总大小
string, // 「聊天与文件」大小
string, // 未知
string, // 「缓存数据」大小
string, // 「其他数据」大小
string, // 未知
]
}
export interface ChatCacheList {
pageCount: number,
infos: ChatCacheListItem[]
}
export interface ChatCacheListItem {
chatType: ChatType,
basicChatCacheInfo: ChatCacheListItemBasic,
guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容
}
export interface ChatCacheListItemBasic {
chatSize: string,
chatTime: string,
uid: string,
uin: string,
remarkName: string,
nickName: string,
chatType?: ChatType,
isChecked?: boolean
}
export enum CacheFileType {
IMAGE = 0,
VIDEO = 1,
AUDIO = 2,
DOCUMENT = 3,
OTHER = 4,
}
export interface CacheFileList {
infos: CacheFileListItem[],
}
export interface CacheFileListItem {
fileSize: string,
fileTime: string,
fileKey: string,
elementId: string,
elementIdStr: string,
fileType: CacheFileType,
path: string,
fileName: string,
senderId: string,
previewPath: string,
senderName: string,
isChecked?: boolean,
}

View File

@@ -21,7 +21,7 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload); const resData = await this._handle(payload);
return OB11Response.ok(resData); return OB11Response.ok(resData);
} catch (e) { } catch (e) {
log("发生错误", e.stack) log("发生错误", e)
return OB11Response.error(e.toString(), 200); return OB11Response.error(e.toString(), 200);
} }
} }
@@ -35,7 +35,7 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData, echo); return OB11Response.ok(resData, echo);
} catch (e) { } catch (e) {
log("发生错误", e.stack.toString()) log("发生错误", e)
return OB11Response.error(e.toString(), 1200, echo) return OB11Response.error(e.toString(), 1200, echo)
} }
} }

View File

@@ -0,0 +1,103 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQApi} from "../../ntqqapi/ntcall";
import fs from "fs";
import Path from "path";
import {
ChatType,
ChatCacheListItemBasic,
CacheFileType
} from '../../ntqqapi/types';
export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache
protected _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => {
try {
const cacheFilePaths: string[] = [];
await NTQQApi.setCacheSilentScan(false);
cacheFilePaths.push((await NTQQApi.getHotUpdateCachePath()));
cacheFilePaths.push((await NTQQApi.getDesktopTmpPath()));
(await NTQQApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value));
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQApi.scanCache();
const cacheSize = parseInt(cacheScanResult.size[6]);
if (cacheScanResult.result !== 0) {
throw('Something went wrong while scanning cache. Code: ' + cacheScanResult.result);
}
await NTQQApi.setCacheSilentScan(true);
if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作
// await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了
deleteCachePath(cacheFilePaths);
}
// 获取聊天记录列表
// NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关
// const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息
// const groupChatCache = await getCacheList(ChatType.group); // 群聊消息
// const chatCacheList = [ ...privateChatCache, ...groupChatCache ];
const chatCacheList: ChatCacheListItemBasic[] = [];
// 获取聊天缓存文件列表
const cacheFileList: string[] = [];
for (const name in CacheFileType) {
if (!isNaN(parseInt(name))) continue;
const fileTypeAny: any = CacheFileType[name];
const fileType: CacheFileType = fileTypeAny;
cacheFileList.push(...(await NTQQApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey));
}
// 一并清除
await NTQQApi.clearChatCache(chatCacheList, cacheFileList);
res();
} catch(e) {
console.error('清理缓存时发生了错误');
rej(e);
}
});
}
}
function deleteCachePath(pathList: string[]) {
const emptyPath = (path: string) => {
if (!fs.existsSync(path)) return;
const files = fs.readdirSync(path);
files.forEach(file => {
const filePath = Path.resolve(path, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) emptyPath(filePath);
else fs.unlinkSync(filePath);
});
fs.rmdirSync(path);
}
for (const path of pathList) {
emptyPath(path);
}
}
function getCacheList(type: ChatType) { // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理
return new Promise<Array<ChatCacheListItemBasic>>((res, rej) => {
NTQQApi.getChatCacheList(type, 1000, 0)
.then(data => {
const list = data.infos.filter(e => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0);
const result = list.map(e => {
const result = { ...e.basicChatCacheInfo };
result.chatType = type;
result.isChecked = true;
return result;
});
res(result);
})
.catch(e => rej(e));
});
}

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {fileCache} from "../../common/data";
import {getConfigUtil} from "../../common/utils"; import {getConfigUtil} from "../../common/utils";
import fs from "fs/promises"; import fs from "fs/promises";
import {dbUtil} from "../../common/db";
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名 file: string // 文件名
@@ -18,7 +18,7 @@ export interface GetFileResponse {
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = fileCache.get(payload.file) const cache = await dbUtil.getFileCache(payload.file)
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig() const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig()
if (!cache) { if (!cache) {
throw new Error('file not found') throw new Error('file not found')

View File

@@ -21,8 +21,7 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
} }
const msg = await dbUtil.getMsgByShortId(payload.message_id) const msg = await dbUtil.getMsgByShortId(payload.message_id)
if (msg) { if (msg) {
const msgData = await OB11Constructor.message(msg); return await OB11Constructor.message(msg)
return msgData
} else { } else {
throw ("消息不存在") throw ("消息不存在")
} }

View File

@@ -2,6 +2,7 @@ import BaseAction from "./BaseAction";
import {getFriend, getUidByUin, uidMaps} from "../../common/data"; import {getFriend, getUidByUin, uidMaps} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types"; import {ActionName} from "./types";
import {log} from "../../common/utils";
interface Payload { interface Payload {
user_id: number, user_id: number,
@@ -12,16 +13,16 @@ export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const qq = payload.user_id.toString(); log("点赞参数", payload)
const friend = await getFriend(qq)
let uid: string;
if (!friend) {
uid = getUidByUin(qq)
}
else{
uid = friend.uid
}
try { try {
const qq = payload.user_id.toString();
const friend = await getFriend(qq)
let uid: string;
if (!friend) {
uid = getUidByUin(qq)
} else {
uid = friend.uid
}
let result = await NTQQApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1); let result = await NTQQApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1);
if (result.result !== 0) { if (result.result !== 0) {
throw result.errMsg throw result.errMsg

View File

@@ -14,10 +14,11 @@ import {uri2local} from "../utils";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types"; import {ActionName, BaseCheckResult} from "./types";
import * as fs from "node:fs"; import * as fs from "node:fs";
import {log} from "../../common/utils"; import {log, sleep} from "../../common/utils";
import {decodeCQCode} from "../cqcode"; import {decodeCQCode} from "../cqcode";
import {dbUtil} from "../../common/db"; import {dbUtil} from "../../common/db";
import {ALLOW_SEND_TEMP_MSG} from "../../common/config"; import {ALLOW_SEND_TEMP_MSG} from "../../common/config";
import {FileCache} from "../../common/types";
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@@ -158,6 +159,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group) const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group)
try { try {
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles) const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {}));
return {message_id: returnMsg.msgShortId} return {message_id: returnMsg.msgShortId}
} catch (e) { } catch (e) {
log("发送消息失败", e.stack.toString()) log("发送消息失败", e.stack.toString())
@@ -193,8 +195,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid peerUid: selfInfo.uid
} }
let selfNodeMsgList: RawMessage[] = []; let selfNodeMsgList: RawMessage[] = []; // 自己给自己发的消息
let originalNodeMsgList: RawMessage[] = []; let originalNodeMsgList: RawMessage[] = [];
let sendForwardElements: SendMessageElement[] = []
for (const messageNode of messageNodes) { for (const messageNode of messageNodes) {
// 一个node表示一个人的消息 // 一个node表示一个人的消息
let nodeId = messageNode.data.id; let nodeId = messageNode.data.id;
@@ -213,9 +216,11 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
deleteAfterSentFiles deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group); } = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements); log("开始生成转发节点", sendElements);
sendForwardElements.push(...sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true); const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
selfNodeMsgList.push(nodeMsg); selfNodeMsgList.push(nodeMsg);
log("转发节点生成成功", nodeMsg.msgId); log("转发节点生成成功", nodeMsg.msgId);
await sleep(500);
} catch (e) { } catch (e) {
log("生效转发消息节点失败", e) log("生效转发消息节点失败", e)
} }
@@ -225,27 +230,28 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let nodeIds: string[] = [] let nodeIds: string[] = []
// 检查是否需要克隆直接引用消息id的节点 // 检查是否需要克隆直接引用消息id的节点
let needSendSelf = false; let needSendSelf = false;
if (selfNodeMsgList.length) { if (sendForwardElements.length) {
needSendSelf = true needSendSelf = true
} else { } else {
needSendSelf = !originalNodeMsgList.every((msg, index) => msg.peerUid === originalNodeMsgList[0].peerUid && msg.recallTime.length < 2) needSendSelf = !originalNodeMsgList.every((msg, index) => msg.peerUid === originalNodeMsgList[0].peerUid && msg.recallTime.length < 2)
} }
if (needSendSelf) { if (needSendSelf) {
nodeIds = selfNodeMsgList.map(msg => msg.msgId); nodeIds = selfNodeMsgList.map(msg => msg.msgId);
let sendElements: SendMessageElement[] = [];
for (const originalNodeMsg of originalNodeMsgList) { for (const originalNodeMsg of originalNodeMsgList) {
if (originalNodeMsg.peerUid === selfInfo.uid && originalNodeMsg.recallTime.length < 2) { if (originalNodeMsg.peerUid === selfInfo.uid && originalNodeMsg.recallTime.length < 2) {
nodeIds.push(originalNodeMsg.msgId) nodeIds.push(originalNodeMsg.msgId)
} else { // 需要进行克隆 } else { // 需要进行克隆
let sendElements: SendMessageElement[] = []
Object.keys(originalNodeMsg.elements).forEach((eleKey) => { Object.keys(originalNodeMsg.elements).forEach((eleKey) => {
if (eleKey !== "elementId") { if (eleKey !== "elementId") {
sendForwardElements.push(originalNodeMsg.elements[eleKey])
sendElements.push(originalNodeMsg.elements[eleKey]) sendElements.push(originalNodeMsg.elements[eleKey])
} }
}) })
try { try {
const nodeMsg = await NTQQApi.sendMsg(selfPeer, sendElements, true); const nodeMsg = await NTQQApi.sendMsg(selfPeer, sendElements, true);
nodeIds.push(nodeMsg.msgId) nodeIds.push(nodeMsg.msgId)
log("克隆转发消息成功") log("克隆转发消息到节点")
} catch (e) { } catch (e) {
log("克隆转发消息失败", e) log("克隆转发消息失败", e)
} }
@@ -262,6 +268,15 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
peerUid: originalNodeMsgList[0].peerUid peerUid: originalNodeMsgList[0].peerUid
} }
} }
// elements之间用换行符分隔
// let _sendForwardElements: SendMessageElement[] = []
// for(let i = 0; i < sendForwardElements.length; i++){
// _sendForwardElements.push(sendForwardElements[i])
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
// }
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
// nodeIds.push(nodeMsg.msgId)
// await sleep(500);
// 开发转发 // 开发转发
try { try {
return await NTQQApi.multiForwardMsg(srcPeer, destPeer, nodeIds) return await NTQQApi.multiForwardMsg(srcPeer, destPeer, nodeIds)
@@ -287,6 +302,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
break; break;
case OB11MessageDataType.at: { case OB11MessageDataType.at: {
if (!group){
continue
}
let atQQ = sendMsg.data?.qq; let atQQ = sendMsg.data?.qq;
if (atQQ) { if (atQQ) {
atQQ = atQQ.toString() atQQ = atQQ.toString()
@@ -305,7 +323,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
case OB11MessageDataType.reply: { case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id; let replyMsgId = sendMsg.data.id;
if (replyMsgId) { if (replyMsgId) {
const replyMsg = await dbUtil.getMsgBySeqId(replyMsgId) const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId))
if (replyMsg) { if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin)) sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin))
} }
@@ -324,10 +342,24 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
case OB11MessageDataType.file: case OB11MessageDataType.file:
case OB11MessageDataType.video: case OB11MessageDataType.video:
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const file = sendMsg.data?.file let file = sendMsg.data?.file
const payloadFileName = sendMsg.data?.name const payloadFileName = sendMsg.data?.name
if (file) { if (file) {
const {path, isLocal, fileName} = (await uri2local(file)) const cache = await dbUtil.getFileCache(file)
if (cache){
if (fs.existsSync(cache.filePath)){
file = "file://" + cache.filePath
}
else if (cache.downloadFunc){
await cache.downloadFunc()
file = cache.filePath;
log("找到文件缓存", file);
}
}
const {path, isLocal, fileName, errMsg} = (await uri2local(file))
if (errMsg){
throw errMsg
}
if (path) { if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件 if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path) deleteAfterSentFiles.push(path)
@@ -361,7 +393,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = false) { private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) { if (!sendElements.length) {
throw ("消息体无法解析") throw ("消息体无法解析")
} }

View File

@@ -1,6 +1,5 @@
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {groupNotifies} from "../../common/data"; import {GroupRequestOperateTypes} from "../../ntqqapi/types";
import {GroupNotify, GroupRequestOperateTypes} from "../../ntqqapi/types";
import {NTQQApi} from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types"; import {ActionName} from "./types";
@@ -17,15 +16,10 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString(); const seq = payload.flag.toString();
const notify: GroupNotify = groupNotifies[seq] await NTQQApi.handleGroupRequest(seq,
try { payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
await NTQQApi.handleGroupRequest(seq, payload.reason
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, )
payload.reason
)
} catch (e) {
throw e
}
return null return null
} }
} }

View File

@@ -31,6 +31,7 @@ import SetGroupCard from "./SetGroupCard";
import GetImage from "./GetImage"; import GetImage from "./GetImage";
import GetRecord from "./GetRecord"; import GetRecord from "./GetRecord";
import GoCQHTTPMarkMsgAsRead from "./MarkMsgAsRead"; import GoCQHTTPMarkMsgAsRead from "./MarkMsgAsRead";
import CleanCache from "./CleanCache";
export const actionHandlers = [ export const actionHandlers = [
new Debug(), new Debug(),
@@ -56,6 +57,7 @@ export const actionHandlers = [
new SetGroupCard(), new SetGroupCard(),
new GetImage(), new GetImage(),
new GetRecord(), new GetRecord(),
new CleanCache(),
//以下为go-cqhttp api //以下为go-cqhttp api
new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendGroupForwardMsg(),

View File

@@ -42,6 +42,7 @@ export enum ActionName {
SetGroupName = "set_group_name", SetGroupName = "set_group_name",
GetImage = "get_image", GetImage = "get_image",
GetRecord = "get_record", GetRecord = "get_record",
CleanCache = "clean_cache",
// 以下为go-cqhttp api // 以下为go-cqhttp api
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg", GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg",
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg", GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",

View File

@@ -8,13 +8,25 @@ import {
OB11User, OB11User,
OB11UserSex OB11UserSex
} from "./types"; } from "./types";
import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types'; import {
import {fileCache, getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data'; AtType,
import {getConfigUtil, log} from "../common/utils"; ChatType,
Group,
GroupMember,
IMAGE_HTTP_HOST,
RawMessage,
SelfInfo,
TipGroupElementType,
User
} from '../ntqqapi/types';
import {getFriend, getGroup, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data';
import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi} from "../ntqqapi/ntcall"; import {NTQQApi} from "../ntqqapi/ntcall";
import {EventType} from "./event/OB11BaseEvent"; import {EventType} from "./event/OB11BaseEvent";
import {encodeCQCode} from "./cqcode"; import {encodeCQCode} from "./cqcode";
import {dbUtil} from "../common/db"; import {dbUtil} from "../common/db";
import {OB11GroupIncreaseEvent} from "./event/notice/OB11GroupIncreaseEvent";
import {OB11GroupBanEvent} from "./event/notice/OB11GroupBanEvent";
export class OB11Constructor { export class OB11Constructor {
@@ -27,7 +39,7 @@ export class OB11Constructor {
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId, message_id: msg.msgShortId,
real_id: msg.msgId, real_id: msg.msgShortId,
message_type: msg.chatType == ChatType.group ? "group" : "private", message_type: msg.chatType == ChatType.group ? "group" : "private",
sender: { sender: {
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin),
@@ -97,7 +109,7 @@ export class OB11Constructor {
} else if (element.replyElement) { } else if (element.replyElement) {
message_data["type"] = "reply" message_data["type"] = "reply"
// log("收到回复消息", element.replyElement.replayMsgSeq) // log("收到回复消息", element.replyElement.replayMsgSeq)
try{ try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq) const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) // log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) { if (replyMsg) {
@@ -105,8 +117,8 @@ export class OB11Constructor {
} else { } else {
continue continue
} }
}catch (e) { } catch (e) {
log("获取不到引用的消息", e.stack) log("获取不到引用的消息", e.stack, element.replyElement.replayMsgSeq)
} }
} else if (element.picElement) { } else if (element.picElement) {
@@ -114,19 +126,25 @@ export class OB11Constructor {
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.fileName message_data["data"]["file"] = element.picElement.fileName
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
message_data["data"]["url"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl const url = element.picElement.originImageUrl
const fileMd5 = element.picElement.md5HexStr
if (url) {
message_data["data"]["url"] = IMAGE_HTTP_HOST + url
} else if (fileMd5 && element.picElement.fileUuid.indexOf("_") === -1) { // fileuuid有下划线的是Linux发送的这个url是另外的格式目前尚未得知如何组装
message_data["data"]["url"] = `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${fileMd5.toUpperCase()}/0`
}
// message_data["data"]["file_id"] = element.picElement.fileUuid // message_data["data"]["file_id"] = element.picElement.fileUuid
message_data["data"]["file_size"] = element.picElement.fileSize message_data["data"]["file_size"] = element.picElement.fileSize
fileCache.set(element.picElement.fileName, { dbUtil.addFileCache(element.picElement.fileName, {
fileName: element.picElement.fileName, fileName: element.picElement.fileName,
filePath: element.picElement.sourcePath, filePath: element.picElement.sourcePath,
fileSize: element.picElement.fileSize.toString(), fileSize: element.picElement.fileSize.toString(),
url: IMAGE_HTTP_HOST + element.picElement.originImageUrl, url: message_data["data"]["url"],
downloadFunc: async () => { downloadFunc: async () => {
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)
} }
}) }).then()
// 不在自动下载图片 // 不在自动下载图片
} else if (element.videoElement) { } else if (element.videoElement) {
@@ -135,7 +153,7 @@ export class OB11Constructor {
message_data["data"]["path"] = element.videoElement.filePath message_data["data"]["path"] = element.videoElement.filePath
// message_data["data"]["file_id"] = element.videoElement.fileUuid // message_data["data"]["file_id"] = element.videoElement.fileUuid
message_data["data"]["file_size"] = element.videoElement.fileSize message_data["data"]["file_size"] = element.videoElement.fileSize
fileCache.set(element.videoElement.fileName, { dbUtil.addFileCache(element.videoElement.fileName, {
fileName: element.videoElement.fileName, fileName: element.videoElement.fileName,
filePath: element.videoElement.filePath, filePath: element.videoElement.filePath,
fileSize: element.videoElement.fileSize, fileSize: element.videoElement.fileSize,
@@ -143,7 +161,7 @@ export class OB11Constructor {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath) element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath)
} }
}) }).then()
// 怎么拿到url呢 // 怎么拿到url呢
} else if (element.fileElement) { } else if (element.fileElement) {
message_data["type"] = OB11MessageDataType.file; message_data["type"] = OB11MessageDataType.file;
@@ -151,7 +169,7 @@ export class OB11Constructor {
// message_data["data"]["path"] = element.fileElement.filePath // message_data["data"]["path"] = element.fileElement.filePath
// message_data["data"]["file_id"] = element.fileElement.fileUuid // message_data["data"]["file_id"] = element.fileElement.fileUuid
message_data["data"]["file_size"] = element.fileElement.fileSize message_data["data"]["file_size"] = element.fileElement.fileSize
fileCache.set(element.fileElement.fileName, { dbUtil.addFileCache(element.fileElement.fileName, {
fileName: element.fileElement.fileName, fileName: element.fileElement.fileName,
filePath: element.fileElement.filePath, filePath: element.fileElement.filePath,
fileSize: element.fileElement.fileSize, fileSize: element.fileElement.fileSize,
@@ -159,7 +177,7 @@ export class OB11Constructor {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, null, element.fileElement.filePath) element.elementId, null, element.fileElement.filePath)
} }
}) }).then()
// 怎么拿到url呢 // 怎么拿到url呢
} else if (element.pttElement) { } else if (element.pttElement) {
message_data["type"] = OB11MessageDataType.voice; message_data["type"] = OB11MessageDataType.voice;
@@ -167,11 +185,11 @@ export class OB11Constructor {
message_data["data"]["path"] = element.pttElement.filePath message_data["data"]["path"] = element.pttElement.filePath
// message_data["data"]["file_id"] = element.pttElement.fileUuid // message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data["data"]["file_size"] = element.pttElement.fileSize message_data["data"]["file_size"] = element.pttElement.fileSize
fileCache.set(element.pttElement.fileName, { dbUtil.addFileCache(element.pttElement.fileName, {
fileName: element.pttElement.fileName, fileName: element.pttElement.fileName,
filePath: element.pttElement.filePath, filePath: element.pttElement.filePath,
fileSize: element.pttElement.fileSize, fileSize: element.pttElement.fileSize,
}) }).then()
// log("收到语音消息", msg) // log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
@@ -186,7 +204,10 @@ 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();
} }
// todo: 解析入群grayTipElement
else if (element.grayTipElement?.aioOpGrayTipElement) {
log("收到 group gray tip 消息", element.grayTipElement.aioOpGrayTipElement)
}
// 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) {
@@ -223,6 +244,54 @@ export class OB11Constructor {
return resMsg; return resMsg;
} }
static async GroupEvent(msg: RawMessage): Promise<OB11GroupIncreaseEvent> {
for (let element of msg.elements) {
const groupElement = element.grayTipElement?.groupElement
if (groupElement) {
// log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) {
log("收到群成员增加消息", groupElement)
await sleep(1000);
const member = await getGroupMember(msg.peerUid, null, groupElement.memberUid);
let memberUin = member?.uin;
if (!memberUin) {
memberUin = (await NTQQApi.getUserDetailInfo(groupElement.memberUid)).uin
}
// log("获取新群成员QQ", memberUin)
const adminMember = await getGroupMember(msg.peerUid, null, groupElement.adminUid);
// log("获取同意新成员入群的管理员", adminMember)
if (memberUin) {
const operatorUin = adminMember?.uin || memberUin
let event = new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin));
// log("构造群增加事件", event)
return event;
}
}
else if (groupElement.type === TipGroupElementType.ban) {
log("收到群群员禁言提示", groupElement)
const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid
let memberUin: string = ""
let duration = parseInt(groupElement.shutUp.duration)
let sub_type: "ban" | "lift_ban" = duration > 0 ? "ban" : "lift_ban"
if (memberUid){
memberUin = (await getGroupMember(msg.peerUid, null, memberUid))?.uin || (await NTQQApi.getUserDetailInfo(memberUid))?.uin
}
else {
memberUin = "0"; // 0表示全员禁言
if (duration > 0) {
duration = -1
}
}
const adminUin = (await getGroupMember(msg.peerUid, null, adminUid))?.uin || (await NTQQApi.getUserDetailInfo(adminUid))?.uin
if (memberUin && adminUin) {
return new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type);
}
}
}
}
}
static friend(friend: User): OB11User { static friend(friend: User): OB11User {
return { return {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),

View File

@@ -46,15 +46,22 @@ export function decodeCQCode(source: string): OB11MessageData[] {
export function encodeCQCode(data: OB11MessageData) { export function encodeCQCode(data: OB11MessageData) {
const CQCodeEscape = (text: string) => { const CQCodeEscapeText = (text: string) => {
return text.replace(/\[/g, '&#91;') return text.replace(/\&/g, '&amp;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;')
};
const CQCodeEscape = (text: string) => {
return text.replace(/\&/g, '&amp;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;') .replace(/\]/g, '&#93;')
.replace(/\&/g, '&amp;')
.replace(/,/g, '&#44;'); .replace(/,/g, '&#44;');
}; };
if (data.type === 'text') { if (data.type === 'text') {
return CQCodeEscape(data.data.text); return CQCodeEscapeText(data.data.text);
} }
let result = '[CQ:' + data.type; let result = '[CQ:' + data.type;
@@ -68,4 +75,4 @@ export function encodeCQCode(data: OB11MessageData) {
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]") // const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好") // const result = parseCQCode("好好好")
// console.log(JSON.stringify(result)) // console.log(JSON.stringify(result))

View File

@@ -0,0 +1,17 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
notice_type = "group_ban";
operator_id: number;
duration: number;
sub_type: "ban" | "lift_ban";
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: "ban" | "lift_ban") {
super();
this.group_id = groupId;
this.operator_id = operatorId;
this.user_id = userId;
this.duration = duration;
this.sub_type = sub_type;
}
}

View File

@@ -5,10 +5,10 @@ export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite") sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite")
operator_id: number; operator_id: number;
constructor(groupId: number, userId: number) { constructor(groupId: number, userId: number, operatorId: number) {
super(); super();
this.group_id = groupId; this.group_id = groupId;
this.operator_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被邀请的,还是主动加入的 this.operator_id = operatorId;
this.user_id = userId; this.user_id = userId;
} }
} }

View File

@@ -28,7 +28,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
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, OB11Response.error(`api处理出错:${e}`, 1200, echo)) wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))
} }
} }

View File

@@ -67,7 +67,7 @@ export interface OB11Message {
self_id?: number, self_id?: number,
time: number, time: number,
message_id: number, message_id: number,
real_id: string, real_id: number,
user_id: number, user_id: number,
group_id?: number, group_id?: number,
message_type: "private" | "group", message_type: "private" | "group",

View File

@@ -1,8 +1,8 @@
import {DATA_DIR, isGIF, log} from "../common/utils"; import {DATA_DIR, isGIF, log} from "../common/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import * as path from 'node:path'; import * as path from 'node:path';
import {fileCache} from "../common/data";
import * as fileType from 'file-type'; import * as fileType from 'file-type';
import {dbUtil} from "../common/db";
const fs = require("fs").promises; const fs = require("fs").promises;
@@ -33,7 +33,13 @@ export async function uri2local(uri: string, fileName: string = null) {
} }
} else if (url.protocol == "http:" || url.protocol == "https:") { } else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件 // 下载文件
let fetchRes = await fetch(url) let fetchRes: Response;
try{
fetchRes = await fetch(url)
}catch (e) {
res.errMsg = `${url}下载失败`
return res
}
if (!fetchRes.ok) { if (!fetchRes.ok) {
res.errMsg = `${url}下载失败,` + fetchRes.statusText res.errMsg = `${url}下载失败,` + fetchRes.statusText
return res return res
@@ -46,7 +52,7 @@ export async function uri2local(uri: string, fileName: string = null) {
fileName = pathInfo.name fileName = pathInfo.name
if (pathInfo.ext){ if (pathInfo.ext){
fileName += pathInfo.ext fileName += pathInfo.ext
res.ext = pathInfo.ext // res.ext = pathInfo.ext
} }
} }
res.fileName = fileName res.fileName = fileName
@@ -67,7 +73,7 @@ export async function uri2local(uri: string, fileName: string = null) {
filePath = pathname filePath = pathname
} }
} else { } else {
const cache = fileCache.get(uri) const cache = await dbUtil.getFileCache(uri);
if (cache) { if (cache) {
filePath = cache.filePath filePath = cache.filePath
} else { } else {

View File

@@ -90,7 +90,7 @@ async function onSettingWindowCreated(view: Element) {
], 'ob11.messagePostFormat', config.ob11.messagePostFormat), ], 'ob11.messagePostFormat', config.ob11.messagePostFormat),
), ),
SettingItem( SettingItem(
'ffmpeg 路径', `<span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`, 'ffmpeg 路径, 用于发送语音时进行转码', `<span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`,
SettingButton('选择', 'config-ffmpeg-select'), SettingButton('选择', 'config-ffmpeg-select'),
), ),
SettingItem( SettingItem(
@@ -138,7 +138,7 @@ async function onSettingWindowCreated(view: Element) {
]), ]),
SettingList([ SettingList([
SettingItem( SettingItem(
'GitHub', 'GitHub和文档',
`https://github.com/LLOneBot/LLOneBot`, `https://github.com/LLOneBot/LLOneBot`,
SettingButton('点个Star', 'open-github'), SettingButton('点个Star', 'open-github'),
), ),
@@ -167,17 +167,16 @@ async function onSettingWindowCreated(view: Element) {
window.LiteLoader.api.openExternal('https://qm.qq.com/q/bDnHRG38aI') window.LiteLoader.api.openExternal('https://qm.qq.com/q/bDnHRG38aI')
}) })
// 生成反向地址列表 // 生成反向地址列表
const buildHostListItem = (type: string, host: string, index: number) => { const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any={}) => {
const dom = { const dom = {
container: document.createElement('setting-item'), container: document.createElement('setting-item'),
input: document.createElement('input'), input: document.createElement('input'),
inputContainer: document.createElement('div'), inputContainer: document.createElement('div'),
deleteBtn: document.createElement('setting-button'), deleteBtn: document.createElement('setting-button'),
}; };
dom.container.classList.add('setting-host-list-item'); dom.container.classList.add('setting-host-list-item');
dom.container.dataset.direction = 'row'; dom.container.dataset.direction = 'row';
Object.assign(dom.input, inputAttrs);
dom.input.classList.add('q-input__inner'); dom.input.classList.add('q-input__inner');
dom.input.type = 'url'; dom.input.type = 'url';
dom.input.value = host; dom.input.value = host;
@@ -200,18 +199,18 @@ async function onSettingWindowCreated(view: Element) {
return dom.container; return dom.container;
}; };
const buildHostList = (hosts: string[], type: string) => { const buildHostList = (hosts: string[], type: string, inputAttr: any={}) => {
const result: HTMLElement[] = []; const result: HTMLElement[] = [];
hosts.forEach((host, index) => { hosts.forEach((host, index) => {
result.push(buildHostListItem(type, host, index)); result.push(buildHostListItem(type, host, index, inputAttr));
}); });
return result; return result;
}; };
const addReverseHost = (type: string, doc: Document = document) => { const addReverseHost = (type: string, doc: Document = document, inputAttr: any={}) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`); const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`);
hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length)); hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr));
ob11Config[type].push(''); ob11Config[type].push('');
}; };
const initReverseHost = (type: string, doc: Document = document) => { const initReverseHost = (type: string, doc: Document = document) => {
@@ -224,8 +223,8 @@ async function onSettingWindowCreated(view: Element) {
initReverseHost('httpHosts', doc); initReverseHost('httpHosts', doc);
initReverseHost('wsHosts', doc); initReverseHost('wsHosts', doc);
doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts')); doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts', document, {'placeholder': '如http://127.0.0.1:5140/onebot' }));
doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts')); doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts', document, {'placeholder': '如ws://127.0.0.1:5140/onebot' }));
doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => { doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => {
window.llonebot.selectFile() window.llonebot.selectFile()

View File

@@ -1 +1 @@
export const version = "3.13.4" export const version = "3.14.0"