Compare commits

..

54 Commits

Author SHA1 Message Date
linyuchen
ec27d73605 fix: copy .node 2024-04-30 23:12:36 +08:00
linyuchen
59cd28a2fd feat: FriendAddNotice 2024-04-30 23:06:50 +08:00
linyuchen
bcb6b51241 feat: send mface with summary param 2024-04-30 19:45:59 +08:00
linyuchen
b00ca24fe3 feat: send mface 2024-04-30 19:42:34 +08:00
linyuchen
3a4cdc1e34 Merge branch 'main' of https://github.com/markyfsun/LLOneBot into mface 2024-04-30 19:35:43 +08:00
linyuchen
de4d901412 refactor: 获取rkey后进行检查rkey是否正确 2024-04-30 19:26:51 +08:00
linyuchen
297c495df9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
2024-04-30 18:58:25 +08:00
student_2333
b78bd235f9 fix 2024-04-30 14:13:40 +08:00
student_2333
23d32a1464 Merge branch 'main' into markyfsun/main 2024-04-30 14:07:18 +08:00
手瓜一十雪
25c3d51d69 Merge pull request #206 from LLOneBot/feat/music-card
feat: music card sign
2024-04-30 13:45:15 +08:00
student_2333
05091798f4 fix 2024-04-30 13:40:15 +08:00
student_2333
78c6050d61 refactor 2024-04-30 13:08:33 +08:00
student_2333
2abdcd23db fix 2024-04-30 13:07:51 +08:00
student_2333
1d7100a053 fix 2024-04-30 13:05:08 +08:00
student_2333
6ff49722d8 feat: music card sign 2024-04-30 12:50:38 +08:00
student_2333
9c6abd5167 Merge branch 'main' into markyfsun/main 2024-04-30 11:35:38 +08:00
student_2333
dc1e1ea21b style: reformat 2024-04-30 11:28:24 +08:00
student_2333
f38e544815 style: reformat 2024-04-30 11:24:33 +08:00
student_2333
bb0fcd8614 chore(dep) 2024-04-30 11:22:26 +08:00
linyuchen
710fa3f686 Update README.md thanks list 2024-04-29 19:30:00 +08:00
linyuchen
91089cdb9e refactor: import native .node 2024-04-29 16:25:27 +08:00
linyuchen
58f544862b refactor: private/group image rkey 2024-04-29 11:57:58 +08:00
linyuchen
09ab8cbe93 fix: private/group image rkey 2024-04-28 10:30:41 +08:00
linyuchen
4ce4f3d3a5 fix: image rkey 2024-04-28 09:26:55 +08:00
linyuchen
b5ab717634 优化发送语音或者不支持的消息类型错误提示 2024-04-28 09:19:14 +08:00
markyfsun
2e55924a19 feat: market face 2024-04-27 23:01:47 +08:00
linyuchen
fe3ac3060a Merge remote-tracking branch 'origin/main' 2024-04-26 01:27:10 +08:00
linyuchen
e7e06d655f optimize get file 2024-04-25 23:28:35 +08:00
linyuchen
dec531c567 fix: get image rkey 2024-04-25 23:27:39 +08:00
linyuchen
05f0985f7f feat: upload private file 2024-04-25 23:27:14 +08:00
linyuchen
ac852cc382 feat: msg emoji like 2024-04-25 23:26:46 +08:00
linyuchen
b7855e91f6 feat: msg emoji like 2024-04-25 23:25:38 +08:00
linyuchen
3ae2d2a1e6 feat: forward single msg 2024-04-25 23:24:58 +08:00
linyuchen
857625469f Merge pull request #199 from disymayufei/patch-2
向README.md中添加了一个警告信息
2024-04-20 17:18:52 +08:00
Disy
ca3f68a42a chore: Update caution message
添加了一个警告信息,希望可以起到警示作用,防止一些小白私自将仓库和插件信息广泛传播出去引发tx的警觉
2024-04-20 14:18:26 +08:00
手瓜一十雪
1d47f89011 Merge pull request #197 from jinyu2022/main
添加CORS允许跨源访问
2024-04-17 15:22:38 +08:00
堇羽
2c24e234c8 添加CORS允许跨源访问 2024-04-17 07:14:23 +00:00
linyuchen
5562a3251d feat: get cookies 2024-04-16 23:55:21 +08:00
linyuchen
019b590f36 refactor: auto escape cq code for send msg 2024-04-16 23:23:19 +08:00
linyuchen
c2b3316603 fix: send empty forward msg
fix: ignore post history msg before login
fix: quit group not sync to groups of data
feat: support post url params
feat: support port http heart
2024-04-16 23:16:25 +08:00
linyuchen
f8890b309b fix: face msg faceType 2024-04-11 18:57:58 +08:00
linyuchen
b5e578733f fix: quick reply friend msg 2024-04-11 18:17:02 +08:00
linyuchen
51602b987e fix: ws 没有上报群文件上传事件 2024-04-08 00:21:24 +08:00
linyuchen
b501af6e0e feat: 骰子魔法表情 & 猜拳魔法表情 2024-04-07 18:51:26 +08:00
linyuchen
81821e74d8 fix: 手动频繁切换聊天窗口时导致旧的窗口接收不到消息 2024-04-07 17:37:52 +08:00
linyuchen
959eab441e Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-04-06 23:58:27 +08:00
linyuchen
441c0c6946 feat: @全体的时候判断剩余次数 2024-04-06 23:57:07 +08:00
linyuchen
240cdade07 fix: getFriend 2024-04-06 23:30:40 +08:00
linyuchen
0132d97bd9 Merge pull request #177 from idanran/main
fix: audio may fail to convert
2024-04-04 12:37:37 +08:00
linyuchen
b34c7f045c fix: at all when member isn't admin 2024-04-04 12:26:28 +08:00
idanran
ab91313e69 fix 2024-04-04 04:22:56 +00:00
idanran
1f8966aaf4 fix: audio may fail to convert 2024-04-04 02:08:08 +00:00
linyuchen
ec073da3f6 feat: 发送戳一戳 2024-04-03 00:03:50 +08:00
linyuchen
80131e0472 fix: send msg auto_escape 2024-04-02 12:51:08 +08:00
147 changed files with 8359 additions and 14087 deletions

View File

@@ -78,4 +78,4 @@ body:
attributes: attributes:
label: OneBot 客户端运行日志 label: OneBot 客户端运行日志
description: 粘贴 OneBot 客户端的相关日志内容到此处 description: 粘贴 OneBot 客户端的相关日志内容到此处
render: shell render: shell

View File

@@ -1,39 +1,39 @@
name: "publish" name: 'publish'
on: on:
push: push:
tags: tags:
- "v*" - 'v*'
jobs: jobs:
build-and-publish: build-and-publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: setup node - name: setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 18
- name: install dependenies - name: install dependenies
run: | run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1 export ELECTRON_SKIP_BINARY_DOWNLOAD=1
npm install npm install
- name: build - name: build
run: npm run build run: npm run build
- name: zip - name: zip
run: | run: |
sudo apt install zip -y sudo apt install zip -y
cp manifest.json ./dist/manifest.json cp manifest.json ./dist/manifest.json
cd ./dist/ cd ./dist/
zip -r ../LLOneBot.zip ./* zip -r ../LLOneBot.zip ./*
- name: publish - name: publish
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "LLOneBot.zip" artifacts: 'LLOneBot.zip'
draft: true draft: true
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}

4
.prettierrc.yml Normal file
View File

@@ -0,0 +1,4 @@
semi: false
singleQuote: true
trailingComma: all
printWidth: 120

14
CHANGELOG Normal file
View File

@@ -0,0 +1,14 @@
# 3.24.0
## 修复
* 修复图片rkey导致链接失效的问题
* 修复/get_image, /get_file 无法获取图片的问题
* 修复上报他人管理员被取消通知
## 新增
* 新增表情回应发送和上报
* 新增商城表情发送,和上报 url
* 新增转发单条消息接口 `forward_friend_single_msg`, `forward_group_single_msg`
* 新增新增好友事件

View File

@@ -1,7 +1,10 @@
# LLOneBot API # LLOneBot API
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发 LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于:B站微博知乎抖音等发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 安装方法 ## 安装方法
@@ -20,11 +23,11 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR - [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录 - [x] 转发消息记录
- [x] 好友点赞api - [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等 - [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息 - [x] 视频消息
@@ -36,17 +39,21 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
- [ ] 框架对接文档 - [ ] 框架对接文档
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>
## Stargazers over time ## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot) [![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 鸣谢 ## 鸣谢
* [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [chronocat](https://github.com/chrononeko/chronocat/) - [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) - [chronocat](https://github.com/chrononeko/chronocat/)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm)
## 友链 ## 友链
* [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架

View File

@@ -1,73 +1,87 @@
import cp from 'vite-plugin-cp'; import cp from 'vite-plugin-cp'
import "./scripts/gen-version" import './scripts/gen-version'
const external = ["silk-wasm", "ws", const external = [
"level", "classic-level", "abstract-level", "level-supports", "level-transcoder", 'silk-wasm',
"module-error", "catering", "node-gyp-build"]; 'ws',
'level',
'classic-level',
'abstract-level',
'level-supports',
'level-transcoder',
'module-error',
'catering',
'node-gyp-build',
]
function genCpModule(module: string) { function genCpModule(module: string) {
return {src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false} return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
} }
let config = { let config = {
main: { main: {
build: { build: {
outDir: "dist/main", outDir: 'dist/main',
emptyOutDir: true, emptyOutDir: true,
lib: { lib: {
formats: ["cjs"], formats: ['cjs'],
entry: {"main": "src/main/main.ts"}, entry: { main: 'src/main/main.ts' },
}, },
rollupOptions: { rollupOptions: {
external, external,
input: "src/main/main.ts", input: 'src/main/main.ts',
} },
},
resolve: {
alias: {
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg'
},
},
plugins: [cp({
targets: [
...external.map(genCpModule),
{src: './manifest.json', dest: 'dist'}, {src: './icon.jpg', dest: 'dist'},
{src: './src/ntqqapi/external/ccpoke/poke-win32-x64.node', dest: 'dist/main/ccpoke/'},
]
})]
}, },
preload: { resolve: {
// vite config options alias: {
build: { './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
outDir: "dist/preload", },
emptyOutDir: true,
lib: {
formats: ["cjs"],
entry: {"preload": "src/preload.ts"},
},
rollupOptions: {
// external: externalAll,
input: "src/preload.ts",
}
},
resolve: {}
}, },
renderer: { plugins: [
// vite config options cp({
build: { targets: [
outDir: "dist/renderer", ...external.map(genCpModule),
emptyOutDir: true, { src: './manifest.json', dest: 'dist' },
lib: { { src: './icon.jpg', dest: 'dist' },
formats: ["es"], { src: './src/ntqqapi/external/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
entry: {"renderer": "src/renderer/index.ts"}, { src: './src/ntqqapi/external/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
}, { src: './src/ntqqapi/external/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
rollupOptions: { ],
// external: externalAll, }),
input: "src/renderer/index.ts", ],
} },
}, preload: {
resolve: {} // vite config options
} build: {
outDir: 'dist/preload',
emptyOutDir: true,
lib: {
formats: ['cjs'],
entry: { preload: 'src/preload.ts' },
},
rollupOptions: {
// external: externalAll,
input: 'src/preload.ts',
},
},
resolve: {},
},
renderer: {
// vite config options
build: {
outDir: 'dist/renderer',
emptyOutDir: true,
lib: {
formats: ['es'],
entry: { renderer: 'src/renderer/index.ts' },
},
rollupOptions: {
// external: externalAll,
input: 'src/renderer/index.ts',
},
},
resolve: {},
},
} }
export default config; export default config

View File

@@ -1,10 +1,10 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.20.7", "name": "LLOneBot v3.24.0",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新", "description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.20.7", "version": "3.24.0",
"icon": "./icon.jpg", "icon": "./icon.jpg",
"authors": [ "authors": [
{ {

6782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,16 @@
"build-mac": "npm run build && npm run deploy-mac", "build-mac": "npm run build && npm run deploy-mac",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win", "build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"" "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"",
"format": "prettier -cw ."
}, },
"author": "", "author": "",
"license": "ISC", "license": "MIT",
"dependencies": { "dependencies": {
"compressing": "^1.10.0", "compressing": "^1.10.0",
"cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
@@ -35,7 +38,7 @@
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ", "eslint-plugin-n": "^15.0.0 || ^16.0.0",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "*", "typescript": "*",

View File

@@ -4,19 +4,19 @@ import { version } from '../src/version'
const manifestPath = path.join(__dirname, '../manifest.json') const manifestPath = path.join(__dirname, '../manifest.json')
function readManifest (): any { function readManifest(): any {
if (fs.existsSync(manifestPath)) { if (fs.existsSync(manifestPath)) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
} }
} }
function writeManifest (manifest: any) { function writeManifest(manifest: any) {
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)) fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
} }
const manifest = readManifest() const manifest = readManifest()
if (version !== manifest.version) { if (version !== manifest.version) {
manifest.version = version manifest.version = version
manifest.name = `LLOneBot v${version}` manifest.name = `LLOneBot v${version}`
writeManifest(manifest) writeManifest(manifest)
} }

View File

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

View File

@@ -1,98 +1,100 @@
import fs from "fs"; import fs from 'fs'
import fsPromise from "fs/promises"; import fsPromise from 'fs/promises'
import {Config, OB11Config} from './types'; import { Config, OB11Config } from './types'
import {mergeNewProperties} from "./utils/helper"; import { mergeNewProperties } from './utils/helper'
import path from "node:path"; import path from 'node:path'
import {selfInfo} from "./data"; import { selfInfo } from './data'
import {DATA_DIR} from "./utils"; import { DATA_DIR } from './utils'
export const HOOK_LOG = false; export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false; export const ALLOW_SEND_TEMP_MSG = false
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string; private readonly configPath: string
private config: Config | null = null; private config: Config | null = null
constructor(configPath: string) { constructor(configPath: string) {
this.configPath = configPath; this.configPath = configPath
}
getConfig(cache = true) {
if (this.config && cache) {
return this.config
} }
getConfig(cache = true) { return this.reloadConfig()
if (this.config && cache) { }
return this.config;
}
return this.reloadConfig(); reloadConfig(): Config {
let ob11Default: OB11Config = {
httpPort: 3000,
httpHosts: [],
httpSecret: '',
wsPort: 3001,
wsHosts: [],
enableHttp: true,
enableHttpPost: true,
enableWs: true,
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
}
let defaultConfig: Config = {
ob11: ob11Default,
heartInterval: 60000,
token: '',
enableLocalFile2Url: false,
debug: false,
log: false,
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false,
musicSignUrl: '',
} }
reloadConfig(): Config { if (!fs.existsSync(this.configPath)) {
let ob11Default: OB11Config = { this.config = defaultConfig
httpPort: 3000, return this.config
httpHosts: [], } else {
httpSecret: "", const data = fs.readFileSync(this.configPath, 'utf-8')
wsPort: 3001, let jsonData: Config = defaultConfig
wsHosts: [], try {
enableHttp: true, jsonData = JSON.parse(data)
enableHttpPost: true, } catch (e) {
enableWs: true, this.config = defaultConfig
enableWsReverse: false, return this.config
messagePostFormat: "array", }
} mergeNewProperties(defaultConfig, jsonData)
let defaultConfig: Config = { this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
ob11: ob11Default, this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
heartInterval: 60000, this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
token: "", // console.log("get config", jsonData);
enableLocalFile2Url: false, this.config = jsonData
debug: false, return this.config
log: false,
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false
};
if (!fs.existsSync(this.configPath)) {
this.config = defaultConfig;
return this.config;
} else {
const data = fs.readFileSync(this.configPath, "utf-8");
let jsonData: Config = defaultConfig;
try {
jsonData = JSON.parse(data)
} catch (e) {
this.config = defaultConfig;
return this.config;
}
mergeNewProperties(defaultConfig, jsonData);
this.checkOldConfig(jsonData.ob11, jsonData, "httpPort", "http");
this.checkOldConfig(jsonData.ob11, jsonData, "httpHosts", "hosts");
this.checkOldConfig(jsonData.ob11, jsonData, "wsPort", "wsPort");
// console.log("get config", jsonData);
this.config = jsonData;
return this.config;
}
} }
}
setConfig(config: Config) { setConfig(config: Config) {
this.config = config; this.config = config
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8") fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8')
} }
private checkOldConfig(currentConfig: Config | OB11Config, private checkOldConfig(currentConfig: Config | OB11Config,
oldConfig: Config | OB11Config, oldConfig: Config | OB11Config,
currentKey: string, oldKey: string) { currentKey: string, oldKey: string) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey]; const oldValue = oldConfig[oldKey]
if (oldValue) { if (oldValue) {
currentConfig[currentKey] = oldValue; currentConfig[currentKey] = oldValue
delete oldConfig[oldKey]; delete oldConfig[oldKey]
}
} }
}
} }
export function getConfigUtil() { export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }

View File

@@ -1,104 +1,109 @@
import { import { type Friend, type FriendRequest, type Group, type GroupMember, type SelfInfo } from '../ntqqapi/types'
type Friend, import { type FileCache, type LLOneBotError } from './types'
type FriendRequest, import { NTQQGroupApi } from '../ntqqapi/api/group'
type Group, import { log } from './utils/log'
type GroupMember, import { isNumeric } from './utils/helper'
type SelfInfo import { NTQQFriendApi } from '../ntqqapi/api'
} from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types'
import {NTQQGroupApi} from "../ntqqapi/api/group";
import {log} from "./utils/log";
import {isNumeric} from "./utils/helper";
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
uid: '', uid: '',
uin: '', uin: '',
nick: '', nick: '',
online: true online: true,
} }
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
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: '',
httpServerError: '', httpServerError: '',
wsServerError: '', wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误' otherError: 'LLOnebot未能正常启动请检查日志查看错误',
} }
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid) ? "uin" : "uid" let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid let filterValue = uinOrUid
let friend = friends.find(friend => friend[filterKey] === filterValue.toString()) let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
// if (!friend) { if (!friend) {
// try { try {
// friends = (await NTQQApi.getFriends(true)) const _friends = (await NTQQFriendApi.getFriends(true))
// friend = friends.find(friend => friend[filterKey] === filterValue.toString()) friend = _friends.find(friend => friend[filterKey] === filterValue.toString())
// } catch (e) { if (friend){
// // log("刷新好友列表失败", e.stack.toString()) friends.push(friend)
// } }
// } } catch (e) {
return friend log("刷新好友列表失败", e.stack.toString())
}
}
return friend
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find(group => group.groupCode === qq.toString()) let group = groups.find((group) => group.groupCode === qq.toString())
if (!group) { if (!group) {
try { try {
const _groups = await NTQQGroupApi.getGroups(true); const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find(group => group.groupCode === qq.toString()) group = _groups.find((group) => group.groupCode === qq.toString())
if (group) { if (group) {
groups.push(group) groups.push(group)
} }
} catch (e) { } catch (e) {
}
} }
return group }
return group
}
export function deleteGroup(groupCode: string) {
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString())
// log(groups, groupCode, groupIndex);
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
} }
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) { export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString() groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString() memberUinOrUid = memberUinOrUid.toString()
const group = await getGroup(groupQQ) const group = await getGroup(groupQQ)
if (group) { if (group) {
const filterKey = isNumeric(memberUinOrUid) ? "uin" : "uid" const filterKey = isNumeric(memberUinOrUid) ? 'uin' : 'uid'
const filterValue = memberUinOrUid const filterValue = memberUinOrUid
let filterFunc: (member: GroupMember) => boolean = member => member[filterKey] === filterValue let filterFunc: (member: GroupMember) => boolean = (member) => member[filterKey] === filterValue
let member = group.members?.find(filterFunc) let member = group.members?.find(filterFunc)
if (!member) { if (!member) {
try { try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ) const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) { if (_members.length > 0) {
group.members = _members group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
} }
return member } catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
} }
return null return member
}
return null
} }
export async function refreshGroupMembers(groupQQ: string) { export async function refreshGroupMembers(groupQQ: string) {
const group = groups.find(group => group.groupCode === groupQQ) const group = groups.find((group) => group.groupCode === groupQQ)
if (group) { if (group) {
group.members = await NTQQGroupApi.getGroupMembers(groupQQ) group.members = await NTQQGroupApi.getGroupMembers(groupQQ)
} }
} }
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号 export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) { export function getUidByUin(uin: string) {
for (const uid in uidMaps) { for (const uid in uidMaps) {
if (uidMaps[uid] === uin) { if (uidMaps[uid] === uin) {
return uid return uid
}
} }
}
} }
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号 export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号

View File

@@ -1,277 +1,275 @@
import {Level} from "level"; import { Level } from 'level'
import {type GroupNotify, RawMessage} from "../ntqqapi/types"; import { type GroupNotify, RawMessage } from '../ntqqapi/types'
import {DATA_DIR} from "./utils"; import { DATA_DIR } from './utils'
import {selfInfo} from "./data"; import { selfInfo } from './data'
import {FileCache} from "./types"; import { FileCache } from './types'
import {log} from "./utils/log"; import { log } from './utils/log'
type ReceiveTempUinMap = Record<string, string>; type ReceiveTempUinMap = Record<string, string>
class DBUtil { class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = "msg_id_"; 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_SHORT_ID = 'msg_short_id_'
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_"; public readonly DB_KEY_PREFIX_MSG_SEQ_ID = 'msg_seq_id_'
public readonly DB_KEY_PREFIX_FILE = "file_"; public readonly DB_KEY_PREFIX_FILE = 'file_'
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_"; public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_'
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = "received_temp_uin_map"; private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map'
public db: Level; public db: Level
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number; private currentShortId: number
/* /*
* 数据库结构 * 数据库结构
* 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 * file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */ * */
constructor() { constructor() {
let initCount = 0; let initCount = 0
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const initDB = () => { const initDB = () => {
initCount++; initCount++
// if (initCount > 50) { // if (initCount > 50) {
// return reject("init db fail") // return reject("init db fail")
// } // }
try {
if (!selfInfo.uin) {
setTimeout(initDB, 300);
return
}
const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`;
this.db = new Level(DB_PATH, {valueEncoding: 'json'});
console.log("llonebot init db success")
resolve(null)
} catch (e) {
console.log("init db fail", e.stack.toString())
setTimeout(initDB, 300);
}
}
initDB();
}).then()
const expiredMilliSecond = 1000 * 60 * 60;
setInterval(() => {
// this.cache = {}
// 清理时间较久的缓存
const now = Date.now()
for (let key in this.cache) {
let message: RawMessage = this.cache[key] as RawMessage;
if (message?.msgTime){
if ((now - (parseInt(message.msgTime) * 1000)) > expiredMilliSecond) {
delete this.cache[key]
// log("clear cache", key, message.msgTime);
}
}
}
}, expiredMilliSecond)
}
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try{
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP));
}catch (e) {
}
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap;
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data;
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then();
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
this.cache[longIdKey] = this.cache[shortIdKey] = msg
}
public clearCache() {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId;
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try { try {
const longId = await this.db.get(shortMsgIdKey); if (!selfInfo.uin) {
const msg = await this.getMsgByLongId(longId) setTimeout(initDB, 300)
this.addCache(msg)
return msg
} catch (e) {
log("getMsgByShortId db error", e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId;
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db.get(longIdKey)
const msg = JSON.parse(data)
this.addCache(msg)
return msg
} catch (e) {
// log("getMsgByLongId db error", e.stack.toString())
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId;
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db.get(seqIdKey);
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
} catch (e) {
log("getMsgBySeqId db error", e.stack.toString())
}
}
async addMsg(msg: RawMessage) {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
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) {
// log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId)
this.updateMsg(msg).then()
return existMsg.msgShortId
}
this.addCache(msg);
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;
msg.msgShortId = shortMsgId;
// log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch();
this.db.put(longIdKey, JSON.stringify(msg)).then().catch();
try {
await this.db.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then().catch();
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg;
}
return shortMsgId
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
}
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
existMsg = msg
}
}
Object.assign(existMsg, msg)
this.db.put(longIdKey, JSON.stringify(existMsg)).then().catch();
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg.msgShortId;
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq;
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg;
}
this.db.put(shortIdKey, msg.msgId).then().catch();
try {
await this.db.get(seqIdKey)
} catch (e) {
this.db.put(seqIdKey, msg.msgId).then().catch();
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
}
private async genMsgShortId(): Promise<number> {
const key = "msg_current_short_id";
if (this.currentShortId === undefined) {
try {
let id: string = await this.db.get(key);
this.currentShortId = parseInt(id);
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++;
this.db.put(key, this.currentShortId.toString()).then().catch();
return this.currentShortId;
}
async addFileCache(fileNameOrUuid: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid;
if (this.cache[key]) {
return return
} }
let cacheDBData = {...data} const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`
delete cacheDBData['downloadFunc'] this.db = new Level(DB_PATH, { valueEncoding: 'json' })
this.cache[fileNameOrUuid] = data; console.log('llonebot init db success')
try { resolve(null)
await this.db.put(key, JSON.stringify(cacheDBData));
} catch (e) { } catch (e) {
log("addFileCache db error", e.stack.toString()) console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300)
} }
}
initDB()
}).then()
const expiredMilliSecond = 1000 * 60 * 60
setInterval(() => {
// this.cache = {}
// 清理时间较久的缓存
const now = Date.now()
for (let key in this.cache) {
let message: RawMessage = this.cache[key] as RawMessage
if (message?.msgTime) {
if (now - parseInt(message.msgTime) * 1000 > expiredMilliSecond) {
delete this.cache[key]
// log("clear cache", key, message.msgTime);
}
}
}
}, expiredMilliSecond)
}
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP))
} catch (e) {}
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
this.cache[longIdKey] = this.cache[shortIdKey] = msg
}
public clearCache() {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
} catch (e) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db.get(longIdKey)
const msg = JSON.parse(data)
this.addCache(msg)
return msg
} catch (e) {
// log("getMsgByLongId db error", e.stack.toString())
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db.get(seqIdKey)
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
} catch (e) {
log('getMsgBySeqId db error', e.stack.toString())
}
}
async addMsg(msg: RawMessage) {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
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) {
// log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId)
this.updateMsg(msg).then()
return existMsg.msgShortId
}
this.addCache(msg)
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
msg.msgShortId = shortMsgId
// log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch()
this.db.put(longIdKey, JSON.stringify(msg)).then().catch()
try {
await this.db.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then().catch()
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg
}
return shortMsgId
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
}
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
existMsg = msg
}
} }
async getFileCache(fileNameOrUuid: string): Promise<FileCache | undefined> { Object.assign(existMsg, msg)
const key = this.DB_KEY_PREFIX_FILE + (fileNameOrUuid); this.db.put(longIdKey, JSON.stringify(existMsg)).then().catch()
if (this.cache[key]) { const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg.msgShortId
return this.cache[key] as FileCache const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
} if (!this.cache[seqIdKey]) {
try { this.cache[seqIdKey] = existMsg
let data = await this.db.get(key); }
return JSON.parse(data); this.db.put(shortIdKey, msg.msgId).then().catch()
} catch (e) { try {
// log("getFileCache db error", e.stack.toString()) await this.db.get(seqIdKey)
} } catch (e) {
this.db.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
}
private async genMsgShortId(): Promise<number> {
const key = 'msg_current_short_id'
if (this.currentShortId === undefined) {
try {
let id: string = await this.db.get(key)
this.currentShortId = parseInt(id)
} catch (e) {
this.currentShortId = -2147483640
}
} }
async addGroupNotify(notify: GroupNotify) { this.currentShortId++
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq; this.db.put(key, this.currentShortId.toString()).then().catch()
let existNotify = this.cache[key] as GroupNotify return this.currentShortId
if (existNotify) { }
return
}
this.cache[key] = notify;
this.db.put(key, JSON.stringify(notify)).then().catch();
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> { async addFileCache(fileNameOrUuid: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq; const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) { if (this.cache[key]) {
return this.cache[key] as GroupNotify return
}
try {
let data = await this.db.get(key);
return JSON.parse(data);
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}
} }
let cacheDBData = { ...data }
delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data
try {
await this.db.put(key, JSON.stringify(cacheDBData))
} catch (e) {
log('addFileCache db error', e.stack.toString())
}
}
async getFileCache(fileNameOrUuid: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return this.cache[key] as FileCache
}
try {
let data = await this.db.get(key)
return JSON.parse(data)
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
}
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().catch()
}
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) {
// log("getGroupNotify db error", e.stack.toString())
}
}
} }
export const dbUtil = new DBUtil(); export const dbUtil = new DBUtil()

View File

@@ -1,114 +1,119 @@
import express, {Express, Request, Response} from "express"; import express, { Express, Request, Response } from 'express'
import http from "http"; import http from 'http'
import {log} from "../utils/log"; import cors from 'cors'
import {getConfigUtil} from "../config"; import { log } from '../utils/log'
import {llonebotError} from "../data"; import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
type RegisterHandler = (res: Response, payload: any) => Promise<any> type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase { export abstract class HttpServerBase {
name: string = "LLOneBot"; name: string = 'LLOneBot'
private readonly expressAPP: Express; private readonly expressAPP: Express
private server: http.Server = null; private server: http.Server = null
constructor() { constructor() {
this.expressAPP = express(); this.expressAPP = express()
this.expressAPP.use(express.urlencoded({extended: true, limit: "5000mb"})); // 添加 CORS 中间件
this.expressAPP.use((req, res, next) => { this.expressAPP.use(cors())
// 兼容处理没有带content-type的请求 this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' }))
// log("req.headers['content-type']", req.headers['content-type']) this.expressAPP.use((req, res, next) => {
req.headers['content-type'] = 'application/json'; // 兼容处理没有带content-type的请求
const originalJson = express.json({limit: "5000mb"}); // log("req.headers['content-type']", req.headers['content-type'])
// 调用原始的express.json()处理器 req.headers['content-type'] = 'application/json'
originalJson(req, res, (err) => { const originalJson = express.json({ limit: '5000mb' })
if (err) { // 调用原始的express.json()处理器
log("Error parsing JSON:", err); originalJson(req, res, (err) => {
return res.status(400).send("Invalid JSON"); if (err) {
} log('Error parsing JSON:', err)
next(); return res.status(400).send('Invalid JSON')
}); }
}); next()
})
})
}
authorize(req: Request, res: Response, next: () => void) {
let serverToken = getConfigUtil().getConfig().token
let clientToken = ''
const authHeader = req.get('authorization')
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
log('receive http header token', clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
clientToken = req.query.access_token[0].toString()
} else {
clientToken = req.query.access_token.toString()
}
log('receive http url token', clientToken)
} }
authorize(req: Request, res: Response, next: () => void) { if (serverToken && clientToken != serverToken) {
let serverToken = getConfigUtil().getConfig().token; return res.status(403).send(JSON.stringify({ message: 'token verify failed!' }))
let clientToken = "" }
const authHeader = req.get("authorization") next()
if (authHeader) { }
clientToken = authHeader.split("Bearer ").pop()
log("receive http header token", clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
clientToken = req.query.access_token[0].toString();
} else {
clientToken = req.query.access_token.toString();
}
log("receive http url token", clientToken)
}
if (serverToken && clientToken != serverToken) { start(port: number) {
return res.status(403).send(JSON.stringify({message: 'token verify failed!'})); try {
} this.expressAPP.get('/', (req: Request, res: Response) => {
next(); res.send(`${this.name}已启动`)
}; })
this.listen(port)
llonebotError.httpServerError = ''
} catch (e) {
log('HTTP服务启动失败', e.toString())
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString()
}
}
start(port: number) { stop() {
try { llonebotError.httpServerError = ''
this.expressAPP.get('/', (req: Request, res: Response) => { if (this.server) {
res.send(`${this.name}已启动`); this.server.close()
}) this.server = null
this.listen(port); }
llonebotError.httpServerError = "" }
} catch (e) {
log("HTTP服务启动失败", e.toString()) restart(port: number) {
llonebotError.httpServerError = "HTTP服务启动失败, " + e.toString() this.stop()
} this.start(port)
}
abstract handleFailed(res: Response, payload: any, err: any): void
registerRouter(method: 'post' | 'get' | string, url: string, handler: RegisterHandler) {
if (!url.startsWith('/')) {
url = '/' + url
} }
stop() { if (!this.expressAPP[method]) {
llonebotError.httpServerError = "" const err = `${this.name} register router failed${method} not exist`
if (this.server) { log(err)
this.server.close() throw err
this.server = null;
}
} }
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body
if (method == 'get') {
payload = req.query
} else if (req.query) {
payload = { ...req.query, ...req.body }
}
log('收到http请求', url, payload)
try {
res.send(await handler(res, payload))
} catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
})
}
restart(port: number) { protected listen(port: number) {
this.stop() this.server = this.expressAPP.listen(port, '0.0.0.0', () => {
this.start(port) const info = `${this.name} started 0.0.0.0:${port}`
} console.log(info)
log(info)
abstract handleFailed(res: Response, payload: any, err: any): void })
}
registerRouter(method: "post" | "get" | string, url: string, handler: RegisterHandler) { }
if (!url.startsWith("/")) {
url = "/" + url
}
if (!this.expressAPP[method]) {
const err = `${this.name} register router failed${method} not exist`;
log(err);
throw err;
}
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body;
if (method == "get") {
payload = req.query
}
log("收到http请求", url, payload);
try {
res.send(await handler(res, payload))
} catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
});
}
protected listen(port: number) {
this.server = this.expressAPP.listen(port, "0.0.0.0", () => {
const info = `${this.name} started 0.0.0.0:${port}`
console.log(info);
log(info);
});
}
}

View File

@@ -1,106 +1,93 @@
import {WebSocket, WebSocketServer} from "ws"; import { WebSocket, WebSocketServer } from 'ws'
import urlParse from "url"; import urlParse from 'url'
import {IncomingMessage} from "node:http"; import { IncomingMessage } from 'node:http'
import {log} from "../utils/log"; import { log } from '../utils/log'
import {getConfigUtil} from "../config"; import { getConfigUtil } from '../config'
import {llonebotError} from "../data"; import { llonebotError } from '../data'
class WebsocketClientBase { class WebsocketClientBase {
private wsClient: WebSocket private wsClient: WebSocket
constructor() { constructor() {}
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg)
} }
}
send(msg: string) { onMessage(msg: string) {}
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg);
}
}
onMessage(msg: string) {
}
} }
export class WebsocketServerBase { export class WebsocketServerBase {
private ws: WebSocketServer = null; private ws: WebSocketServer = null
constructor() { constructor() {
console.log(`llonebot websocket service started`) console.log(`llonebot websocket service started`)
}
start(port: number) {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e) {
llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString()
} }
this.ws.on('connection', (wsClient, req) => {
const url = req.url.split('?').shift()
this.authorize(wsClient, req)
this.onConnect(wsClient, url, req)
wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url, msg.toString())
})
})
}
start(port: number) { stop() {
try { llonebotError.wsServerError = ''
this.ws = new WebSocketServer({port, this.ws.close((err) => {
maxPayload: 1024 * 1024 * 1024 log('ws server close failed!', err)
}); })
llonebotError.wsServerError = '' this.ws = null
}catch (e) { }
llonebotError.wsServerError = "正向ws服务启动失败, " + e.toString()
}
this.ws.on("connection", (wsClient, req) => {
const url = req.url.split("?").shift()
this.authorize(wsClient, req);
this.onConnect(wsClient, url, req);
wsClient.on("message", async (msg) => {
this.onMessage(wsClient, url, msg.toString())
})
})
}
stop() { restart(port: number) {
llonebotError.wsServerError = '' this.stop()
this.ws.close((err) => { this.start(port)
log("ws server close failed!", err) }
});
this.ws = null;
}
restart(port: number) { authorize(wsClient: WebSocket, req) {
this.stop(); let token = getConfigUtil().getConfig().token
this.start(port); const url = req.url.split('?').shift()
} log('ws connect', url)
let clientToken: string = ''
authorize(wsClient: WebSocket, req) { const authHeader = req.headers['authorization']
let token = getConfigUtil().getConfig().token; if (authHeader) {
const url = req.url.split("?").shift(); clientToken = authHeader.split('Bearer ').pop()
log("ws connect", url) log('receive ws header token', clientToken)
let clientToken: string = "" } else {
const authHeader = req.headers['authorization']; const parsedUrl = urlParse.parse(req.url, true)
if (authHeader) { const urlToken = parsedUrl.query.access_token
clientToken = authHeader.split("Bearer ").pop() if (urlToken) {
log("receive ws header token", clientToken); if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else { } else {
const parsedUrl = urlParse.parse(req.url, true); clientToken = urlToken
const urlToken = parsedUrl.query.access_token;
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log("receive ws url token", clientToken);
}
}
if (token && clientToken != token) {
this.authorizeFailed(wsClient)
return wsClient.close()
} }
log('receive ws url token', clientToken)
}
} }
if (token && clientToken != token) {
authorizeFailed(wsClient: WebSocket) { this.authorizeFailed(wsClient)
return wsClient.close()
} }
}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { authorizeFailed(wsClient: WebSocket) {}
} onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {}
onMessage(wsClient: WebSocket, url: string, msg: string) { onMessage(wsClient: WebSocket, url: string, msg: string) {}
} sendHeart() {}
}
sendHeart() {
}
}

View File

@@ -9,13 +9,13 @@ export interface OB11Config {
enableWs?: boolean enableWs?: boolean
enableWsReverse?: boolean enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string' messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
} }
export interface CheckVersion { export interface CheckVersion {
result: boolean, result: boolean,
version: string version: string
} }
export interface Config { export interface Config {
imageRKey?: string;
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval?: number // ms
@@ -27,6 +27,8 @@ export interface Config {
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
enablePoke?: boolean enablePoke?: boolean
musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean
} }
export interface LLOneBotError { export interface LLOneBotError {

View File

@@ -1,137 +1,130 @@
import fs from "fs"; import fs from 'fs'
import {encode, getDuration, getWavFileInfo, isWav} from "silk-wasm"; import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'
import fsPromise from "fs/promises"; import fsPromise from 'fs/promises'
import {log} from "./log"; import { log } from './log'
import path from "node:path"; import path from 'node:path'
import {DATA_DIR, TEMP_DIR} from "./index"; import { DATA_DIR, TEMP_DIR } from './index'
import {v4 as uuidv4} from "uuid"; import { v4 as uuidv4 } from 'uuid'
import {getConfigUtil} from "../config"; import { getConfigUtil } from '../config'
import ffmpeg from "fluent-ffmpeg"; import { spawn } from 'node:child_process'
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) { function getFileHeader(filePath: string) {
// 定义要读取的字节数 // 定义要读取的字节数
const bytesToRead = 7; const bytesToRead = 7
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: "r",
});
const fileHeader = buffer.toString("hex", 0, bytesToRead);
return fileHeader;
} catch (err) {
console.error("读取文件错误:", err);
return;
}
}
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath));
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try { try {
const pttPath = path.join(TEMP_DIR, uuidv4()); const buffer = fs.readFileSync(filePath, {
if (getFileHeader(filePath) !== "02232153494c4b") { encoding: null,
log(`语音文件${filePath}需要转换成silk`) flag: 'r',
const _isWav = await isWavFile(filePath); })
const wavPath = pttPath + ".wav"
const convert = async () => {
return await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav")
.audioChannels(1)
.audioFrequency(24000)
.on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", () => {
filePath = wavPath
resolve(wavPath);
});
})
}
let wav: Buffer
if (!_isWav) {
log(`语音文件${filePath}正在转换成wav`)
await convert()
} else {
wav = fs.readFileSync(filePath)
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const {fmt} = getWavFileInfo(wav)
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
wav = undefined
await convert()
}
}
wav ||= fs.readFileSync(filePath);
const silk = await encode(wav, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => {
});
// const gDuration = await guessDuration(pttPath)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000
};
} else {
const silk = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(silk) / 1000
} catch (e) {
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
duration = await guessDuration(filePath);
}
return { const fileHeader = buffer.toString('hex', 0, bytesToRead)
converted: false, return fileHeader
path: filePath, } catch (err) {
duration: duration, console.error('读取文件错误:', err)
}; return
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
} }
} }
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath))
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try {
const pttPath = path.join(TEMP_DIR, uuidv4())
if (getFileHeader(filePath) !== '02232153494c4b') {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath)
const pcmPath = pttPath + '.pcm'
let sampleRate = 0
const convert = () => {
return new Promise<Buffer>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
reject(Error(`FFmpeg处理转换失败`))
})
})
}
let input: Buffer
if (!_isWav) {
input = await convert()
} else {
input = fs.readFileSync(filePath)
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
input = await convert()
}
}
const silk = await encode(input, sampleRate)
fs.writeFileSync(pttPath, silk.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
}
} else {
const silk = fs.readFileSync(filePath)
let duration = 0
try {
duration = getDuration(silk) / 1000
} catch (e) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack)
duration = await guessDuration(filePath)
}
return {
converted: false,
path: filePath,
duration,
}
}
} catch (error) {
log('convert silk failed', error.stack)
return {}
}
}

View File

@@ -1,258 +1,255 @@
import fs from "fs"; import fs from 'fs'
import fsPromise from "fs/promises"; import fsPromise from 'fs/promises'
import crypto from "crypto"; import crypto from 'crypto'
import util from "util"; import util from 'util'
import path from "node:path"; import path from 'node:path'
import {v4 as uuidv4} from "uuid"; import { v4 as uuidv4 } from 'uuid'
import {log, TEMP_DIR} from "./index"; import { log, TEMP_DIR } from './index'
import {dbUtil} from "../db"; import { dbUtil } from '../db'
import * as fileType from "file-type"; import * as fileType from 'file-type'
import {net} from "electron"; import { net } from 'electron'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4); const buffer = Buffer.alloc(4)
const fd = fs.openSync(path, 'r'); const fd = fs.openSync(path, 'r')
fs.readSync(fd, buffer, 0, 4, 0); fs.readSync(fd, buffer, 0, 4, 0)
fs.closeSync(fd); fs.closeSync(fd)
return buffer.toString() === 'GIF8' return buffer.toString() === 'GIF8'
} }
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const startTime = Date.now(); const startTime = Date.now()
function check() { function check() {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
resolve(); resolve()
} else if (Date.now() - startTime > timeout) { } else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`)); reject(new Error(`文件不存在: ${path}`))
} else { } else {
setTimeout(check, 100); setTimeout(check, 100)
} }
} }
check(); check()
}); })
} }
export async function file2base64(path: string) { export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile)
let result = { let result = {
err: "", err: '',
data: "" data: '',
} }
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try { try {
// 读取文件内容 await checkFileReceived(path, 5000)
// if (!fs.existsSync(path)){ } catch (e: any) {
// path = path.replace("\\Ori\\", "\\Thumb\\"); result.err = e.toString()
// } return result
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
} }
return result; const data = await readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err) {
result.err = err.toString()
}
return result
} }
export function calculateFileMD5(filePath: string): Promise<string> { export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 创建一个流式读取器 // 创建一个流式读取器
const stream = fs.createReadStream(filePath); const stream = fs.createReadStream(filePath)
const hash = crypto.createHash('md5'); const hash = crypto.createHash('md5')
stream.on('data', (data: Buffer) => { stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态 // 当读取到数据时,更新哈希对象的状态
hash.update(data); hash.update(data)
}); })
stream.on('end', () => { stream.on('end', () => {
// 文件读取完成,计算哈希 // 文件读取完成,计算哈希
const md5 = hash.digest('hex'); const md5 = hash.digest('hex')
resolve(md5); resolve(md5)
}); })
stream.on('error', (err: Error) => { stream.on('error', (err: Error) => {
// 处理可能的读取错误 // 处理可能的读取错误
reject(err); reject(err)
}); })
}); })
} }
export interface HttpDownloadOptions { export interface HttpDownloadOptions {
url: string; url: string
headers?: Record<string, string> | string; headers?: Record<string, string> | string
} }
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> { export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let chunks: Buffer[] = []; let chunks: Buffer[] = []
let url: string; let url: string
let headers: Record<string, string> = { let headers: Record<string, string> = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36" 'User-Agent':
}; 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
if (typeof options === "string") { }
url = options; if (typeof options === 'string') {
} else { url = options
url = options.url; } else {
if (options.headers) { url = options.url
if (typeof options.headers === "string") { if (options.headers) {
headers = JSON.parse(options.headers); if (typeof options.headers === 'string') {
} else { headers = JSON.parse(options.headers)
headers = options.headers; } else {
} headers = options.headers
} }
} }
const fetchRes = await net.fetch(url, headers); }
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`) const fetchRes = await net.fetch(url, headers)
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob(); const blob = await fetchRes.blob()
let buffer = await blob.arrayBuffer(); let buffer = await blob.arrayBuffer()
return Buffer.from(buffer); return Buffer.from(buffer)
} }
type Uri2LocalRes = { type Uri2LocalRes = {
success: boolean, success: boolean
errMsg: string, errMsg: string
fileName: string, fileName: string
ext: string, ext: string
path: string, path: string
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> { export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> {
let res = { let res = {
success: false, success: false,
errMsg: "", errMsg: '',
fileName: "", fileName: '',
ext: "", ext: '',
path: "", path: '',
isLocal: false isLocal: false,
} }
if (!fileName) { if (!fileName) {
fileName = uuidv4(); fileName = uuidv4()
} }
let filePath = path.join(TEMP_DIR, fileName) let filePath = path.join(TEMP_DIR, fileName)
let url = null; let url = null
try { try {
url = new URL(uri); url = new URL(uri)
} catch (e) { } catch (e) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == "base64:") {
// base64转成文件
let base64Data = uri.split("base64://")[1]
try {
const buffer = Buffer.from(base64Data, 'base64');
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件
let buffer: Buffer = null;
try {
buffer = await httpDownload(uri);
} catch (e) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName)
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string;
if (url.protocol === "file:") {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32") {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri);
if (cache) {
filePath = cache.filePath
} else {
filePath = uri;
}
}
res.isLocal = true
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
if (ext) {
log("获取文件类型", ext, filePath)
fs.renameSync(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
}
res.success = true
res.path = filePath
return res return res
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == 'base64:') {
// base64转成文件
let base64Data = uri.split('base64://')[1]
try {
const buffer = Buffer.from(base64Data, 'base64')
fs.writeFileSync(filePath, buffer)
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer = null
try {
buffer = await httpDownload(uri)
} catch (e) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName)
fs.writeFileSync(filePath, buffer)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string
if (url.protocol === 'file:') {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === 'win32') {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri)
if (cache) {
filePath = cache.filePath
} else {
filePath = uri
}
}
res.isLocal = true
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
if (ext) {
log('获取文件类型', ext, filePath)
fs.renameSync(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
}
res.success = true
res.path = filePath
return res
} }
export async function copyFolder(sourcePath: string, destPath: string) { export async function copyFolder(sourcePath: string, destPath: string) {
try { try {
const entries = await fsPromise.readdir(sourcePath, {withFileTypes: true}); const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true })
await fsPromise.mkdir(destPath, {recursive: true}); await fsPromise.mkdir(destPath, { recursive: true })
for (let entry of entries) { for (let entry of entries) {
const srcPath = path.join(sourcePath, entry.name); const srcPath = path.join(sourcePath, entry.name)
const dstPath = path.join(destPath, entry.name); const dstPath = path.join(destPath, entry.name)
if (entry.isDirectory()) { if (entry.isDirectory()) {
await copyFolder(srcPath, dstPath); await copyFolder(srcPath, dstPath)
} else { } else {
try { try {
await fsPromise.copyFile(srcPath, dstPath); await fsPromise.copyFile(srcPath, dstPath)
} catch (error) { } catch (error) {
console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`); console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`)
// 这里可以决定是否要继续复制其他文件 // 这里可以决定是否要继续复制其他文件
}
}
} }
} catch (error) { }
console.error('复制文件夹时出错:', error);
} }
} } catch (error) {
console.error('复制文件夹时出错:', error)
}
}

View File

@@ -1,48 +1,48 @@
export function truncateString(obj: any, maxLength = 500) { export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') { if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => { Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') { if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断 // 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) { if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'; obj[key] = obj[key].substring(0, maxLength) + '...'
} }
} else if (typeof obj[key] === 'object') { } else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用 // 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength); truncateString(obj[key], maxLength)
} }
}); })
} }
return obj; return obj
} }
export function isNumeric(str: string) { export function isNumeric(str: string) {
return /^\d+$/.test(str); return /^\d+$/.test(str)
} }
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms))
} }
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 // 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) { export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => { Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制 // 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) { if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]; oldObj[key] = newObj[key]
} else { } else {
// 如果老对象和新对象的当前属性都是对象,则递归合并 // 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]); mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') { } else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖 // 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]; oldObj[key] = newObj[key]
} }
} }
}); })
} }
export function isNull(value: any) { export function isNull(value: any) {
return value === undefined || value === null; return value === undefined || value === null
} }
/** /**
@@ -52,17 +52,16 @@ export function isNull(value: any) {
* @returns 处理后的字符串,超过长度的地方将会换行 * @returns 处理后的字符串,超过长度的地方将会换行
*/ */
export function wrapText(str: string, maxLength: number): string { export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果 // 初始化一个空字符串用于存放结果
let result: string = ''; let result: string = ''
// 循环遍历字符串每次步进maxLength个字符 // 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) { for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串 // 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符 // 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n'; if (i > 0) result += '\n'
result += str.substring(i, i + maxLength); result += str.substring(i, i + maxLength)
} }
return result; return result
} }

View File

@@ -1,5 +1,5 @@
import path from "node:path"; import path from 'node:path'
import fs from "fs"; import fs from 'fs'
export * from './file' export * from './file'
export * from './helper' export * from './helper'
@@ -7,12 +7,12 @@ export * from './log'
export * from './qqlevel' export * from './qqlevel'
export * from './qqpkg' export * from './qqpkg'
export * from './upgrade' export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, "temp"); export const TEMP_DIR = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin; export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) { if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, {recursive: true}); fs.mkdirSync(TEMP_DIR, { recursive: true })
} }
export {getVideoInfo} from "./video"; export { getVideoInfo } from './video'
export {checkFfmpeg} from "./video"; export { checkFfmpeg } from './video'
export {encodeSilk} from "./audio"; export { encodeSilk } from './audio'

View File

@@ -1,37 +1,35 @@
import {selfInfo} from "../data"; import { selfInfo } from '../data'
import fs from "fs"; import fs from 'fs'
import path from "node:path"; import path from 'node:path'
import {DATA_DIR, truncateString} from "./index"; import { DATA_DIR, truncateString } from './index'
import {getConfigUtil} from "../config"; import { getConfigUtil } from '../config'
const date = new Date(); const date = new Date()
const logFileName = `llonebot-${date.toLocaleString("zh-CN")}.log`.replace(/\//g, "-").replace(/:/g, "-"); const logFileName = `llonebot-${date.toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
const logDir = path.join(DATA_DIR, "logs"); const logDir = path.join(DATA_DIR, 'logs')
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, {recursive: true}); fs.mkdirSync(logDir, { recursive: true })
} }
export function log(...msg: any[]) { export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) { if (!getConfigUtil().getConfig().log) {
return //console.log(...msg); return //console.log(...msg);
} }
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : "" const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ''
let logMsg = ""; let logMsg = ''
for (let msgItem of msg) { for (let msgItem of msg) {
// 判断是否是对象 // 判断是否是对象
if (typeof msgItem === "object") { if (typeof msgItem === 'object') {
let obj = JSON.parse(JSON.stringify(msgItem)); let obj = JSON.parse(JSON.stringify(msgItem))
logMsg += JSON.stringify(truncateString(obj)) + " "; logMsg += JSON.stringify(truncateString(obj)) + ' '
continue; continue
}
logMsg += msgItem + " ";
} }
let currentDateTime = new Date().toLocaleString(); logMsg += msgItem + ' '
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` }
// sendLog(...msg); let currentDateTime = new Date().toLocaleString()
// console.log(msg) logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => { // sendLog(...msg);
// console.log(msg)
}) fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => {})
} }

View File

@@ -1,7 +1,7 @@
// QQ等级换算 // QQ等级换算
import {QQLevel} from "../../ntqqapi/types"; import { QQLevel } from '../../ntqqapi/types'
export function calcQQLevel(level: QQLevel) { export function calcQQLevel(level: QQLevel) {
const {crownNum, sunNum, moonNum, starNum} = level const { crownNum, sunNum, moonNum, starNum } = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
} }

View File

@@ -1,12 +1,12 @@
import path from "path"; import path from 'path'
type QQPkgInfo = { type QQPkgInfo = {
version: string; version: string
buildVersion: string; buildVersion: string
platform: string; platform: string
eleArch: string; eleArch: string
} }
export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json")) export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, 'app/package.json'))
export const isQQ998: boolean = qqPkgInfo.buildVersion >= "22106" export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

37
src/common/utils/sign.ts Normal file
View File

@@ -0,0 +1,37 @@
import { log } from './log'
export interface IdMusicSignPostData {
type: 'qq' | '163'
id: string | number
}
export interface CustomMusicSignPostData {
type: 'custom'
url: string
audio: string
title: string
image?: string
singer?: string
}
export type MusicSignPostData = IdMusicSignPostData | CustomMusicSignPostData
export class MusicSign {
private readonly url: string
constructor(url: string) {
this.url = url
}
async sign(postData: MusicSignPostData): Promise<string> {
const resp = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
})
if (!resp.ok) throw new Error(resp.statusText)
const data = await resp.text()
log('音乐消息生成成功', data)
return data
}
}

View File

@@ -1,98 +1,97 @@
import { version } from "../../version"; import { version } from '../../version'
import * as path from "node:path"; import * as path from 'node:path'
import * as fs from "node:fs"; import * as fs from 'node:fs'
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from "."; import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.'
import compressing from "compressing"; import compressing from 'compressing'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/']
const downloadMirrorHosts = ["https://mirror.ghproxy.com/"]; const checkVersionMirrorHosts = ['https://521github.com']
const checkVersionMirrorHosts = ["https://521github.com"];
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion(); const latestVersionText = await getRemoteVersion()
const latestVersion = latestVersionText.split("."); const latestVersion = latestVersionText.split('.')
log("llonebot last version", latestVersion); log('llonebot last version', latestVersion)
const currentVersion: string[] = version.split("."); const currentVersion: string[] = version.split('.')
log("llonebot current version", currentVersion); log('llonebot current version', currentVersion)
for (let k of [0, 1, 2]) { for (let k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) { if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) {
log("") log('')
return { result: true, version: latestVersionText }; return { result: true, version: latestVersionText }
} } else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) {
else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) { break
break;
}
} }
return { result: false, version: version }; }
return { result: false, version: version }
} }
export async function upgradeLLOneBot() { export async function upgradeLLOneBot() {
const latestVersion = await getRemoteVersion(); const latestVersion = await getRemoteVersion()
if (latestVersion && latestVersion != "") { if (latestVersion && latestVersion != '') {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip"; const downloadUrl = 'https://github.com/LLOneBot/LLOneBot/releases/download/v' + latestVersion + '/LLOneBot.zip'
const filePath = path.join(TEMP_DIR, "./update-" + latestVersion + ".zip"); const filePath = path.join(TEMP_DIR, './update-' + latestVersion + '.zip')
let downloadSuccess = false; let downloadSuccess = false
// 多镜像下载 // 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) { for (const mirrorGithub of downloadMirrorHosts) {
try { try {
const buffer = await httpDownload(mirrorGithub + downloadUrl); const buffer = await httpDownload(mirrorGithub + downloadUrl)
fs.writeFileSync(filePath, buffer) fs.writeFileSync(filePath, buffer)
downloadSuccess = true; downloadSuccess = true
break; break
} catch (e) { } catch (e) {
log("llonebot upgrade error", e); log('llonebot upgrade error', e)
} }
}
if (!downloadSuccess) {
log("llonebot upgrade error", "download failed");
return false;
}
const temp_ver_dir = path.join(TEMP_DIR, "LLOneBot" + latestVersion);
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
compressing.zip.uncompress(filePath, temp_ver_dir).then(() => {
resolve(true);
}).catch((reason: any) => {
log("llonebot upgrade failed, ", reason);
if (reason?.errno == -4082) {
resolve(true);
}
resolve(false);
});
});
}
const uncompressedResult = await uncompressedPromise();
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR);
return uncompressedResult;
} }
return false; if (!downloadSuccess) {
log('llonebot upgrade error', 'download failed')
return false
}
const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion)
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
compressing.zip
.uncompress(filePath, temp_ver_dir)
.then(() => {
resolve(true)
})
.catch((reason: any) => {
log('llonebot upgrade failed, ', reason)
if (reason?.errno == -4082) {
resolve(true)
}
resolve(false)
})
})
}
const uncompressedResult = await uncompressedPromise()
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR)
return uncompressedResult
}
return false
} }
export async function getRemoteVersion() { export async function getRemoteVersion() {
let Version = ""; let Version = ''
for (let i = 0; i < checkVersionMirrorHosts.length; i++) { for (let i = 0; i < checkVersionMirrorHosts.length; i++) {
let mirrorGithub = checkVersionMirrorHosts[i]; let mirrorGithub = checkVersionMirrorHosts[i]
let tVersion = await getRemoteVersionByMirror(mirrorGithub); let tVersion = await getRemoteVersionByMirror(mirrorGithub)
if (tVersion && tVersion != "") { if (tVersion && tVersion != '') {
Version = tVersion; Version = tVersion
break; break
}
} }
return Version; }
return Version
} }
export async function getRemoteVersionByMirror(mirrorGithub: string) { export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = "error"; let releasePage = 'error'
try { try {
releasePage = (await httpDownload(mirrorGithub + "/LLOneBot/LLOneBot/releases")).toString(); releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
// log("releasePage", releasePage); // log("releasePage", releasePage);
if (releasePage === "error") return ""; if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]; return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]
} catch { } catch {}
} return ''
return ""; }
}

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,12 @@
import {webContents} from 'electron'; import { webContents } from 'electron'
function sendIPCMsg(channel: string, ...data: any) { function sendIPCMsg(channel: string, ...data: any) {
let contents = webContents.getAllWebContents(); let contents = webContents.getAllWebContents()
for (const content of contents) { for (const content of contents) {
try { try {
content.send(channel, ...data) content.send(channel, ...data)
} catch (e) { } catch (e) {
console.log("llonebot send ipc msg to render error:", e) console.log('llonebot send ipc msg to render error:', e)
}
} }
}
} }

View File

@@ -1,478 +1,506 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import {BrowserWindow, dialog, ipcMain} from 'electron'; import { BrowserWindow, dialog, ipcMain } from 'electron'
import * as fs from 'node:fs'; import * as fs from 'node:fs'
import {Config} from "../common/types"; import { Config } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
CHANNEL_ERROR, CHANNEL_ERROR,
CHANNEL_GET_CONFIG, CHANNEL_GET_CONFIG,
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_SELECT_FILE, CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
CHANNEL_UPDATE, CHANNEL_UPDATE,
} from "../common/channels"; } from '../common/channels'
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import {DATA_DIR} from "../common/utils"; import { DATA_DIR } from '../common/utils'
import { import {
friendRequests, friendRequests,
getFriend, getFriend,
getGroup, getGroup,
getGroupMember, groups, getGroupMember,
llonebotError, groups,
refreshGroupMembers, llonebotError,
selfInfo, refreshGroupMembers,
uidMaps selfInfo,
} from "../common/data"; uidMaps,
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook"; } from '../common/data'
import {OB11Constructor} from "../onebot11/constructor"; import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import { import {
ChatType, ChatType,
FriendRequestNotify, FriendRequestNotify,
GroupMemberRole, GroupMemberRole,
GroupNotifies, GroupNotifies,
GroupNotifyTypes, GroupNotifyTypes,
RawMessage RawMessage,
} from "../ntqqapi/types"; } from '../ntqqapi/types'
import {ob11HTTPServer} from "../onebot11/server/http"; import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; import { OB11FriendRecallNoticeEvent } from '../onebot11/event/notice/OB11FriendRecallNoticeEvent'
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; import { OB11GroupRecallNoticeEvent } from '../onebot11/event/notice/OB11GroupRecallNoticeEvent'
import {postOB11Event} from "../onebot11/server/postOB11Event"; import { postOB11Event } from '../onebot11/server/postOB11Event'
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent"; import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest"; import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest"; import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import * as path from "node:path"; import * as path from 'node:path'
import {dbUtil} from "../common/db"; import { dbUtil } from '../common/db'
import {setConfig} from "./setConfig"; import { setConfig } from './setConfig'
import {NTQQUserApi} from "../ntqqapi/api/user"; import { NTQQUserApi } from '../ntqqapi/api/user'
import {NTQQGroupApi} from "../ntqqapi/api/group"; import { NTQQGroupApi } from '../ntqqapi/api/group'
import {registerPokeHandler} from "../ntqqapi/external/ccpoke"; import { crychic } from '../ntqqapi/external/crychic'
import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; import { OB11FriendPokeEvent, OB11GroupPokeEvent } from '../onebot11/event/notice/OB11PokeEvent'
import {checkNewVersion, upgradeLLOneBot} from "../common/utils/upgrade"; import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import {log} from "../common/utils/log"; import { log } from '../common/utils/log'
import {getConfigUtil} from "../common/config"; import { getConfigUtil } from '../common/config'
import {checkFfmpeg} from "../common/utils/video"; import { checkFfmpeg } from '../common/utils/video'
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
let running = false; let running = false
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log("llonebot main onLoad"); log('llonebot main onLoad')
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion(); return checkNewVersion()
}); })
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => { ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => {
return upgradeLLOneBot(); return upgradeLLOneBot()
}); })
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => { ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
const selectPath = new Promise<string>((resolve, reject) => { const selectPath = new Promise<string>((resolve, reject) => {
dialog dialog
.showOpenDialog({ .showOpenDialog({
title: "请选择ffmpeg", title: '请选择ffmpeg',
properties: ["openFile"], properties: ['openFile'],
buttonLabel: "确定", buttonLabel: '确定',
}) })
.then((result) => { .then((result) => {
log("选择文件", result); log('选择文件', result)
if (!result.canceled) { if (!result.canceled) {
const _selectPath = path.join(result.filePaths[0]); const _selectPath = path.join(result.filePaths[0])
resolve(_selectPath); resolve(_selectPath)
// let config = getConfigUtil().getConfig() // let config = getConfigUtil().getConfig()
// config.ffmpeg = path.join(result.filePaths[0]); // config.ffmpeg = path.join(result.filePaths[0]);
// getConfigUtil().setConfig(config); // getConfigUtil().setConfig(config);
} }
resolve("") resolve('')
}) })
.catch((err) => { .catch((err) => {
reject(err); reject(err)
});
}) })
try {
return await selectPath;
} catch (e) {
log("选择文件出错", e)
return ""
}
}) })
if (!fs.existsSync(DATA_DIR)) { try {
fs.mkdirSync(DATA_DIR, {recursive: true}); return await selectPath
} catch (e) {
log('选择文件出错', e)
return ''
} }
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { })
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) if (!fs.existsSync(DATA_DIR)) {
llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常" fs.mkdirSync(DATA_DIR, { recursive: true })
let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError; }
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
error = error.replace("\n\n", "\n") const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
error = error.trim(); llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常'
log("查询llonebot错误信息", error); let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
return error; let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
}) error = error.replace('\n\n', '\n')
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { error = error.trim()
const config = getConfigUtil().getConfig() log('查询llonebot错误信息', error)
return config; return error
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => { ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
if (!ask) { const config = getConfigUtil().getConfig()
setConfig(config).then().catch(e => { return config
log("保存设置失败", e.stack) })
}); ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => {
return if (!ask) {
} setConfig(config)
dialog.showMessageBox(mainWindow, { .then()
type: 'question', .catch((e) => {
buttons: ['确认', '取消'], log('保存设置失败', e.stack)
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认" })
title: '确认保存', return
message: '是否保存?', }
detail: 'LLOneBot配置已更改是否保存' dialog
}).then(result => { .showMessageBox(mainWindow, {
if (result.response === 0) { type: 'question',
setConfig(config).then().catch(e => { buttons: ['确认', '取消'],
log("保存设置失败", e.stack) defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
}); title: '确认保存',
} else { message: '是否保存?',
} detail: 'LLOneBot配置已更改是否保存',
}).catch(err => { })
log("保存设置询问弹窗错误", err); .then((result) => {
}); if (result.response === 0) {
}) setConfig(config)
.then()
ipcMain.on(CHANNEL_LOG, (event, arg) => { .catch((e) => {
log(arg); log('保存设置失败', e.stack)
})
async function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) {
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message);
// }
OB11Constructor.message(message).then((msg) => {
if (debug) {
msg.raw = message;
} else {
if (msg.message.length === 0) {
return
}
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin);
}
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);
}
}) })
}
}
async function startReceiveHook() {
if (getConfigUtil().getConfig().enablePoke) {
registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id));
} else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id));
}
postOB11Event(pokeEvent);
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList);
} catch (e) {
log("report message error: ", e.stack.toString());
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then();
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postOB11Event(friendRecallEvent);
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId
)
postOB11Event(groupRecallEvent);
}
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
dbUtil.updateMsg(message).then();
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord]);
} catch (e) {
log("report self message error: ", e.stack.toString());
}
})
registerReceiveHook<{
"doubt": boolean,
"oldestUnreadSeq": string,
"unreadCount": number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies;
try {
notify = await NTQQGroupApi.getGroupNotifies();
} catch (e) {
// log("获取群通知详情失败", e);
return
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) {
try {
notify.time = Date.now();
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq);
if (existNotify) {
continue
}
log("收到群通知", notify);
await dbUtil.addGroupNotify(notify);
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid);
log("有管理员变动通知");
refreshGroupMembers(notify.group.groupCode).then()
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
log("开始获取变动的管理员")
if (member1) {
log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set";
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true);
} else {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
}
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log("有成员退出通知", notify);
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid);
let operatorId = member1.uin;
let subType: GroupDecreaseSubType = "leave";
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid);
operatorId = member2.uin;
subType = "kick";
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin), parseInt(operatorId), subType)
postOB11Event(groupDecreaseEvent, true);
} catch (e) {
log("获取群通知的成员信息失败", notify, e.stack.toString())
}
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log("有加群请求");
let groupRequestEvent = new OB11GroupRequestEvent();
groupRequestEvent.group_id = parseInt(notify.group.groupCode);
let requestQQ = ""
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin;
} catch (e) {
log("获取加群人QQ号失败", e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0;
groupRequestEvent.sub_type = "add"
groupRequestEvent.comment = notify.postscript;
groupRequestEvent.flag = notify.seq;
postOB11Event(groupRequestEvent);
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log("收到邀请我加群通知")
let groupInviteEvent = new OB11GroupRequestEvent();
groupInviteEvent.group_id = parseInt(notify.group.groupCode);
let user_id = (await getFriend(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id);
groupInviteEvent.sub_type = "invite";
groupInviteEvent.flag = notify.seq;
postOB11Event(groupInviteEvent);
}
} catch (e) {
log("解析群通知失败", e.stack.toString());
}
}
} else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime;
if (req.isUnread && (parseInt(req.reqTime) > startTime / 1000)) {
friendRequests[flag] = req;
log("有新的好友请求", req);
let friendRequestEvent = new OB11FriendRequestEvent();
try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin);
} catch (e) {
log("获取加好友者QQ号失败", e);
}
friendRequestEvent.flag = flag;
friendRequestEvent.comment = req.extWords;
postOB11Event(friendRequestEvent);
}
}
})
}
let startTime = 0;
async function start() {
log("llonebot pid", process.pid)
llonebotError.otherError = "";
startTime = Date.now();
dbUtil.getReceivedTempUinMap().then(m => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key;
}
})
startReceiveHook().then();
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
}
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
}
log("LLOneBot start")
}
let getSelfNickCount = 0;
const init = async () => {
try {
log("start get self info")
const _ = await NTQQUserApi.getSelfInfo();
log("get self info api result:", _);
Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin;
} catch (e) {
log("retry get self info", e);
}
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin;
selfInfo.uid = globalThis.authData?.uid;
selfInfo.nick = selfInfo.uin;
}
log("self info", selfInfo, globalThis.authData);
if (selfInfo.uin) {
async function getUserNick() {
try {
getSelfNickCount++;
const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
return
}
} catch (e) {
log("get self nickname failed", e.stack);
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000);
}
}
getUserNick().then()
start().then();
} else { } else {
setTimeout(init, 1000)
} }
} })
setTimeout(init, 1000); .catch((err) => {
} log('保存设置询问弹窗错误', err)
})
})
ipcMain.on(CHANNEL_LOG, (event, arg) => {
log(arg)
})
async function postReceiveMsg(msgList: RawMessage[]) {
const { debug, reportSelfMessage } = getConfigUtil().getConfig()
for (let message of msgList) {
// 过滤启动之前的消息
// log('收到新消息', message);
if (parseInt(message.msgTime) < startTime / 1000) {
continue
}
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message)
// }
OB11Constructor.message(message)
.then((msg) => {
if (debug) {
msg.raw = message
} else {
if (msg.message.length === 0) {
return
}
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
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)
}
})
OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
if (friendAddEvent) {
// log("post friend add event", friendAddEvent);
postOB11Event(friendAddEvent)
}
})
}
}
async function startReceiveHook() {
if (getConfigUtil().getConfig().enablePoke) {
crychic.loadNode()
crychic.registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id))
} else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id))
}
postOB11Event(pokeEvent)
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList)
} catch (e) {
log('report message error: ', e.stack.toString())
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
// log("message update", message)
if (message.recallTime != '0') {
//todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then()
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(
parseInt(message.senderUin),
oriMessage.msgShortId,
)
postOB11Event(friendRecallEvent)
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId,
)
postOB11Event(groupRecallEvent)
}
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
dbUtil.updateMsg(message).then()
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const { reportSelfMessage } = getConfigUtil().getConfig()
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord])
} catch (e) {
log('report self message error: ', e.stack.toString())
}
})
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies
try {
notify = await NTQQGroupApi.getGroupNotifies()
} catch (e) {
// log("获取群通知详情失败", e);
return
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) {
try {
notify.time = Date.now()
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq)
if (existNotify) {
continue
}
log('收到群通知', notify)
await dbUtil.addGroupNotify(notify)
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notify.type)) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
log('有管理员变动通知')
refreshGroupMembers(notify.group.groupCode).then()
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
log('开始获取变动的管理员')
if (member1) {
log('变动管理员获取成功')
groupAdminNoticeEvent.user_id = parseInt(member1.uin)
groupAdminNoticeEvent.sub_type = [GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notify.type) ? 'unset' : 'set'
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true)
} else {
log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
}
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid)
let operatorId = member1.uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid)
operatorId = member2.uin
subType = 'kick'
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1.uin),
parseInt(operatorId),
subType,
)
postOB11Event(groupDecreaseEvent, true)
} catch (e) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
}
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log('有加群请求')
let groupRequestEvent = new OB11GroupRequestEvent()
groupRequestEvent.group_id = parseInt(notify.group.groupCode)
let requestQQ = ''
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0
groupRequestEvent.sub_type = 'add'
groupRequestEvent.comment = notify.postscript
groupRequestEvent.flag = notify.seq
postOB11Event(groupRequestEvent)
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
let groupInviteEvent = new OB11GroupRequestEvent()
groupInviteEvent.group_id = parseInt(notify.group.groupCode)
let user_id = (await getFriend(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id)
groupInviteEvent.sub_type = 'invite'
groupInviteEvent.flag = notify.seq
postOB11Event(groupInviteEvent)
}
} catch (e) {
log('解析群通知失败', e.stack.toString())
}
}
} else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) {
friendRequests[flag] = req
log('有新的好友请求', req)
let friendRequestEvent = new OB11FriendRequestEvent()
try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
friendRequestEvent.flag = flag
friendRequestEvent.comment = req.extWords
postOB11Event(friendRequestEvent)
}
}
})
}
let startTime = 0 // 毫秒
async function start() {
log('llonebot pid', process.pid)
llonebotError.otherError = ''
startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key
}
})
startReceiveHook().then()
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort)
}
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start()
}
if (config.ob11.enableHttpHeart) {
httpHeart.start()
}
log('LLOneBot start')
}
let getSelfNickCount = 0
const init = async () => {
try {
log('start get self info')
const _ = await NTQQUserApi.getSelfInfo()
log('get self info api result:', _)
Object.assign(selfInfo, _)
selfInfo.nick = selfInfo.uin
} catch (e) {
log('retry get self info', e)
}
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin
selfInfo.uid = globalThis.authData?.uid
selfInfo.nick = selfInfo.uin
}
log('self info', selfInfo, globalThis.authData)
if (selfInfo.uin) {
async function getUserNick() {
try {
getSelfNickCount++
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
log('self info', userInfo)
if (userInfo) {
selfInfo.nick = userInfo.nick
return
}
} catch (e) {
log('get self nickname failed', e.stack)
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000)
}
}
getUserNick().then()
start().then()
} else {
setTimeout(init, 1000)
}
}
setTimeout(init, 1000)
}
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (selfInfo.uid) {
return return
} }
mainWindow = window; mainWindow = window
log("window create", window.webContents.getURL().toString()) log('window create', window.webContents.getURL().toString())
try { try {
hookNTQQApiCall(window); hookNTQQApiCall(window)
hookNTQQApiReceive(window); hookNTQQApiReceive(window)
} catch (e) { } catch (e) {
log("LLOneBot hook error: ", e.toString()) log('LLOneBot hook error: ', e.toString())
} }
} }
try { try {
onLoad(); onLoad()
} catch (e: any) { } catch (e: any) {
console.log(e.toString()) console.log(e.toString())
} }
// 这两个函数都是可选的 // 这两个函数都是可选的
export { export { onBrowserWindowCreated }
onBrowserWindowCreated
}

View File

@@ -1,60 +1,67 @@
import {Config} from "../common/types"; import { Config } from '../common/types'
import {ob11HTTPServer} from "../onebot11/server/http"; import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import {llonebotError} from "../common/data"; import { llonebotError } from '../common/data'
import {getConfigUtil} from "../common/config"; import { getConfigUtil } from '../common/config'
import {checkFfmpeg, log} from "../common/utils"; import { checkFfmpeg, log } from '../common/utils'
export async function setConfig(config: Config) { export async function setConfig(config: Config) {
let oldConfig = {...(getConfigUtil().getConfig())}; let oldConfig = { ...getConfigUtil().getConfig() }
getConfigUtil().setConfig(config) getConfigUtil().setConfig(config)
if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) { if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) {
ob11HTTPServer.restart(config.ob11.httpPort); ob11HTTPServer.restart(config.ob11.httpPort)
} }
// 判断是否启用或关闭HTTP服务 // 判断是否启用或关闭HTTP服务
if (!config.ob11.enableHttp) { if (!config.ob11.enableHttp) {
ob11HTTPServer.stop(); ob11HTTPServer.stop()
} else {
ob11HTTPServer.start(config.ob11.httpPort)
}
// 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort)
llonebotError.wsServerError = ''
}
// 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort)
} else { } else {
ob11HTTPServer.start(config.ob11.httpPort); ob11WebsocketServer.stop()
}
// 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort);
llonebotError.wsServerError = ''
}
// 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
} }
}
// 判断是否启用或关闭反向ws
if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (config.ob11.enableWsReverse) { if (config.ob11.enableWsReverse) {
// 判断反向ws地址有变化 ob11ReverseWebsockets.start()
if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) { } else {
log("反向ws地址有变化, 重启反向ws服务") ob11ReverseWebsockets.stop()
ob11ReverseWebsockets.restart();
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
log("反向ws地址有变化, 重启反向ws服务")
ob11ReverseWebsockets.restart();
break;
}
}
}
} }
log("old config", oldConfig) }
log("配置已更新", config) if (config.ob11.enableWsReverse) {
checkFfmpeg(config.ffmpeg).then() // 判断反向ws地址有变化
} if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
log('反向ws地址有变化, 重启反向ws服务')
ob11ReverseWebsockets.restart()
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
log('反向ws地址有变化, 重启反向ws服务')
ob11ReverseWebsockets.restart()
break
}
}
}
}
if (config.ob11.enableHttpHeart) {
// 启动http心跳
httpHeart.start()
} else {
// 关闭http心跳
httpHeart.stop()
}
log('old config', oldConfig)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
}

View File

@@ -1,236 +1,342 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { import {
CacheFileList, CacheFileList,
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult, CacheScanResult,
ChatCacheList, ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, RawMessage,
} from "../types"; } from '../types'
import path from "path"; import path from 'path'
import fs from "fs"; import fs from 'fs'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {log} from "../../common/utils/log"; import { log } from '../../common/utils'
import https from 'https'
import { sleep } from '../../common/utils'
import { hookApi } from '../external/moehook/hook'
let privateImageRKey = ''
let groupImageRKey = ''
let lastGetPrivateRKeyTime = 0
let lastGetGroupRKeyTime = 0
const rkeyExpireTime = 1000 * 60 * 30
export class NTQQFileApi { export class NTQQFileApi {
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath],
}) })
} }
static async getFileMd5(filePath: string) { static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({ return await callNTQQApi<string>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5, methodName: NTQQApiMethod.FILE_MD5,
args: [filePath] args: [filePath],
}) })
} }
static async copyFile(filePath: string, destPath: string) { static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({ return await callNTQQApi<string>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY, methodName: NTQQApiMethod.FILE_COPY,
args: [{ args: [{
fromPath: filePath, fromPath: filePath,
toPath: destPath toPath: destPath,
}] }],
}) })
} }
static async getFileSize(filePath: string) { static async getFileSize(filePath: string) {
return await callNTQQApi<number>({ return await callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath] className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath],
}) })
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath); const md5 = await NTQQFileApi.getFileMd5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext let ext = (await NTQQFileApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
ext = "." + ext ext = '.' + ext
} else { } else {
ext = "" 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: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ""
}
}]
})
log("media path", mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQFileApi.getFileSize(filePath);
return {
md5,
fileName,
path: mediaPath,
fileSize,
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: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
}],
})
log('media path', mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath)
const fileSize = await NTQQFileApi.getFileSize(filePath)
return {
md5,
fileName,
path: mediaPath,
fileSize,
ext,
}
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, isFile: boolean = false) { static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, force: boolean = false) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
return sourcePath if (force) {
} fs.unlinkSync(sourcePath)
const apiParams = [ } else {
{
getReq: {
fileModelId: "0",
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => {
log("media 下载完成判断", payload.notifyInfo.msgId, msgId);
return payload.notifyInfo.msgId == msgId;
}
})
return sourcePath return sourcePath
}
} }
const apiParams = [
{
getReq: {
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId
},
})
return sourcePath
}
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number, height: number }>({ return await callNTQQApi<{ width: number, height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath] className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath],
}) })
}
static async getImageUrl(msg: RawMessage) {
const isPrivateImage = msg.chatType !== ChatType.group
const msgElement = msg.elements.find(e => !!e.picElement)
if (!msgElement) {
return ''
} }
const url = msgElement.picElement.originImageUrl // 没有域名
const md5HexStr = msgElement.picElement.md5HexStr
const fileMd5 = msgElement.picElement.md5HexStr
const fileUuid = msgElement.picElement.fileUuid
if (url) {
if (url.startsWith('/download')) {
// console.log('rkey', rkey);
if (url.includes('&rkey=')) {
return IMAGE_HTTP_HOST_NT + url
}
if (!hookApi.isAvailable()) {
log('hookApi is not available')
return ''
}
const saveRKey = (rkey: string) => {
if (isPrivateImage) {
privateImageRKey = rkey
lastGetPrivateRKeyTime = Date.now()
} else {
groupImageRKey = rkey
lastGetGroupRKeyTime = Date.now()
}
}
const refreshRKey = async () => {
log('获取图片rkey...')
NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, msgElement.elementId, '', msgElement.picElement.sourcePath, false).then().catch(() => {
})
await sleep(1000)
const _rkey = hookApi.getRKey()
if (_rkey) {
const imageUrl = IMAGE_HTTP_HOST_NT + url + _rkey
// 验证_rkey是否有效
try {
await new Promise((res, rej) => {
https.get(imageUrl, response => {
if (response.statusCode !== 200) {
rej('图片rkey获取失败')
} else {
res(response)
}
}).on('error', e => {
rej(e)
})
})
log('图片rkey获取成功', _rkey)
saveRKey(_rkey)
return _rkey
}catch (e) {
log('图片rkey有误', imageUrl)
}
}
}
const existsRKey = isPrivateImage ? privateImageRKey : groupImageRKey
const lastGetRKeyTime = isPrivateImage ? lastGetPrivateRKeyTime : lastGetGroupRKeyTime
if ((Date.now() - lastGetRKeyTime > rkeyExpireTime)) {
// rkey过期
const newRKey = await refreshRKey()
if (newRKey) {
return IMAGE_HTTP_HOST_NT + url + `${newRKey}`
} else {
log('图片rkey获取失败', url)
if(existsRKey){
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
return ''
}
}
// 使用未过期的rkey
if (existsRKey) {
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url
}
} else if (fileMd5 || md5HexStr) {
// 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
}
log('图片url获取失败', msg)
return ''
}
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) { static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE, methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{ args: [{
isSilent isSilent,
}, null] }, null],
}); })
} }
static getCacheSessionPathList() { static getCacheSessionPathList() {
return callNTQQApi<{ return callNTQQApi<{
key: string, key: string,
value: string value: string
}[]>({ }[]>({
className: NTQQApiClass.OS_API, className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION, methodName: NTQQApiMethod.CACHE_PATH_SESSION,
}); })
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么 return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR, methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{ args: [{
keys: cacheKeys keys: cacheKeys,
}, null] }, null],
}); })
} }
static addCacheScannedPaths(pathMap: object = {}) { static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({ return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{ args: [{
pathMap: {...pathMap}, pathMap: { ...pathMap },
}, null] }, null],
}); })
} }
static scanCache() { static scanCache() {
callNTQQApi<GeneralCallResult>({ callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true, classNameIsRegister: true,
}).then(); }).then()
return callNTQQApi<CacheScanResult>({ return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN, methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null], args: [null, null],
timeoutSecond: 300, timeoutSecond: 300,
}); })
} }
static getHotUpdateCachePath() { static getHotUpdateCachePath() {
return callNTQQApi<string>({ return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API, className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE,
}); })
} }
static getDesktopTmpPath() { static getDesktopTmpPath() {
return callNTQQApi<string>({ return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API, className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP,
}); })
} }
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => { return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({ callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET, methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [{ args: [{
chatType: type, chatType: type,
pageSize, pageSize,
order: 1, order: 1,
pageIndex pageIndex,
}, null] }, null],
}).then(list => res(list)) }).then(list => res(list))
.catch(e => rej(e)); .catch(e => rej(e))
}); })
} }
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : {fileType: fileType}; const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return callNTQQApi<CacheFileList>({ return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET, methodName: NTQQApiMethod.CACHE_FILE_GET,
args: [{ args: [{
fileType: fileType, fileType: fileType,
restart: true, restart: true,
pageSize: pageSize, pageSize: pageSize,
order: 1, order: 1,
lastRecord: _lastRecord, lastRecord: _lastRecord,
}, null] }, null],
}) })
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{ args: [{
chats, chats,
fileKeys fileKeys,
}, null] }, null],
}); })
} }
} }

View File

@@ -1,61 +1,65 @@
import {Friend, FriendRequest} from "../types"; import { Friend, FriendRequest } from '../types'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import {friendRequests} from "../../common/data"; import { friendRequests } from '../../common/data'
import { log } from '../../common/utils'
export class NTQQFriendApi{ export class NTQQFriendApi {
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { 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: ReceiveCmdS.FRIENDS,
cbCmd: ReceiveCmdS.FRIENDS afterFirstCmd: false,
}) })
let _friends: Friend[] = []; // log('获取好友列表', data)
for (const fData of data.data) { let _friends: Friend[] = []
_friends.push(...fData.buddyList) for (const fData of data.data) {
} _friends.push(...fData.buddyList)
return _friends
} }
static async likeFriend(uid: string, count = 1) { return _friends
return await callNTQQApi<GeneralCallResult>({ }
methodName: NTQQApiMethod.LIKE_FRIEND, static async likeFriend(uid: string, count = 1) {
args: [{ return await callNTQQApi<GeneralCallResult>({
doLikeUserInfo: { methodName: NTQQApiMethod.LIKE_FRIEND,
friendUid: uid, args: [
sourceId: 71, {
doLikeCount: count, doLikeUserInfo: {
doLikeTollCount: 0 friendUid: uid,
} sourceId: 71,
}, null] doLikeCount: count,
}) doLikeTollCount: 0,
},
},
null,
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
throw `flat: ${flag}, 对应的好友请求不存在`
} }
static async handleFriendRequest(flag: string, accept: boolean,) { const result = await callNTQQApi<GeneralCallResult>({
const request: FriendRequest = friendRequests[flag] methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
if (!request) { args: [
throw `flat: ${flag}, 对应的好友请求不存在` {
} approvalInfo: {
const result = await callNTQQApi<GeneralCallResult>({ friendUid: request.friendUid,
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, reqTime: request.reqTime,
args: [ accept,
{ },
"approvalInfo": { },
"friendUid": request.friendUid, ],
"reqTime": request.reqTime, })
accept delete friendRequests[flag]
} return result
} }
] }
})
delete friendRequests[flag];
return result;
}
}

View File

@@ -1,205 +1,232 @@
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types"; import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes } from '../types'
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import {uidMaps} from "../../common/data"; import { deleteGroup, uidMaps } from '../../common/data'
import {dbUtil} from "../../common/db"; import { dbUtil } from '../../common/db'
import {log} from "../../common/utils/log"; import { log } from '../../common/utils/log'
import {NTQQWindowApi, NTQQWindows} from "./window"; import { NTQQWindowApi, NTQQWindows } from './window'
export class NTQQGroupApi{ export class NTQQGroupApi {
static async getGroups(forced = false) { static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS let cbCmd = ReceiveCmdS.GROUPS
if (process.platform != "win32") { if (process.platform != 'win32') {
cbCmd = ReceiveCmdS.GROUPS_STORE cbCmd = ReceiveCmdS.GROUPS_STORE
}
const result = await callNTQQApi<{
updateType: number
groupList: Group[]
}>({ methodName: NTQQApiMethod.GROUPS, args: [{ force_update: forced }, undefined], cbCmd })
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [
{
sceneId: sceneId,
num: num,
},
null,
],
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values)
for (const member of members) {
uidMaps[member.uid] = member.uin
}
// log(uidMaps);
// log("members info", values);
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
}
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: 14 }, null],
})
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies()
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow, [], ReceiveCmdS.GROUP_NOTIFY)
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false,
operateMsg: {
operateType: operateType, // 2 拒绝
targetMsg: {
seq: seq, // 通知序列号
type: notify.type,
groupCode: notify.group.groupCode,
postscript: reason,
},
},
},
null,
],
})
}
static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
if (result.result === 0) {
deleteGroup(groupQQ)
}
return result
}
static async kickMember(
groupQQ: string,
kickUids: string[],
refuseForever: boolean = false,
kickReason: string = '',
) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
}
static async banMember(groupQQ: string, memList: Array<{ uid: string; timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
}
static async getGroupAtAllRemainCount(groupCode: string) {
return await callNTQQApi<
GeneralCallResult & {
atInfo: {
canAtAll: boolean
RemainAtAllCountForUin: number
RemainAtAllCountForGroup: number
atTimesMsg: string
canNotAtAllMsg: ''
} }
const result = await callNTQQApi<{ }
updateType: number, >({
groupList: Group[] methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT,
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) args: [
return result.groupList {
} groupCode,
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> { },
const sceneId = await callNTQQApi({ null,
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, ],
args: [{ })
groupCode: groupQQ, }
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId,
num: num
},
null
]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values) // 头衔不可用
for (const member of members) { static async setGroupTitle(groupQQ: string, uid: string, title: string) {
uidMaps[member.uid] = member.uin; return await callNTQQApi<GeneralCallResult>({
} methodName: NTQQApiMethod.SET_GROUP_TITLE,
// log(uidMaps); args: [
// log("members info", values); {
log(`get group ${groupQQ} members success`) groupCode: groupQQ,
return members uid,
} catch (e) { title,
log(`get group ${groupQQ} members failed`, e) },
return [] null,
} ],
} })
static async getGroupNotifies() { }
// 获取管理员变更 static publishGroupBulletin(groupQQ: string, title: string, content: string) {}
// 加群通知,退出通知,需要管理员权限 }
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
{"doubt": false, "startSeq": "", "number": 14},
null
]
});
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies();
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow,[], ReceiveCmdS.GROUP_NOTIFY);
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
});
}
static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{"groupCode": groupQQ},
null
]
})
}
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return await callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
}
]
}
)
}
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName
}, null
]
})
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title
}, null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
}
}

View File

@@ -1,7 +1,7 @@
export * from "./file"; export * from './file'
export * from "./friend"; export * from './friend'
export * from "./group"; export * from './group'
export * from "./msg"; export * from './msg'
export * from "./user"; export * from './user'
export * from "./webapi"; export * from './webapi'
export * from "./window"; export * from './window'

View File

@@ -1,221 +1,247 @@
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import {ChatType, RawMessage, SendMessageElement} from "../types"; import { ChatType, RawMessage, SendMessageElement } from '../types'
import {dbUtil} from "../../common/db"; import { dbUtil } from '../../common/db'
import {selfInfo} from "../../common/data"; import { selfInfo } from '../../common/data'
import {ReceiveCmdS, registerReceiveHook} from "../hook"; import { ReceiveCmdS, registerReceiveHook } from '../hook'
import {log} from "../../common/utils/log"; import { log } from '../../common/utils/log'
import {sleep} from "../../common/utils/helper"; import { sleep } from '../../common/utils/helper'
import {isQQ998} from "../../common/utils"; import { isQQ998 } from '../../common/utils'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunnc
export interface Peer { export interface Peer {
chatType: ChatType chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串 peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: "" guildId?: ''
} }
export class NTQQMsgApi { export class NTQQMsgApi {
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({ // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
methodName: NTQQApiMethod.GET_MULTI_MSG, // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
args: [{ // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
peer, return await callNTQQApi<GeneralCallResult>({
rootMsgId, methodName: NTQQApiMethod.EMOJI_LIKE,
parentMsgId args: [
}, null] {
}) peer,
} msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
}
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
static async activateChat(peer: Peer) { static async activateChat(peer: Peer) {
// await this.fetchRecentContact(); // await this.fetchRecentContact();
// await sleep(500); // await sleep(500);
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW, methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{peer, cnt: 20}, null] args: [{ peer, cnt: 20 }, null],
}) })
} }
static async activateChatAndGetHistory(peer: Peer) { static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact(); // await this.fetchRecentContact();
// await sleep(500); // await sleep(500);
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY, methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样 // 参数似乎不是这样
args: [{peer, cnt: 20}, null] args: [{ peer, cnt: 20 }, null],
}) })
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number) { static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新 // 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({ return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG, methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG,
args: [{ args: [
peer, {
msgId, peer,
cnt: count, msgId,
queryOrder: true, cnt: count,
}, null] queryOrder: true,
}) },
} null,
static async fetchRecentContact(){ ],
await callNTQQApi({ })
methodName: NTQQApiMethod.RECENT_CONTACT, }
args: [ static async fetchRecentContact() {
{ await callNTQQApi({
fetchParam: { methodName: NTQQApiMethod.RECENT_CONTACT,
anchorPointContact: { args: [
contactId: '', {
sortField: '', fetchParam: {
pos: 0, anchorPointContact: {
}, contactId: '',
relativeMoveCount: 0, sortField: '',
listType: 2, // 1普通消息2群助手内的消息 pos: 0,
count: 200,
fetchOld: true,
},
}
]
})
}
static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({
methodName: NTQQApiMethod.RECALL_MSG,
args: [{
peer,
msgIds
}, null]
})
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[],
waitComplete = true, timeout = 10000) {
const peerUid = peer.peerUid
// 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0;
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw ("发送超时")
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500);
checkLastSendUsingTime += 500;
return await waitLastSend();
} else {
return;
}
}
await waitLastSend();
let sentMessage: RawMessage = null;
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid];
sentMessage = rawMessage;
}
let checkSendCompleteUsingTime = 0;
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) {
return sentMessage
}
} else {
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw ('发送超时')
}
await sleep(500)
return await checkSendComplete()
}
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
return await checkSendComplete()
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [
destPeer
],
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
})
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: selfInfo.nick}
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map()
}, },
null, relativeMoveCount: 0,
] listType: 2, // 1普通消息2群助手内的消息
return await new Promise<RawMessage>((resolve, reject) => { count: 200,
let complete = false fetchOld: true,
setTimeout(() => { },
if (!complete) { },
reject("转发消息超时"); ],
} })
}, 5000) }
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord static async recallMsg(peer: Peer, msgIds: string[]) {
// 需要判断它是转发的消息,并且识别到是当前转发的这一条 return await callNTQQApi({
const arkElement = msg.elements.find(ele => ele.arkElement) methodName: NTQQApiMethod.RECALL_MSG,
if (!arkElement) { args: [
// log("收到的不是转发消息") {
return peer,
} msgIds,
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) },
if (forwardData.app != 'com.tencent.multimsg') { null,
return ],
} })
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { }
complete = true
await dbUtil.addMsg(msg) static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
resolve(msg) const peerUid = peer.peerUid
log('转发消息成功:', payload)
} // 等待上一个相同的peer发送完
}) let checkLastSendUsingTime = 0
callNTQQApi<GeneralCallResult>({ const waitLastSend = async () => {
methodName: NTQQApiMethod.MULTI_FORWARD_MSG, if (checkLastSendUsingTime > timeout) {
args: apiArgs throw '发送超时'
}).then(result => { }
log("转发消息结果:", result, apiArgs) let lastSending = sendMessagePool[peer.peerUid]
if (result.result !== 0) { if (lastSending) {
complete = true; // log("有正在发送的消息,等待中...")
reject("转发消息失败," + JSON.stringify(result)); await sleep(500)
} checkLastSendUsingTime += 500
}) return await waitLastSend()
}) } else {
return
}
}
await waitLastSend()
let sentMessage: RawMessage = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
} }
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) {
return sentMessage
}
} else {
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw '发送超时'
}
await sleep(500)
return await checkSendComplete()
}
} callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [
{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map(),
},
null,
],
}).then()
return await checkSendComplete()
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const msgInfos = msgIds.map((id) => {
return { msgId: id, senderShowName: selfInfo.nick }
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
]
return await new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject('转发消息超时')
}
}, 5000)
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find((ele) => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs,
}).then((result) => {
log('转发消息结果:', result, apiArgs)
if (result.result !== 0) {
complete = true
reject('转发消息失败,' + JSON.stringify(result))
}
})
})
}
}

View File

@@ -1,117 +1,159 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import {SelfInfo, User} from "../types"; import { Group, SelfInfo, User } from '../types'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {uidMaps} from "../../common/data"; import { selfInfo, uidMaps } from '../../common/data'
import {NTQQWindowApi, NTQQWindows} from "./window"; import { NTQQWindowApi, NTQQWindows } from './window'
import {isQQ998, sleep} from "../../common/utils"; import { isQQ998, log, sleep } from '../../common/utils'
let userInfoCache: Record<string, User> = {}; // uid: User let userInfoCache: Record<string, User> = {} // uid: User
export class NTQQUserApi{ export class NTQQUserApi {
static async setQQAvatar(filePath: string) { static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR, methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [{ args: [
path:filePath {
}, null], path: filePath,
timeoutSecond: 10 // 10秒不一定够 },
}); null,
],
timeoutSecond: 10, // 10秒不一定够
})
}
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO,
timeoutSecond: 2,
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{ force: true, uids: [uid] }, undefined],
cbCmd: ReceiveCmdS.USER_INFO,
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel = false) {
// this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
const fetchInfo = async () => {
const result = await callNTQQApi<{ info: User }>({
methodName,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid,
},
null,
],
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000)
}
let userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
// return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey='
static async getCookieWithoutSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: 'qun.qq.com',
},
],
})
}
static async getSkey(groupName: string, groupCode: string): Promise<{ data: string }> {
return await NTQQWindowApi.openWindow<{ data: string }>(
NTQQWindows.GroupHomeWorkWindow,
[
{
groupName,
groupCode,
source: 'funcbar',
},
],
ReceiveCmdS.SKEY_UPDATE,
1,
)
// return await callNTQQApi<string>({
// className: NTQQApiClass.GROUP_HOME_WORK,
// methodName: NTQQApiMethod.UPDATE_SKEY,
// args: [
// {
// domain: "qun.qq.com"
// }
// ]
// })
// return await callNTQQApi<GeneralCallResult>({
// methodName: NTQQApiMethod.GET_SKEY,
// args: [
// {
// "domains": [
// "qzone.qq.com",
// "qlive.qq.com",
// "qun.qq.com",
// "gamecenter.qq.com",
// "vip.qq.com",
// "qianbao.qq.com",
// "qidian.qq.com"
// ],
// "isForNewPCQQ": false
// },
// null
// ]
// })
}
static async getCookie(group: Group) {
let cookies = await this.getCookieWithoutSkey()
let skey = ''
for (let i = 0; i < 2; i++) {
skey = (await this.getSkey(group.groupName, group.groupCode)).data
skey = skey.trim()
if (skey) {
break
}
await sleep(1000)
}
if (!skey) {
throw new Error('获取skey失败')
}
const bkn = NTQQUserApi.genBkn(skey)
cookies = cookies.replace('skey=;', `skey=${skey};`)
return { cookies, bkn }
}
static genBkn(sKey: string) {
sKey = sKey || ''
let hash = 5381
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code
} }
static async getSelfInfo() { return (hash & 0x7fffffff).toString()
return await callNTQQApi<SelfInfo>({ }
className: NTQQApiClass.GLOBAL_DATA, }
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined],
cbCmd: ReceiveCmdS.USER_INFO
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel=false) {
// this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
const fetchInfo = async ()=>{
const result = await callNTQQApi<{ info: User }>({
methodName,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid
},
null
]
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000);
}
let userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
static async getPSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: "qun.qq.com"
}
]
})
}
static async getSkey(groupName: string, groupCode: string): Promise<{data: string}> {
return await NTQQWindowApi.openWindow<{data: string}>(NTQQWindows.GroupHomeWorkWindow, [{
groupName,
groupCode,
"source": "funcbar"
}], ReceiveCmdS.SKEY_UPDATE, 1);
// return await callNTQQApi<string>({
// className: NTQQApiClass.GROUP_HOME_WORK,
// methodName: NTQQApiMethod.UPDATE_SKEY,
// args: [
// {
// domain: "qun.qq.com"
// }
// ]
// })
// return await callNTQQApi<GeneralCallResult>({
// methodName: NTQQApiMethod.GET_SKEY,
// args: [
// {
// "domains": [
// "qzone.qq.com",
// "qlive.qq.com",
// "qun.qq.com",
// "gamecenter.qq.com",
// "vip.qq.com",
// "qianbao.qq.com",
// "qidian.qq.com"
// ],
// "isForNewPCQQ": false
// },
// null
// ]
// })
}
}

View File

@@ -1,86 +1,76 @@
import {groups} from "../../common/data"; import { groups } from '../../common/data'
import {log} from "../../common/utils"; import { log } from '../../common/utils'
import {NTQQUserApi} from "./user"; import { NTQQUserApi } from './user'
export class WebApi{ export class WebApi {
private static bkn: string; private static bkn: string
private static skey: string; private static skey: string
private static pskey: string; private static pskey: string
private static cookie: string private static cookie: string
private defaultHeaders: Record<string,string> = { private defaultHeaders: Record<string, string> = {
"User-Agent": "QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0" 'User-Agent': 'QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0',
}
constructor() {}
public async addGroupDigest(groupCode: string, msgSeq: string) {
const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`
const res = await this.request(url)
return await res.json()
}
public async getGroupDigest(groupCode: string) {
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`
const res = await this.request(url)
log(res.headers)
return await res.json()
}
private genBkn(sKey: string) {
return NTQQUserApi.genBkn(sKey)
}
private async init() {
if (!WebApi.bkn) {
const group = groups[0]
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data
WebApi.bkn = this.genBkn(WebApi.skey)
let cookie = await NTQQUserApi.getCookieWithoutSkey()
const pskeyRegex = /p_skey=([^;]+)/
const match = cookie.match(pskeyRegex)
const pskeyValue = match ? match[1] : null
WebApi.pskey = pskeyValue
if (cookie.indexOf('skey=;') !== -1) {
cookie = cookie.replace('skey=;', `skey=${WebApi.skey};`)
}
WebApi.cookie = cookie
// for(const kv of WebApi.cookie.split(";")){
// const [key, value] = kv.split("=");
// }
// log("set cookie", key, value)
// await session.defaultSession.cookies.set({
// url: 'https://qun.qq.com', // 你要请求的域名
// name: key.trim(),
// value: value.trim(),
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒
// });
// }
} }
}
constructor() { private async request(url: string, method: 'GET' | 'POST' = 'GET', headers: Record<string, string> = {}) {
await this.init()
url += '&bkn=' + WebApi.bkn
let _headers: Record<string, string> = {
...this.defaultHeaders,
...headers,
Cookie: WebApi.cookie,
credentials: 'include',
} }
log('request', url, _headers)
public async addGroupDigest(groupCode: string, msgSeq: string){ const options = {
const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292` method: method,
const res = await this.request(url) headers: _headers,
return await res.json()
} }
return fetch(url, options)
public async getGroupDigest(groupCode: string){ }
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20` }
const res = await this.request(url)
log(res.headers)
return await res.json()
}
private genBkn(sKey: string){
sKey = sKey || "";
let hash = 5381;
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
private async init(){
if (!WebApi.bkn) {
const group = groups[0];
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data;
WebApi.bkn = this.genBkn(WebApi.skey);
let cookie = await NTQQUserApi.getPSkey();
const pskeyRegex = /p_skey=([^;]+)/;
const match = cookie.match(pskeyRegex);
const pskeyValue = match ? match[1] : null;
WebApi.pskey = pskeyValue;
if (cookie.indexOf("skey=;") !== -1) {
cookie = cookie.replace("skey=;", `skey=${WebApi.skey};`);
}
WebApi.cookie = cookie;
// for(const kv of WebApi.cookie.split(";")){
// const [key, value] = kv.split("=");
// }
// log("set cookie", key, value)
// await session.defaultSession.cookies.set({
// url: 'https://qun.qq.com', // 你要请求的域名
// name: key.trim(),
// value: value.trim(),
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒
// });
// }
}
}
private async request(url: string, method: "GET" | "POST" = "GET", headers: Record<string, string> = {}){
await this.init();
url += "&bkn=" + WebApi.bkn;
let _headers: Record<string, string> = {
...this.defaultHeaders, ...headers,
"Cookie": WebApi.cookie,
credentials: 'include'
}
log("request", url, _headers)
const options = {
method: method,
headers: _headers
}
return fetch(url, options)
}
}

View File

@@ -1,49 +1,50 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import {ReceiveCmd} from "../hook"; import { ReceiveCmd } from '../hook'
import {BrowserWindow} from "electron"; import { BrowserWindow } from 'electron'
export interface NTQQWindow{ export interface NTQQWindow {
windowName: string, windowName: string
windowUrlHash: string, windowUrlHash: string
} }
export class NTQQWindows{ export class NTQQWindows {
static GroupHomeWorkWindow: NTQQWindow = { static GroupHomeWorkWindow: NTQQWindow = {
windowName: "GroupHomeWorkWindow", windowName: 'GroupHomeWorkWindow',
windowUrlHash: "#/group-home-work" windowUrlHash: '#/group-home-work',
} }
static GroupNotifyFilterWindow: NTQQWindow = { static GroupNotifyFilterWindow: NTQQWindow = {
windowName: "GroupNotifyFilterWindow", windowName: 'GroupNotifyFilterWindow',
windowUrlHash: "#/group-notify-filter" windowUrlHash: '#/group-notify-filter',
} }
static GroupEssenceWindow: NTQQWindow = { static GroupEssenceWindow: NTQQWindow = {
windowName: "GroupEssenceWindow", windowName: 'GroupEssenceWindow',
windowUrlHash: "#/group-essence" windowUrlHash: '#/group-essence',
} }
} }
export class NTQQWindowApi{ export class NTQQWindowApi {
// 打开窗口并获取对应的下发事件
// 打开窗口并获取对应的下发事件 static async openWindow<R = GeneralCallResult>(
static async openWindow<R=GeneralCallResult>(ntQQWindow: NTQQWindow, args: any[], cbCmd: ReceiveCmd=null, autoCloseSeconds: number=2){ ntQQWindow: NTQQWindow,
const result = await callNTQQApi<R>({ args: any[],
className: NTQQApiClass.WINDOW_API, cbCmd: ReceiveCmd = null,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW, autoCloseSeconds: number = 2,
cbCmd, ) {
afterFirstCmd: false, const result = await callNTQQApi<R>({
args: [ className: NTQQApiClass.WINDOW_API,
ntQQWindow.windowName, methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW,
...args cbCmd,
] afterFirstCmd: false,
}) args: [ntQQWindow.windowName, ...args],
setTimeout(() => { })
for (const w of BrowserWindow.getAllWindows()) { setTimeout(() => {
// log("close window", w.webContents.getURL()) for (const w of BrowserWindow.getAllWindows()) {
if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) { // log("close window", w.webContents.getURL())
w.close(); if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) {
} w.close()
} }
}, autoCloseSeconds * 1000); }
return result; }, autoCloseSeconds * 1000)
} return result
} }
}

View File

@@ -1,252 +1,347 @@
import { import {
AtType, AtType,
ElementType, ElementType,
PicType, FaceIndex,
SendArkElement, FaceType,
SendFaceElement, PicType,
SendFileElement, SendArkElement,
SendPicElement, SendFaceElement,
SendPttElement, SendFileElement,
SendReplyElement, SendMarketFaceElement,
SendTextElement, SendPicElement,
SendVideoElement SendPttElement,
} from "./types"; SendReplyElement,
import {promises as fs} from "node:fs"; SendTextElement,
import ffmpeg from "fluent-ffmpeg" SendVideoElement,
import {NTQQFileApi} from "./api/file"; } from './types'
import {calculateFileMD5, isGIF} from "../common/utils/file"; import { promises as fs } from 'node:fs'
import {log} from "../common/utils/log"; import ffmpeg from 'fluent-ffmpeg'
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video"; import { NTQQFileApi } from './api/file'
import {encodeSilk} from "../common/utils/audio"; import { calculateFileMD5, isGIF } from '../common/utils/file'
import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils'
export const mFaceCache = new Map<string, string>(); // emojiId -> faceName
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
static text(content: string): SendTextElement { static poke(groupCode: string, uin: string) {
return { return null
elementType: ElementType.TEXT, }
elementId: "",
textElement: { static text(content: string): SendTextElement {
content, return {
atType: AtType.notAt, elementType: ElementType.TEXT,
atUid: "", elementId: '',
atTinyId: "", textElement: {
atNtUid: "", content,
}, atType: AtType.notAt,
}; atUid: '',
atTinyId: '',
atNtUid: '',
},
}
}
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: '',
textElement: {
content: `@${atName}`,
atType,
atUid,
atTinyId: '',
atNtUid,
},
}
}
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return {
elementType: ElementType.REPLY,
elementId: '',
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
},
}
}
static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType)
if (fileSize === 0) {
throw '文件异常大小为0'
}
const imageSize = await NTQQFileApi.getImageSize(picPath)
const picElement = {
md5HexStr: md5,
fileSize: fileSize.toString(),
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary,
}
log('图片信息', picElement)
return {
elementType: ElementType.PIC,
elementId: '',
picElement,
}
}
static async file(filePath: string, fileName: string = ''): Promise<SendFileElement> {
const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) {
throw '文件异常大小为0'
}
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: '',
fileElement: {
fileName: fileName || _fileName,
filePath: path,
fileSize: fileSize.toString(),
},
} }
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { return element
return { }
elementType: ElementType.TEXT,
elementId: "", static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
textElement: { try{
content: `@${atName}`, await fs.stat(filePath)
atType, }catch (e) {
atUid, throw `文件${filePath}异常,不存在`
atTinyId: "",
atNtUid,
},
};
} }
log("复制视频到QQ目录", filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO)
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { log("复制视频到QQ目录完成", path)
return { if (fileSize === 0) {
elementType: ElementType.REPLY, throw '文件异常大小为0'
elementId: "",
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
}
}
} }
const pathLib = require('path')
static async pic(picPath: string, summary: string = "", subType: 0|1=0): Promise<SendPicElement> { let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType); thumbDir = pathLib.dirname(thumbDir)
if (fileSize === 0) { // log("thumb 目录", thumb)
throw "文件异常大小为0"; let videoInfo = {
} width: 1920,
const imageSize = await NTQQFileApi.getImageSize(picPath); height: 1080,
const picElement = { time: 15,
md5HexStr: md5, format: 'mp4',
fileSize: fileSize.toString(), size: fileSize,
picWidth: imageSize.width, filePath,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: "",
fileSubId: "",
thumbFileSize: 0,
summary
};
log("图片信息", picElement)
return {
elementType: ElementType.PIC,
elementId: "",
picElement,
};
} }
try {
static async file(filePath: string, fileName: string = ""): Promise<SendFileElement> { videoInfo = await getVideoInfo(path)
const {md5, fileName: _fileName, path, fileSize} = await NTQQFileApi.uploadFile(filePath, ElementType.FILE); log('视频信息', videoInfo)
if (fileSize === 0) { } catch (e) {
throw "文件异常大小为0"; log('获取视频信息失败', e)
}
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: "",
fileElement: {
fileName: fileName || _fileName,
"filePath": path,
"fileSize": (fileSize).toString(),
}
}
return element;
} }
const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumbDir, thumbFileName)
log('开始生成视频缩略图', filePath)
let completed = false
static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise<SendVideoElement> { function useDefaultThumb() {
let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); if (completed) return
if (fileSize === 0) { log('获取视频封面失败,使用默认封面')
throw "文件异常大小为0"; fs.writeFile(thumbPath, defaultVideoThumb)
} .then(() => {
const pathLib = require("path"); resolve(thumbPath)
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) })
thumb = pathLib.dirname(thumb) .catch(reject)
// log("thumb 目录", thumb) }
let videoInfo = {
width: 1920, height: 1080, setTimeout(useDefaultThumb, 5000)
time: 15, ffmpeg(filePath)
format: "mp4", .on('end', () => {})
size: fileSize, .on('error', (err) => {
filePath if (diyThumbPath) {
}; fs.copyFile(diyThumbPath, thumbPath)
try { .then(() => {
videoInfo = await getVideoInfo(path); completed = true
log("视频信息", videoInfo) resolve(thumbPath)
} catch (e) { })
log("获取视频信息失败", e) .catch(reject)
} } else {
const createThumb = new Promise<string>((resolve, reject) => { useDefaultThumb()
const thumbFileName = `${md5}_0.png` }
const thumbPath = pathLib.join(thumb, thumbFileName)
ffmpeg(filePath)
.on("end", () => {
})
.on("error", (err) => {
log("获取视频封面失败,使用默认封面", err)
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
}).catch(reject)
} else {
fs.writeFile(thumbPath, defaultVideoThumb).then(() => {
resolve(thumbPath);
}).catch(reject)
}
})
.screenshots({
timestamps: [0],
filename: thumbFileName,
folder: thumb,
size: videoInfo.width + "x" + videoInfo.height
}).on("end", () => {
resolve(thumbPath);
});
}) })
let thumbPath = new Map() .screenshots({
const _thumbPath = await createThumb; timestamps: [0],
const thumbSize = (await fs.stat(_thumbPath)).size; filename: thumbFileName,
// log("生成缩略图", _thumbPath) folder: thumbDir,
thumbPath.set(0, _thumbPath) size: videoInfo.width + 'x' + videoInfo.height,
const thumbMd5 = await calculateFileMD5(_thumbPath); })
let element: SendVideoElement = { .on('end', () => {
elementType: ElementType.VIDEO, log('生成视频缩略图', thumbPath)
elementId: "", completed = true
videoElement: { resolve(thumbPath)
fileName: fileName || _fileName, })
filePath: path, })
videoMd5: md5, let thumbPath = new Map()
thumbMd5, const _thumbPath = await createThumb
fileTime: videoInfo.time, log('生成视频缩略图', _thumbPath)
thumbPath: thumbPath, const thumbSize = (await fs.stat(_thumbPath)).size
thumbSize, // log("生成缩略图", _thumbPath)
thumbWidth: videoInfo.width, thumbPath.set(0, _thumbPath)
thumbHeight: videoInfo.height, const thumbMd5 = await calculateFileMD5(_thumbPath)
fileSize: "" + fileSize, let element: SendVideoElement = {
// fileUuid: "", elementType: ElementType.VIDEO,
// transferStatus: 0, elementId: '',
// progress: 0, videoElement: {
// invalidState: 0, fileName: fileName || _fileName,
// fileSubId: "", filePath: path,
// fileBizId: null, videoMd5: md5,
// originVideoMd5: "", thumbMd5,
// fileFormat: 2, fileTime: videoInfo.time,
// import_rich_media_context: null, thumbPath: thumbPath,
// sourceVideoCodecFormat: 2 thumbSize,
} thumbWidth: videoInfo.width,
} thumbHeight: videoInfo.height,
return element; fileSize: '' + fileSize,
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
},
} }
log('videoElement', element)
return element
}
static async ptt(pttPath: string): Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath); const { converted, path: silkPath, duration } = await encodeSilk(pttPath)
// log("生成语音", silkPath, duration); if (!silkPath) {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT); throw '语音转换失败, 请检查语音文件是否正常'
if (fileSize === 0) {
throw "文件异常大小为0";
}
if (converted) {
fs.unlink(silkPath).then();
}
return {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
],
fileSubId: "",
playState: 1,
autoConvertText: 0,
}
};
} }
// log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT)
if (fileSize === 0) {
throw '文件异常大小为0'
}
if (converted) {
fs.unlink(silkPath).then()
}
return {
elementType: ElementType.PTT,
elementId: '',
pttElement: {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17],
fileSubId: '',
playState: 1,
autoConvertText: 0,
},
}
}
static face(faceId: number): SendFaceElement { static face(faceId: number): SendFaceElement {
return { faceId = parseInt(faceId.toString())
elementType: ElementType.FACE, return {
elementId: "", elementType: ElementType.FACE,
faceElement: { elementId: '',
faceIndex: faceId, faceElement: {
faceType: 1 faceIndex: faceId,
} faceType: faceId < 222 ? FaceType.normal : FaceType.normal2,
} },
} }
}
static ark(data: any): SendArkElement { static mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement {
return { return {
elementType: ElementType.ARK, elementType: ElementType.MFACE,
elementId: "", marketFaceElement: {
arkElement: { emojiPackageId,
bytesData: data, emojiId,
linkInfo: null, key,
subElementType: null faceName: faceName || mFaceCache.get(emojiId) || '[商城表情]',
} },
}
} }
} }
static dice(resultId: number | null): SendFaceElement {
// 实际测试并不能控制结果
// 随机1到6
if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: FaceIndex.dice,
faceType: FaceType.dice,
faceText: '[骰子]',
packId: '1',
stickerId: '33',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
}
}
// 猜拳(石头剪刀布)表情
static rps(resultId: number | null): SendFaceElement {
// 实际测试并不能控制结果
if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: FaceIndex.RPS,
faceText: '[包剪锤]',
faceType: 3,
packId: '1',
stickerId: '34',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
}
}
static ark(data: string): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: '',
arkElement: {
bytesData: data,
linkInfo: null,
subElementType: null,
},
}
}
}

View File

@@ -1,28 +0,0 @@
import {log} from "../../../common/utils/log";
let pokeEngine: any = null
type PokeHandler = (id: string, isGroup: boolean)=>void
let pokeRecords: Record<string, number> = {}
export function registerPokeHandler(handler: PokeHandler){
if(!pokeEngine){
try {
pokeEngine = require("./ccpoke/poke-win32-x64.node")
pokeEngine.performHooks();
}catch (e) {
log("戳一戳引擎加载失败", e)
return
}
}
pokeEngine.setHandlerForPokeHook((id: string, isGroup: boolean)=>{
let existTime = pokeRecords[id]
if (existTime){
if (Date.now() - existTime < 1500){
return
}
}
pokeRecords[id] = Date.now()
handler(id, isGroup);
})
}

Binary file not shown.

19
src/ntqqapi/external/cpmodule.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import * as os from "os";
import path from "node:path";
import fs from "fs";
export function getModuleWithArchName(moduleName: string) {
const systemPlatform = os.platform()
const cpuArch = os.arch()
return `${moduleName}-${systemPlatform}-${cpuArch}.node`
}
export function cpModule(moduleName: string) {
const currentDir = path.resolve(__dirname);
const fileName = `./${getModuleWithArchName(moduleName)}`
try {
fs.copyFileSync(path.join(currentDir, fileName), path.join(currentDir, `${moduleName}.node`));
} catch (e) {
}
}

Binary file not shown.

54
src/ntqqapi/external/crychic/index.ts vendored Normal file
View File

@@ -0,0 +1,54 @@
import {log} from "../../../common/utils";
import {NTQQApi} from "../../ntcall";
import {cpModule} from "../cpmodule";
type PokeHandler = (id: string, isGroup: boolean) => void
type CrychicHandler = (event: string, id: string, isGroup: boolean) => void
let pokeRecords: Record<string, number> = {}
class Crychic{
private crychic: any = undefined
loadNode(){
if (!this.crychic){
try {
cpModule('crychic');
this.crychic = require("./crychic.node")
this.crychic.init()
}catch (e) {
log("crychic加载失败", e)
}
}
}
registerPokeHandler(fn: PokeHandler){
this.registerHandler((event, id, isGroup)=>{
if (event === "poke"){
let existTime = pokeRecords[id]
if (existTime) {
if (Date.now() - existTime < 1500) {
return
}
}
pokeRecords[id] = Date.now()
fn(id, isGroup);
}
})
}
registerHandler(fn: CrychicHandler){
if (!this.crychic) return;
this.crychic.setCryHandler(fn)
}
sendFriendPoke(friendUid: string){
if (!this.crychic) return;
this.crychic.sendFriendPoke(parseInt(friendUid))
NTQQApi.fetchUnitedCommendConfig().then()
}
sendGroupPoke(groupCode: string, memberUin: string){
if (!this.crychic) return;
this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode))
NTQQApi.fetchUnitedCommendConfig().then()
}
}
export const crychic = new Crychic()

Binary file not shown.

Binary file not shown.

34
src/ntqqapi/external/moehook/hook.ts vendored Normal file
View File

@@ -0,0 +1,34 @@
import * as os from "os";
import fs from "fs";
import path from "node:path";
import {cpModule} from "../cpmodule";
interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: () => string
}
class HookApi {
private readonly moeHook: MoeHook | null = null;
constructor() {
cpModule('MoeHoo');
try {
this.moeHook = require('./MoeHoo.node');
console.log("hook rkey地址", this.moeHook!.HookRkey());
} catch (e) {
console.log('加载 moehoo 失败', e);
}
}
getRKey(): string {
return this.moeHook?.GetRkey() || '';
}
isAvailable() {
return !!this.moeHook;
}
}
export const hookApi = new HookApi();

View File

@@ -1,456 +1,528 @@
import {BrowserWindow} from 'electron'; import { BrowserWindow } from 'electron'
import {NTQQApiClass} from "./ntcall"; import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import {NTQQMsgApi, sendMessagePool} from "./api/msg" import { NTQQMsgApi, sendMessagePool } from './api/msg'
import {ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User} from "./types"; import { ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types'
import {friends, getGroupMember, groups, selfInfo, tempGroupCodeMap, uidMaps} from "../common/data"; import {
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; deleteGroup,
import {v4 as uuidv4} from "uuid" friends,
import {postOB11Event} from "../onebot11/server/postOB11Event"; getFriend,
import {getConfigUtil, HOOK_LOG} from "../common/config"; getGroupMember,
import fs from "fs"; groups,
import {dbUtil} from "../common/db"; selfInfo,
import {NTQQGroupApi} from "./api/group"; tempGroupCodeMap,
import {log} from "../common/utils/log"; uidMaps,
import {sleep} from "../common/utils/helper"; } from '../common/data'
import {OB11Constructor} from "../onebot11/constructor"; import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { v4 as uuidv4 } from 'uuid'
import { postOB11Event } from '../onebot11/server/postOB11Event'
import { getConfigUtil, HOOK_LOG } from '../common/config'
import fs from 'fs'
import { dbUtil } from '../common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '../common/utils/log'
import { isNumeric, sleep } from '../common/utils/helper'
import { OB11Constructor } from '../onebot11/constructor'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export let ReceiveCmdS = { export let ReceiveCmdS = {
RECENT_CONTACT: "nodeIKernelRecentContactListener/onRecentContactListChangedVer2", RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2',
UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate", UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate", UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate',
NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`,
NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`, NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`,
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_STORE: "onGroupListUpdate", GROUPS_STORE: 'onGroupListUpdate',
GROUP_MEMBER_INFO_UPDATE: "nodeIKernelGroupListener/onMemberInfoChange", GROUP_MEMBER_INFO_UPDATE: 'nodeIKernelGroupListener/onMemberInfoChange',
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", CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete", MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: "onSkeyUpdate" SKEY_UPDATE: 'onSkeyUpdate',
} }
export type ReceiveCmd = typeof ReceiveCmdS[keyof typeof ReceiveCmdS] export type ReceiveCmd = (typeof ReceiveCmdS)[keyof typeof ReceiveCmdS]
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: {
{ cmdName: ReceiveCmd
cmdName: ReceiveCmd, cmdType: 'event'
cmdType: "event", payload: PayloadType
payload: PayloadType }[]
}[]
} }
let 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
}> = []
let callHooks: Array<{
method: NTQQApiMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
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) => {
// console.log("hookNTQQApiReceive", channel, args) // console.log("hookNTQQApiReceive", channel, args)
let isLogger = false let isLogger = false
try { try {
isLogger = args[0]?.eventName?.startsWith("ns-LoggerApi") isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { } catch (e) {}
if (!isLogger) {
} try {
if (!isLogger) { HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
try { } catch (e) {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args) log('hook log error', e, args)
} catch (e) { }
log("hook log error", e, args)
}
}
try {
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
}
} catch (e) {
log("hook error", e, receiveData.payload)
}
}).then()
}
}
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]);
}).then()
delete hookApiCallbacks[callbackId];
}
}
} catch (e) {
log("hookNTQQApiReceive error", e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args);
} }
window.webContents.send = patchSend; try {
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
}
} catch (e) {
log('hook error', e, receiveData.payload)
}
}).then()
}
}
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
}).then()
delete hookApiCallbacks[callbackId]
}
}
} catch (e) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args)
}
window.webContents.send = patchSend
} }
export function hookNTQQApiCall(window: BrowserWindow) { export function hookNTQQApiCall(window: BrowserWindow) {
// 监听调用NTQQApi // 监听调用NTQQApi
let 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) {
// console.log(thisArg, args); // console.log(thisArg, args);
let isLogger = false let isLogger = false
try { try {
isLogger = args[3][0].eventName.startsWith("ns-LoggerApi") isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) { } catch (e) {}
if (!isLogger) {
} try {
if (!isLogger) { HOOK_LOG && log('call NTQQ api', thisArg, args)
} catch (e) {}
try {
const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => {
try { try {
HOOK_LOG && log("call NTQQ api", thisArg, args); let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
}
} catch (e) { } catch (e) {
log('hook call error', e, _args)
} }
}).then()
} }
return target.apply(thisArg, args); })
} catch (e) {}
}
return target.apply(thisArg, args)
},
})
if (webContents._events['-ipc-message']?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg
} else {
webContents._events['-ipc-message'] = proxyIpcMsg
}
const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
// console.log(args);
HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs)
}, },
}); })
if (webContents._events["-ipc-message"]?.[0]) { let ret = target.apply(thisArg, args)
webContents._events["-ipc-message"][0] = proxyIpcMsg; try {
} else { HOOK_LOG && log('call NTQQ invoke api return', ret)
webContents._events["-ipc-message"] = proxyIpcMsg; } catch (e) {}
} return ret
},
const ipc_invoke_proxy = webContents._events["-ipc-invoke"]?.[0] || webContents._events["-ipc-invoke"]; })
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { if (webContents._events['-ipc-invoke']?.[0]) {
apply(target, thisArg, args) { webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
// console.log(args); } else {
HOOK_LOG && log("call NTQQ invoke api", thisArg, args) webContents._events['-ipc-invoke'] = proxyIpcInvoke
args[0]["_replyChannel"]["sendReply"] = new Proxy(args[0]["_replyChannel"]["sendReply"], { }
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs);
}
});
let ret = target.apply(thisArg, args);
try {
HOOK_LOG && log("call NTQQ invoke api return", ret)
} catch (e) {
}
return ret;
}
});
if (webContents._events["-ipc-invoke"]?.[0]) {
webContents._events["-ipc-invoke"][0] = proxyIpcInvoke;
} else {
webContents._events["-ipc-invoke"] = proxyIpcInvoke;
}
} }
export function registerReceiveHook<PayloadType>(method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void): string { export function registerReceiveHook<PayloadType>(
const id = uuidv4() method: ReceiveCmd | ReceiveCmd[],
if (!Array.isArray(method)) { hookFunc: (payload: PayloadType) => void,
method = [method] ): string {
} const id = uuidv4()
receiveHooks.push({ if (!Array.isArray(method)) {
method, method = [method]
hookFunc, }
id receiveHooks.push({
}) method,
return id; hookFunc,
id,
})
return id
}
export function registerCallHook(
method: NTQQApiMethod | NTQQApiMethod[],
hookFunc: (callParams: unknown[]) => void | Promise<void>,
): void {
if (!Array.isArray(method)) {
method = [method]
}
callHooks.push({
method,
hookFunc,
})
} }
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)
} }
let activatedGroups: string[] = []; let activatedGroups: string[] = []
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) { for (let group of _groups) {
log("update group", group) log('update group', group)
// if (!activatedGroups.includes(group.groupCode)) { if (group.privilegeFlag === 0) {
NTQQMsgApi.activateChat({peerUid: group.groupCode, chatType: ChatType.group}).then((r) => { deleteGroup(group.groupCode)
// activatedGroups.push(group.groupCode); continue
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
}).catch(log)
// }
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
Object.assign(existGroup, group);
} else {
groups.push(group);
existGroup = group;
}
if (needUpdate) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode);
if (members) {
existGroup.members = members;
}
}
} }
log('update group', group)
// if (!activatedGroups.includes(group.groupCode)) {
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group })
.then((r) => {
// activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
})
.catch(log)
// }
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
Object.assign(existGroup, group)
} else {
groups.push(group)
existGroup = group
}
if (needUpdate) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
if (members) {
existGroup.members = members
}
}
}
} }
async function processGroupEvent(payload: { groupList: Group[] }) { async function processGroupEvent(payload: { groupList: Group[] }) {
try { try {
const newGroupList = payload.groupList; const newGroupList = payload.groupList
for (const group of newGroupList) { for (const group of newGroupList) {
let 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) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`); log(`群(${group.groupCode})成员数量减少${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 NTQQGroupApi.getGroupMembers(group.groupCode); const newMembers = await NTQQGroupApi.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)
} }
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOB11Event(new OB11GroupDecreaseEvent(parseInt(group.groupCode), parseInt(member.uin), parseInt(member.uin), "leave"));
break;
}
}
}
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOB11Event(
new OB11GroupDecreaseEvent(
parseInt(group.groupCode),
parseInt(member.uin),
parseInt(member.uin),
'leave',
),
)
break
} }
}
} }
if (group.privilegeFlag === 0) {
updateGroups(newGroupList, false).then(); deleteGroup(group.groupCode)
} catch (e) { }
updateGroups(payload.groupList).then(); }
log("更新群信息错误", e.stack.toString());
} }
updateGroups(newGroupList, false).then()
} catch (e) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
} }
// 群列表变动 // 群列表变动
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
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 }>(ReceiveCmdS.GROUPS_STORE, (payload) => { registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
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<{
groupCode: string, groupCode: string
dataSource: number, dataSource: number
members: Set<GroupMember> members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => { }>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode; const groupCode = payload.groupCode
const members = Array.from(payload.members.values()); const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members) // log("群成员信息变动", groupCode, members)
for (const member of members) { for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin); const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) { if (existMember) {
Object.assign(existMember, member); Object.assign(existMember, member)
}
} }
// const existGroup = groups.find(g => g.groupCode == groupCode); }
// if (existGroup) { // const existGroup = groups.find(g => g.groupCode == groupCode);
// log("对比群成员", existGroup.members, members) // if (existGroup) {
// for (const member of members) { // log("对比群成员", existGroup.members, members)
// const existMember = existGroup.members.find(m => m.uin == member.uin); // for (const member of members) {
// if (existMember) { // const existMember = existGroup.members.find(m => m.uin == member.uin);
// log("对比群名片", existMember.cardName, member.cardName) // if (existMember) {
// if (existMember.cardName != member.cardName) { // log("对比群名片", existMember.cardName, member.cardName)
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName)); // if (existMember.cardName != member.cardName) {
// } // postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// Object.assign(existMember, member); // }
// } // Object.assign(existMember, member);
// } // }
// } // }
// }
}) })
// 好友列表变动 // 好友列表变动
registerReceiveHook<{ registerReceiveHook<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] data: { categoryId: number; categroyName: string; categroyMbCount: number; buddyList: User[] }[]
}>(ReceiveCmdS.FRIENDS, payload => { }>(ReceiveCmdS.FRIENDS, (payload) => {
for (const fData of payload.data) { for (const fData of payload.data) {
const _friends = fData.buddyList; const _friends = fData.buddyList
for (let friend of _friends) { for (let friend of _friends) {
NTQQMsgApi.activateChat({peerUid: friend.uid, chatType: ChatType.friend}).then() NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let 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 {
Object.assign(existFriend, friend) Object.assign(existFriend, friend)
} }
}
} }
}
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid // 保存一下uid
for (const message of payload.msgList) { for (const message of payload.msgList) {
const uid = message.senderUid; const uid = message.senderUid
const uin = message.senderUin; const uin = message.senderUin
if (uid && uin) { if (uid && uin) {
if (message.chatType === ChatType.temp) { if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then(receivedTempUinMap => { dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) { if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid; receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap) dbUtil.setReceivedTempUinMap(receivedTempUinMap)
} }
}) })
} }
uidMaps[uid] = uin; uidMaps[uid] = uin
}
}
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
} }
} const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
// 自动清理新消息文件
const {autoDeleteFile} = getConfigUtil().getConfig();
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat;
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log("删除文件成功", path)
});
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
} }
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
} }
}
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({msgRecord}) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.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.msgId, 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)
}
} }
}
}) })
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20 selfInfo.online = info.info.status !== 20
}) })
let activatedPeerUids: string[] = [] let activatedPeerUids: string[] = []
registerReceiveHook<{ registerReceiveHook<{
changedRecentContactLists: { changedRecentContactLists: {
listType: number, sortedContactList: string[], listType: number
changedList: { sortedContactList: string[]
id: string, // peerUid changedList: {
chatType: ChatType id: string // peerUid
}[] chatType: ChatType
}[] }[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => { }>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) { for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) { for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue; if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id) activatedPeerUids.push(changedContact.id)
const peer = {peerUid: changedContact.id, chatType: changedContact.chatType} const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) { if (changedContact.chatType === ChatType.temp) {
log("收到临时会话消息", peer) log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then( NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
() => { NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
NTQQMsgApi.getMsgHistory(peer, "", 20).then(({msgList}) => { let lastTempMsg = msgList.pop()
let lastTempMsg = msgList.pop() log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
log("激活窗口之前的第一条临时会话消息:", lastTempMsg) if (Date.now() / 1000 - parseInt(lastTempMsg.msgTime) < 5) {
if ((Date.now() / 1000) - parseInt(lastTempMsg.msgTime) < 5) { OB11Constructor.message(lastTempMsg).then((r) => postOB11Event(r))
OB11Constructor.message(lastTempMsg).then(r => postOB11Event(r))
}
})
}
)
} else {
NTQQMsgApi.activateChat(peer).then()
} }
} })
})
} else {
NTQQMsgApi.activateChat(peer).then()
}
} }
}) }
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
} else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})

View File

@@ -1,197 +1,216 @@
import {ipcMain} from "electron"; import { ipcMain } from 'electron'
import {hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook} from "./hook"; import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook'
import {v4 as uuidv4} from "uuid" import { v4 as uuidv4 } from 'uuid'
import {log} from "../common/utils/log"; import { log } from '../common/utils/log'
import {NTQQWindow, NTQQWindowApi, NTQQWindows} from "./api/window"; import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window'
import {WebApi} from "./api/webapi"; import { WebApi } from './api/webapi'
import {HOOK_LOG} from "../common/config"; import { HOOK_LOG } from '../common/config'
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = "ns-ntApi", NT_API = 'ns-ntApi',
FS_API = "ns-FsApi", FS_API = 'ns-FsApi',
OS_API = "ns-OsApi", OS_API = 'ns-OsApi',
WINDOW_API = "ns-WindowApi", WINDOW_API = 'ns-WindowApi',
HOTUPDATE_API = "ns-HotUpdateApi", HOTUPDATE_API = 'ns-HotUpdateApi',
BUSINESS_API = "ns-BusinessApi", BUSINESS_API = 'ns-BusinessApi',
GLOBAL_DATA = "ns-GlobalDataApi", GLOBAL_DATA = 'ns-GlobalDataApi',
SKEY_API = "ns-SkeyApi", SKEY_API = 'ns-SkeyApi',
GROUP_HOME_WORK = "ns-GroupHomeWork", GROUP_HOME_WORK = 'ns-GroupHomeWork',
GROUP_ESSENCE = "ns-GroupEssence", GROUP_ESSENCE = 'ns-GroupEssence',
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息 ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息 ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelf", HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = "nodeIKernelMsgService/getMultiMsg", GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
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',
USER_DETAIL_INFO_WITH_BIZ_INFO = "nodeIKernelProfileService/getUserDetailInfoWithBizInfo", USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
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",
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",
SET_GROUP_TITLE = "nodeIKernelGroupService/modifyMemberSpecialTitle",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan', RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', SEND_MSG = 'nodeIKernelMsgService/sendMsg',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath', EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo', DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo', FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo', MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies',
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
// 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',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
OPEN_EXTRA_WINDOW = 'openExternalWindow', 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',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
GET_SKEY = "nodeIKernelTipOffService/getPskey", CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
UPDATE_SKEY = "updatePskey" CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_SKEY = 'nodeIKernelTipOffService/getPskey',
UPDATE_SKEY = 'updatePskey',
FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的
} }
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',
} }
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
} }
export function callNTQQApi<ReturnType>(params: NTQQApiParams) { export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let { let {
className, methodName, channel, args, className,
cbCmd, timeoutSecond: timeout, methodName,
classNameIsRegister, cmdCB, afterFirstCmd channel,
} = params; args,
className = className ?? NTQQApiClass.NT_API; cbCmd,
channel = channel ?? NTQQApiChannel.IPC_UP_2; timeoutSecond: timeout,
args = args ?? []; classNameIsRegister,
timeout = timeout ?? 5; cmdCB,
afterFirstCmd = afterFirstCmd ?? true; afterFirstCmd,
const uuid = uuidv4(); } = params
HOOK_LOG && log("callNTQQApi", channel, className, methodName, args, uuid) className = className ?? NTQQApiClass.NT_API
return new Promise((resolve: (data: ReturnType) => void, reject) => { channel = channel ?? NTQQApiChannel.IPC_UP_2
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) args = args ?? []
const _timeout = timeout * 1000 timeout = timeout ?? 5
let success = false afterFirstCmd = afterFirstCmd ?? true
let eventName = className + "-" + channel[channel.length - 1]; const uuid = uuidv4()
if (classNameIsRegister) { HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid)
eventName += "-register"; return new Promise((resolve: (data: ReturnType) => void, reject) => {
} // log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const apiArgs = [methodName, ...args] const _timeout = timeout * 1000
if (!cbCmd) { let success = false
// QQ后端会返回结果并且可以根据uuid识别 let eventName = className + '-' + channel[channel.length - 1]
hookApiCallbacks[uuid] = (r: ReturnType) => { if (classNameIsRegister) {
success = true eventName += '-register'
resolve(r) }
}; const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别
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(payload)) {
removeReceiveHook(hookId)
success = true
resolve(payload)
}
} else {
removeReceiveHook(hookId)
success = true
resolve(payload)
}
})
}
!afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback()
} else { } else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 success = true
const secondCallback = () => { reject(`ntqq api call failed, ${result.errMsg}`)
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
} else {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
})
}
!afterFirstCmd && secondCallback();
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback();
} else {
success = true
reject(`ntqq api call failed, ${result.errMsg}`);
}
}
} }
setTimeout(() => { }
// log("ntqq api timeout", success, channel, className, methodName) }
if (!success) { setTimeout(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs); // log("ntqq api timeout", success, channel, className, methodName)
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) if (!success) {
} log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs)
}, _timeout) reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
}
}, _timeout)
ipcMain.emit( ipcMain.emit(
channel, channel,
{ {
sender: { sender: {
send: (..._args: unknown[]) => { send: (..._args: unknown[]) => {},
}, },
}, },
}, { type: 'request', callbackId: uuid, eventName },
{type: 'request', callbackId: uuid, eventName}, apiArgs,
apiArgs )
) })
})
} }
export interface GeneralCallResult { export interface GeneralCallResult {
result: number, // 0: success result: number // 0: success
errMsg: string errMsg: string
} }
export class NTQQApi { export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[],) { static async call(className: NTQQApiClass, cmdName: string, args: any[]) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
className, className,
methodName: cmdName, methodName: cmdName,
args: [ args: [...args],
...args, })
] }
})
} static async fetchUnitedCommendConfig() {
} return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args: [
{
groups: ['100243'],
},
],
})
}
}

View File

@@ -1,65 +1,66 @@
import {ChatType} from "./msg"; import { ChatType } from './msg'
export interface CacheScanResult { export interface CacheScanResult {
result: number, result: number
size: [ // 单位为字节 size: [
string, // 系统总存储空间 // 单位为字节
string, // 系统可用存储空间 string, // 系统存储空间
string, // 系统用存储空间 string, // 系统用存储空间
string, // QQ总大小 string, // 系统已用存储空间
string, // 「聊天与文件」大小 string, // QQ总大小
string, // 未知 string, // 「聊天与文件」大小
string, // 「缓存数据」大小 string, // 未知
string, // 「其他数据」大小 string, // 「缓存数据」大小
string, // 未知 string, // 「其他数据」大小
] string, // 未知
]
} }
export interface ChatCacheList { export interface ChatCacheList {
pageCount: number, pageCount: number
infos: ChatCacheListItem[] infos: ChatCacheListItem[]
} }
export interface ChatCacheListItem { export interface ChatCacheListItem {
chatType: ChatType, chatType: ChatType
basicChatCacheInfo: ChatCacheListItemBasic, basicChatCacheInfo: ChatCacheListItemBasic
guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容 guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容
} }
export interface ChatCacheListItemBasic { export interface ChatCacheListItemBasic {
chatSize: string, chatSize: string
chatTime: string, chatTime: string
uid: string, uid: string
uin: string, uin: string
remarkName: string, remarkName: string
nickName: string, nickName: string
chatType?: ChatType, chatType?: ChatType
isChecked?: boolean isChecked?: boolean
} }
export enum CacheFileType { export enum CacheFileType {
IMAGE = 0, IMAGE = 0,
VIDEO = 1, VIDEO = 1,
AUDIO = 2, AUDIO = 2,
DOCUMENT = 3, DOCUMENT = 3,
OTHER = 4, OTHER = 4,
} }
export interface CacheFileList { export interface CacheFileList {
infos: CacheFileListItem[], infos: CacheFileListItem[]
} }
export interface CacheFileListItem { export interface CacheFileListItem {
fileSize: string, fileSize: string
fileTime: string, fileTime: string
fileKey: string, fileKey: string
elementId: string, elementId: string
elementIdStr: string, elementIdStr: string
fileType: CacheFileType, fileType: CacheFileType
path: string, path: string
fileName: string, fileName: string
senderId: string, senderId: string
previewPath: string, previewPath: string
senderName: string, senderName: string
isChecked?: boolean, isChecked?: boolean
} }

View File

@@ -1,56 +1,56 @@
import {QQLevel, Sex} from "./user"; import { QQLevel, Sex } from './user'
export interface Group { export interface Group {
groupCode: string, groupCode: string
maxMember: number, maxMember: number
memberCount: number, memberCount: number
groupName: string, groupName: string
groupStatus: 0, groupStatus: 0
memberRole: 2, memberRole: 2
isTop: boolean, isTop: boolean
toppedTimestamp: "0", toppedTimestamp: '0'
privilegeFlag: number, //65760 privilegeFlag: number //65760
isConf: boolean, isConf: boolean
hasModifyConfGroupFace: boolean, hasModifyConfGroupFace: boolean
hasModifyConfGroupName: boolean, hasModifyConfGroupName: boolean
remarkName: string, remarkName: string
hasMemo: boolean, hasMemo: boolean
groupShutupExpireTime: string, //"0", groupShutupExpireTime: string //"0",
personShutupExpireTime: string, //"0", personShutupExpireTime: string //"0",
discussToGroupUin: string, //"0", discussToGroupUin: string //"0",
discussToGroupMaxMsgSeq: number, discussToGroupMaxMsgSeq: number
discussToGroupTime: number, discussToGroupTime: number
groupFlagExt: number, //1073938496, groupFlagExt: number //1073938496,
authGroupType: number, //0, authGroupType: number //0,
groupCreditLevel: number, //0, groupCreditLevel: number //0,
groupFlagExt3: number, //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
"memberUin": string, //"0", memberUin: string //"0",
"memberUid": string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
}, }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
} }
export enum GroupMemberRole { export enum GroupMemberRole {
normal = 2, normal = 2,
admin = 3, admin = 3,
owner = 4 owner = 4,
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle: string; memberSpecialTitle: string
avatarPath: string; avatarPath: string
cardName: string; cardName: string
cardType: number; cardType: number
isDelete: boolean; isDelete: boolean
nick: string; nick: string
qid: string; qid: string
remark: string; remark: string
role: GroupMemberRole; // 群主:4, 管理员:3群员:2 role: GroupMemberRole // 群主:4, 管理员:3群员:2
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚 shutUpTime: number // 禁言时间,单位是什么暂时不清楚
uid: string; // 加密的字符串 uid: string // 加密的字符串
uin: string; // QQ号 uin: string // QQ号
isRobot: boolean; isRobot: boolean
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
} }

View File

@@ -1,7 +1,5 @@
export * from './user'
export * from './user'; export * from './group'
export * from './group'; export * from './msg'
export * from './msg'; export * from './notify'
export * from './notify'; export * from './cache'
export * from './cache';

View File

@@ -1,410 +1,416 @@
import {GroupMemberRole} from "./group"; import { GroupMemberRole } from './group'
import exp from "constants"; import exp from 'constants'
export enum ElementType { export enum ElementType {
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
PTT = 4, PTT = 4,
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
ARK = 10, ARK = 10,
MFACE = 11,
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT, elementType: ElementType.TEXT
elementId: "", elementId: ''
textElement: { textElement: {
content: string, content: string
atType: number, atType: number
atUid: string, atUid: string
atTinyId: string, atTinyId: string
atNtUid: string, atNtUid: string
} }
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT, elementType: ElementType.PTT
elementId: "", elementId: ''
pttElement: { pttElement: {
fileName: string, fileName: string
filePath: string, filePath: string
md5HexStr: string, md5HexStr: string
fileSize: number, fileSize: number
duration: number, // 单位是秒 duration: number // 单位是秒
formatType: number, formatType: number
voiceType: number, voiceType: number
voiceChangeType: number, voiceChangeType: number
canConvert2Text: boolean, canConvert2Text: boolean
waveAmplitudes: number[], waveAmplitudes: number[]
fileSubId: "", fileSubId: ''
playState: number, playState: number
autoConvertText: number, autoConvertText: number
} }
} }
export enum PicType { export enum PicType {
gif = 2000, gif = 2000,
jpg = 1000 jpg = 1000,
} }
export enum PicSubType { export enum PicSubType {
normal = 0, // 普通图片,大图 normal = 0, // 普通图片,大图
face = 1 // 表情包小图 face = 1, // 表情包小图
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC, elementType: ElementType.PIC
elementId: "", elementId: ''
picElement: { picElement: {
md5HexStr: string, md5HexStr: string
fileSize: number | string, fileSize: number | string
picWidth: number, picWidth: number
picHeight: number, picHeight: number
fileName: string, fileName: string
sourcePath: string, sourcePath: string
original: boolean, original: boolean
picType: PicType, picType: PicType
picSubType: PicSubType, picSubType: PicSubType
fileUuid: string, fileUuid: string
fileSubId: string, fileSubId: string
thumbFileSize: number, thumbFileSize: number
summary: string, summary: string
}, }
} }
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY, elementType: ElementType.REPLY
elementId: "", elementId: ''
replyElement: { replyElement: {
replayMsgSeq: string, replayMsgSeq: string
replayMsgId: string, replayMsgId: string
senderUin: string, senderUin: string
senderUinStr: string, senderUinStr: string
} }
} }
export interface SendFaceElement { export interface SendFaceElement {
elementType: ElementType.FACE, elementType: ElementType.FACE
elementId: "", elementId: ''
faceElement: FaceElement faceElement: FaceElement
}
export interface SendMarketFaceElement {
elementType: ElementType.MFACE
marketFaceElement: MarketFaceElement
} }
export interface FileElement { export interface FileElement {
"fileMd5"?: "", fileMd5?: ''
"fileName": string, fileName: string
"filePath": string, filePath: string
"fileSize": string, fileSize: string
"picHeight"?: number, picHeight?: number
"picWidth"?: number, picWidth?: number
"picThumbPath"?: {}, picThumbPath?: {}
"file10MMd5"?: "", file10MMd5?: ''
"fileSha"?: "", fileSha?: ''
"fileSha3"?: "", fileSha3?: ''
"fileUuid"?: "", fileUuid?: ''
"fileSubId"?: "", fileSubId?: ''
"thumbFileSize"?: number, thumbFileSize?: number
fileBizId?: number fileBizId?: number
} }
export interface SendFileElement { export interface SendFileElement {
elementType: ElementType.FILE elementType: ElementType.FILE
elementId: "", elementId: ''
fileElement: FileElement fileElement: FileElement
} }
export interface SendVideoElement { export interface SendVideoElement {
elementType: ElementType.VIDEO elementType: ElementType.VIDEO
elementId: "", elementId: ''
videoElement: VideoElement videoElement: VideoElement
} }
export interface SendArkElement { export interface SendArkElement {
elementType: ElementType.ARK, elementType: ElementType.ARK
elementId: "", elementId: ''
arkElement: ArkElement arkElement: ArkElement
} }
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement =
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement | SendVideoElement | SendArkElement | SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
atAll = 1, atAll = 1,
atUser = 2 atUser = 2,
} }
export enum ChatType { export enum ChatType {
friend = 1, friend = 1,
group = 2, group = 2,
temp = 100 temp = 100,
} }
export interface PttElement { export interface PttElement {
canConvert2Text: boolean; canConvert2Text: boolean
duration: number; // 秒数 duration: number // 秒数
fileBizId: null; fileBizId: null
fileId: number; // 0 fileId: number // 0
fileName: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr" fileName: string // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
filePath: string; // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr" filePath: string // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
fileSize: string; // "4261" fileSize: string // "4261"
fileSubId: string; // "0" fileSubId: string // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1 formatType: string // 1
invalidState: number; // 0 invalidState: number // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6" md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0 playState: number // 0
progress: number; // 0 progress: number // 0
text: string; // "" text: string // ""
transferStatus: number; // 0 transferStatus: number // 0
translateStatus: number; // 0 translateStatus: number // 0
voiceChangeType: number; // 0 voiceChangeType: number // 0
voiceType: number; // 0 voiceType: number // 0
waveAmplitudes: number[]; waveAmplitudes: number[]
} }
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'
export const IMAGE_HTTP_HOST_NT = "https://multimedia.nt.qq.com.cn" export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement { export interface PicElement {
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string; originImageMd5?: string
sourcePath: string; // 图片本地路径 sourcePath: string // 图片本地路径
thumbPath: Map<number, string>; thumbPath: Map<number, string>
picWidth: number; picWidth: number
picHeight: number; picHeight: number
fileSize: number; fileSize: number
fileName: string; fileName: string
fileUuid: string; fileUuid: string
md5HexStr?: string; md5HexStr?: string
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12, INVITE_NEW_MEMBER = 12,
MEMBER_NEW_TITLE = 17 MEMBER_NEW_TITLE = 17,
} }
export interface GrayTipElement { export interface GrayTipElement {
subElementType: GrayTipElementSubType; subElementType: GrayTipElementSubType
revokeElement: { revokeElement: {
operatorRole: string; operatorRole: string
operatorUid: string; operatorUid: string
operatorNick: string; operatorNick: string
operatorRemark: string; operatorRemark: string
operatorMemRemark?: string; operatorMemRemark?: string
wording: string; // 自定义的撤回提示语 wording: string // 自定义的撤回提示语
} }
aioOpGrayTipElement: TipAioOpGrayTipElement, aioOpGrayTipElement: TipAioOpGrayTipElement
groupElement: TipGroupElement, groupElement: TipGroupElement
xmlElement: { xmlElement: {
content: string; templId: string
}, content: string
jsonGrayTipElement: { }
jsonStr: string; jsonGrayTipElement: {
} jsonStr: string
}
}
export enum FaceType {
normal = 1, // 小黄脸
normal2 = 2, // 新小黄脸, 从faceIndex 222开始
dice = 3, // 骰子
}
export enum FaceIndex {
dice = 358,
RPS = 359, // 石头剪刀布
} }
export interface FaceElement { export interface FaceElement {
faceIndex: number, faceIndex: number
faceType: 1 faceType: FaceType
faceText?: string
packId?: string
stickerId?: string
sourceType?: number
stickerType?: number
resultId?: string
surpriseId?: string
randomType?: number
} }
export interface MarketFaceElement { export interface MarketFaceElement {
"itemType": 6, emojiPackageId: number
"faceInfo": 1, faceName?: string
"emojiPackageId": 203875, emojiId: string
"subType": 3, key: string
"mediaType": 0,
"imageWidth": 200,
"imageHeight": 200,
"faceName": string,
"emojiId": "094d53bd1c9ac5d35d04b08e8a6c992c",
"key": "a8b1dd0aebc8d910",
"param": null,
"mobileParam": null,
"sourceType": null,
"startTime": null,
"endTime": null,
"emojiType": 1,
"hasIpProduct": null,
"voiceItemHeightArr": null,
"sourceName": null,
"sourceJumpUrl": null,
"sourceTypeName": null,
"backColor": null,
"volumeColor": null,
"staticFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c_aio.png",
"dynamicFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c",
"supportSize": [
{
"width": 300,
"height": 300
},
{
"width": 200,
"height": 200
}
],
"apngSupportSize": null
} }
export interface VideoElement { export interface VideoElement {
"filePath": string, filePath: string
"fileName": string, fileName: string
"videoMd5"?: string, videoMd5?: string
"thumbMd5"?: string thumbMd5?: string
"fileTime"?: number, // second fileTime?: number // second
"thumbSize"?: number, // byte thumbSize?: number // byte
"fileFormat"?: number, // 2表示mp4 fileFormat?: number // 2表示mp4
"fileSize"?: string, // byte fileSize?: string // byte
"thumbWidth"?: number, thumbWidth?: number
"thumbHeight"?: number, thumbHeight?: number
"busiType"?: 0, // 未知 busiType?: 0 // 未知
"subBusiType"?: 0, // 未知 subBusiType?: 0 // 未知
"thumbPath"?: Map<number, any>, thumbPath?: Map<number, any>
"transferStatus"?: 0, // 未知 transferStatus?: 0 // 未知
"progress"?: 0, // 下载进度? progress?: 0 // 下载进度?
"invalidState"?: 0, // 未知 invalidState?: 0 // 未知
"fileUuid"?: string, // 可以用于下载链接? fileUuid?: string // 可以用于下载链接?
"fileSubId"?: "", fileSubId?: ''
"fileBizId"?: null, fileBizId?: null
"originVideoMd5"?: "", originVideoMd5?: ''
"import_rich_media_context"?: null, import_rich_media_context?: null
"sourceVideoCodecFormat"?: number sourceVideoCodecFormat?: number
} }
export interface MarkdownElement { export interface MarkdownElement {
content: string, content: string
} }
export interface InlineKeyboardElementRowButton{ export interface InlineKeyboardElementRowButton {
"id": "", id: ''
"label": string, label: string
"visitedLabel": string, visitedLabel: string
"style": 1, // 未知 style: 1 // 未知
"type": 2, // 未知 type: 2 // 未知
"clickLimit": 0, // 未知 clickLimit: 0 // 未知
"unsupportTips": "请升级新版手机QQ", unsupportTips: '请升级新版手机QQ'
"data": string, data: string
"atBotShowChannelList": false, atBotShowChannelList: false
"permissionType": 2, permissionType: 2
"specifyRoleIds": [], specifyRoleIds: []
"specifyTinyids": [], specifyTinyids: []
"isReply": false, isReply: false
"anchor": 0, anchor: 0
"enter": false, enter: false
"subscribeDataTemplateIds": [] subscribeDataTemplateIds: []
} }
export interface InlineKeyboardElement { export interface InlineKeyboardElement {
rows: [{ rows: [
buttons: InlineKeyboardElementRowButton[] {
}] buttons: InlineKeyboardElementRowButton[]
},
]
} }
export interface TipAioOpGrayTipElement { // 这是什么提示来着? export interface TipAioOpGrayTipElement {
operateType: number, // 这是什么提示来着?
peerUid: string, operateType: number
fromGrpCodeOfTmpChat: string, peerUid: string
fromGrpCodeOfTmpChat: string
} }
export enum TipGroupElementType { export enum TipGroupElementType {
memberIncrease = 1, memberIncrease = 1,
kicked = 3, // 被移出群 kicked = 3, // 被移出群
ban = 8 ban = 8,
} }
export interface TipGroupElement { export interface TipGroupElement {
"type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个 type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, // 暂时不知 role: 0 // 暂时不知
"groupName": string, // 暂时获取不到 groupName: string // 暂时获取不到
"memberUid": string, memberUid: string
"memberNick": string, memberNick: string
"memberRemark": string, memberRemark: string
"adminUid": string, adminUid: string
"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
"otherAddByYourQRCode": null, otherAddByYourQRCode: null
"youAddByOtherQRCode": null, youAddByOtherQRCode: null
"otherInviteOther": null, otherInviteOther: null
"otherInviteYou": null, otherInviteYou: null
"youInviteOther": null youInviteOther: null
}, }
"shutUp"?: { shutUp?: {
"curTime": string, curTime: string
"duration": string, // 禁言时间,秒 duration: string // 禁言时间,秒
"admin": { admin: {
"uid": string, uid: string
"card": string, card: string
"name": string, name: string
"role": GroupMemberRole role: GroupMemberRole
},
"member": {
"uid": string
"card": string,
"name": string,
"role": GroupMemberRole
}
} }
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
} }
export interface MultiForwardMsgElement{ export interface MultiForwardMsgElement {
xmlContent: string, // xml格式的消息内容 xmlContent: string // xml格式的消息内容
resId: string, resId: string
fileName: string, fileName: string
} }
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string
msgShortId?: number; // 自己维护的消息id msgType: number
msgTime: string; // 时间戳,秒 subMsgType: number
msgSeq: string; msgShortId?: number // 自己维护的消息id
senderUid: string; msgTime: string // 时间戳,秒
senderUin?: string; // 发送者QQ号 msgSeq: string
peerUid: string; // 群号 或者 QQ uid senderUid: string
peerUin: string; // 群号 或者 发送者QQ号 senderUin?: string // 发送者QQ号
sendNickName: string; peerUid: string // 群号 或者 QQ uid
sendMemberName?: string; // 发送者群名片 peerUin: string // 群号 或者 发送者QQ号
chatType: ChatType; sendNickName: string
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送 sendMemberName?: string // 发送者群名片
recallTime: string; // 撤回时间, "0"是没有撤回 chatType: ChatType
elements: { sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
elementId: string, recallTime: string // 撤回时间, "0"是没有撤回
elementType: ElementType; elements: {
replyElement: { elementId: string
senderUid: string; // 原消息发送者QQ号 elementType: ElementType
sourceMsgIsIncPic: boolean; // 原消息是否有图片 replyElement: {
sourceMsgText: string; senderUid: string // 原消息发送者QQ号
replayMsgSeq: string; // 消息的msgSeq可以通过这个找到源消息的msgId sourceMsgIsIncPic: boolean // 消息是否有图片
}; sourceMsgText: string
textElement: { replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
atType: AtType; }
atUid: string; // QQ号 textElement: {
content: string; atType: AtType
atNtUid: string; // uid atUid: string // QQ
}; content: string
picElement: PicElement; atNtUid: string // uid号
pttElement: PttElement; }
arkElement: ArkElement; picElement: PicElement
grayTipElement: GrayTipElement; pttElement: PttElement
faceElement: FaceElement; arkElement: ArkElement
videoElement: VideoElement; grayTipElement: GrayTipElement
fileElement: FileElement; faceElement: FaceElement
marketFaceElement: MarketFaceElement; videoElement: VideoElement
inlineKeyboardElement: InlineKeyboardElement; fileElement: FileElement
markdownElement: MarkdownElement; marketFaceElement: MarketFaceElement
multiForwardMsgElement: MultiForwardMsgElement; inlineKeyboardElement: InlineKeyboardElement
}[]; markdownElement: MarkdownElement
} multiForwardMsgElement: MultiForwardMsgElement
}[]
}

View File

@@ -1,65 +1,65 @@
export enum GroupNotifyTypes { export enum GroupNotifyTypes {
INVITE_ME = 1, INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群 INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7, JOIN_REQUEST = 7,
ADMIN_SET = 8, ADMIN_SET = 8,
KICK_MEMBER = 9, KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出 MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12, ADMIN_UNSET = 12, // 我被取消管理员
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员
} }
export interface GroupNotifies { export interface GroupNotifies {
doubt: boolean, doubt: boolean
nextStartSeq: string, nextStartSeq: string
notifies: GroupNotify[], notifies: GroupNotify[]
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
IGNORE = 0, IGNORE = 0,
WAIT_HANDLE = 1, WAIT_HANDLE = 1,
APPROVE = 2, APPROVE = 2,
REJECT = 3 REJECT = 3,
} }
export interface GroupNotify { export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify time: number // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 唯一标识符转成数字再除以1000应该就是时间戳 seq: string // 唯一标识符转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes, type: GroupNotifyTypes
status: GroupNotifyStatus, // 0是已忽略1是未处理2是已同意 status: GroupNotifyStatus // 0是已忽略1是未处理2是已同意
group: { groupCode: string, groupName: string }, group: { groupCode: string; groupName: string }
user1: { uid: string, nickName: string }, // 被设置管理员的人 user1: { uid: string; nickName: string } // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者 user2: { uid: string; nickName: string } // 操作者
actionUser: { uid: string, nickName: string }, //未知 actionUser: { uid: string; nickName: string } //未知
actionTime: string, actionTime: string
invitationExt: { invitationExt: {
srcType: number, // 0?未知 srcType: number // 0?未知
groupCode: string, waitStatus: number groupCode: string
}, waitStatus: number
postscript: string, // 加群用户填写的验证信息 }
repeatSeqs: [], postscript: string // 加群用户填写的验证信息
warningTips: string repeatSeqs: []
warningTips: string
} }
export enum GroupRequestOperateTypes { export enum GroupRequestOperateTypes {
approve = 1, approve = 1,
reject = 2 reject = 2,
} }
export interface FriendRequest { export interface FriendRequest {
friendUid: string, friendUid: string
reqTime: string, // 时间戳,秒 reqTime: string // 时间戳,秒
extWords: string, // 申请人填写的验证消息 extWords: string // 申请人填写的验证消息
isUnread: boolean, isUnread: boolean
friendNick: string, friendNick: string
sourceId: number, sourceId: number
groupCode: string groupCode: string
} }
export interface FriendRequestNotify { export interface FriendRequestNotify {
data: { data: {
unreadNums: number, unreadNums: number
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }

View File

@@ -1,75 +1,75 @@
export enum Sex { export enum Sex {
male = 0, male = 0,
female = 2, female = 2,
unknown = 255, unknown = 255,
} }
export interface QQLevel { export interface QQLevel {
"crownNum": number, crownNum: number
"sunNum": number, sunNum: number
"moonNum": number, moonNum: number
"starNum": number starNum: number
} }
export interface User { export interface User {
uid: string; // 加密的字符串 uid: string // 加密的字符串
uin: string; // QQ号 uin: string // QQ号
nick: string; nick: string
avatarUrl?: string; avatarUrl?: string
longNick?: string; // 签名 longNick?: string // 签名
remark?: string; remark?: string
sex?: Sex; sex?: Sex
qqLevel?: QQLevel, qqLevel?: QQLevel
qid?: string qid?: string
"birthday_year"?: number, birthday_year?: number
"birthday_month"?: number, birthday_month?: number
"birthday_day"?: number, birthday_day?: number
"topTime"?: string, topTime?: string
"constellation"?: number, constellation?: number
"shengXiao"?: number, shengXiao?: number
"kBloodType"?: number, kBloodType?: number
"homeTown"?: string, //"0-0-0", homeTown?: string //"0-0-0",
"makeFriendCareer"?: number, makeFriendCareer?: number
"pos"?: string, pos?: string
"eMail"?: string eMail?: string
"phoneNum"?: string, phoneNum?: string
"college"?: string, college?: string
"country"?: string, country?: string
"province"?: string, province?: string
"city"?: string, city?: string
"postCode"?: string, postCode?: string
"address"?: string, address?: string
"isBlock"?: boolean, isBlock?: boolean
"isSpecialCareOpen"?: boolean, isSpecialCareOpen?: boolean
"isSpecialCareZone"?: boolean, isSpecialCareZone?: boolean
"ringId"?: string, ringId?: string
"regTime"?: number, regTime?: number
interest?: string, interest?: string
"labels"?: string[], labels?: string[]
"isHideQQLevel"?: number, isHideQQLevel?: number
"privilegeIcon"?: { privilegeIcon?: {
"jumpUrl": string, jumpUrl: string
"openIconList": unknown[], openIconList: unknown[]
"closeIconList": unknown[] closeIconList: unknown[]
}, }
"photoWall"?: { photoWall?: {
"picList": unknown[] picList: unknown[]
}, }
"vipFlag"?: boolean, vipFlag?: boolean
"yearVipFlag"?: boolean, yearVipFlag?: boolean
"svipFlag"?: boolean, svipFlag?: boolean
"vipLevel"?: number, vipLevel?: number
"status"?: number, status?: number
"qidianMasterFlag"?: number, qidianMasterFlag?: number
"qidianCrewFlag"?: number, qidianCrewFlag?: number
"qidianCrewFlag2"?: number, qidianCrewFlag2?: number
"extStatus"?: number, extStatus?: number
"recommendImgFlag"?: number, recommendImgFlag?: number
"disableEmojiShortCuts"?: number, disableEmojiShortCuts?: number
"pendantId"?: string, pendantId?: string
} }
export interface SelfInfo extends User { export interface SelfInfo extends User {
online?: boolean; online?: boolean
} }
export interface Friend extends User {} export interface Friend extends User {}

View File

@@ -1,49 +1,49 @@
import {ActionName, BaseCheckResult} from "./types" import { ActionName, BaseCheckResult } from './types'
import {OB11Response} from "./OB11Response" import { OB11Response } from './OB11Response'
import {OB11Return} from "../types"; import { OB11Return } from '../types'
import {log} from "../../common/utils/log"; import { log } from '../../common/utils/log'
class BaseAction<PayloadType, ReturnDataType> { class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> { protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return { return {
valid: true, valid: true,
}
} }
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> { public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload); const result = await this.check(payload)
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message, 400); return OB11Response.error(result.message, 400)
}
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
log("发生错误", e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || "未知错误,可能操作超时", 200);
}
} }
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData)
} catch (e) {
log('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
}
}
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload) const result = await this.check(payload)
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message, 1400) return OB11Response.error(result.message, 1400)
}
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo);
} catch (e) {
log("发生错误", e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}
} }
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo)
} catch (e) {
log('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}
}
protected async _handle(payload: PayloadType): Promise<ReturnDataType> { protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
throw `pleas override ${this.actionName} _handle`; throw `pleas override ${this.actionName} _handle`
} }
} }
export default BaseAction export default BaseAction

View File

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

View File

@@ -1,110 +1,110 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import fs from "fs/promises"; import fs from 'fs/promises'
import {dbUtil} from "../../../common/db"; import { dbUtil } from '../../../common/db'
import {getConfigUtil} from "../../../common/config"; import { getConfigUtil } from '../../../common/config'
import {log, sleep, uri2local} from "../../../common/utils"; import { log, sleep, uri2local } from '../../../common/utils'
import {NTQQFileApi} from "../../../ntqqapi/api/file"; import { NTQQFileApi } from '../../../ntqqapi/api/file'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {FileElement, RawMessage, VideoElement} from "../../../ntqqapi/types"; import { FileElement, RawMessage, VideoElement } from '../../../ntqqapi/types'
import { FileCache } from '../../../common/types'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid
} }
export interface GetFileResponse { export interface GetFileResponse {
file?: string // path file?: string // path
url?: string url?: string
file_size?: string file_size?: string
file_name?: string file_name?: string
base64?: string base64?: string
} }
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage): {id: string, element: VideoElement | FileElement}{ private getElement(msg: RawMessage): { id: string; element: VideoElement | FileElement } {
let element = msg.elements.find(e=>e.fileElement) let element = msg.elements.find((e) => e.fileElement)
if (!element){ if (!element) {
element = msg.elements.find(e=>e.videoElement) element = msg.elements.find((e) => e.videoElement)
return {id: element.elementId, element: element.videoElement} return { id: element.elementId, element: element.videoElement }
}
return {id: element.elementId, element: element.fileElement}
} }
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { return { id: element.elementId, element: element.fileElement }
const cache = await dbUtil.getFileCache(payload.file) }
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig() private async download(cache: FileCache, file: string) {
if (!cache) { log('需要调用 NTQQ 下载文件api')
throw new Error('file not found') if (cache.msgId) {
} let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (cache.downloadFunc) { if (msg) {
await cache.downloadFunc() log('找到了文件 msg', msg)
let element = this.getElement(msg)
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, element.id, '', '', true)
await sleep(1000)
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg).element.filePath
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file)
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (!cache) {
throw new Error('file not found')
}
if (cache.downloadFunc) {
await cache.downloadFunc()
}
try {
await fs.access(cache.filePath, fs.constants.F_OK)
} catch (e) {
// log("file not found", e)
if (cache.url) {
const downloadResult = await uri2local(cache.url)
if (downloadResult.success) {
cache.filePath = downloadResult.path
dbUtil.addFileCache(payload.file, cache).then()
} else {
await this.download(cache, payload.file)
} }
} else {
// 没有url的可能是私聊文件或者群文件需要自己下载
await this.download(cache, payload.file)
}
}
let res: GetFileResponse = {
file: cache.filePath,
url: cache.url,
file_size: cache.fileSize,
file_name: cache.fileName,
}
if (enableLocalFile2Url) {
if (!cache.url) {
try { try {
await fs.access(cache.filePath, fs.constants.F_OK) res.base64 = await fs.readFile(cache.filePath, 'base64')
} catch (e) { } catch (e) {
log("file not found", e) throw new Error('文件下载失败. ' + e)
if (cache.url){
const downloadResult = await uri2local(cache.url)
if (downloadResult.success) {
cache.filePath = downloadResult.path
dbUtil.addFileCache(payload.file, cache).then()
} else {
throw new Error("file download failed. " + downloadResult.errMsg)
}
}
else{
// 没有url的可能是私聊文件或者群文件需要自己下载
log("需要调用 NTQQ 下载文件api")
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg){
log("找到了文件 msg", msg)
let element = this.getElement(msg);
log("找到了文件 element", element);
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.id, "", "", true)
await sleep(1000);
msg = await dbUtil.getMsgByLongId(cache.msgId)
log("下载完成后的msg", msg)
cache.filePath = this.getElement(msg).element.filePath
dbUtil.addFileCache(payload.file, cache).then()
}
}
}
} }
let res: GetFileResponse = { }
file: cache.filePath,
url: cache.url,
file_size: cache.fileSize,
file_name: cache.fileName
}
if (enableLocalFile2Url) {
if (!cache.url) {
try{
res.base64 = await fs.readFile(cache.filePath, 'base64')
}catch (e) {
throw new Error("文件下载失败. " + e)
}
}
}
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
} }
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
}
} }
export default class GetFile extends GetFileBase { export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile actionName = ActionName.GetFile
protected async _handle(payload: {file_id: string, file: string}): Promise<GetFileResponse> { protected async _handle(payload: { file_id: string; file: string }): Promise<GetFileResponse> {
if (!payload.file_id) { if (!payload.file_id) {
throw new Error('file_id 不能为空') throw new Error('file_id 不能为空')
}
payload.file = payload.file_id
return super._handle(payload);
} }
} payload.file = payload.file_id
return super._handle(payload)
}
}

View File

@@ -1,7 +1,6 @@
import {GetFileBase} from "./GetFile"; import { GetFileBase } from './GetFile'
import {ActionName} from "../types"; import { ActionName } from '../types'
export default class GetImage extends GetFileBase { export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage actionName = ActionName.GetImage
} }

View File

@@ -1,15 +1,15 @@
import {GetFileBase, GetFilePayload, GetFileResponse} from "./GetFile"; import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import {ActionName} from "../types"; import { ActionName } from '../types'
interface Payload extends GetFilePayload { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
} }
export default class GetRecord extends GetFileBase { export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> { protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload); let res = super._handle(payload)
return res; return res
} }
} }

View File

@@ -1,73 +1,72 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import fs from "fs"; import fs from 'fs'
import {join as joinPath} from "node:path"; import { join as joinPath } from 'node:path'
import {calculateFileMD5, httpDownload, TEMP_DIR} from "../../../common/utils"; import { calculateFileMD5, httpDownload, TEMP_DIR } from '../../../common/utils'
import {v4 as uuid4} from "uuid"; import { v4 as uuid4 } from 'uuid'
interface Payload { interface Payload {
thread_count?: number thread_count?: number
url?: string url?: string
base64?: string base64?: string
name?: string name?: string
headers?: string | string[] headers?: string | string[]
} }
interface FileResponse { interface FileResponse {
file: string file: string
} }
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> { export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile actionName = ActionName.GoCQHTTP_DownloadFile
protected async _handle(payload: Payload): Promise<FileResponse> { protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name const isRandomName = !payload.name
let name = payload.name || uuid4(); let name = payload.name || uuid4()
const filePath = joinPath(TEMP_DIR, name); const filePath = joinPath(TEMP_DIR, name)
if (payload.base64) { if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64') fs.writeFileSync(filePath, payload.base64, 'base64')
} else if (payload.url) { } else if (payload.url) {
const headers = this.getHeaders(payload.headers); const headers = this.getHeaders(payload.headers)
let buffer = await httpDownload({url: payload.url, headers: headers}) let buffer = await httpDownload({ url: payload.url, headers: headers })
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary'); fs.writeFileSync(filePath, Buffer.from(buffer), 'binary')
} else { } else {
throw new Error("不存在任何文件, 无法下载") throw new Error('不存在任何文件, 无法下载')
}
if (fs.existsSync(filePath)) {
if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath);
const newPath = joinPath(TEMP_DIR, md5);
fs.renameSync(filePath, newPath);
return { file: newPath }
}
return { file: filePath }
} else {
throw new Error("文件写入失败, 检查权限")
}
} }
if (fs.existsSync(filePath)) {
getHeaders(headersIn?: string | string[]): Record<string, string> { if (isRandomName) {
const headers = {}; // 默认实现要名称未填写时文件名为文件 md5
if (typeof headersIn == 'string') { const md5 = await calculateFileMD5(filePath)
headersIn = headersIn.split('[\\r\\n]'); const newPath = joinPath(TEMP_DIR, md5)
} fs.renameSync(filePath, newPath)
if (Array.isArray(headersIn)) { return { file: newPath }
for (const headerItem of headersIn) { }
const spilt = headerItem.indexOf('='); return { file: filePath }
if (spilt < 0) { } else {
headers[headerItem] = ""; throw new Error('文件写入失败, 检查权限')
} else {
const key = headerItem.substring(0, spilt);
headers[key] = headerItem.substring(0, spilt + 1);
}
}
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/octet-stream';
}
return headers;
} }
} }
getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {}
if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]')
}
if (Array.isArray(headersIn)) {
for (const headerItem of headersIn) {
const spilt = headerItem.indexOf('=')
if (spilt < 0) {
headers[headerItem] = ''
} else {
const key = headerItem.substring(0, spilt)
headers[key] = headerItem.substring(0, spilt + 1)
}
}
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/octet-stream'
}
return headers
}
}

View File

@@ -1,39 +1,45 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11ForwardMessage, OB11Message, OB11MessageData} from "../../types"; import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import {NTQQMsgApi, Peer} from "../../../ntqqapi/api"; import { NTQQMsgApi, Peer } from '../../../ntqqapi/api'
import {dbUtil} from "../../../common/db"; import { dbUtil } from '../../../common/db'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {ActionName} from "../types"; import { ActionName } from '../types'
interface Payload { interface Payload {
message_id: string; // long msg id message_id: string // long msg id
} }
interface Response{ interface Response {
messages: (OB11Message & {content: OB11MessageData})[] messages: (OB11Message & { content: OB11MessageData })[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any>{ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const rootMsg = await dbUtil.getMsgByLongId(payload.message_id) const rootMsg = await dbUtil.getMsgByLongId(payload.message_id)
if (!rootMsg){ if (!rootMsg) {
throw Error("msg not found") throw Error('msg not found')
}
let data = await NTQQMsgApi.getMultiMsg({chatType: rootMsg.chatType, peerUid: rootMsg.peerUid}, rootMsg.msgId, rootMsg.msgId)
if (data.result !== 0){
throw Error("找不到相关的聊天记录" + data.errMsg)
}
let msgList = data.msgList
let messages = await Promise.all(msgList.map(async msg => {
let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg);
return resMsg
}))
messages.map(msg => {
(<OB11ForwardMessage>msg).content = msg.message;
delete msg.message;
})
return {messages}
} }
} let data = await NTQQMsgApi.getMultiMsg(
{ chatType: rootMsg.chatType, peerUid: rootMsg.peerUid },
rootMsg.msgId,
rootMsg.msgId,
)
if (data.result !== 0) {
throw Error('找不到相关的聊天记录' + data.errMsg)
}
let msgList = data.msgList
let messages = await Promise.all(
msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg)
return resMsg
}),
)
messages.map((msg) => {
;(<OB11ForwardMessage>msg).content = msg.message
delete msg.message
})
return { messages }
}
}

View File

@@ -1,39 +1,46 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11Message, OB11User} from "../../types"; import { OB11Message, OB11User } from '../../types'
import {groups} from "../../../common/data"; import { groups } from '../../../common/data'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {ChatType} from "../../../ntqqapi/types"; import { ChatType } from '../../../ntqqapi/types'
import {dbUtil} from "../../../common/db"; import { dbUtil } from '../../../common/db'
import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {log} from "../../../common/utils"; import { log } from '../../../common/utils'
interface Payload { interface Payload {
group_id: number group_id: number
message_seq: number, message_seq: number
count: number count: number
} }
interface Response{ interface Response {
messages: OB11Message[] messages: OB11Message[]
} }
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> { export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const group = groups.find(group => group.groupCode === payload.group_id.toString()) const group = groups.find((group) => group.groupCode === payload.group_id.toString())
if (!group) { if (!group) {
throw `${payload.group_id}不存在` throw `${payload.group_id}不存在`
}
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0"
// log("startMsgId", startMsgId)
let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, parseInt(payload.count?.toString()) || 20)).msgList
await Promise.all(msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg)
}))
const ob11MsgList = await Promise.all(msgList.map(msg=>OB11Constructor.message(msg)))
return {"messages": ob11MsgList}
} }
} const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0'
// log("startMsgId", startMsgId)
let msgList = (
await NTQQMsgApi.getMsgHistory(
{ chatType: ChatType.group, peerUid: group.groupCode },
startMsgId,
parseInt(payload.count?.toString()) || 20,
)
).msgList
await Promise.all(
msgList.map(async (msg) => {
msg.msgShortId = await dbUtil.addMsg(msg)
}),
)
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg)))
return { messages: ob11MsgList }
}
}

View File

@@ -1,20 +1,19 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11User} from "../../types"; import { OB11User } from '../../types'
import {getUidByUin, uidMaps} from "../../../common/data"; import { getUidByUin, uidMaps } from '../../../common/data'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import { NTQQUserApi } from '../../../ntqqapi/api/user'
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> { protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString() const user_id = payload.user_id.toString()
const uid = getUidByUin(user_id) const uid = getUidByUin(user_id)
if (!uid) { if (!uid) {
throw new Error("查无此人") throw new Error('查无此人')
}
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
} }
} return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
}
}

View File

@@ -1,20 +1,20 @@
import SendMsg, {convertMessage2List} from "../msg/SendMsg"; import SendMsg, { convertMessage2List } from '../msg/SendMsg'
import {OB11PostSendMsg} from "../../types"; import { OB11PostSendMsg } from '../../types'
import {ActionName} from "../types"; import { ActionName } from '../types'
export class GoCQHTTPSendForwardMsg extends SendMsg { export class GoCQHTTPSendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg; actionName = ActionName.GoCQHTTP_SendForwardMsg
protected async check(payload: OB11PostSendMsg) { protected async check(payload: OB11PostSendMsg) {
if (payload.messages) payload.message = convertMessage2List(payload.messages); if (payload.messages) payload.message = convertMessage2List(payload.messages)
return super.check(payload); return super.check(payload)
} }
} }
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg { export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg; actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
} }
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg { export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg; actionName = ActionName.GoCQHTTP_SendGroupForwardMsg
} }

View File

@@ -0,0 +1,49 @@
import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } from '../../../common/data'
import { ActionName } from '../types'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import { ChatType, SendFileElement } from '../../../ntqqapi/types'
import fs from 'fs'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg'
import { uri2local } from '../../../common/utils'
interface Payload {
user_id: number
group_id?: number
file: string
name: string
folder: string
}
class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
getPeer(payload: Payload): Peer {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) }
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
}
protected async _handle(payload: Payload): Promise<null> {
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (downloadResult.errMsg) {
throw new Error(downloadResult.errMsg)
}
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name)
await NTQQMsgApi.sendMsg(this.getPeer(payload), [sendFileEle])
return null
}
}
export class GoCQHTTPUploadGroupFile extends GoCQHTTPUploadFileBase {
actionName = ActionName.GoCQHTTP_UploadGroupFile
}
export class GoCQHTTPUploadPrivateFile extends GoCQHTTPUploadFileBase {
actionName = ActionName.GoCQHTTP_UploadPrivateFile
}

View File

@@ -1,37 +0,0 @@
import BaseAction from "../BaseAction";
import {getGroup} from "../../../common/data";
import {ActionName} from "../types";
import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import {ChatType, SendFileElement} from "../../../ntqqapi/types";
import fs from "fs";
import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
import {uri2local} from "../../../common/utils";
interface Payload{
group_id: number
file: string
name: string
folder: string
}
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id.toString());
if (!group){
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file;
if (fs.existsSync(file)){
file = `file://${file}`
}
const downloadResult = await uri2local(file);
if (downloadResult.errMsg){
throw new Error(downloadResult.errMsg)
}
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await NTQQMsgApi.sendMsg({chatType: ChatType.group, peerUid: group.groupCode}, [sendFileEle]);
return null
}
}

View File

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

View File

@@ -1,22 +1,31 @@
import {OB11Group} from '../../types'; import { OB11Group } from '../../types'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {groups} from "../../../common/data"; import { groups } from '../../../common/data'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {NTQQGroupApi} from "../../../ntqqapi/api"; import { NTQQGroupApi } from '../../../ntqqapi/api'
import {log} from "../../../common/utils"; import { log } from '../../../common/utils'
interface Payload {
class GetGroupList extends BaseAction<null, OB11Group[]> { no_cache: boolean | string
actionName = ActionName.GetGroupList
protected async _handle(payload: null) {
// if (groups.length === 0) {
// const groups = await NTQQGroupApi.getGroups(true)
// log("get groups", groups)
// }
return OB11Constructor.groups(groups);
}
} }
export default GetGroupList class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) {
if (
groups.length === 0
|| payload?.no_cache === true || payload?.no_cache === 'true'
) {
try {
const groups = await NTQQGroupApi.getGroups(true)
log("强制刷新群列表, 数量:", groups.length)
return OB11Constructor.groups(groups)
} catch (e) {}
}
return OB11Constructor.groups(groups)
}
}
export default GetGroupList

View File

@@ -1,35 +1,34 @@
import {OB11GroupMember} from '../../types'; import { OB11GroupMember } from '../../types'
import {getGroupMember} from "../../../common/data"; import { getGroupMember } from '../../../common/data'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import { NTQQUserApi } from '../../../ntqqapi/api/user'
import {log} from "../../../common/utils/log"; import { log } from '../../../common/utils/log'
import {isNull} from "../../../common/utils/helper"; import { isNull } from '../../../common/utils/helper'
export interface PayloadType { export interface PayloadType {
group_id: number group_id: number
user_id: number user_id: number
} }
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> { class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: PayloadType) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) { if (member) {
if (isNull(member.sex)){ if (isNull(member.sex)) {
log("获取群成员详细信息") log('获取群成员详细信息')
let info = (await NTQQUserApi.getUserDetailInfo(member.uid, true)) let info = await NTQQUserApi.getUserDetailInfo(member.uid, true)
log("群成员详细信息结果", info) log('群成员详细信息结果', info)
Object.assign(member, info); Object.assign(member, info)
} }
return OB11Constructor.groupMember(payload.group_id.toString(), member) return OB11Constructor.groupMember(payload.group_id.toString(), member)
} else { } else {
throw (`群成员${payload.user_id}不存在`) throw `群成员${payload.user_id}不存在`
}
} }
}
} }
export default GetGroupMemberInfo export default GetGroupMemberInfo

View File

@@ -1,29 +1,31 @@
import {OB11GroupMember} from '../../types'; import { OB11GroupMember } from '../../types'
import {getGroup} from "../../../common/data"; import { getGroup } from '../../../common/data'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {NTQQGroupApi} from "../../../ntqqapi/api/group"; import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils'
export interface PayloadType { export interface PayloadType {
group_id: number group_id: number,
no_cache: boolean | string
} }
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> { class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType) { protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString()); const group = await getGroup(payload.group_id.toString())
if (group) { if (group) {
if (!group.members?.length) { if (!group.members?.length || payload.no_cache === true || payload.no_cache === 'true') {
group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
} log('强制刷新群成员列表, 数量: ', group.members.length)
return OB11Constructor.groupMembers(group); }
} else { return OB11Constructor.groupMembers(group)
throw (`${payload.group_id}不存在`) } else {
} throw `${payload.group_id}不存在`
} }
}
} }
export default GetGroupMemberList export default GetGroupMemberList

View File

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

View File

@@ -1,18 +1,17 @@
import SendMsg from "../msg/SendMsg"; import SendMsg from '../msg/SendMsg'
import {ActionName, BaseCheckResult} from "../types"; import { ActionName, BaseCheckResult } from '../types'
import {OB11PostSendMsg} from "../../types"; import { OB11PostSendMsg } from '../../types'
import {log} from "../../../common/utils/log";
import { log } from '../../../common/utils/log'
class SendGroupMsg extends SendMsg { class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg actionName = ActionName.SendGroupMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
delete payload.user_id; delete payload.user_id
payload.message_type = "group" payload.message_type = 'group'
return super.check(payload); return super.check(payload)
} }
} }
export default SendGroupMsg export default SendGroupMsg

View File

@@ -1,26 +1,27 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {GroupRequestOperateTypes} from "../../../ntqqapi/types"; import { GroupRequestOperateTypes } from '../../../ntqqapi/types'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {NTQQGroupApi} from "../../../ntqqapi/api/group"; import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload { interface Payload {
flag: string, flag: string
// sub_type: "add" | "invite", // sub_type: "add" | "invite",
// type: "add" | "invite" // type: "add" | "invite"
approve: boolean, approve: boolean
reason: string reason: string
} }
export default class SetGroupAddRequest extends BaseAction<Payload, null> { export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest actionName = ActionName.SetGroupAddRequest
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 approve = payload.approve.toString() === "true"; const approve = payload.approve.toString() === 'true'
await NTQQGroupApi.handleGroupRequest(seq, await NTQQGroupApi.handleGroupRequest(
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, seq,
payload.reason approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
) payload.reason,
return null )
} return null
} }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,92 +8,109 @@ import GetGroupMemberInfo from './group/GetGroupMemberInfo'
import SendGroupMsg from './group/SendGroupMsg' import SendGroupMsg from './group/SendGroupMsg'
import SendPrivateMsg from './msg/SendPrivateMsg' import SendPrivateMsg from './msg/SendPrivateMsg'
import SendMsg from './msg/SendMsg' import SendMsg from './msg/SendMsg'
import DeleteMsg from "./msg/DeleteMsg"; import DeleteMsg from './msg/DeleteMsg'
import BaseAction from "./BaseAction"; import BaseAction from './BaseAction'
import GetVersionInfo from "./system/GetVersionInfo"; import GetVersionInfo from './system/GetVersionInfo'
import CanSendRecord from "./system/CanSendRecord"; import CanSendRecord from './system/CanSendRecord'
import CanSendImage from "./system/CanSendImage"; import CanSendImage from './system/CanSendImage'
import GetStatus from "./system/GetStatus"; import GetStatus from './system/GetStatus'
import {GoCQHTTPSendForwardMsg, GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg"; import {
import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo"; GoCQHTTPSendForwardMsg,
import SendLike from "./user/SendLike"; GoCQHTTPSendGroupForwardMsg,
import SetGroupAddRequest from "./group/SetGroupAddRequest"; GoCQHTTPSendPrivateForwardMsg,
import SetGroupLeave from "./group/SetGroupLeave"; } from './go-cqhttp/SendForwardMsg'
import GetGuildList from "./group/GetGuildList"; import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo'
import Debug from "./llonebot/Debug"; import SendLike from './user/SendLike'
import SetFriendAddRequest from "./user/SetFriendAddRequest"; import SetGroupAddRequest from './group/SetGroupAddRequest'
import SetGroupWholeBan from "./group/SetGroupWholeBan"; import SetGroupLeave from './group/SetGroupLeave'
import SetGroupName from "./group/SetGroupName"; import GetGuildList from './group/GetGuildList'
import SetGroupBan from "./group/SetGroupBan"; import Debug from './llonebot/Debug'
import SetGroupKick from "./group/SetGroupKick"; import SetFriendAddRequest from './user/SetFriendAddRequest'
import SetGroupAdmin from "./group/SetGroupAdmin"; import SetGroupWholeBan from './group/SetGroupWholeBan'
import SetGroupCard from "./group/SetGroupCard"; import SetGroupName from './group/SetGroupName'
import GetImage from "./file/GetImage"; import SetGroupBan from './group/SetGroupBan'
import GetRecord from "./file/GetRecord"; import SetGroupKick from './group/SetGroupKick'
import GoCQHTTPMarkMsgAsRead from "./msg/MarkMsgAsRead"; import SetGroupAdmin from './group/SetGroupAdmin'
import CleanCache from "./system/CleanCache"; import SetGroupCard from './group/SetGroupCard'
import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile"; import GetImage from './file/GetImage'
import {GetConfigAction, SetConfigAction} from "./llonebot/Config"; import GetRecord from './file/GetRecord'
import GetGroupAddRequest from "./llonebot/GetGroupAddRequest"; import GoCQHTTPMarkMsgAsRead from './msg/MarkMsgAsRead'
import CleanCache from './system/CleanCache'
import { GoCQHTTPUploadGroupFile, GoCQHTTPUploadPrivateFile } from './go-cqhttp/UploadFile'
import { GetConfigAction, SetConfigAction } from './llonebot/Config'
import GetGroupAddRequest from './llonebot/GetGroupAddRequest'
import SetQQAvatar from './llonebot/SetQQAvatar' import SetQQAvatar from './llonebot/SetQQAvatar'
import GoCQHTTPDownloadFile from "./go-cqhttp/DownloadFile"; import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile'
import GoCQHTTPGetGroupMsgHistory from "./go-cqhttp/GetGroupMsgHistory"; import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory'
import GetFile from "./file/GetFile"; import GetFile from './file/GetFile'
import {GoCQHTTGetForwardMsgAction} from "./go-cqhttp/GetForwardMsg"; import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardSingleGroupMsg } from './msg/ForwardSingleMsg'
export const actionHandlers = [ export const actionHandlers = [
new GetFile(), new GetFile(),
new Debug(), new Debug(),
new GetConfigAction(), new GetConfigAction(),
new SetConfigAction(), new SetConfigAction(),
new GetGroupAddRequest(), new GetGroupAddRequest(),
new SetQQAvatar(), new SetQQAvatar(),
// onebot11 // onebot11
new SendLike(), new SendLike(),
new GetMsg(), new GetMsg(),
new GetLoginInfo(), new GetLoginInfo(),
new GetFriendList(), new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new GetGroupList(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new GetGroupInfo(),
new DeleteMsg(), new GetGroupMemberList(),
new SetGroupAddRequest(), new GetGroupMemberInfo(),
new SetFriendAddRequest(), new SendGroupMsg(),
new SetGroupLeave(), new SendPrivateMsg(),
new GetVersionInfo(), new SendMsg(),
new CanSendRecord(), new DeleteMsg(),
new CanSendImage(), new SetGroupAddRequest(),
new GetStatus(), new SetFriendAddRequest(),
new SetGroupWholeBan(), new SetGroupLeave(),
new SetGroupBan(), new GetVersionInfo(),
new SetGroupKick(), new CanSendRecord(),
new SetGroupAdmin(), new CanSendImage(),
new SetGroupName(), new GetStatus(),
new SetGroupCard(), new SetGroupWholeBan(),
new GetImage(), new SetGroupBan(),
new GetRecord(), new SetGroupKick(),
new CleanCache(), new SetGroupAdmin(),
new SetGroupName(),
//以下为go-cqhttp api new SetGroupCard(),
new GoCQHTTPSendForwardMsg(), new GetImage(),
new GoCQHTTPSendGroupForwardMsg(), new GetRecord(),
new GoCQHTTPSendPrivateForwardMsg(), new CleanCache(),
new GoCQHTTPGetStrangerInfo(), new GetCookies(),
new GoCQHTTPDownloadFile(), new SetMsgEmojiLike(),
new GetGuildList(), new ForwardFriendSingleMsg(),
new GoCQHTTPMarkMsgAsRead(), new ForwardSingleGroupMsg(),
new GoCQHTTPUploadGroupFile(), //以下为go-cqhttp api
new GoCQHTTPGetGroupMsgHistory(), new GoCQHTTPSendForwardMsg(),
new GoCQHTTGetForwardMsgAction(), new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GoCQHTTPDownloadFile(),
new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(),
new GoCQHTTPUploadPrivateFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(),
] ]
function initActionMap() { function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>(); const actionMap = new Map<string, BaseAction<any, any>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {
actionMap.set(action.actionName, action); actionMap.set(action.actionName, action)
} actionMap.set(action.actionName + '_async', action)
actionMap.set(action.actionName + '_rate_limited', action)
}
return actionMap return actionMap
} }
export const actionMap = initActionMap(); export const actionMap = initActionMap()

View File

@@ -1,20 +1,19 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {Config} from "../../../common/types"; import { Config } from '../../../common/types'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {setConfig} from "../../../main/setConfig"; import { setConfig } from '../../../main/setConfig'
import {getConfigUtil} from "../../../common/config"; import { getConfigUtil } from '../../../common/config'
export class GetConfigAction extends BaseAction<null, Config> { export class GetConfigAction extends BaseAction<null, Config> {
actionName = ActionName.GetConfig actionName = ActionName.GetConfig
protected async _handle(payload: null): Promise<Config> { protected async _handle(payload: null): Promise<Config> {
return getConfigUtil().getConfig() return getConfigUtil().getConfig()
} }
} }
export class SetConfigAction extends BaseAction<Config, void> { export class SetConfigAction extends BaseAction<Config, void> {
actionName = ActionName.SetConfig actionName = ActionName.SetConfig
protected async _handle(payload: Config): Promise<void> { protected async _handle(payload: Config): Promise<void> {
setConfig(payload).then(); setConfig(payload).then()
} }
} }

View File

@@ -1,42 +1,42 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
// import * as ntqqApi from "../../../ntqqapi/api"; // import * as ntqqApi from "../../../ntqqapi/api";
import { import {
NTQQMsgApi, NTQQMsgApi,
NTQQFriendApi, NTQQFriendApi,
NTQQGroupApi, NTQQGroupApi,
NTQQUserApi, NTQQUserApi,
NTQQFileApi, NTQQFileApi,
NTQQFileCacheApi, NTQQFileCacheApi,
NTQQWindowApi, NTQQWindowApi,
} from "../../../ntqqapi/api"; } from '../../../ntqqapi/api'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {log} from "../../../common/utils/log"; import { log } from '../../../common/utils/log'
interface Payload { interface Payload {
method: string, method: string
args: any[], args: any[]
} }
export default class Debug extends BaseAction<Payload, any> { export default class Debug extends BaseAction<Payload, any> {
actionName = ActionName.Debug actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
log("debug call ntqq api", payload); log('debug call ntqq api', payload)
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi]
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
log("ntqqApiClass", ntqqApiClass) log('ntqqApiClass', ntqqApiClass)
const method = ntqqApiClass[payload.method] const method = ntqqApiClass[payload.method]
if (method) { if (method) {
const result = method(...payload.args); const result = method(...payload.args)
if (method.constructor.name === "AsyncFunction") { if (method.constructor.name === 'AsyncFunction') {
return await result return await result
}
return result
}
} }
throw `${payload.method}方法 不存在` return result
}
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
} }
} throw `${payload.method}方法 不存在`
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

@@ -1,33 +1,33 @@
import {GroupNotify, GroupNotifyStatus} from "../../../ntqqapi/types"; import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {uidMaps} from "../../../common/data"; import { uidMaps } from '../../../common/data'
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import { NTQQUserApi } from '../../../ntqqapi/api/user'
import {NTQQGroupApi} from "../../../ntqqapi/api/group"; import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import {log} from "../../../common/utils/log"; import { log } from '../../../common/utils/log'
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
group_id: number, group_id: number
user_id: number, user_id: number
flag: string flag: string
} }
export default class GetGroupAddRequest extends BaseAction<null, OB11GroupRequestNotify[]> { export default class GetGroupAddRequest extends BaseAction<null, OB11GroupRequestNotify[]> {
actionName = ActionName.GetGroupIgnoreAddRequest actionName = ActionName.GetGroupIgnoreAddRequest
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> { protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQGroupApi.getGroupIgnoreNotifies() const data = await NTQQGroupApi.getGroupIgnoreNotifies()
log(data); log(data)
let notifies: GroupNotify[] = data.notifies.filter(notify => notify.status === GroupNotifyStatus.WAIT_HANDLE); let notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE)
let returnData: OB11GroupRequestNotify[] = [] let returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin
returnData.push({ returnData.push({
group_id: parseInt(notify.group.groupCode), group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin), user_id: parseInt(uin),
flag: notify.seq flag: notify.seq,
}) })
}
return returnData;
} }
} return returnData
}
}

View File

@@ -1,43 +1,43 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import * as fs from "node:fs"; import * as fs from 'node:fs'
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import { NTQQUserApi } from '../../../ntqqapi/api/user'
import {checkFileReceived, uri2local} from "../../../common/utils/file"; import { checkFileReceived, uri2local } from '../../../common/utils/file'
// import { log } from "../../../common/utils"; // import { log } from "../../../common/utils";
interface Payload { interface Payload {
file: string file: string
} }
export default class SetAvatar extends BaseAction<Payload, null> { export default class SetAvatar extends BaseAction<Payload, null> {
actionName = ActionName.SetQQAvatar actionName = ActionName.SetQQAvatar
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const {path, isLocal, errMsg} = (await uri2local(payload.file)) const { path, isLocal, errMsg } = await uri2local(payload.file)
if (errMsg){ if (errMsg) {
throw `头像${payload.file}设置失败,file字段可能格式不正确` throw `头像${payload.file}设置失败,file字段可能格式不正确`
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQUserApi.setQQAvatar(path)
if (!isLocal){
fs.unlink(path, () => {})
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
if (ret['result'] == 1004022) {
throw `头像${payload.file}设置失败,文件可能不是图片格式`
} else if(ret['result'] != 0) {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`
}
} else {
if (!isLocal){
fs.unlink(path, () => {})
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`
}
return null
} }
} if (path) {
await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQUserApi.setQQAvatar(path)
if (!isLocal) {
fs.unlink(path, () => {})
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
if (ret['result'] == 1004022) {
throw `头像${payload.file}设置失败,文件可能不是图片格式`
} else if (ret['result'] != 0) {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`
}
} else {
if (!isLocal) {
fs.unlink(path, () => {})
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`
}
return null
}
}

View File

@@ -1,22 +1,28 @@
import {ActionName} from "../types"; import { ActionName } from '../types'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {dbUtil} from "../../../common/db"; import { dbUtil } from '../../../common/db'
import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
interface Payload { interface Payload {
message_id: number message_id: number
} }
class DeleteMsg extends BaseAction<Payload, void> { class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg actionName = ActionName.DeleteMsg
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id) let msg = await dbUtil.getMsgByShortId(payload.message_id)
await NTQQMsgApi.recallMsg({ if (!msg) {
chatType: msg.chatType, throw `消息${payload.message_id}不存在`
peerUid: msg.peerUid
}, [msg.msgId])
} }
await NTQQMsgApi.recallMsg(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
[msg.msgId],
)
}
} }
export default DeleteMsg export default DeleteMsg

View File

@@ -0,0 +1,43 @@
import BaseAction from '../BaseAction'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api'
import { ChatType, RawMessage } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { getUidByUin } from '../../../common/data'
import { ActionName } from '../types'
interface Payload {
message_id: number
group_id: number
user_id?: number
}
class ForwardSingleMsg extends BaseAction<Payload, null> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) }
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
}
protected async _handle(payload: Payload): Promise<null> {
const msg = await dbUtil.getMsgByShortId(payload.message_id)
const peer = await this.getTargetPeer(payload)
await NTQQMsgApi.forwardMsg(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
peer,
[msg.msgId],
)
return null
}
}
export class ForwardFriendSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardFriendSingleMsg
}
export class ForwardSingleGroupMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardGroupSingleMsg
}

View File

@@ -1,33 +1,32 @@
import {OB11Message} from '../../types'; import { OB11Message } from '../../types'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {dbUtil} from "../../../common/db"; import { dbUtil } from '../../../common/db'
export interface PayloadType { export interface PayloadType {
message_id: number message_id: number
} }
export type ReturnDataType = OB11Message export type ReturnDataType = OB11Message
class GetMsg extends BaseAction<PayloadType, OB11Message> { class GetMsg extends BaseAction<PayloadType, OB11Message> {
actionName = ActionName.GetMsg actionName = ActionName.GetMsg
protected async _handle(payload: PayloadType) { protected async _handle(payload: PayloadType) {
// log("history msg ids", Object.keys(msgHistory)); // log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id) { if (!payload.message_id) {
throw ("参数message_id不能为空") throw '参数message_id不能为空'
}
let msg = await dbUtil.getMsgByShortId(payload.message_id)
if(!msg) {
msg = await dbUtil.getMsgByLongId(payload.message_id.toString())
}
if (!msg){
throw ("消息不存在")
}
return await OB11Constructor.message(msg)
} }
let msg = await dbUtil.getMsgByShortId(payload.message_id)
if (!msg) {
msg = await dbUtil.getMsgByLongId(payload.message_id.toString())
}
if (!msg) {
throw '消息不存在'
}
return await OB11Constructor.message(msg)
}
} }
export default GetMsg export default GetMsg

View File

@@ -1,14 +1,14 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
interface Payload{ interface Payload {
message_id: number message_id: number
} }
export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null>{ export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead actionName = ActionName.GoCQHTTP_MarkMsgAsRead
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
return null return null
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
import SendMsg from "./SendMsg"; import SendMsg from './SendMsg'
import {ActionName, BaseCheckResult} from "../types"; import { ActionName, BaseCheckResult } from '../types'
import {OB11PostSendMsg} from "../../types"; import { OB11PostSendMsg } from '../../types'
class SendPrivateMsg extends SendMsg { class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg actionName = ActionName.SendPrivateMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
payload.message_type = "private" payload.message_type = 'private'
return super.check(payload); return super.check(payload)
} }
} }
export default SendPrivateMsg export default SendPrivateMsg

View File

@@ -0,0 +1,32 @@
import { ActionName } from '../types'
import BaseAction from '../BaseAction'
import { dbUtil } from '../../../common/db'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
interface Payload {
message_id: number
emoji_id: string
}
export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike
protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
if (!payload.emoji_id) {
throw new Error('emojiId not found')
}
return await NTQQMsgApi.setEmojiLike(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
msg.msgSeq,
payload.emoji_id,
true,
)
}
}

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,15 @@
import {OB11User} from '../../types'; import { OB11User } from '../../types'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {selfInfo} from "../../../common/data"; import { selfInfo } from '../../../common/data'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
class GetLoginInfo extends BaseAction<null, OB11User> { class GetLoginInfo extends BaseAction<null, OB11User> {
actionName = ActionName.GetLoginInfo actionName = ActionName.GetLoginInfo
protected async _handle(payload: null) { protected async _handle(payload: null) {
return OB11Constructor.selfInfo(selfInfo); return OB11Constructor.selfInfo(selfInfo)
} }
} }
export default GetLoginInfo export default GetLoginInfo

View File

@@ -1,16 +1,15 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11Status} from "../../types"; import { OB11Status } from '../../types'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {selfInfo} from "../../../common/data"; import { selfInfo } from '../../../common/data'
export default class GetStatus extends BaseAction<any, OB11Status> { export default class GetStatus extends BaseAction<any, OB11Status> {
actionName = ActionName.GetStatus actionName = ActionName.GetStatus
protected async _handle(payload: any): Promise<OB11Status> { protected async _handle(payload: any): Promise<OB11Status> {
return { return {
online: selfInfo.online, online: selfInfo.online,
good: true good: true,
}
} }
} }
}

View File

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

View File

@@ -1,64 +1,69 @@
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
export interface ValidCheckResult { export interface ValidCheckResult {
valid: true valid: true
[k: string | number]: any [k: string | number]: any
} }
export interface InvalidCheckResult { export interface InvalidCheckResult {
valid: false valid: false
message: string message: string
[k: string | number]: any [k: string | number]: any
} }
export enum ActionName { export enum ActionName {
// llonebot // llonebot
GetGroupIgnoreAddRequest = "get_group_ignore_add_request", GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
SetQQAvatar = "set_qq_avatar", SetQQAvatar = 'set_qq_avatar',
GetConfig = "get_config", GetConfig = 'get_config',
SetConfig = "set_config", SetConfig = 'set_config',
Debug = "llonebot_debug", Debug = 'llonebot_debug',
GetFile = "get_file", GetFile = 'get_file',
// onebot 11 // onebot 11
SendLike = "send_like", SendLike = 'send_like',
GetLoginInfo = "get_login_info", GetLoginInfo = 'get_login_info',
GetFriendList = "get_friend_list", GetFriendList = 'get_friend_list',
GetGroupInfo = "get_group_info", GetGroupInfo = 'get_group_info',
GetGroupList = "get_group_list", GetGroupList = 'get_group_list',
GetGroupMemberInfo = "get_group_member_info", GetGroupMemberInfo = 'get_group_member_info',
GetGroupMemberList = "get_group_member_list", GetGroupMemberList = 'get_group_member_list',
GetMsg = "get_msg", GetMsg = 'get_msg',
SendMsg = "send_msg", SendMsg = 'send_msg',
SendGroupMsg = "send_group_msg", SendGroupMsg = 'send_group_msg',
SendPrivateMsg = "send_private_msg", SendPrivateMsg = 'send_private_msg',
DeleteMsg = "delete_msg", DeleteMsg = 'delete_msg',
SetGroupAddRequest = "set_group_add_request", SetMsgEmojiLike = 'set_msg_emoji_like',
SetFriendAddRequest = "set_friend_add_request", SetGroupAddRequest = 'set_group_add_request',
SetGroupLeave = "set_group_leave", SetFriendAddRequest = 'set_friend_add_request',
GetVersionInfo = "get_version_info", SetGroupLeave = 'set_group_leave',
GetStatus = "get_status", GetVersionInfo = 'get_version_info',
CanSendRecord = "can_send_record", GetStatus = 'get_status',
CanSendImage = "can_send_image", CanSendRecord = 'can_send_record',
SetGroupKick = "set_group_kick", CanSendImage = 'can_send_image',
SetGroupBan = "set_group_ban", SetGroupKick = 'set_group_kick',
SetGroupWholeBan = "set_group_whole_ban", SetGroupBan = 'set_group_ban',
SetGroupAdmin = "set_group_admin", SetGroupWholeBan = 'set_group_whole_ban',
SetGroupCard = "set_group_card", SetGroupAdmin = 'set_group_admin',
SetGroupName = "set_group_name", SetGroupCard = 'set_group_card',
GetImage = "get_image", SetGroupName = 'set_group_name',
GetRecord = "get_record", GetImage = 'get_image',
CleanCache = "clean_cache", GetRecord = 'get_record',
// 以下为go-cqhttp api CleanCache = 'clean_cache',
GoCQHTTP_SendForwardMsg = "send_forward_msg", GetCookies = 'get_cookies',
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg", ForwardFriendSingleMsg = 'forward_friend_single_msg',
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg", ForwardGroupSingleMsg = 'forward_group_single_msg',
GoCQHTTP_GetStrangerInfo = "get_stranger_info", // 以下为go-cqhttp api
GetGuildList = "get_guild_list", GoCQHTTP_SendForwardMsg = 'send_forward_msg',
GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read", GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_UploadGroupFile = "upload_group_file", GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_DownloadFile = "download_file", GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
GoCQHTTP_GetGroupMsgHistory = "get_group_msg_history", GetGuildList = 'get_guild_list',
GoCQHTTP_GetForwardMsg = "get_forward_msg", GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
} GoCQHTTP_UploadGroupFile = 'upload_group_file',
GoCQHTTP_UploadPrivateFile = 'upload_private_file',
GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
}

Some files were not shown because too many files have changed in this diff Show More