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
LiteLoaderQQNTOneBot11协议插件
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
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="图片名称"/>
*关于插件的安装方法: 下载后解压复制到插件目录*
*插件目录:`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 (没有计划支持)
## 示例
## HTTP 调用示例
![](doc/image/example.jpg)
## 一些坑
## 支持的 api 和功能详情
<details>
<summary>下载了插件但是没有看到在NTQQ中生效</summary>
<br/>
检查是否下载的是插件release的版本如果是源码的话需要自行编译。依然不生效请查阅<a href="https://liteloaderqqnt.github.io/guide/plugins.html">LiteLoaderQQNT的文档</a>
</details>
<br/>
<https://llonebot.github.io/zh-CN/develop/api>
<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
- [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] 文件消息
- [ ] 音乐卡片
- [ ] 无头模式
- [ ] 群禁言事件上报
- [ ] 优化加群成功事件上报
- [ ] 清理缓存api
- [ ] 框架对接文档
## onebot11文档
<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)
* chronocat
* [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,
"type": "extension",
"name": "LLOneBot v3.13.4",
"name": "LLOneBot v3.14.0",
"slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi",
"version": "3.13.4",
"thumbnail": "./icon.png",
"version": "3.14.0",
"icon": "./icon.png",
"authors": [
{
"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 friends: Friend[] = []
export let groupNotifies: Map<string, GroupNotify> = new Map<string, GroupNotify>()
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = {
ffmpegError: '',
otherError: ''
}
export const fileCache = new Map<string, FileCache>()
export async function getFriend(qq: string, uid: string = ""): Promise<Friend | undefined> {
let filterKey = uid ? "uid" : "uin"

View File

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

View File

@@ -11,8 +11,21 @@ export abstract class HttpServerBase {
constructor() {
this.expressAPP = express();
this.expressAPP.use(express.urlencoded({extended: true, limit: "500mb"}));
this.expressAPP.use(json({limit: "500mb"}));
this.expressAPP.use(express.urlencoded({extended: true, limit: "5000mb"}));
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) {

View File

@@ -16,7 +16,6 @@ import {
friendRequests, getFriend,
getGroup,
getGroupMember,
groupNotifies,
llonebotError, refreshGroupMembers,
selfInfo
} from "../common/data";
@@ -155,8 +154,10 @@ function onLoad() {
async function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) {
// log("收到新消息", message.msgSeq)
message.msgShortId = await dbUtil.addMsg(message)
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message);
// }
OB11Constructor.message(message).then((msg) => {
if (debug) {
@@ -169,6 +170,12 @@ function onLoad() {
postOB11Event(msg);
// log("post msg", msg)
}).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) => {
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") {
// 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
@@ -249,19 +256,17 @@ function onLoad() {
for (const notify of notifies) {
try {
notify.time = Date.now();
const notifyTime = parseInt(notify.seq) / 1000
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) {
// continue;
// }
let existNotify = groupNotifies[notify.seq];
let existNotify = await dbUtil.getGroupNotify(notify.seq);
if (existNotify) {
if (Date.now() - existNotify.time < 3000) {
continue
}
continue
}
log("收到群通知", notify);
groupNotifies[notify.seq] = notify;
await dbUtil.addGroupNotify(notify);
// let member2: GroupMember;
// if (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> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC);
if (fileSize === 0){
throw "文件异常大小为0";
}
const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = {
md5HexStr: md5,
@@ -89,6 +92,9 @@ export class SendMsgElementConstructor {
picWidth = 768;
}
const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE);
if (fileSize === 0){
throw "文件异常大小为0";
}
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: "",
@@ -112,6 +118,9 @@ export class SendMsgElementConstructor {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
if (fileSize === 0){
throw "文件异常大小为0";
}
if (converted) {
fs.unlink(silkPath, () => {
});

View File

@@ -1,72 +1,73 @@
import {type BrowserWindow} from 'electron'
import {getConfigUtil, log, sleep} from '../common/utils'
import {NTQQApi, type NTQQApiClass, sendMessagePool} from './ntcall'
import {type Group, type RawMessage, type User} from './types'
import {friends, groups, selfInfo, tempGroupCodeMap} from '../common/data'
import {OB11GroupDecreaseEvent} from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import {OB11GroupIncreaseEvent} from '../onebot11/event/notice/OB11GroupIncreaseEvent'
import {v4 as uuidv4} from 'uuid'
import {postOB11Event} from '../onebot11/server/postOB11Event'
import {HOOK_LOG} from '../common/config'
import fs from 'fs'
import {BrowserWindow} from 'electron';
import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {Group, RawMessage, User} from "./types";
import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {v4 as uuidv4} from "uuid"
import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from "../common/config";
import fs from "fs";
import {dbUtil} from "../common/db";
export const hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export enum ReceiveCmd {
UPDATE_MSG = 'nodeIKernelMsgListener/onMsgInfoListUpdate',
NEW_MSG = 'nodeIKernelMsgListener/onRecvMsg',
SELF_SEND_MSG = 'nodeIKernelMsgListener/onAddSendMsg',
USER_INFO = 'nodeIKernelProfileListener/onProfileSimpleChanged',
USER_DETAIL_INFO = 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
GROUPS = 'nodeIKernelGroupListener/onGroupListUpdate',
GROUPS_UNIX = 'onGroupListUpdate',
FRIENDS = 'onBuddyListChange',
MEDIA_DOWNLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaDownloadComplete',
UNREAD_GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated',
GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupSingleScreenNotifies',
FRIEND_REQUEST = 'nodeIKernelBuddyListener/onBuddyReqChange',
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged",
GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies",
FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange",
SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH = "nodeIKernelStorageCleanListener/onFinishScan",
}
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: {
'type': 'request'
'eventName': NTQQApiClass
'callbackId'?: string
}
"type": "request",
"eventName": NTQQApiClass,
"callbackId"?: string
},
1:
Array<{
cmdName: ReceiveCmd
cmdType: 'event'
{
cmdName: ReceiveCmd,
cmdType: "event",
payload: PayloadType
}>
}[]
}
const receiveHooks: Array<{
method: ReceiveCmd
let receiveHooks: Array<{
method: ReceiveCmd,
hookFunc: ((payload: any) => void | Promise<void>)
id: string
}> = []
export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send
const originalSend = window.webContents.send;
const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args))
if (args?.[1] instanceof Array) {
for (const receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (const hook of receiveHooks) {
for (let hook of receiveHooks) {
if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => {
try {
const _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
}
} catch (e) {
log('hook error', e, receiveData.payload)
log("hook error", e, receiveData.payload)
}
}).then()
}
@@ -75,35 +76,35 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
hookApiCallbacks[callbackId](args[1]);
}).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) {
// 监听调用NTQQApi
const webContents = window.webContents as any
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
let webContents = window.webContents as any;
const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"];
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
HOOK_LOG && log('call NTQQ api', thisArg, args)
return target.apply(thisArg, args)
}
})
if (webContents._events['-ipc-message']?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg
HOOK_LOG && log("call NTQQ api", thisArg, args);
return target.apply(thisArg, args);
},
});
if (webContents._events["-ipc-message"]?.[0]) {
webContents._events["-ipc-message"][0] = proxyIpcMsg;
} else {
webContents._events['-ipc-message'] = proxyIpcMsg
webContents._events["-ipc-message"] = proxyIpcMsg;
}
}
@@ -114,29 +115,29 @@ export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (
hookFunc,
id
})
return id
return id;
}
export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex(h => h.id === id)
receiveHooks.splice(index, 1)
receiveHooks.splice(index, 1);
}
async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (const group of _groups) {
let existGroup = groups.find(g => g.groupCode == group.groupCode)
for (let group of _groups) {
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
Object.assign(existGroup, group)
Object.assign(existGroup, group);
} else {
groups.push(group)
existGroup = group
groups.push(group);
existGroup = group;
}
if (needUpdate) {
const members = await NTQQApi.getGroupMembers(group.groupCode)
const members = await NTQQApi.getGroupMembers(group.groupCode);
if (members) {
existGroup.members = members
existGroup.members = members;
}
}
}
@@ -144,83 +145,66 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
async function processGroupEvent(payload) {
try {
const newGroupList = payload.groupList
const newGroupList = payload.groupList;
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.memberCount > group.memberCount) {
const oldMembers = existGroup.members
const oldMembers = existGroup.members;
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQApi.getGroupMembers(group.groupCode)
await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度
group.members = newMembers;
const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
for (const member of newMembers) {
newMembersSet.add(member.uin)
newMembersSet.add(member.uin);
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)))
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
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break;
}
}
}
}
}
updateGroups(newGroupList, false).then()
updateGroups(newGroupList, false).then();
} catch (e) {
updateGroups(payload.groupList).then()
console.log(e)
updateGroups(payload.groupList).then();
console.log(e);
}
}
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => {
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
updateGroups(payload.groupList).then();
} else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
if (process.platform == "win32") {
processGroupEvent(payload).then();
}
}
})
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => {
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
updateGroups(payload.groupList).then();
} else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
if (process.platform != "win32") {
processGroupEvent(payload).then();
}
}
})
registerReceiveHook<{
data: Array<{ categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }>
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => {
for (const fData of payload.data) {
const _friends = fData.buddyList
for (const friend of _friends) {
const existFriend = friends.find(f => f.uin == friend.uin)
const _friends = fData.buddyList;
for (let friend of _friends) {
let existFriend = friends.find(f => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
} else {
@@ -230,11 +214,11 @@ registerReceiveHook<{
}
})
registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile, autoDeleteFileSecond} = getConfigUtil().getConfig()
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile} = getConfigUtil().getConfig();
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgSeq)
dbUtil.addMsg(message).then()
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
if (!autoDeleteFile) {
continue
@@ -255,27 +239,27 @@ registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) =>
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
log("删除文件成功", path)
});
}
}
}, autoDeleteFileSecond * 1000)
}, 60 * 1000)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => {
const message = msgRecord
const peerUid = message.peerUid
const message = msgRecord;
const peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgSeq);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
sendCallback(message);
} 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 {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from './hook'
import {log, sleep} from '../common/utils'
import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log, sleep} from "../common/utils";
import {
type ChatType,
ChatType,
ElementType,
type Friend,
type FriendRequest,
type Group, GroupMember,
type GroupMemberRole,
type GroupNotifies,
type GroupNotify,
type GroupRequestOperateTypes,
type RawMessage,
type SelfInfo,
type SendMessageElement,
type User
} from './types'
import * as fs from 'node:fs'
import {friendRequests, groupNotifies, selfInfo, uidMaps} from '../common/data'
import {v4 as uuidv4} from 'uuid'
import path from 'path'
Friend,
FriendRequest,
Group,
GroupMember,
GroupMemberRole,
GroupNotifies,
GroupNotify,
GroupRequestOperateTypes,
RawMessage,
SelfInfo,
SendMessageElement,
User,
CacheScanResult,
ChatCacheList, ChatCacheListItemBasic,
CacheFileList, CacheFileListItem, CacheFileType,
} 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";
interface IPCReceiveEvent {
@@ -35,89 +39,104 @@ export type IPCReceiveDetail = [
]
export enum NTQQApiClass {
NT_API = 'ns-ntApi',
FS_API = 'ns-FsApi',
GLOBAL_DATA = 'ns-GlobalDataApi'
NT_API = "ns-ntApi",
FS_API = "ns-FsApi",
OS_API = "ns-OsApi",
HOTUPDATE_API = "ns-HotUpdateApi",
BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi"
}
export enum NTQQApiMethod {
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
FILE_TYPE = 'getFileType',
FILE_MD5 = 'getFileMd5',
FILE_COPY = 'copyFile',
IMAGE_SIZE = 'getImageSizeFromPath',
FILE_SIZE = 'getFileSize',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
SEND_MSG = 'nodeIKernelMsgService/sendMsg',
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment", // 逐条转发
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies',
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUPS = "nodeIKernelGroupService/getGroupList",
GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
FILE_TYPE = "getFileType",
FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile",
IMAGE_SIZE = "getImageSizeFromPath",
FILE_SIZE = "getFileSize",
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp',
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName',
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole',
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = "nodeIKernelGroupService/kickMember",
MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp",
MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp",
SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName",
SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
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 {
IPC_UP_2 = 'IPC_UP_2',
IPC_UP_3 = 'IPC_UP_3',
IPC_UP_1 = 'IPC_UP_1',
IPC_UP_2 = "IPC_UP_2",
IPC_UP_3 = "IPC_UP_3",
IPC_UP_1 = "IPC_UP_1",
}
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ''
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ""
}
interface NTQQApiParams {
methodName: NTQQApiMethod | string
className?: NTQQApiClass
channel?: NTQQApiChannel
methodName: NTQQApiMethod | string,
className?: NTQQApiClass,
channel?: NTQQApiChannel,
classNameIsRegister?: boolean
args?: unknown[]
cbCmd?: ReceiveCmd | null
cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number
args?: unknown[],
cbCmd?: ReceiveCmd | null,
cmdCB?: (payload: any) => boolean;
afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number,
}
async function callNTQQApi<ReturnType>(params: NTQQApiParams) {
function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let {
className, methodName, channel, args,
cbCmd, timeoutSecond: timeout,
classNameIsRegister, cmdCB, afterFirstCmd
} = params
className = className ?? NTQQApiClass.NT_API
channel = channel ?? NTQQApiChannel.IPC_UP_2
args = args ?? []
timeout = timeout ?? 5
afterFirstCmd = afterFirstCmd ?? true
const uuid = uuidv4()
} = params;
className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2;
args = args ?? [];
timeout = timeout ?? 5;
afterFirstCmd = afterFirstCmd ?? true;
const uuid = uuidv4();
// 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)
const _timeout = timeout * 1000
let success = false
let eventName = className + '-' + channel[channel.length - 1]
let eventName = className + "-" + channel[channel.length - 1];
if (classNameIsRegister) {
eventName += '-register'
eventName += "-register";
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
@@ -125,40 +144,40 @@ async function callNTQQApi<ReturnType>(params: NTQQApiParams) {
hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true
resolve(r)
}
};
} else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (cmdCB) {
if (!!cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId)
removeReceiveHook(hookId);
success = true
resolve(payload)
resolve(payload);
}
} else {
removeReceiveHook(hookId)
removeReceiveHook(hookId);
success = true
resolve(payload)
resolve(payload);
}
})
}
!afterFirstCmd && secondCallback()
!afterFirstCmd && secondCallback();
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback()
afterFirstCmd && secondCallback();
} else {
success = true
reject(`ntqq api call failed, ${result.errMsg}`)
reject(`ntqq api call failed, ${result.errMsg}`);
}
}
}
setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName)
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}`)
}
}, _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 {
result: number // 0: success
result: number, // 0: success
errMsg: string
}
export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static async likeFriend(uid: string, count = 1) {
@@ -198,9 +219,7 @@ export class NTQQApi {
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
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) {
const data = await callNTQQApi<{
data: Array<{
categoryId: number
categroyName: string
categroyMbCount: number
data: {
categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}>
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS
})
const _friends: Friend[] = []
let _friends: Friend[] = [];
for (const fData of data.data) {
_friends.push(...fData.buddyList)
}
@@ -256,11 +275,11 @@ export class NTQQApi {
static async getGroups(forced = false) {
let cbCmd = ReceiveCmd.GROUPS
if (process.platform != 'win32') {
if (process.platform != "win32") {
cbCmd = ReceiveCmd.GROUPS_UNIX
}
const result = await callNTQQApi<{
updateType: number
updateType: number,
groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList
@@ -271,7 +290,7 @@ export class NTQQApi {
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow'
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId);
@@ -281,8 +300,8 @@ export class NTQQApi {
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId,
num
sceneId: sceneId,
num: num
},
null
]
@@ -343,35 +362,35 @@ export class NTQQApi {
// 上传文件到QQ的文件夹
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
if (ext) {
ext = '.' + ext
ext = "." + ext
} else {
ext = ''
ext = ""
}
let fileName = `${path.basename(filePath)}`
if (!fileName.includes('.')) {
fileName += ext
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf(".") === -1) {
fileName += ext;
}
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{
path_info: {
md5HexStr: md5,
fileName,
elementType,
fileName: fileName,
elementType: elementType,
elementSubType: 0,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
file_uuid: ""
}
}]
})
log('media path', mediaPath)
await NTQQApi.copyFile(filePath, mediaPath)
const fileSize = await NTQQApi.getFileSize(filePath)
log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath);
return {
md5,
fileName,
@@ -388,16 +407,16 @@ export class NTQQApi {
const apiParams = [
{
getReq: {
msgId,
chatType,
peerUid,
elementId,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
}
filePath: thumbPath,
},
},
undefined
undefined,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
@@ -406,7 +425,7 @@ export class NTQQApi {
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath
return payload.notifyInfo.filePath == sourcePath;
}
})
return sourcePath
@@ -426,30 +445,30 @@ export class NTQQApi {
const peerUid = peer.peerUid
// 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0
let checkLastSendUsingTime = 0;
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw ('发送超时')
throw ("发送超时")
}
const lastSending = sendMessagePool[peer.peerUid]
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
await sleep(500);
checkLastSendUsingTime += 500;
return await waitLastSend();
} else {
return;
}
}
await waitLastSend()
await waitLastSend();
let sentMessage: RawMessage = null
let sentMessage: RawMessage = null;
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
delete sendMessagePool[peerUid];
sentMessage = rawMessage;
}
let checkSendCompleteUsingTime = 0
let checkSendCompleteUsingTime = 0;
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
@@ -469,14 +488,13 @@ export class NTQQApi {
await sleep(500)
return await checkSendComplete()
}
log("开始发送消息", peer, msgElements)
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map()
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
return await checkSendComplete()
@@ -512,13 +530,13 @@ export class NTQQApi {
commentElements: [],
msgAttributeInfos: new Map()
},
null
null,
]
return await new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject('转发消息超时')
reject("转发消息超时");
}
}, 5000)
registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
@@ -544,10 +562,10 @@ export class NTQQApi {
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log('转发消息结果:', result, apiArgs)
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true
reject('转发消息失败,' + JSON.stringify(result))
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
@@ -558,56 +576,56 @@ export class NTQQApi {
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmd.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
{doubt: false, startSeq: '', number: 14},
{"doubt": false, "startSeq": "", "number": 14},
null
]
})
});
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq]
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
delete groupNotifies[seq]
// delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false,
operateMsg: {
operateType, // 2 拒绝
targetMsg: {
seq, // 通知序列号
type: notify.type,
groupCode: notify.group.groupCode,
postscript: reason
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
})
});
}
static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{groupCode: groupQQ},
{"groupCode": groupQQ},
null
]
})
}
static async handleFriendRequest(sourceId: number, accept: boolean) {
static async handleFriendRequest(sourceId: number, accept: boolean,) {
const request: FriendRequest = friendRequests[sourceId]
if (!request) {
throw `sourceId ${sourceId}, 对应的好友请求不存在`
@@ -616,16 +634,16 @@ export class NTQQApi {
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
approvalInfo: {
friendUid: request.friendUid,
reqTime: request.reqTime,
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[sourceId]
return result
delete friendRequests[sourceId];
return result;
}
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
@@ -637,7 +655,7 @@ export class NTQQApi {
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason
kickReason,
}
]
}
@@ -652,7 +670,7 @@ export class NTQQApi {
args: [
{
groupCode: groupQQ,
memList
memList,
}
]
}
@@ -712,4 +730,107 @@ export class NTQQApi {
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,
jpg = 1000
}
export interface SendPicElement {
elementType: ElementType.PIC,
elementId: "",
@@ -218,8 +219,8 @@ export interface PttElement {
export interface ArkElement {
bytesData: string;
linkInfo:null,
subElementType:null
linkInfo: null,
subElementType: null
}
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
@@ -233,6 +234,7 @@ export interface PicElement {
fileSize: number;
fileName: string;
fileUuid: string;
md5HexStr?: string;
}
export interface GrayTipElement {
@@ -244,7 +246,8 @@ export interface GrayTipElement {
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
aioOpGrayTipElement: TipAioOpGrayTipElement
aioOpGrayTipElement: TipAioOpGrayTipElement,
groupElement: TipGroupElement
}
export interface FaceElement {
@@ -277,15 +280,20 @@ export interface VideoElement {
"sourceVideoCodecFormat": 0
}
export interface TipAioOpGrayTipElement{
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
operateType: number,
peerUid: string,
fromGrpCodeOfTmpChat: string,
}
export enum TipGroupElementType {
memberIncrease = 1,
ban = 8
}
export interface TipGroupElement {
"type": 1, // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0,
"type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, // 暂时不知
"groupName": string, // 暂时获取不到
"memberUid": string,
"memberNick": string,
@@ -294,7 +302,7 @@ export interface TipGroupElement {
"adminNick": string,
"adminRemark": string,
"createGroup": null,
"memberAdd": {
"memberAdd"?: {
"showType": 1,
"otherAdd": null,
"otherAddByOtherQRCode": null,
@@ -304,7 +312,22 @@ export interface TipGroupElement {
"otherInviteYou": 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 {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 转成数字再除以1000应该就是时间戳
seq: string, // 唯一标识符,转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes,
status: 0, // 未知
group: { groupCode: string, groupName: string },
@@ -402,4 +425,68 @@ export interface FriendRequestNotify {
unreadNums: number,
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);
return OB11Response.ok(resData);
} catch (e) {
log("发生错误", e.stack)
log("发生错误", e)
return OB11Response.error(e.toString(), 200);
}
}
@@ -35,7 +35,7 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo);
} catch (e) {
log("发生错误", e.stack.toString())
log("发生错误", e)
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 {fileCache} from "../../common/data";
import {getConfigUtil} from "../../common/utils";
import fs from "fs/promises";
import {dbUtil} from "../../common/db";
export interface GetFilePayload {
file: string // 文件名
@@ -18,7 +18,7 @@ export interface GetFileResponse {
export class GetFileBase extends BaseAction<GetFilePayload, 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()
if (!cache) {
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)
if (msg) {
const msgData = await OB11Constructor.message(msg);
return msgData
return await OB11Constructor.message(msg)
} else {
throw ("消息不存在")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,13 +8,25 @@ import {
OB11User,
OB11UserSex
} from "./types";
import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types';
import {fileCache, getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data';
import {getConfigUtil, log} from "../common/utils";
import {
AtType,
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 {EventType} from "./event/OB11BaseEvent";
import {encodeCQCode} from "./cqcode";
import {dbUtil} from "../common/db";
import {OB11GroupIncreaseEvent} from "./event/notice/OB11GroupIncreaseEvent";
import {OB11GroupBanEvent} from "./event/notice/OB11GroupBanEvent";
export class OB11Constructor {
@@ -27,7 +39,7 @@ export class OB11Constructor {
user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId,
real_id: msg.msgId,
real_id: msg.msgShortId,
message_type: msg.chatType == ChatType.group ? "group" : "private",
sender: {
user_id: parseInt(msg.senderUin),
@@ -97,7 +109,7 @@ export class OB11Constructor {
} else if (element.replyElement) {
message_data["type"] = "reply"
// log("收到回复消息", element.replyElement.replayMsgSeq)
try{
try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) {
@@ -105,8 +117,8 @@ export class OB11Constructor {
} else {
continue
}
}catch (e) {
log("获取不到引用的消息", e.stack)
} catch (e) {
log("获取不到引用的消息", e.stack, element.replyElement.replayMsgSeq)
}
} else if (element.picElement) {
@@ -114,19 +126,25 @@ export class OB11Constructor {
// message_data["data"]["file"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.fileName
// 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_size"] = element.picElement.fileSize
fileCache.set(element.picElement.fileName, {
dbUtil.addFileCache(element.picElement.fileName, {
fileName: element.picElement.fileName,
filePath: element.picElement.sourcePath,
fileSize: element.picElement.fileSize.toString(),
url: IMAGE_HTTP_HOST + element.picElement.originImageUrl,
url: message_data["data"]["url"],
downloadFunc: async () => {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.picElement.thumbPath?.get(0) || "", element.picElement.sourcePath)
}
})
}).then()
// 不在自动下载图片
} else if (element.videoElement) {
@@ -135,7 +153,7 @@ export class OB11Constructor {
message_data["data"]["path"] = element.videoElement.filePath
// message_data["data"]["file_id"] = element.videoElement.fileUuid
message_data["data"]["file_size"] = element.videoElement.fileSize
fileCache.set(element.videoElement.fileName, {
dbUtil.addFileCache(element.videoElement.fileName, {
fileName: element.videoElement.fileName,
filePath: element.videoElement.filePath,
fileSize: element.videoElement.fileSize,
@@ -143,7 +161,7 @@ export class OB11Constructor {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath)
}
})
}).then()
// 怎么拿到url呢
} else if (element.fileElement) {
message_data["type"] = OB11MessageDataType.file;
@@ -151,7 +169,7 @@ export class OB11Constructor {
// message_data["data"]["path"] = element.fileElement.filePath
// message_data["data"]["file_id"] = element.fileElement.fileUuid
message_data["data"]["file_size"] = element.fileElement.fileSize
fileCache.set(element.fileElement.fileName, {
dbUtil.addFileCache(element.fileElement.fileName, {
fileName: element.fileElement.fileName,
filePath: element.fileElement.filePath,
fileSize: element.fileElement.fileSize,
@@ -159,7 +177,7 @@ export class OB11Constructor {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, null, element.fileElement.filePath)
}
})
}).then()
// 怎么拿到url呢
} else if (element.pttElement) {
message_data["type"] = OB11MessageDataType.voice;
@@ -167,11 +185,11 @@ export class OB11Constructor {
message_data["data"]["path"] = element.pttElement.filePath
// message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data["data"]["file_size"] = element.pttElement.fileSize
fileCache.set(element.pttElement.fileName, {
dbUtil.addFileCache(element.pttElement.fileName, {
fileName: element.pttElement.fileName,
filePath: element.pttElement.filePath,
fileSize: element.pttElement.fileSize,
})
}).then()
// log("收到语音消息", msg)
// 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["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) {
// let filePath: string = message_data.data.file;
// if (!enableLocalFile2Url) {
@@ -223,6 +244,54 @@ export class OB11Constructor {
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 {
return {
user_id: parseInt(friend.uin),

View File

@@ -46,15 +46,22 @@ export function decodeCQCode(source: string): OB11MessageData[] {
export function encodeCQCode(data: OB11MessageData) {
const CQCodeEscape = (text: string) => {
return text.replace(/\[/g, '&#91;')
const CQCodeEscapeText = (text: string) => {
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, '&amp;')
.replace(/,/g, '&#44;');
};
if (data.type === 'text') {
return CQCodeEscape(data.data.text);
return CQCodeEscapeText(data.data.text);
}
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("好好好")
// 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")
operator_id: number;
constructor(groupId: number, userId: number) {
constructor(groupId: number, userId: number, operatorId: number) {
super();
this.group_id = groupId;
this.operator_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被邀请的,还是主动加入的
this.operator_id = operatorId;
this.user_id = userId;
}
}

View File

@@ -28,7 +28,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
let handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult)
} 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,
time: number,
message_id: number,
real_id: string,
real_id: number,
user_id: number,
group_id?: number,
message_type: "private" | "group",

View File

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

View File

@@ -90,7 +90,7 @@ async function onSettingWindowCreated(view: Element) {
], 'ob11.messagePostFormat', config.ob11.messagePostFormat),
),
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'),
),
SettingItem(
@@ -138,7 +138,7 @@ async function onSettingWindowCreated(view: Element) {
]),
SettingList([
SettingItem(
'GitHub',
'GitHub和文档',
`https://github.com/LLOneBot/LLOneBot`,
SettingButton('点个Star', 'open-github'),
),
@@ -167,17 +167,16 @@ async function onSettingWindowCreated(view: Element) {
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 = {
container: document.createElement('setting-item'),
input: document.createElement('input'),
inputContainer: document.createElement('div'),
deleteBtn: document.createElement('setting-button'),
};
dom.container.classList.add('setting-host-list-item');
dom.container.dataset.direction = 'row';
Object.assign(dom.input, inputAttrs);
dom.input.classList.add('q-input__inner');
dom.input.type = 'url';
dom.input.value = host;
@@ -200,18 +199,18 @@ async function onSettingWindowCreated(view: Element) {
return dom.container;
};
const buildHostList = (hosts: string[], type: string) => {
const buildHostList = (hosts: string[], type: string, inputAttr: any={}) => {
const result: HTMLElement[] = [];
hosts.forEach((host, index) => {
result.push(buildHostListItem(type, host, index));
result.push(buildHostListItem(type, host, index, inputAttr));
});
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`);
hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length));
hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr));
ob11Config[type].push('');
};
const initReverseHost = (type: string, doc: Document = document) => {
@@ -224,8 +223,8 @@ async function onSettingWindowCreated(view: Element) {
initReverseHost('httpHosts', doc);
initReverseHost('wsHosts', doc);
doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts'));
doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts'));
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', document, {'placeholder': '如ws://127.0.0.1:5140/onebot' }));
doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => {
window.llonebot.selectFile()

View File

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