Compare commits

...

62 Commits

Author SHA1 Message Date
idranme
6b03b01a24 Merge pull request #319 from LLOneBot/dev
chore: v3.28.0
2024-08-06 02:08:51 +08:00
idranme
18f01b7f21 chore: v3.28.0 2024-08-06 02:08:00 +08:00
idranme
897f691d6c make ts happy 2024-08-06 01:47:51 +08:00
idranme
a9902d9109 sync 2024-08-05 22:49:48 +08:00
idranme
5d78fdd6a4 fix 2024-08-05 22:07:04 +08:00
idranme
72eb013371 fix 2024-08-05 20:44:28 +08:00
idranme
808777c044 fix: import path 2024-08-05 19:18:15 +08:00
idranme
a2d1379866 sync 2024-08-05 19:09:41 +08:00
idranme
c41a8556fa Change description 2024-08-05 00:23:41 +08:00
idranme
fa2df2a3cd opt 2024-08-04 23:11:59 +08:00
idranme
b28dd3a723 Update publish.yml 2024-08-04 22:44:20 +08:00
idranme
6ffa41e0d6 prioritise local versions 2024-08-04 22:14:07 +08:00
idranme
85df3794e8 optimise 2024-08-04 22:07:55 +08:00
idranme
4bee2ba062 reduce icon size 2024-08-04 20:35:31 +08:00
idranme
4bf992c4a9 chore: deps 2024-08-04 20:31:29 +08:00
idranme
898e856150 poke require >=25765 2024-08-04 20:22:07 +08:00
idranme
c86797afc8 chore: remove unused eslint 2024-08-04 19:54:32 +08:00
idranme
799593b788 chore: support yarn berry 2024-08-04 19:48:17 +08:00
idranme
74d9a083aa Update README.md 2024-08-04 19:28:13 +08:00
idranme
cae525429a Update README.md 2024-08-04 19:21:44 +08:00
idranme
cc0d1e2a9b Merge pull request #316 from idranme/uuid
refa: deps
2024-08-04 18:36:01 +08:00
idranme
34ecfcfa16 Merge branch 'dev' into uuid 2024-08-04 18:35:11 +08:00
idranme
79c5041216 Merge pull request #318 from LLOneBot/dev 2024-08-04 18:07:03 +08:00
idranme
8fb53260ab chore: v3.27.4 2024-08-04 10:05:03 +00:00
idranme
07d9ac823a Merge pull request #317 from LLOneBot/dev
chore: v3.27.4
2024-08-04 17:48:24 +08:00
idranme
b571ef434c chore 2024-08-02 20:50:34 +00:00
idranme
c1f4dcd6a6 chore 2024-08-02 20:40:50 +00:00
idranme
4c5befbe44 chore 2024-08-02 20:39:26 +00:00
linyuchen
296cd4d0a3 Merge pull request #315 from idranme/main
feat: at segment add name
2024-08-02 23:11:14 +08:00
linyuchen
e77a2ca34a Merge pull request #311 from cnxysoft/dev
BUG修复
2024-08-02 23:09:35 +08:00
idranme
f3af0d18bc refa: deps 2024-08-02 12:00:13 +00:00
idranme
406e3c7e6b opt 2024-08-02 10:49:30 +00:00
idranme
3f5ca8ebfa chore 2024-08-02 10:31:37 +00:00
idranme
6e8389e833 chore 2024-08-02 10:26:18 +00:00
idranme
71aedca4c6 feat: the name attribute of the at message segment 2024-08-02 10:23:48 +00:00
Alen
6410689549 BUG修复
尝试修复设精事件shortId和senderId
2024-08-01 21:56:44 +08:00
linyuchen
6d0e2269cc Merge pull request #304 from cnxysoft/dev
功能更新
2024-07-28 14:52:58 +08:00
linyuchen
2e28fc678c Merge branch 'dev' into dev 2024-07-28 14:52:17 +08:00
linyuchen
8204f4407f Merge pull request #300 from super1207/dev
Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev
2024-07-26 09:57:37 +08:00
Alen
9f1d4c4db2 功能修改
修改群管变更事件获取渠道,让所有群角色都能收到群管变更通知
2024-07-25 17:25:40 +08:00
Alen
8ba47635d3 功能更新
1.增加设精事件上报(目前上报的shortId经常出错,实际消息体却是正确的,待解决)
2.增加设精/取消设精api接口
3.poke事件增加raw信息上报
2024-07-25 01:02:48 +08:00
Alen
5fa2427c51 修改poke事件
新增poke事件支持上传raw信息
2024-07-24 19:04:07 +08:00
Alen
aa8739d016 Merge remote-tracking branch 'upstream/main' into dev 2024-07-24 11:48:55 +08:00
super1207
79f0329da7 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-07-20 18:01:30 +08:00
super1207
7a33a36f44 add get_event api 2024-07-20 17:58:00 +08:00
linyuchen
808424d08e Merge branch 'main' into dev 2024-07-20 17:08:59 +08:00
linyuchen
d0967785de chore: v3.27.3 2024-07-20 16:58:03 +08:00
linyuchen
eccabb8189 Merge pull request #299 from Natsukage/main
fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
2024-07-20 15:25:27 +08:00
夏影
c9374ff515 fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
Added logic to skip name-value pairs in encodeCQCode when value cannot be converted to string, preventing errors caused by undefined values. This ensures the function can handle such cases gracefully and continue processing other valid data.
2024-07-20 00:49:34 +08:00
Alen
92c4889924 Merge remote-tracking branch 'upstream/main' 2024-07-16 23:19:32 +08:00
linyuchen
f9454039a1 fix: old poke event 2024-07-16 21:52:15 +08:00
linyuchen
bc4511e175 chore: v3.27.2 2024-07-16 21:43:50 +08:00
linyuchen
f191103f99 Merge pull request #294 from cnxysoft/dev
修复戳一戳
2024-07-16 21:38:17 +08:00
linyuchen
408463f63b Merge branch 'dev' into dev 2024-07-16 21:21:50 +08:00
Alen
fb96c4272e 修复戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 21:01:19 +08:00
Alen
c6b302d5a8 修复好友戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 20:27:44 +08:00
linyuchen
1dd468e2ff fix: #290 2024-07-13 16:25:00 +08:00
linyuchen
2a1aa8c649 feat: image subType 2024-07-13 14:26:23 +08:00
linyuchen
1633734e08 Merge branch 'dev' 2024-07-13 14:09:45 +08:00
linyuchen
dff92e6f27 chore: version 3.27.0
feat: support poke
feat: LLOneBot global switch
2024-07-13 14:09:03 +08:00
linyuchen
dba5e30d5d doc: plugin description 2024-07-10 13:48:05 +08:00
linyuchen
2d04ab2e72 fix: crychic crash 2024-07-10 13:47:44 +08:00
99 changed files with 1927 additions and 680 deletions

View File

@@ -9,10 +9,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup node - name: setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18

15
.gitignore vendored
View File

@@ -1,6 +1,15 @@
node_modules/ .yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
package-lock.json package-lock.json
dist/ yarn.lock
out/ node_modules
dist
out
.idea/ .idea/
.DS_Store .DS_Store

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -1,6 +1,6 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站微博知乎抖音等发布和讨论*任何*与本插件存在相关性的信息** > **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站微博知乎抖音等发布和讨论*任何*与本插件存在相关性的信息**
@@ -13,13 +13,13 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 设置界面 ## 设置界面
<img src="./doc/image/setting.png" width="500px" alt="图片名称"/> <img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例 ## HTTP 调用示例
![](doc/image/example.jpg) <img src="./doc/image/example.jpg" width="500px" alt="HTTP调用示例"/>
## 支持的 api 和功能详情 ## 支持的 API
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
@@ -35,13 +35,8 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
- [x] 群禁言事件上报 - [x] 群禁言事件上报
- [x] 优化加群成功事件上报 - [x] 优化加群成功事件上报
- [x] 清理缓存api - [x] 清理缓存api
- [ ] 无头模式
- [ ] 框架对接文档 - [ ] 框架对接文档
## onebot11文档
<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)

View File

@@ -1,6 +1,6 @@
import cp from 'vite-plugin-cp' import cp from 'vite-plugin-cp'
import './scripts/gen-version'
import path from 'node:path' import path from 'node:path'
import './scripts/gen-manifest'
const external = [ const external = [
'silk-wasm', 'silk-wasm',
@@ -32,6 +32,7 @@ let config = {
external, external,
input: 'src/main/main.ts', input: 'src/main/main.ts',
}, },
minify: true,
}, },
resolve: { resolve: {
alias: { alias: {
@@ -44,8 +45,8 @@ let config = {
targets: [ targets: [
...external.map(genCpModule), ...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, { src: './manifest.json', dest: 'dist' },
{ src: './icon.jpg', dest: 'dist' }, { src: './icon.webp', dest: 'dist' },
{ src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' }, // { src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' }, // { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' }, // { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
], ],

BIN
icon.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
icon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,11 +1,11 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.26.7", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发",
"version": "3.26.7", "version": "3.28.0",
"icon": "./icon.jpg", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",

View File

@@ -16,34 +16,27 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"compressing": "^1.10.0", "compressing": "^1.10.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"fast-xml-parser": "^4.3.6", "fast-xml-parser": "^4.4.1",
"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",
"silk-wasm": "^3.6.0", "silk-wasm": "^3.6.1",
"utf-8-validate": "^6.0.3", "ws": "^8.18.0"
"uuid": "^9.0.1",
"ws": "^8.16.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.11.24", "@types/node": "^20.11.24",
"@types/uuid": "^9.0.8", "@types/ws": "^8.5.12",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"electron": "^29.0.1", "electron": "^29.0.1",
"electron-vite": "^2.0.0", "electron-vite": "^2.3.0",
"eslint": "^8.0.1", "typescript": "^5.5.4",
"eslint-plugin-import": "^2.25.2", "vite": "^5.3.5",
"eslint-plugin-n": "^15.0.0 || ^16.0.0",
"eslint-plugin-promise": "^6.0.0",
"ts-node": "^10.9.2",
"typescript": "*",
"vite": "^5.1.4",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
} },
"packageManager": "yarn@4.4.0"
} }

38
scripts/gen-manifest.ts Normal file
View File

@@ -0,0 +1,38 @@
import { version } from '../src/version'
import { writeFileSync } from 'node:fs'
const manifest = {
manifest_version: 4,
type: 'extension',
name: 'LLOneBot',
slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用以 QQ 机器人开发',
version,
icon: './icon.webp',
authors: [
{
name: 'linyuchen',
link: 'https://github.com/linyuchen'
}
],
repository: {
repo: 'linyuchen/LiteLoaderQQNT-OneBotApi',
branch: 'main',
release: {
tag: 'latest',
name: 'LLOneBot.zip'
}
},
platform: [
'win32',
'linux',
'darwin'
],
injects: {
renderer: './renderer/index.js',
main: './main/main.cjs',
preload: './preload/preload.cjs'
}
}
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2))

View File

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

View File

@@ -1,10 +1,10 @@
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'] as string, { valueEncoding: 'json' })
async function getGroupNotify() { async function getGroupNotify() {
let keys = await db.keys().all() let keys = await db.keys().all()
let result = [] let result: string[] = []
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_')) {

View File

@@ -1,7 +1,5 @@
import fs from 'fs' import fs from 'node:fs'
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'
@@ -43,6 +41,7 @@ export class ConfigUtil {
enableQOAutoQuote: false enableQOAutoQuote: false
} }
let defaultConfig: Config = { let defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
token: '', token: '',
@@ -52,7 +51,6 @@ export class ConfigUtil {
reportSelfMessage: false, reportSelfMessage: false,
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
enablePoke: false,
musicSignUrl: '', musicSignUrl: '',
} }

View File

@@ -45,7 +45,7 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
if (friend) { if (friend) {
friends.push(friend) friends.push(friend)
} }
} catch (e) { } catch (e: any) {
log('刷新好友列表失败', e.stack.toString()) log('刷新好友列表失败', e.stack.toString())
} }
} }

View File

@@ -14,9 +14,9 @@ class DBUtil {
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 | undefined
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 | undefined
/* /*
* 数据库结构 * 数据库结构
@@ -44,7 +44,7 @@ class DBUtil {
this.db = new Level(DB_PATH, { valueEncoding: 'json' }) this.db = new Level(DB_PATH, { valueEncoding: 'json' })
console.log('llonebot init db success') console.log('llonebot init db success')
resolve(null) resolve(null)
} catch (e) { } catch (e: any) {
console.log('init db fail', e.stack.toString()) console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300) setTimeout(initDB, 300)
} }
@@ -72,13 +72,13 @@ class DBUtil {
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> { public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try { try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)) this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!)
} catch (e) { } } catch (e) { }
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
} }
public setReceivedTempUinMap(data: ReceiveTempUinMap) { public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then() this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
} }
private addCache(msg: RawMessage) { private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
@@ -91,30 +91,30 @@ class DBUtil {
this.cache = {} this.cache = {}
} }
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> { async getMsgByShortId(shortMsgId: number): Promise<RawMessage | undefined> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) { if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey]) // log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage return this.cache[shortMsgIdKey] as RawMessage
} }
try { try {
const longId = await this.db.get(shortMsgIdKey) const longId = await this.db?.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId) const msg = await this.getMsgByLongId(longId!)
this.addCache(msg) this.addCache(msg!)
return msg return msg
} catch (e) { } catch (e: any) {
log('getMsgByShortId db error', e.stack.toString()) log('getMsgByShortId db error', e.stack.toString())
} }
} }
async getMsgByLongId(longId: string): Promise<RawMessage> { async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) { if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage return this.cache[longIdKey] as RawMessage
} }
try { try {
const data = await this.db.get(longIdKey) const data = await this.db?.get(longIdKey)
const msg = JSON.parse(data) const msg = JSON.parse(data!)
this.addCache(msg) this.addCache(msg)
return msg return msg
} catch (e) { } catch (e) {
@@ -122,17 +122,17 @@ class DBUtil {
} }
} }
async getMsgBySeqId(seqId: string): Promise<RawMessage> { async getMsgBySeqId(seqId: string): Promise<RawMessage | undefined> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) { if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage return this.cache[seqIdKey] as RawMessage
} }
try { try {
const longId = await this.db.get(seqIdKey) const longId = await this.db?.get(seqIdKey)
const msg = await this.getMsgByLongId(longId) const msg = await this.getMsgByLongId(longId!)
this.addCache(msg) this.addCache(msg!)
return msg return msg
} catch (e) { } catch (e: any) {
log('getMsgBySeqId db error', e.stack.toString()) log('getMsgBySeqId db error', e.stack.toString())
} }
} }
@@ -141,7 +141,7 @@ class DBUtil {
// 有则更新,无则添加 // 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId); // log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) { if (!existMsg) {
try { try {
existMsg = await this.getMsgByLongId(msg.msgId) existMsg = await this.getMsgByLongId(msg.msgId)
@@ -161,13 +161,13 @@ class DBUtil {
msg.msgShortId = shortMsgId msg.msgShortId = shortMsgId
this.addCache(msg) this.addCache(msg)
// log("新增消息记录", msg.msgId) // log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch() this.db?.put(shortIdKey, msg.msgId).then().catch()
this.db.put(longIdKey, JSON.stringify(msg)).then().catch() this.db?.put(longIdKey, JSON.stringify(msg)).then().catch()
try { try {
await this.db.get(seqIdKey) await this.db?.get(seqIdKey)
} catch (e) { } catch (e) {
// log("新的seqId", seqIdKey) // log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then().catch() this.db?.put(seqIdKey, msg.msgId).then().catch()
} }
if (!this.cache[seqIdKey]) { if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg this.cache[seqIdKey] = msg
@@ -178,7 +178,7 @@ class DBUtil {
async updateMsg(msg: RawMessage) { async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) { if (!existMsg) {
try { try {
existMsg = await this.getMsgByLongId(msg.msgId) existMsg = await this.getMsgByLongId(msg.msgId)
@@ -187,18 +187,18 @@ class DBUtil {
} }
} }
Object.assign(existMsg, msg) Object.assign(existMsg!, msg)
this.db.put(longIdKey, JSON.stringify(existMsg)).then().catch() this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg.msgShortId const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
if (!this.cache[seqIdKey]) { if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg this.cache[seqIdKey] = existMsg!
} }
this.db.put(shortIdKey, msg.msgId).then().catch() this.db?.put(shortIdKey, msg.msgId).then().catch()
try { try {
await this.db.get(seqIdKey) await this.db?.get(seqIdKey)
} catch (e) { } catch (e) {
this.db.put(seqIdKey, msg.msgId).then().catch() this.db?.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey); // log("更新seqId error", e.stack, seqIdKey);
} }
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId); // log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
@@ -208,15 +208,15 @@ class DBUtil {
const key = 'msg_current_short_id' const key = 'msg_current_short_id'
if (this.currentShortId === undefined) { if (this.currentShortId === undefined) {
try { try {
let id: string = await this.db.get(key) const id = await this.db?.get(key)
this.currentShortId = parseInt(id) this.currentShortId = parseInt(id!)
} catch (e) { } catch (e) {
this.currentShortId = -2147483640 this.currentShortId = -2147483640
} }
} }
this.currentShortId++ this.currentShortId++
this.db.put(key, this.currentShortId.toString()).then().catch() this.db?.put(key, this.currentShortId.toString()).then().catch()
return this.currentShortId return this.currentShortId
} }
@@ -229,8 +229,8 @@ class DBUtil {
delete cacheDBData['downloadFunc'] delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data this.cache[fileNameOrUuid] = data
try { try {
await this.db.put(key, JSON.stringify(cacheDBData)) await this.db?.put(key, JSON.stringify(cacheDBData))
} catch (e) { } catch (e: any) {
log('addFileCache db error', e.stack.toString()) log('addFileCache db error', e.stack.toString())
} }
} }
@@ -241,8 +241,8 @@ class DBUtil {
return this.cache[key] as FileCache return this.cache[key] as FileCache
} }
try { try {
let data = await this.db.get(key) const data = await this.db?.get(key)
return JSON.parse(data) return JSON.parse(data!)
} catch (e) { } catch (e) {
// log("getFileCache db error", e.stack.toString()) // log("getFileCache db error", e.stack.toString())
} }
@@ -255,7 +255,7 @@ class DBUtil {
return return
} }
this.cache[key] = notify this.cache[key] = notify
this.db.put(key, JSON.stringify(notify)).then().catch() this.db?.put(key, JSON.stringify(notify)).then().catch()
} }
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> { async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
@@ -264,8 +264,8 @@ class DBUtil {
return this.cache[key] as GroupNotify return this.cache[key] as GroupNotify
} }
try { try {
let data = await this.db.get(key) const data = await this.db?.get(key)
return JSON.parse(data) return JSON.parse(data!)
} catch (e) { } catch (e) {
// log("getGroupNotify db error", e.stack.toString()) // log("getGroupNotify db error", e.stack.toString())
} }

View File

@@ -1,5 +1,5 @@
import express, { Express, Request, Response } from 'express' import express, { Express, Request, Response } from 'express'
import http from 'http' import http from 'node:http'
import cors from 'cors' import cors from 'cors'
import { log } from '../utils/log' import { log } from '../utils/log'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
@@ -10,7 +10,7 @@ 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 = null
constructor() { constructor() {
this.expressAPP = express() this.expressAPP = express()
@@ -38,7 +38,7 @@ export abstract class HttpServerBase {
let clientToken = '' let clientToken = ''
const authHeader = req.get('authorization') const authHeader = req.get('authorization')
if (authHeader) { if (authHeader) {
clientToken = authHeader.split('Bearer ').pop() clientToken = authHeader.split('Bearer ').pop()!
log('receive http header token', clientToken) log('receive http header token', clientToken)
} else if (req.query.access_token) { } else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) { if (Array.isArray(req.query.access_token)) {
@@ -62,7 +62,7 @@ export abstract class HttpServerBase {
}) })
this.listen(port) this.listen(port)
llonebotError.httpServerError = '' llonebotError.httpServerError = ''
} catch (e) { } catch (e: any) {
log('HTTP服务启动失败', e.toString()) log('HTTP服务启动失败', e.toString())
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString() llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString()
} }
@@ -103,7 +103,7 @@ export abstract class HttpServerBase {
log('收到http请求', url, payload) log('收到http请求', url, payload)
try { try {
res.send(await handler(res, payload)) res.send(await handler(res, payload))
} catch (e) { } catch (e: any) {
this.handleFailed(res, payload, e.stack.toString()) this.handleFailed(res, payload, e.stack.toString())
} }
}) })

View File

@@ -6,7 +6,7 @@ import { getConfigUtil } from '../config'
import { llonebotError } from '../data' import { llonebotError } from '../data'
class WebsocketClientBase { class WebsocketClientBase {
private wsClient: WebSocket private wsClient: WebSocket | undefined
constructor() { } constructor() { }
@@ -20,7 +20,7 @@ class WebsocketClientBase {
} }
export class WebsocketServerBase { export class WebsocketServerBase {
private ws: WebSocketServer = null private ws: WebSocketServer | null = null
constructor() { constructor() {
console.log(`llonebot websocket service started`) console.log(`llonebot websocket service started`)
@@ -30,22 +30,22 @@ export class WebsocketServerBase {
try { try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 }) this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = '' llonebotError.wsServerError = ''
} catch (e) { } catch (e: any) {
llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString() llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString()
} }
this.ws.on('connection', (wsClient, req) => { this.ws?.on('connection', (wsClient, req) => {
const url = req.url.split('?').shift() const url = req.url?.split('?').shift()
this.authorize(wsClient, req) this.authorize(wsClient, req)
this.onConnect(wsClient, url, req) this.onConnect(wsClient, url!, req)
wsClient.on('message', async (msg) => { wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url, msg.toString()) this.onMessage(wsClient, url!, msg.toString())
}) })
}) })
} }
stop() { stop() {
llonebotError.wsServerError = '' llonebotError.wsServerError = ''
this.ws.close((err) => { this.ws?.close((err) => {
log('ws server close failed!', err) log('ws server close failed!', err)
}) })
this.ws = null this.ws = null

View File

@@ -17,6 +17,7 @@ export interface CheckVersion {
version: string version: string
} }
export interface Config { export interface Config {
enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval?: number // ms
@@ -27,7 +28,6 @@ export interface Config {
autoDeleteFile?: boolean autoDeleteFile?: boolean
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
enablePoke?: boolean
musicSignUrl?: string musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean ignoreBeforeLoginMsg?: boolean
} }

View File

@@ -0,0 +1,231 @@
import { NodeIQQNTWrapperSession } from '@/ntqqapi/wrapper'
import { randomUUID } from 'node:crypto'
interface Internal_MapKey {
timeout: number
createtime: number
func: (...arg: any[]) => any
checker: ((...args: any[]) => boolean) | undefined
}
export class ListenerClassBase {
[key: string]: string
}
export interface ListenerIBase {
new(listener: any): ListenerClassBase
}
export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
constructor() {
}
createProxyDispatch(ListenerMainName: string) {
const current = this
return new Proxy({}, {
get(target: any, prop: any, receiver: any) {
// console.log('get', prop, typeof target[prop])
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver)
}
})
}
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession
}
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
}
if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '')
const eventName = eventNameArr[1]
//getNodeIKernelGroupListener,GroupService
//console.log('2', eventName)
const services = (this.WrapperSession as unknown as eventType)[serviceName]()
let event = services[eventName]
//重新绑定this
event = event.bind(services)
if (event) {
return event as T
}
return undefined
}
}
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.CreatEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
}
return Listener as T
}
//统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid)
return
}
if (task.checker && task.checker(...args)) {
task.func(...args)
}
})
}
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
reject(new Error('NTEvent EventName:' + EventName + ' timeout'))
}
}, timeout)
const retData = await EventFunc!(...args)
complete = true
resolve(retData)
})
}
async RegisterListen<ListenerType extends (...args: any[]) => void>(ListenerName = '', waitTimes = 1, timeout = 5000, checker: (...args: Parameters<ListenerType>) => boolean) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
const databack = () => {
if (complete == 0) {
reject(new Error(' ListenerName:' + ListenerName + ' timeout'))
} else {
resolve(retData!)
}
}
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++
retData = args
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
})
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
let retEvent: any = {}
const databack = () => {
if (complete == 0) {
reject(new Error('Timeout: NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'))
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!])
}
}
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: any[]) => {
complete++
//console.log('func', ...args)
retData = args as Parameters<ListenerType>
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[]))
})
}
}
export const NTEventDispatch = new NTEventWrapper()
// 示例代码 快速创建事件
// let NTEvent = new NTEventWrapper()
// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise<Number>>('NodeIKernelProfileLikeService/GetTest')
// if (TestEvent) {
// TestEvent(true)
// }
// 示例代码 快速创建监听Listener类
// let NTEvent = new NTEventWrapper()
// NTEvent.CreatListenerFunction<NodeIKernelMsgListener>('NodeIKernelMsgListener', 'core')
// 调用接口
//let NTEvent = new NTEventWrapper()
//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise<Number>, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true)
// 注册监听 解除监听
// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb)
// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core')
// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode)
// GetTest('test')
// always模式
// NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode,(...args:any[])=>{ console.log(args) })

View File

@@ -4,9 +4,9 @@ import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk
import { log } from './log' import { log } from './log'
import path from 'node:path' import path from 'node:path'
import { TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) { function getFileHeader(filePath: string) {
@@ -61,7 +61,7 @@ export async function encodeSilk(filePath: string) {
try { try {
const file = await fsPromise.readFile(filePath) const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, uuidv4()) const pttPath = path.join(TEMP_DIR, randomUUID())
if (!isSilk(file)) { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = isWav(file) const _isWav = isWav(file)
@@ -114,7 +114,7 @@ export async function encodeSilk(filePath: string) {
let duration = 0 let duration = 0
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
} catch (e) { } catch (e: any) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack) log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack)
duration = await guessDuration(filePath) duration = await guessDuration(filePath)
} }
@@ -125,7 +125,7 @@ export async function encodeSilk(filePath: string) {
duration, duration,
} }
} }
} catch (error) { } catch (error: any) {
log('convert silk failed', error.stack) log('convert silk failed', error.stack)
return {} return {}
} }

View File

@@ -1,13 +1,10 @@
import fs from 'fs' import fs from 'node:fs'
import fsPromise from 'fs/promises' import fsPromise from 'node:fs/promises'
import crypto from 'crypto'
import util from 'util'
import path from 'node:path' import path from 'node:path'
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 { randomUUID, createHash } from 'node:crypto'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4) const buffer = Buffer.alloc(4)
@@ -37,7 +34,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
} }
export async function file2base64(path: string) { export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile)
let result = { let result = {
err: '', err: '',
data: '', data: '',
@@ -53,10 +49,10 @@ export async function file2base64(path: string) {
result.err = e.toString() result.err = e.toString()
return result return result
} }
const data = await readFile(path) const data = await fsPromise.readFile(path)
// 转换为Base64编码 // 转换为Base64编码
result.data = data.toString('base64') result.data = data.toString('base64')
} catch (err) { } catch (err: any) {
result.err = err.toString() result.err = err.toString()
} }
return result return result
@@ -66,7 +62,7 @@ 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 = createHash('md5')
stream.on('data', (data: Buffer) => { stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态 // 当读取到数据时,更新哈希对象的状态
@@ -91,7 +87,6 @@ export interface HttpDownloadOptions {
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 url: string let url: string
let headers: Record<string, string> = { let headers: Record<string, string> = {
'User-Agent': 'User-Agent':
@@ -109,12 +104,10 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
} }
} }
} }
const fetchRes = await net.fetch(url, {headers}) const fetchRes = await fetch(url, { headers })
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`) if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob() return Buffer.from(await fetchRes.arrayBuffer())
let buffer = await blob.arrayBuffer()
return Buffer.from(buffer)
} }
type Uri2LocalRes = { type Uri2LocalRes = {
@@ -126,7 +119,7 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> { export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> {
let res = { let res = {
success: false, success: false,
errMsg: '', errMsg: '',
@@ -136,13 +129,13 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
isLocal: false, isLocal: false,
} }
if (!fileName) { if (!fileName) {
fileName = uuidv4() fileName = randomUUID()
} }
let filePath = path.join(TEMP_DIR, fileName) let filePath = path.join(TEMP_DIR, fileName)
let url = null let url: URL | null = null
try { try {
url = new URL(uri) url = new URL(uri)
} catch (e) { } catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res return res
} }
@@ -153,17 +146,17 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
let base64Data = uri.split('base64://')[1] let base64Data = uri.split('base64://')[1]
try { try {
const buffer = Buffer.from(base64Data, 'base64') const buffer = Buffer.from(base64Data, 'base64')
fs.writeFileSync(filePath, buffer) await fsPromise.writeFile(filePath, buffer)
} catch (e: any) { } catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString() res.errMsg = `base64文件下载失败,` + e.toString()
return res return res
} }
} else if (url.protocol == 'http:' || url.protocol == 'https:') { } else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件 // 下载文件
let buffer: Buffer = null let buffer: Buffer | null = null
try { try {
buffer = await httpDownload(uri) buffer = await httpDownload(uri)
} catch (e) { } catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString() res.errMsg = `${url}下载失败,` + e.toString()
return res return res
} }
@@ -178,8 +171,8 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
} }
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_') fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
res.fileName = fileName res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName) filePath = path.join(TEMP_DIR, randomUUID() + fileName)
fs.writeFileSync(filePath, buffer) await fsPromise.writeFile(filePath, buffer)
} catch (e: any) { } catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString() res.errMsg = `${url}下载失败,` + e.toString()
return res return res
@@ -215,10 +208,10 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
// } // }
if (!res.isLocal && !res.ext) { if (!res.isLocal && !res.ext) {
try { try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext const ext = (await fileType.fileTypeFromFile(filePath))?.ext
if (ext) { if (ext) {
log('获取文件类型', ext, filePath) log('获取文件类型', ext, filePath)
fs.renameSync(filePath, filePath + `.${ext}`) await fsPromise.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}` filePath += `.${ext}`
res.fileName += `.${ext}` res.fileName += `.${ext}`
res.ext = ext res.ext = ext

View File

@@ -41,7 +41,7 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
}) })
} }
export function isNull(value: any) { export function isNull(value: any): value is null | undefined | void {
return value === undefined || value === null return value === undefined || value === null
} }

View File

@@ -91,7 +91,7 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) {
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 ''
} }

View File

@@ -31,10 +31,10 @@ export async function getVideoInfo(filePath: string) {
console.log('未找到视频流信息。') console.log('未找到视频流信息。')
} }
resolve({ resolve({
width: videoStream.width, width: videoStream?.width!,
height: videoStream.height, height: videoStream?.height!,
time: parseInt(videoStream.duration), time: parseInt(videoStream?.duration!),
format: metadata.format.format_name, format: metadata.format.format_name!,
size, size,
filePath, filePath,
}) })
@@ -67,7 +67,7 @@ export async function encodeMp4(filePath: string) {
return videoInfo return videoInfo
} }
export function checkFfmpeg(newPath: string = null): Promise<boolean> { export function checkFfmpeg(newPath: string | null = null): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
log('开始检查ffmpeg', newPath) log('开始检查ffmpeg', newPath)
if (newPath) { if (newPath) {

2
src/global.d.ts vendored
View File

@@ -3,6 +3,6 @@ import { type LLOneBot } from './preload'
declare global { declare global {
interface Window { interface Window {
llonebot: LLOneBot llonebot: LLOneBot
LiteLoader: any LiteLoader: Record<string, any>
} }
} }

View File

@@ -13,7 +13,7 @@ import {
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, qqPkgInfo } from '../common/utils'
import { import {
friendRequests, friendRequests,
getFriend, getFriend,
@@ -36,11 +36,8 @@ import {
RawMessage, RawMessage,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { OB11FriendRecallNoticeEvent } from '../onebot11/event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from '../onebot11/event/notice/OB11GroupRecallNoticeEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
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'
@@ -48,17 +45,15 @@ 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 { crychic } from '../ntqqapi/native/crychic'
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'
import '../ntqqapi/native/wrapper' import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api' import { sentMessages } from '@/ntqqapi/api'
import { NTEventDispatch } from '../common/utils/EventTask'
let running = false import { wrapperApi, wrapperConstructor } from '../ntqqapi/wrapper'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
@@ -128,7 +123,7 @@ function onLoad() {
return return
} }
dialog dialog
.showMessageBox(mainWindow, { .showMessageBox(mainWindow!, {
type: 'question', type: 'question',
buttons: ['确认', '取消'], buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认" defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
@@ -191,37 +186,31 @@ function onLoad() {
postOb11Event(groupEvent) postOb11Event(groupEvent)
} }
}) })
OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => { OB11Constructor.PrivateEvent(message).then((privateEvent) => {
if (friendAddEvent) { log(message)
// log("post friend add event", friendAddEvent); if (privateEvent) {
postOb11Event(friendAddEvent) // log("post private event", privateEvent);
postOb11Event(privateEvent)
} }
}) })
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
// log(message)
// if (friendAddEvent) {
// // log("post friend add event", friendAddEvent);
// postOb11Event(friendAddEvent)
// }
// })
} }
} }
async function startReceiveHook() { async function startReceiveHook() {
startHook().then() startHook()
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<{ registerReceiveHook<{
msgList: Array<RawMessage> msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => { }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try { try {
await postReceiveMsg(payload.msgList) await postReceiveMsg(payload.msgList)
} catch (e) { } catch (e: any) {
log('report message error: ', e.stack.toString()) log('report message error: ', e.stack.toString())
} }
}) })
@@ -265,7 +254,7 @@ function onLoad() {
// log("reportSelfMessage", payload) // log("reportSelfMessage", payload)
try { try {
await postReceiveMsg([payload.msgRecord]) await postReceiveMsg([payload.msgRecord])
} catch (e) { } catch (e: any) {
log('report self message error: ', e.stack.toString()) log('report self message error: ', e.stack.toString())
} }
}) })
@@ -305,34 +294,36 @@ function onLoad() {
// if (notify.user2.uid) { // if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid); // member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// } // }
if ( // 原本的群管变更通知事件处理
[GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes( // if (
notify.type, // [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
) // notify.type,
) { // )
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid) // ) {
log('有管理员变动通知') // const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
refreshGroupMembers(notify.group.groupCode).then() // log('有管理员变动通知')
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent() // refreshGroupMembers(notify.group.groupCode).then()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode) // let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
log('开始获取变动的管理员') // groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
if (member1) { // log('开始获取变动的管理员')
log('变动管理员获取成功') // if (member1) {
groupAdminNoticeEvent.user_id = parseInt(member1.uin) // log('变动管理员获取成功')
groupAdminNoticeEvent.sub_type = [ // groupAdminNoticeEvent.user_id = parseInt(member1.uin)
GroupNotifyTypes.ADMIN_UNSET, // groupAdminNoticeEvent.sub_type = [
GroupNotifyTypes.ADMIN_UNSET_OTHER, // GroupNotifyTypes.ADMIN_UNSET,
].includes(notify.type) // GroupNotifyTypes.ADMIN_UNSET_OTHER,
? 'unset' // ].includes(notify.type)
: 'set' // ? 'unset'
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal; // : 'set'
postOb11Event(groupAdminNoticeEvent, true) // // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
} // postOb11Event(groupAdminNoticeEvent, true)
else { // }
log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode)) // else {
} // log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
} // }
else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { // }
// else
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify) log('有成员退出通知', notify)
try { try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid) const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid)
@@ -341,7 +332,7 @@ function onLoad() {
if (notify.user2.uid) { if (notify.user2.uid) {
// 是被踢的 // 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid) const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid)
operatorId = member2.uin operatorId = member2?.uin!
subType = 'kick' subType = 'kick'
} }
let groupDecreaseEvent = new OB11GroupDecreaseEvent( let groupDecreaseEvent = new OB11GroupDecreaseEvent(
@@ -351,14 +342,12 @@ function onLoad() {
subType, subType,
) )
postOb11Event(groupDecreaseEvent, true) postOb11Event(groupDecreaseEvent, true)
} catch (e) { } catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString()) log('获取群通知的成员信息失败', notify, e.stack.toString())
} }
} }
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求') log('有加群请求')
let groupRequestEvent = new OB11GroupRequestEvent()
groupRequestEvent.group_id = parseInt(notify.group.groupCode)
let requestQQ = uidMaps[notify.user1.uid] let requestQQ = uidMaps[notify.user1.uid]
if (!requestQQ) { if (!requestQQ) {
try { try {
@@ -367,40 +356,47 @@ function onLoad() {
log('获取加群人QQ号失败', e) log('获取加群人QQ号失败', e)
} }
} }
groupRequestEvent.user_id = parseInt(requestQQ) || 0 let invitorId: number
groupRequestEvent.sub_type = 'add'
groupRequestEvent.comment = notify.postscript
groupRequestEvent.flag = notify.seq
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) { if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite' // groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid] let invitorQQ = uidMaps[notify.user2.uid]
if (!invitorQQ) { if (!invitorQQ) {
try { try {
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)) let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))
groupRequestEvent.invitor_id = parseInt(invitor.uin) invitorId = parseInt(invitor.uin)
} catch (e) { } catch (e) {
groupRequestEvent.invitor_id = 0 invitorId = 0
log('获取邀请人QQ号失败', e) log('获取邀请人QQ号失败', e)
} }
} }
} }
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0,
notify.seq,
notify.postscript,
invitorId!,
'add'
)
postOb11Event(groupRequestEvent) postOb11Event(groupRequestEvent)
} }
else if (notify.type == GroupNotifyTypes.INVITE_ME) { else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知') log('收到邀请我加群通知')
let groupInviteEvent = new OB11GroupRequestEvent() let userId = uidMaps[notify.user2.uid]
groupInviteEvent.group_id = parseInt(notify.group.groupCode) if (!userId) {
let user_id = uidMaps[notify.user2.uid] userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
} }
groupInviteEvent.user_id = parseInt(user_id) const groupInviteEvent = new OB11GroupRequestEvent(
groupInviteEvent.sub_type = 'invite' parseInt(notify.group.groupCode),
// groupInviteEvent.invitor_id = parseInt(user_id) parseInt(userId),
groupInviteEvent.flag = notify.seq notify.seq,
undefined,
undefined,
'invite'
)
postOb11Event(groupInviteEvent) postOb11Event(groupInviteEvent)
} }
} catch (e) { } catch (e: any) {
log('解析群通知失败', e.stack.toString()) log('解析群通知失败', e.stack.toString())
} }
} }
@@ -412,19 +408,18 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) { for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime const flag = req.friendUid + req.reqTime
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) { if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) {
friendRequests[flag] = req friendRequests[flag] = req
log('有新的好友请求', req) log('有新的好友请求', req)
let friendRequestEvent = new OB11FriendRequestEvent() let userId: number
try { try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid) const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin) userId = parseInt(requester.uin)
} catch (e) { } catch (e) {
log('获取加好友者QQ号失败', e) log('获取加好友者QQ号失败', e)
} }
friendRequestEvent.flag = flag const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
friendRequestEvent.comment = req.extWords
postOb11Event(friendRequestEvent) postOb11Event(friendRequestEvent)
} }
} }
@@ -435,6 +430,11 @@ function onLoad() {
async function start() { async function start() {
log('llonebot pid', process.pid) log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
llonebotError.otherError = '' llonebotError.otherError = ''
startTime = Date.now() startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => { dbUtil.getReceivedTempUinMap().then((m) => {
@@ -442,6 +442,7 @@ function onLoad() {
uidMaps[value] = key uidMaps[value] = key
} }
}) })
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: wrapperApi.NodeIQQNTWrapperSession! })
try { try {
log('start get groups') log('start get groups')
const _groups = await NTQQGroupApi.getGroups() const _groups = await NTQQGroupApi.getGroups()
@@ -469,7 +470,6 @@ function onLoad() {
} }
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) { if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort) ob11HTTPServer.start(config.ob11.httpPort)
} }
@@ -513,7 +513,7 @@ function onLoad() {
selfInfo.nick = userInfo.nick selfInfo.nick = userInfo.nick
return return
} }
} catch (e) { } catch (e: any) {
log('get self nickname failed', e.stack) log('get self nickname failed', e.stack)
} }
if (getSelfNickCount < 10) { if (getSelfNickCount < 10) {
@@ -541,7 +541,7 @@ function onBrowserWindowCreated(window: BrowserWindow) {
try { try {
hookNTQQApiCall(window) hookNTQQApiCall(window)
hookNTQQApiReceive(window) hookNTQQApiReceive(window)
} catch (e) { } catch (e: any) {
log('LLOneBot hook error: ', e.toString()) log('LLOneBot hook error: ', e.toString())
} }
} }

View File

@@ -11,22 +11,24 @@ import {
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement, IMAGE_HTTP_HOST_NT, PicElement,
} from '../types' } from '../types'
import path from 'path' import path from 'node:path'
import fs from 'fs' import fs from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/native/wrapper' import { wrapperApi } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/api/msg' import { Peer } from '@/ntqqapi/types/msg'
export class NTQQFileApi { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
return (await wrapperApi.NodeIQQNTWrapperSession.getRichMediaService().getVideoPlayUrlV2(peer, const session = wrapperApi.NodeIQQNTWrapperSession
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId, msgId,
elementId, elementId,
0, 0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url; { downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url;
} }
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,

View File

@@ -1,8 +1,10 @@
import { Friend, FriendRequest } from '../types' import { Friend, FriendRequest, FriendV2 } 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' import { wrapperApi } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '../../common/utils/EventTask'
export class NTQQFriendApi { export class NTQQFriendApi {
static async getFriends(forced = false) { static async getFriends(forced = false) {
@@ -26,6 +28,7 @@ export class NTQQFriendApi {
} }
return _friends return _friends
} }
static async likeFriend(uid: string, count = 1) { static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND, methodName: NTQQApiMethod.LIKE_FRIEND,
@@ -42,6 +45,7 @@ export class NTQQFriendApi {
], ],
}) })
} }
static async handleFriendRequest(flag: string, accept: boolean) { static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag] const request: FriendRequest = friendRequests[flag]
if (!request) { if (!request) {
@@ -62,4 +66,16 @@ export class NTQQFriendApi {
delete friendRequests[flag] delete friendRequests[flag]
return result return result
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
const session = wrapperApi.NodeIQQNTWrapperSession
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values())
}
} }

View File

@@ -5,9 +5,9 @@ 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'
import { wrapperApi } from '../wrapper'
export class NTQQGroupApi { export class NTQQGroupApi {
static async activateMemberListChange() { static async activateMemberListChange() {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE, methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE,
@@ -15,6 +15,7 @@ export class NTQQGroupApi {
args: [], args: [],
}) })
} }
static async activateMemberInfoChange() { static async activateMemberInfoChange() {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE, methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE,
@@ -22,6 +23,7 @@ export class NTQQGroupApi {
args: [], args: [],
}) })
} }
static async getGroupAllInfo(groupCode: string, source: number = 4) { static async getGroupAllInfo(groupCode: string, source: number = 4) {
return await callNTQQApi<GeneralCallResult & Group>({ return await callNTQQApi<GeneralCallResult & Group>({
methodName: NTQQApiMethod.GET_GROUP_ALL_INFO, methodName: NTQQApiMethod.GET_GROUP_ALL_INFO,
@@ -34,6 +36,7 @@ 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') {
@@ -51,6 +54,7 @@ export class NTQQGroupApi {
log('get groups result', result) log('get groups result', result)
return result.groupList return result.groupList
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({ const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
@@ -61,7 +65,7 @@ export class NTQQGroupApi {
}, },
], ],
}) })
// log("get group member sceneId", sceneId); // log("get group member sceneId", sceneId)
try { try {
const result = await callNTQQApi<{ const result = await callNTQQApi<{
result: { infos: any } result: { infos: any }
@@ -82,8 +86,8 @@ export class NTQQGroupApi {
for (const member of members) { for (const member of members) {
uidMaps[member.uid] = member.uin uidMaps[member.uid] = member.uin
} }
// log(uidMaps); // log(uidMaps)
// log("members info", values); // log("members info", values)
log(`get group ${groupQQ} members success`) log(`get group ${groupQQ} members success`)
return members return members
} catch (e) { } catch (e) {
@@ -91,6 +95,7 @@ export class NTQQGroupApi {
return [] return []
} }
} }
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) { static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO, methodName: NTQQApiMethod.GROUP_MEMBERS_INFO,
@@ -104,6 +109,7 @@ export class NTQQGroupApi {
], ],
}) })
} }
static async getGroupNotifies() { static async getGroupNotifies() {
// 获取管理员变更 // 获取管理员变更
// 加群通知,退出通知,需要管理员权限 // 加群通知,退出通知,需要管理员权限
@@ -118,6 +124,7 @@ export class NTQQGroupApi {
args: [{ doubt: false, startSeq: '', number: 14 }, null], args: [{ doubt: false, startSeq: '', number: 14 }, null],
}) })
} }
static async getGroupIgnoreNotifies() { static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies() await NTQQGroupApi.getGroupNotifies()
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>( return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
@@ -126,12 +133,13 @@ export class NTQQGroupApi {
ReceiveCmdS.GROUP_NOTIFY, ReceiveCmdS.GROUP_NOTIFY,
) )
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq) const notify = await dbUtil.getGroupNotify(seq)
if (!notify) { if (!notify) {
throw `${seq}对应的加群通知不存在` throw `${seq}对应的加群通知不存在`
} }
// delete groupNotifies[seq]; // delete groupNotifies[seq]
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [ args: [
@@ -151,6 +159,7 @@ export class NTQQGroupApi {
], ],
}) })
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({ const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP, methodName: NTQQApiMethod.QUIT_GROUP,
@@ -161,6 +170,7 @@ export class NTQQGroupApi {
} }
return result return result
} }
static async kickMember( static async kickMember(
groupQQ: string, groupQQ: string,
kickUids: string[], kickUids: string[],
@@ -179,7 +189,8 @@ export class NTQQGroupApi {
], ],
}) })
} }
static async banMember(groupQQ: string, memList: Array<{ uid: string; timeStamp: number }>) {
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_MEMBER, methodName: NTQQApiMethod.MUTE_MEMBER,
@@ -191,6 +202,7 @@ export class NTQQGroupApi {
], ],
}) })
} }
static async banGroup(groupQQ: string, shutUp: boolean) { static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP, methodName: NTQQApiMethod.MUTE_GROUP,
@@ -203,6 +215,7 @@ export class NTQQGroupApi {
], ],
}) })
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log) NTQQGroupApi.activateMemberListChange().then().catch(log)
const res = await callNTQQApi<GeneralCallResult>({ const res = await callNTQQApi<GeneralCallResult>({
@@ -217,8 +230,9 @@ export class NTQQGroupApi {
], ],
}) })
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log) NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res; return res
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE, methodName: NTQQApiMethod.SET_MEMBER_ROLE,
@@ -232,6 +246,7 @@ export class NTQQGroupApi {
], ],
}) })
} }
static async setGroupName(groupQQ: string, groupName: string) { static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME, methodName: NTQQApiMethod.SET_GROUP_NAME,
@@ -281,5 +296,34 @@ export class NTQQGroupApi {
], ],
}) })
} }
static publishGroupBulletin(groupQQ: string, title: string, content: string) { } static publishGroupBulletin(groupQQ: string, title: string, content: string) { }
static async removeGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param)
}
static async addGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
}
} }

View File

@@ -1,22 +1,17 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement } from '../types' import { ChatType, RawMessage, SendMessageElement, Peer } 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'
import { wrapperApi } from '@/ntqqapi/wrapper'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ''
}
async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) { async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) {
// 等待上一个相同的peer发送完 // 等待上一个相同的peer发送完
const peerUid = peer.peerUid const peerUid = peer.peerUid
@@ -38,7 +33,7 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
} }
await waitLastSend() await waitLastSend()
let sentMessage: RawMessage = null let sentMessage: RawMessage | null = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid] delete sendMessagePool[peerUid]
sentMessage = rawMessage sentMessage = rawMessage
@@ -293,4 +288,8 @@ export class NTQQMsgApi {
}) })
}) })
} }
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
const session = wrapperApi.NodeIQQNTWrapperSession
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
}
} }

View File

@@ -3,57 +3,62 @@
import { log } from '@/common/utils' import { log } from '@/common/utils'
interface ServerRkeyData { interface ServerRkeyData {
group_rkey: string; group_rkey: string
private_rkey: string; private_rkey: string
expired_time: number; expired_time: number
} }
class RkeyManager { class RkeyManager {
serverUrl: string = ''; serverUrl: string = ''
private rkeyData: ServerRkeyData = { private rkeyData: ServerRkeyData = {
group_rkey: '', group_rkey: '',
private_rkey: '', private_rkey: '',
expired_time: 0 expired_time: 0
};
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
} }
constructor(serverUrl: string) {
this.serverUrl = serverUrl
}
async getRkey() { async getRkey() {
if (this.isExpired()) { if (this.isExpired()) {
try { try {
await this.refreshRkey(); await this.refreshRkey()
} catch (e) { } catch (e) {
log('获取rkey失败', e); log('获取rkey失败', e)
} }
} }
return this.rkeyData; return this.rkeyData
} }
isExpired(): boolean { isExpired(): boolean {
const now = new Date().getTime() / 1000; const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`); // console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time; return now > this.rkeyData.expired_time
} }
async refreshRkey(): Promise<any> { async refreshRkey(): Promise<any> {
//刷新rkey //刷新rkey
this.rkeyData = await this.fetchServerRkey(); this.rkeyData = await this.fetchServerRkey()
} }
async fetchServerRkey() { async fetchServerRkey() {
return new Promise<ServerRkeyData>((resolve, reject) => { return new Promise<ServerRkeyData>((resolve, reject) => {
fetch(this.serverUrl) fetch(this.serverUrl)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return reject(response.statusText); // 请求失败,返回错误信息 return reject(response.statusText) // 请求失败,返回错误信息
} }
return response.json(); // 解析 JSON 格式的响应体 return response.json() // 解析 JSON 格式的响应体
}) })
.then(data => { .then(data => {
resolve(data); resolve(data)
}) })
.catch(error => { .catch(error => {
reject(error); reject(error)
}); })
}); })
} }
} }
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey');
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey')

View File

@@ -2,19 +2,21 @@ import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../
import { Group, SelfInfo, User } from '../types' import { Group, SelfInfo, User } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { selfInfo, uidMaps } from '../../common/data' import { selfInfo, uidMaps } from '../../common/data'
import { NTQQWindowApi, NTQQWindows } from './window'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils' import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper' import { wrapperApi } from '@/ntqqapi/wrapper'
import * as https from 'https'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
let userInfoCache: Record<string, User> = {} // uid: User const userInfoCache: Record<string, User> = {} // uid: User
export interface ClientKeyData extends GeneralCallResult { export interface ClientKeyData extends GeneralCallResult {
url: string; url: string
keyIndex: string; keyIndex: string
clientKey: string; clientKey: string
expireTime: string; expireTime: string
} }
export class NTQQUserApi { export class NTQQUserApi {
@@ -48,8 +50,49 @@ export class NTQQUserApi {
return result.profiles.get(uid) return result.profiles.get(uid)
} }
// 26702
static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
1,
5000,
(profile) => {
if (profile.uid === uid) {
return true;
}
return false;
},
'BuddyProfileStore',
[
uid
],
UserDetailSource.KSERVER,
[
ProfileBizType.KALL
]
)
const RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: ''
}
return RetUser
}
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
// this.getUserInfo(uid); if (+qqPkgInfo.buildVersion >= 26702) {
return this.fetchUserDetailInfo(uid)
}
// this.getUserInfo(uid)
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
if (!withBizInfo) { if (!withBizInfo) {
methodName = NTQQApiMethod.USER_DETAIL_INFO methodName = NTQQApiMethod.USER_DETAIL_INFO
@@ -82,7 +125,7 @@ export class NTQQUserApi {
await fetchInfo() await fetchInfo()
await sleep(1000) await sleep(1000)
} }
let userInfo = await fetchInfo() const userInfo = await fetchInfo()
userInfoCache[uid] = userInfo userInfoCache[uid] = userInfo
return userInfo return userInfo
} }
@@ -99,16 +142,17 @@ export class NTQQUserApi {
], ],
}) })
} }
static async getQzoneCookies() { static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27' const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string; } = {}; let cookies: { [key: string]: string } = {}
try { try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl); cookies = await RequestUtil.HttpsGetCookies(requestUrl)
} catch (e: any) { } catch (e: any) {
log('获取QZone Cookies失败', e) log('获取QZone Cookies失败', e)
cookies = {} cookies = {}
} }
return cookies; return cookies
} }
static async getSkey(): Promise<string> { static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey() const clientKeyData = await this.getClientKey()
@@ -117,24 +161,24 @@ export class NTQQUserApi {
} }
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey + '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex; + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey; return (await RequestUtil.HttpsGetCookies(url))?.skey
} }
@cacheFunc(60 * 30 * 1000) @cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) { static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) { if (domain.endsWith("qzone.qq.com")) {
let data = (await NTQQUserApi.getQzoneCookies()); let data = (await NTQQUserApi.getQzoneCookies())
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin; const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue }; return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue }
} }
const skey = await this.getSkey(); const skey = await this.getSkey()
const pskey = (await this.getPSkey([domain])).get(domain); const pskey = (await this.getPSkey([domain])).get(domain)
if (!pskey || !skey) { if (!pskey || !skey) {
throw new Error('获取Cookies失败') throw new Error('获取Cookies失败')
} }
const bkn = NTQQUserApi.genBkn(skey) const bkn = NTQQUserApi.genBkn(skey)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`; const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`
return { cookies, bkn } return { cookies, bkn }
} }
@@ -151,7 +195,8 @@ export class NTQQUserApi {
} }
static async getPSkey(domains: string[]): Promise<Map<string, string>> { static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const res = await wrapperApi.NodeIQQNTWrapperSession.getTipOffService().getPskey(domains, true) const session = wrapperApi.NodeIQQNTWrapperSession
const res = await session?.getTipOffService().getPskey(domains, true)
if (res.result !== 0) { if (res.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`) throw new Error(`获取Pskey失败: ${res.errMsg}`)
} }
@@ -159,7 +204,7 @@ export class NTQQUserApi {
} }
static async getClientKey(): Promise<ClientKeyData> { static async getClientKey(): Promise<ClientKeyData> {
return await wrapperApi.NodeIQQNTWrapperSession.getTicketService().forceFetchClientKey('') const session = wrapperApi.NodeIQQNTWrapperSession
return await session?.getTicketService().forceFetchClientKey('')
} }
} }

View File

@@ -1,7 +1,8 @@
import { WebGroupData, groups, selfInfo } from '@/common/data'; import { WebGroupData, groups, selfInfo } from '@/common/data'
import { log } from '@/common/utils/log'; import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user'; import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request'; import { RequestUtil } from '@/common/utils/request'
export enum WebHonorType { export enum WebHonorType {
ALL = 'all', ALL = 'all',
TALKACTIVE = 'talkative', TALKACTIVE = 'talkative',
@@ -10,6 +11,7 @@ export enum WebHonorType {
STORONGE_NEWBI = 'strong_newbie', STORONGE_NEWBI = 'strong_newbie',
EMOTION = 'emotion' EMOTION = 'emotion'
} }
export interface WebApiGroupMember { export interface WebApiGroupMember {
uin: number uin: number
role: number role: number
@@ -27,6 +29,7 @@ export interface WebApiGroupMember {
qage: number qage: number
rm: number rm: number
} }
interface WebApiGroupMemberRet { interface WebApiGroupMemberRet {
ec: number ec: number
errcode: number errcode: number
@@ -41,6 +44,7 @@ interface WebApiGroupMemberRet {
search_count: number search_count: number
extmode: number extmode: number
} }
export interface WebApiGroupNoticeFeed { export interface WebApiGroupNoticeFeed {
u: number//发送者 u: number//发送者
fid: string//fid fid: string//fid
@@ -69,6 +73,7 @@ export interface WebApiGroupNoticeFeed {
is_read: number is_read: number
is_all_confirm: number is_all_confirm: number
} }
export interface WebApiGroupNoticeRet { export interface WebApiGroupNoticeRet {
ec: number ec: number
em: string em: string
@@ -89,6 +94,7 @@ export interface WebApiGroupNoticeRet {
svrt: number svrt: number
ad: number ad: number
} }
interface GroupEssenceMsg { interface GroupEssenceMsg {
group_code: string group_code: string
msg_seq: number msg_seq: number
@@ -102,6 +108,7 @@ interface GroupEssenceMsg {
msg_content: any[] msg_content: any[]
can_be_removed: true can_be_removed: true
} }
export interface GroupEssenceMsgRet { export interface GroupEssenceMsgRet {
retcode: number retcode: number
retmsg: string retmsg: string
@@ -112,22 +119,24 @@ export interface GroupEssenceMsgRet {
config_page_url: string config_page_url: string
} }
} }
export class WebApi { export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet> { static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> {
const { cookies: CookieValue, bkn: Bkn } = (await NTQQUserApi.getCookies('qun.qq.com')) const { cookies: CookieValue, bkn: Bkn } = (await NTQQUserApi.getCookies('qun.qq.com'))
const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'; const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'
let ret; let ret: GroupEssenceMsgRet
try { try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue }); ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue })
} catch { } catch {
return undefined; return undefined
} }
//console.log(url, CookieValue); //console.log(url, CookieValue)
if (ret.retcode !== 0) { if (ret.retcode !== 0) {
return undefined; return undefined
} }
return ret; return ret
} }
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode); log('webapi 获取群成员', GroupCode);
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>(); let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
@@ -190,6 +199,7 @@ export class WebApi {
// const res = await this.request(url); // const res = await this.request(url);
// return await res.json(); // return await res.json();
// } // }
static async setGroupNotice(GroupCode: string, Content: string = '') { static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn} //https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1} //qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
@@ -213,6 +223,7 @@ export class WebApi {
} }
return undefined; return undefined;
} }
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> { static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey(); const _Skey = await NTQQUserApi.getSkey();
@@ -236,6 +247,7 @@ export class WebApi {
} }
return undefined; return undefined;
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {
sKey = sKey || ''; sKey = sKey || '';
let hash = 5381; let hash = 5381;
@@ -247,6 +259,7 @@ export class WebApi {
return (hash & 0x7FFFFFFF).toString(); return (hash & 0x7FFFFFFF).toString();
} }
//实现未缓存 考虑2h缓存 //实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) { async function getDataInternal(Internal_groupCode: string, Internal_type: number) {

View File

@@ -27,7 +27,7 @@ export class NTQQWindowApi {
static async openWindow<R = GeneralCallResult>( static async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: any[], args: any[],
cbCmd: ReceiveCmd = null, cbCmd: ReceiveCmd | null = null,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await callNTQQApi<R>({ const result = await callNTQQApi<R>({

View File

@@ -283,7 +283,7 @@ export class SendMsgElementConstructor {
if (faceId >= 222){ if (faceId >= 222){
faceType = 2 faceType = 2
} }
if (face.AniStickerType){ if (face?.AniStickerType){
faceType = 3; faceType = 3;
} }
return { return {
@@ -292,10 +292,10 @@ export class SendMsgElementConstructor {
faceElement: { faceElement: {
faceIndex: faceId, faceIndex: faceId,
faceType, faceType,
faceText: face.QDes, faceText: face?.QDes,
stickerId: face.AniStickerId, stickerId: face?.AniStickerId,
stickerType: face.AniStickerType, stickerType: face?.AniStickerType,
packId: face.AniStickerPackId, packId: face?.AniStickerPackId,
sourceType: 1, sourceType: 1,
}, },
} }
@@ -329,7 +329,7 @@ export class SendMsgElementConstructor {
stickerId: '33', stickerId: '33',
sourceType: 1, sourceType: 1,
stickerType: 2, stickerType: 2,
resultId: resultId.toString(), resultId: resultId?.toString(),
surpriseId: '', surpriseId: '',
// "randomType": 1, // "randomType": 1,
}, },
@@ -351,7 +351,7 @@ export class SendMsgElementConstructor {
stickerId: '34', stickerId: '34',
sourceType: 1, sourceType: 1,
stickerType: 2, stickerType: 2,
resultId: resultId.toString(), resultId: resultId?.toString(),
surpriseId: '', surpriseId: '',
// "randomType": 1, // "randomType": 1,
}, },

View File

@@ -1,4 +1,4 @@
import { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg' import { NTQQMsgApi, sendMessagePool } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types' import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types'
@@ -13,7 +13,6 @@ import {
uidMaps, uidMaps,
} from '@/common/data' } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { v4 as uuidv4 } from 'uuid'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config' import { getConfigUtil, HOOK_LOG } from '@/common/config'
import fs from 'fs' import fs from 'fs'
@@ -23,6 +22,8 @@ import { log } from '@/common/utils'
import { isNumeric, sleep } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { randomUUID } from 'node:crypto'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -122,7 +123,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
delete hookApiCallbacks[callbackId] delete hookApiCallbacks[callbackId]
} }
} }
} catch (e) { } catch (e: any) {
log('hookNTQQApiReceive error', e.stack.toString(), args) log('hookNTQQApiReceive error', e.stack.toString(), args)
} }
originalSend.call(window.webContents, channel, ...args) originalSend.call(window.webContents, channel, ...args)
@@ -156,7 +157,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
try { try {
let _ = hook.hookFunc(callParams) let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') { if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then() (_ as Promise<void>).then()
} }
} catch (e) { } catch (e) {
log('hook call error', e, _args) log('hook call error', e, _args)
@@ -203,7 +204,7 @@ export function registerReceiveHook<PayloadType>(
method: ReceiveCmd | ReceiveCmd[], method: ReceiveCmd | ReceiveCmd[],
hookFunc: (payload: PayloadType) => void, hookFunc: (payload: PayloadType) => void,
): string { ): string {
const id = uuidv4() const id = randomUUID()
if (!Array.isArray(method)) { if (!Array.isArray(method)) {
method = [method] method = [method]
} }
@@ -295,7 +296,7 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢 // 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin) let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) { if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue continue
} }
for (const member of oldMembers) { for (const member of oldMembers) {
@@ -319,7 +320,7 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
} }
updateGroups(newGroupList, false).then() updateGroups(newGroupList, false).then()
} catch (e) { } catch (e: any) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString()) log('更新群信息错误', e.stack.toString())
} }
@@ -369,6 +370,14 @@ export async function startHook() {
postOb11Event( postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName), new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
) )
} else if (member.role != existMember.role) {
log('有管理员变动通知')
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent(
member.role == GroupMemberRole.admin ? 'set' : 'unset',
parseInt(groupCode),
parseInt(member.uin)
)
postOb11Event(groupAdminNoticeEvent, true)
} }
Object.assign(existMember, member) Object.assign(existMember, member)
} }
@@ -445,7 +454,7 @@ export async function startHook() {
const pttPath = msgElement.pttElement?.filePath const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()] const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath] const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) { if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath)) pathList.push(...Object.values(msgElement.picElement.thumbPath))
@@ -463,7 +472,7 @@ export async function startHook() {
}) })
} }
} }
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000) }, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
} }
} }
}) })
@@ -478,7 +487,7 @@ export async function startHook() {
if (sendCallback) { if (sendCallback) {
try { try {
sendCallback(message) sendCallback(message)
} catch (e) { } catch (e: any) {
log('receive self msg error', e.stack) log('receive self msg error', e.stack)
} }
} }
@@ -510,8 +519,8 @@ export async function startHook() {
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))
} }
}) })
}) })
@@ -542,5 +551,4 @@ export async function startHook() {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
}) })
}) })
} }

View File

@@ -0,0 +1,44 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void
onProfileDetailInfoChanged(profile: User): void
onStatusUpdate(...args: unknown[]): void
onSelfStatusChanged(...args: unknown[]): void
onStrangerRemarkChanged(...args: unknown[]): void
}
export interface NodeIKernelProfileListener extends IProfileListener {
new(listener: IProfileListener): NodeIKernelProfileListener
}
export class ProfileListener implements IProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]) {
}
onProfileDetailInfoChanged(profile: User) {
}
onStatusUpdate(...args: unknown[]) {
}
onSelfStatusChanged(...args: unknown[]) {
}
onStrangerRemarkChanged(...args: unknown[]) {
}
}

View File

@@ -0,0 +1 @@
export * from './NodeIKernelProfileListener'

View File

@@ -1,19 +0,0 @@
let Process = require('process')
let os = require('os')
Process.dlopenOrig = Process.dlopen
export const wrapperApi: any = {}
Process.dlopen = function(module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
let dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
let ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
},
})
}
return dlopenRet
}

View File

@@ -1,10 +1,8 @@
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 { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window'
import { HOOK_LOG } from '../common/config' import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto'
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = 'ns-ntApi', NT_API = 'ns-ntApi',
@@ -130,7 +128,7 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
args = args ?? [] args = args ?? []
timeout = timeout ?? 5 timeout = timeout ?? 5
afterFirstCmd = afterFirstCmd ?? true afterFirstCmd = afterFirstCmd ?? true
const uuid = uuidv4() const uuid = randomUUID()
HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) // log("callNTQQApiPromise", channel, className, methodName, args, uuid)

View File

@@ -0,0 +1,125 @@
import { GeneralCallResult } from './common'
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService {
// 26702 以上
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: Array<{
categoryId: number,
categorySortId: number,
categroyName: string,
categroyMbCount: number,
onlineCount: number,
buddyUids: Array<string>
}>
}>
//26702 以上
getBuddyListFromCache(callFrom: string): Promise<Array<
{
categoryId: number,//9999应该跳过 那是兜底数据吧
categorySortId: number,//排序方式
categroyName: string,//分类名
categroyMbCount: number,//不懂
onlineCount: number,//在线数目
buddyUids: Array<string>//Uids
}>>
addKernelBuddyListener(listener: any): number
getAllBuddyCount(): number
removeKernelBuddyListener(listener: unknown): void
getBuddyList(nocache: boolean): Promise<GeneralCallResult>
getBuddyNick(uid: number): string
getBuddyRemark(uid: number): string
setBuddyRemark(uid: number, remark: string): void
getAvatarUrl(uid: number): string
isBuddy(uid: string): boolean
getCategoryNameWithUid(uid: number): string
getTargetBuddySetting(uid: number): unknown
getTargetBuddySettingByType(uid: number, type: number): unknown
getBuddyReqUnreadCnt(): number
getBuddyReq(): unknown
delBuddyReq(uid: number): void
clearBuddyReqUnreadCnt(): void
reqToAddFriends(uid: number, msg: string): void
setSpacePermission(uid: number, permission: number): void
approvalFriendRequest(arg: {
friendUid: string
reqTime: string
accept: boolean
}): Promise<void>
delBuddy(uid: number): void
delBatchBuddy(uids: number[]): void
getSmartInfos(uid: number): unknown
setBuddyCategory(uid: number, category: number): void
setBatchBuddyCategory(uids: number[], category: number): void
addCategory(category: string): void
delCategory(category: string): void
renameCategory(oldCategory: string, newCategory: string): void
resortCategory(categorys: string[]): void
pullCategory(uid: number, category: string): void
setTop(uid: number, isTop: boolean): void
SetSpecialCare(uid: number, isSpecialCare: boolean): void
setMsgNotify(uid: number, isNotify: boolean): void
hasBuddyList(): boolean
setBlock(uid: number, isBlock: boolean): void
isBlocked(uid: number): boolean
modifyAddMeSetting(setting: unknown): void
getAddMeSetting(): unknown
getDoubtBuddyReq(): unknown
getDoubtBuddyUnreadNum(): number
approvalDoubtBuddyReq(uid: number, isAgree: boolean): void
delDoubtBuddyReq(uid: number): void
delAllDoubtBuddyReq(): void
reportDoubtBuddyReqUnread(): void
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,106 @@
import { AnyCnameRecord } from 'node:dns'
import { SimpleInfo } from '../types'
import { GeneralCallResult } from './common'
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string,string>>//uin->uid
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string,string>>
// {
// coreInfo: CoreInfo,
// baseInfo: BaseInfo,
// status: null,
// vasInfo: null,
// relationFlags: null,
// otherFlags: null,
// intimate: null
// }
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
addKernelProfileListener(listener: any): number
removeKernelProfileListener(listenerId: number): void
prepareRegionConfig(...args: unknown[]): unknown
getLocalStrangerRemark(): Promise<AnyCnameRecord>
enumCountryOptions(): Array<string>
enumProvinceOptions(Country: string): Array<string>
enumCityOptions(Country: string, Province: string): unknown
enumAreaOptions(...args: unknown[]): unknown
//SimpleInfo
// this.uid = ""
// this.uid = str
// this.uin = j2
// this.isBuddy = z
// this.coreInfo = coreInfo
// this.baseInfo = baseInfo
// this.status = statusInfo
// this.vasInfo = vasInfo
// this.relationFlags = relationFlag
// this.otherFlags = otherFlag
// this.intimate = intimate
modifySelfProfile(...args: unknown[]): Promise<unknown>
modifyDesktopMiniProfile(param: any): Promise<GeneralCallResult>
setNickName(NickName: string): Promise<unknown>
setLongNick(longNick: string): Promise<unknown>
setBirthday(...args: unknown[]): Promise<unknown>
setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown>
setRecommendImgFlag(...args: unknown[]): Promise<unknown>
getUserSimpleInfo(force: boolean, uids: string[],): Promise<unknown>
getUserDetailInfo(uid: string): Promise<unknown>
getUserDetailInfoWithBizInfo(uid: string, Biz: any[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<any>
getZplanAvatarInfos(args: string[]): Promise<unknown>
getStatus(uid: string): Promise<unknown>
startStatusPolling(isForceReset: boolean): Promise<unknown>
getSelfStatus(): Promise<unknown>
setdisableEmojiShortCuts(...args: unknown[]): unknown
getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown>
//profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList)
getCoreInfo(name: string, arg: any[]): unknown
//m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>())
isNull(): boolean
}

View File

@@ -0,0 +1,16 @@
export enum GeneralCallResultStatus {
OK = 0
// ERROR = 1
}
export interface GeneralCallResult {
result: GeneralCallResultStatus
errMsg: string
}
export interface forceFetchClientKeyRetType extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}

View File

@@ -0,0 +1,2 @@
export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService'

View File

@@ -1,5 +1,4 @@
import { GroupMemberRole } from './group' import { GroupMemberRole } from './group'
import exp from 'constants'
export enum ElementType { export enum ElementType {
TEXT = 1, TEXT = 1,
@@ -188,6 +187,7 @@ 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 {
picSubType: PicSubType
picType: PicType // 有这玩意儿吗 picType: PicType // 有这玩意儿吗
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
@@ -226,6 +226,7 @@ export interface GrayTipElement {
content: string content: string
} }
jsonGrayTipElement: { jsonGrayTipElement: {
busiId: number
jsonStr: string jsonStr: string
} }
} }
@@ -413,3 +414,9 @@ export interface RawMessage {
multiForwardMsgElement: MultiForwardMsgElement multiForwardMsgElement: MultiForwardMsgElement
}[] }[]
} }
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string
}

View File

@@ -77,8 +77,185 @@ export interface Friend extends User {
} }
export interface CategoryFriend { export interface CategoryFriend {
categoryId: number; categoryId: number
categroyName: string; categroyName: string
categroyMbCount: number; categroyMbCount: number
buddyList: User[] buddyList: User[]
} }
export interface CoreInfo {
uid: string
uin: string
nick: string
remark: string
}
export interface BaseInfo {
qid: string
longNick: string
birthday_year: number
birthday_month: number
birthday_day: number
age: number
sex: number
eMail: string
phoneNum: string
categoryId: number
richTime: number
richBuffer: string
}
interface MusicInfo {
buf: string
}
interface VideoBizInfo {
cid: string
tvUrl: string
synchType: string
}
interface VideoInfo {
name: string
}
interface ExtOnlineBusinessInfo {
buf: string
customStatus: any
videoBizInfo: VideoBizInfo
videoInfo: VideoInfo
}
interface ExtBuffer {
buf: string
}
interface UserStatus {
uid: string
uin: string
status: number
extStatus: number
batteryStatus: number
termType: number
netType: number
iconType: number
customStatus: any
setTime: string
specialFlag: number
abiFlag: number
eNetworkType: number
showName: string
termDesc: string
musicInfo: MusicInfo
extOnlineBusinessInfo: ExtOnlineBusinessInfo
extBuffer: ExtBuffer
}
interface PrivilegeIcon {
jumpUrl: string
openIconList: any[]
closeIconList: any[]
}
interface VasInfo {
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
bigClub: boolean
bigClubLevel: number
nameplateVipType: number
grayNameplateFlag: number
superVipTemplateId: number
diyFontId: number
pendantId: number
pendantDiyId: number
faceId: number
vipFont: number
vipFontType: number
magicFont: number
fontEffect: number
newLoverDiamondFlag: number
extendNameplateId: number
diyNameplateIDs: any[]
vipStartFlag: number
vipDataFlag: number
gameNameplateId: string
gameLastLoginTime: string
gameRank: number
gameIconShowFlag: boolean
gameCardId: string
vipNameColorId: string
privilegeIcon: PrivilegeIcon
}
export interface SimpleInfo {
uid?: string
uin?: string
coreInfo: CoreInfo
baseInfo: BaseInfo
status: UserStatus | null
vasInfo: VasInfo | null
relationFlags: RelationFlags | null
otherFlags: any | null
intimate: any | null
}
interface RelationFlags {
topTime: string
isBlock: boolean
isMsgDisturb: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
isBlocked: boolean
recommendImgFlag: number
disableEmojiShortCuts: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
isHideQQLevel: number
isHidePrivilegeIcon: number
}
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt {
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
college: string
country: string
province: string
city: string
postCode: string
address: string
regTime: number
interest: string
labels: any[]
qqLevel: QQLevel
}
interface Pic {
picId: string
picTime: number
picUrlMap: Record<string, string>
}
interface PhotoWall {
picList: Pic[]
}
export interface UserDetailInfoListenerArg {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: PhotoWall
}

68
src/ntqqapi/wrapper.ts Normal file
View File

@@ -0,0 +1,68 @@
import { NodeIKernelBuddyService } from './services/NodeIKernelBuddyService'
import os from 'node:os'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
[key: string]: any
getBuddyService(): NodeIKernelBuddyService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
export interface WrapperConstructor {
[key: string]: any
NodeIKernelBuddyListener?: any
NodeIKernelGroupListener?: any
NodeQQNTWrapperUtil?: any
NodeIKernelMsgListener?: any
NodeIQQNTWrapperEngine?: any
NodeIGlobalAdapter?: any
NodeIDependsAdapter?: any
NodeIDispatcherAdapter?: any
NodeIKernelSessionListener?: any
NodeIKernelLoginService?: any
NodeIKernelLoginListener?: any
NodeIKernelProfileService?: any
NodeIKernelProfileListener?: any
}
export const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {}
const constructor = [
'NodeIKernelBuddyListener',
'NodeIKernelGroupListener',
'NodeQQNTWrapperUtil',
'NodeIKernelMsgListener',
'NodeIQQNTWrapperEngine',
'NodeIGlobalAdapter',
'NodeIDependsAdapter',
'NodeIDispatcherAdapter',
'NodeIKernelSessionListener',
'NodeIKernelLoginService',
'NodeIKernelLoginListener',
'NodeIKernelProfileService',
'NodeIKernelProfileListener',
]
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
}
return dlopenRet
}

View File

@@ -4,8 +4,8 @@ import { OB11Return } from '../types'
import { log } from '../../common/utils/log' import { log } from '../../common/utils/log'
class BaseAction<PayloadType, ReturnDataType> { abstract class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName abstract actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> { protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return { return {
@@ -21,7 +21,7 @@ class BaseAction<PayloadType, ReturnDataType> {
try { try {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData) return OB11Response.ok(resData)
} catch (e) { } catch (e: any) {
log('发生错误', e) log('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200) return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
} }
@@ -35,7 +35,7 @@ class BaseAction<PayloadType, ReturnDataType> {
try { try {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData, echo) return OB11Response.ok(resData, echo)
} catch (e) { } catch (e: any) {
log('发生错误', e) log('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo) return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
} }

View File

@@ -20,7 +20,7 @@ export interface GetFileResponse {
base64?: string base64?: string
} }
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement { private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement {
let element = msg.elements.find((e) => e.elementId === elementId) let element = msg.elements.find((e) => e.elementId === elementId)
if (!element) { if (!element) {
@@ -41,7 +41,7 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
// 等待文件下载完成 // 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId) msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg) log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg, cache.elementId).filePath cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000) await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then() dbUtil.addFileCache(file, cache).then()
} }

View File

@@ -14,7 +14,7 @@ export default class GetRecord extends GetFileBase {
protected async _handle(payload: Payload): Promise<GetFileResponse> { protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = await super._handle(payload) let res = await super._handle(payload)
res.file = await decodeSilk(res.file, payload.out_format) res.file = await decodeSilk(res.file!, payload.out_format)
res.file_name = path.basename(res.file) res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString() res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){ if (getConfigUtil().getConfig().enableLocalFile2Url){

View File

@@ -0,0 +1,24 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { dbUtil } from '@/common/db';
interface Payload {
message_id: number | string;
}
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.removeGroupEssence(
msg.peerUid,
msg.msgId
);
}
}

View File

@@ -3,7 +3,7 @@ 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 { randomUUID } from 'node:crypto'
interface Payload { interface Payload {
thread_count?: number thread_count?: number
@@ -22,7 +22,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
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 || randomUUID()
const filePath = joinPath(TEMP_DIR, name) const filePath = joinPath(TEMP_DIR, name)
if (payload.base64) { if (payload.base64) {

View File

@@ -1,6 +1,6 @@
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 } 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'
@@ -37,12 +37,13 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
let messages = await Promise.all( let messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg) let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg) resMsg.message_id = (await dbUtil.addMsg(msg))!
return resMsg return resMsg
}), }),
) )
messages.map((msg) => { messages.map(v => {
;(<OB11ForwardMessage>msg).content = msg.message const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message delete msg.message
}) })
return { messages } return { messages }

View File

@@ -6,7 +6,6 @@ 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'
interface Payload { interface Payload {
group_id: number group_id: number

View File

@@ -0,0 +1,23 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { dbUtil } from '@/common/db';
interface Payload {
message_id: number | string;
}
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.addGroupEssence(
msg.peerUid,
msg.msgId
);
}
}

View File

@@ -1,11 +1,12 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } from '../../../common/data' import { getGroup, getUidByUin } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '../../../ntqqapi/types' import { ChatType, SendFileElement } from '@/ntqqapi/types'
import fs from 'fs' import fs from 'fs'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { uri2local } from '../../../common/utils' import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
interface Payload { interface Payload {
user_id: number user_id: number
@@ -20,9 +21,9 @@ class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> {
getPeer(payload: Payload): Peer { getPeer(payload: Payload): Peer {
if (payload.user_id) { if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) } return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
} }
return { chatType: ChatType.group, peerUid: payload.group_id.toString() } return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! }
} }
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {

View File

@@ -1,24 +1,24 @@
import { GroupEssenceMsgRet, WebApi } from "@/ntqqapi/api"; import { GroupEssenceMsgRet, WebApi } from '@/ntqqapi/api'
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
pages?: number; pages?: number
} }
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet> { export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet | void> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg; actionName = ActionName.GoCQHTTP_GetEssenceMsg
protected async _handle(payload: PayloadType) { protected async _handle(payload: PayloadType) {
throw '此 api 暂不支持' throw '此 api 暂不支持'
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0'); const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0')
if (!ret) { if (!ret) {
throw new Error('获取失败'); throw new Error('获取失败')
} }
// ret.map((item) => { // ret.map((item) => {
// //
// }) // })
return ret; return ret
} }
} }

View File

@@ -1,22 +1,23 @@
import { WebApi, WebHonorType } from "@/ntqqapi/api"; import { WebApi, WebHonorType } from '@/ntqqapi/api'
import { ActionName } from "../types"; import { ActionName } from '../types'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
interface Payload { interface Payload {
group_id: number, group_id: number
type?: WebHonorType type?: WebHonorType
} }
export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> { export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo; actionName = ActionName.GetGroupHonorInfo
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
// console.log(await NTQQUserApi.getRobotUinRange()); // console.log(await NTQQUserApi.getRobotUinRange())
if (!payload.group_id) { if (!payload.group_id) {
throw '缺少参数group_id'; throw '缺少参数group_id'
} }
if (!payload.type) { if (!payload.type) {
payload.type = WebHonorType.ALL; payload.type = WebHonorType.ALL
} }
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type); return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type)
} }
} }

View File

@@ -2,13 +2,11 @@ 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'
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 as Partial<OB11PostSendMsg>).user_id
payload.message_type = 'group' payload.message_type = 'group'
return super.check(payload) return super.check(payload)
} }

View File

@@ -50,6 +50,10 @@ import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSing
import { GetGroupEssence } from './group/GetGroupEssence' import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo' import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation' import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent'
export const actionHandlers = [ export const actionHandlers = [
new GetFile(), new GetFile(),
@@ -59,6 +63,7 @@ export const actionHandlers = [
new GetGroupAddRequest(), new GetGroupAddRequest(),
new SetQQAvatar(), new SetQQAvatar(),
new GetFriendWithCategory(), new GetFriendWithCategory(),
new GetEvent(),
// onebot11 // onebot11
new SendLike(), new SendLike(),
new GetMsg(), new GetMsg(),
@@ -106,7 +111,9 @@ export const actionHandlers = [
new GoCQHTTPUploadPrivateFile(), new GoCQHTTPUploadPrivateFile(),
new GoCQHTTPGetGroupMsgHistory(), new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(), new GoCQHTTGetForwardMsgAction(),
new GoCQHTTHandleQuickOperation() new GoCQHTTHandleQuickOperation(),
new GoCQHTTPSetEssenceMsg(),
new GoCQHTTPDelEssenceMsg()
] ]
function initActionMap() { function initActionMap() {

View File

@@ -0,0 +1,23 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { getHttpEvent } from '../../server/event-for-http'
import { PostEventType } from '../../server/post-ob11-event'
// import { log } from "../../../common/utils";
interface Payload {
key: string
timeout: number
}
export default class GetEvent extends BaseAction<Payload, PostEventType[]> {
actionName = ActionName.GetEvent
protected async _handle(payload: Payload): Promise<PostEventType[]> {
let key = ''
if (payload.key) {
key = payload.key;
}
let timeout = parseInt(payload.timeout?.toString()) || 0;
let evts = await getHttpEvent(key,timeout);
return evts;
}
}

View File

@@ -1,9 +1,10 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api' import { NTQQMsgApi } from '@/ntqqapi/api'
import { ChatType, RawMessage } from '../../../ntqqapi/types' import { ChatType, RawMessage } from '@/ntqqapi/types'
import { dbUtil } from '../../../common/db' import { dbUtil } from '@/common/db'
import { getUidByUin } from '../../../common/data' import { getUidByUin } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types'
interface Payload { interface Payload {
message_id: number message_id: number
@@ -15,16 +16,16 @@ interface Response {
message_id: number message_id: number
} }
class ForwardSingleMsg extends BaseAction<Payload, Response> { abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> { protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) } return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
} }
return { chatType: ChatType.group, peerUid: payload.group_id.toString() } return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
} }
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const msg = await dbUtil.getMsgByShortId(payload.message_id) const msg = (await dbUtil.getMsgByShortId(payload.message_id))!
const peer = await this.getTargetPeer(payload) const peer = await this.getTargetPeer(payload)
const sentMsg = await NTQQMsgApi.forwardMsg( const sentMsg = await NTQQMsgApi.forwardMsg(
{ {
@@ -35,7 +36,7 @@ class ForwardSingleMsg extends BaseAction<Payload, Response> {
[msg.msgId], [msg.msgId],
) )
const ob11MsgId = await dbUtil.addMsg(sentMsg) const ob11MsgId = await dbUtil.addMsg(sentMsg)
return {message_id: ob11MsgId} return { message_id: ob11MsgId! }
} }
} }

View File

@@ -23,7 +23,7 @@ import {
OB11MessageVideo, OB11MessageVideo,
OB11PostSendMsg, OB11PostSendMsg,
} from '../../types' } from '../../types'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg' import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor' import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types' import { ActionName, BaseCheckResult } from '../types'
@@ -37,6 +37,7 @@ import { uri2local } from '../../../common/utils'
import { crychic } from '../../../ntqqapi/native/crychic' import { crychic } from '../../../ntqqapi/native/crychic'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { NTQQGroupApi } from '../../../ntqqapi/api'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign'
import { Peer } from '../../../ntqqapi/types/msg'
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@@ -141,7 +142,7 @@ export async function createSendElements(
.RemainAtAllCountForUin .RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount) log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin) const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin)
isAdmin = self.role === GroupMemberRole.admin || self.role === GroupMemberRole.owner isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) { } catch (e) {
} }
} }
@@ -170,8 +171,8 @@ export async function createSendElements(
SendMsgElementConstructor.reply( SendMsgElementConstructor.reply(
replyMsg.msgSeq, replyMsg.msgSeq,
replyMsg.msgId, replyMsg.msgId,
replyMsg.senderUin, replyMsg.senderUin!,
replyMsg.senderUin, replyMsg.senderUin!,
), ),
) )
} }
@@ -250,7 +251,7 @@ export async function createSendElements(
await SendMsgElementConstructor.pic( await SendMsgElementConstructor.pic(
path, path,
sendMsg.data.summary || '', sendMsg.data.summary || '',
<PicSubType>parseInt(sendMsg.data?.subType?.toString()) || 0, <PicSubType>parseInt(sendMsg.data?.subType?.toString()!) || 0,
), ),
) )
} }
@@ -265,16 +266,16 @@ export async function createSendElements(
case OB11MessageDataType.poke: { case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id let qq = sendMsg.data?.qq || sendMsg.data?.id
if (qq) { if (qq) {
if ('groupCode' in target) { if ('groupCode' in target!) {
crychic.sendGroupPoke(target.groupCode, qq.toString()) crychic.sendGroupPoke(target.groupCode, qq.toString())
} }
else { else {
if (!qq) { if (!qq) {
qq = parseInt(target.uin) qq = parseInt(target?.uin!)
} }
crychic.sendFriendPoke(qq.toString()) crychic.sendFriendPoke(qq.toString())
} }
sendElements.push(SendMsgElementConstructor.poke('', '')) sendElements.push(SendMsgElementConstructor.poke('', '')!)
} }
} }
break break
@@ -386,10 +387,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let group: Group | undefined = undefined let group: Group | undefined = undefined
let friend: Friend | undefined = undefined let friend: Friend | undefined = undefined
const genGroupPeer = async () => { const genGroupPeer = async () => {
group = await getGroup(payload.group_id.toString()) group = await getGroup(payload.group_id?.toString()!)
peer.chatType = ChatType.group peer.chatType = ChatType.group
// peer.name = group.name // peer.name = group.name
peer.peerUid = group.groupCode peer.peerUid = group?.groupCode!
} }
const genFriendPeer = () => { const genFriendPeer = () => {
@@ -428,8 +429,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) { if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try { try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return { message_id: returnMsg.msgShortId } return { message_id: returnMsg?.msgShortId! }
} catch (e) { } catch (e: any) {
throw '发送转发消息失败 ' + e.toString() throw '发送转发消息失败 ' + e.toString()
} }
} }
@@ -446,8 +447,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
const postData: MusicSignPostData = { ...music.data } const postData: MusicSignPostData = { ...music.data }
if (type === 'custom' && music.data.content) { if (type === 'custom' && music.data.content) {
;(postData as CustomMusicSignPostData).singer = music.data.content const data = postData as CustomMusicSignPostData
delete (postData as OB11MessageCustomMusic['data']).content data.singer = music.data.content
delete (data as OB11MessageCustomMusic['data']).content
} }
if (type === 'custom') { if (type === 'custom') {
const customMusicData = music.data as CustomMusicSignPostData const customMusicData = music.data as CustomMusicSignPostData
@@ -492,7 +494,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
})) }))
return { message_id: returnMsg.msgShortId } return { message_id: returnMsg.msgShortId! }
} }
private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number { private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {
@@ -502,7 +504,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
return 0 return 0
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage> { private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
log('克隆的目标消息', msg) log('克隆的目标消息', msg)
let sendElements: SendMessageElement[] = [] let sendElements: SendMessageElement[] = []
for (const ele of msg.elements) { for (const ele of msg.elements) {
@@ -548,11 +550,11 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeId) { if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)) let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId))
if (!needClone) { if (!needClone) {
nodeMsgIds.push(nodeMsg.msgId) nodeMsgIds.push(nodeMsg?.msgId!)
} }
else { else {
if (nodeMsg.peerUid !== selfInfo.uid) { if (nodeMsg?.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg) const cloneMsg = await this.cloneMsg(nodeMsg!)
if (cloneMsg) { if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId) nodeMsgIds.push(cloneMsg.msgId)
} }
@@ -604,7 +606,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发 // 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = [] let nodeMsgArray: Array<RawMessage> = []
let srcPeer: Peer = null let srcPeer: Peer | null = null
let needSendSelf = false let needSendSelf = false
for (const [index, msgId] of nodeMsgIds.entries()) { for (const [index, msgId] of nodeMsgIds.entries()) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId) const nodeMsg = await dbUtil.getMsgByLongId(msgId)
@@ -647,7 +649,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
try { try {
log('开发转发', nodeMsgIds) log('开发转发', nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds) return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
} catch (e) { } catch (e) {
log('forward failed', e) log('forward failed', e)
return null return null

View File

@@ -5,8 +5,8 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db' import { dbUtil } from '@/common/db'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, Peer } from '@/ntqqapi/api' import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ChatType, Group, GroupRequestOperateTypes } from '@/ntqqapi/types' import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data' import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils' import { isNull, log } from '@/common/utils'
@@ -71,17 +71,17 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peerUid: msg.user_id.toString(), peerUid: msg.user_id.toString(),
} }
if (msg.message_type == 'private') { if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString()) peer.peerUid = getUidByUin(msg.user_id.toString())!
if (msg.sub_type === 'group') { if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
} }
} }
else { else {
peer.chatType = ChatType.group peer.chatType = ChatType.group
peer.peerUid = msg.group_id.toString() peer.peerUid = msg.group_id?.toString()!
} }
if (reply) { if (reply) {
let group: Group = null let group: Group | null = null
let replyMessage: OB11MessageData[] = [] let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) { if (ob11Config.enableQOAutoQuote) {
replyMessage.push({ replyMessage.push({
@@ -93,7 +93,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
if (msg.message_type == 'group') { if (msg.message_type == 'group') {
group = await getGroup(msg.group_id.toString()) group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) { if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({ replyMessage.push({
type: 'at', type: 'at',
@@ -104,7 +104,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
} }
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group) const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!)
log(`发送消息给`, peer, sendElements) log(`发送消息给`, peer, sendElements)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
} }
@@ -112,15 +112,15 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
// handle group msg // handle group msg
if (groupMsgQuickAction.delete) { if (groupMsgQuickAction.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage.msgId]).then().catch(log) NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log)
} }
if (groupMsgQuickAction.kick) { if (groupMsgQuickAction.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage.senderUid]).then().catch(log) NTQQGroupApi.kickMember(peer.peerUid, [rawMessage?.senderUid!]).then().catch(log)
} }
if (groupMsgQuickAction.ban) { if (groupMsgQuickAction.ban) {
NTQQGroupApi.banMember(peer.peerUid, [ NTQQGroupApi.banMember(peer.peerUid, [
{ {
uid: rawMessage.senderUid, uid: rawMessage?.senderUid!,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30, timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
}, },
]).then().catch(log) ]).then().catch(log)

View File

@@ -1,9 +1,8 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import fs from 'fs' import fs from 'node:fs'
import Path from 'path' import Path from 'node:path'
import { ChatType, ChatCacheListItemBasic, CacheFileType } from '../../../ntqqapi/types' import { ChatType, ChatCacheListItemBasic, CacheFileType } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { NTQQFileApi, NTQQFileCacheApi } from '../../../ntqqapi/api/file' import { NTQQFileApi, NTQQFileCacheApi } from '../../../ntqqapi/api/file'
export default class CleanCache extends BaseAction<void, void> { export default class CleanCache extends BaseAction<void, void> {
@@ -12,14 +11,16 @@ export default class CleanCache extends BaseAction<void, void> {
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))
const list = await NTQQFileCacheApi.getCacheSessionPathList()
list.forEach((e) => cacheFilePaths.push(e.value))
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知 // await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQFileCacheApi.scanCache() const cacheScanResult = await NTQQFileCacheApi.scanCache()

View File

@@ -8,7 +8,7 @@ export default class GetStatus extends BaseAction<any, OB11Status> {
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

@@ -22,6 +22,7 @@ export enum ActionName {
Debug = 'llonebot_debug', Debug = 'llonebot_debug',
GetFile = 'get_file', GetFile = 'get_file',
GetFriendsWithCategory = 'get_friends_with_category', GetFriendsWithCategory = 'get_friends_with_category',
GetEvent = 'get_event',
// onebot 11 // onebot 11
SendLike = 'send_like', SendLike = 'send_like',
GetLoginInfo = 'get_login_info', GetLoginInfo = 'get_login_info',
@@ -70,4 +71,6 @@ export enum ActionName {
GoCQHTTP_GetEssenceMsg = "get_essence_msg_list", GoCQHTTP_GetEssenceMsg = "get_essence_msg_list",
GoCQHTTP_HandleQuickOperation = ".handle_quick_operation", GoCQHTTP_HandleQuickOperation = ".handle_quick_operation",
GetGroupHonorInfo = "get_group_honor_info", GetGroupHonorInfo = "get_group_honor_info",
GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
} }

View File

@@ -5,6 +5,7 @@ import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQFriendApi } from '@/ntqqapi/api' import { NTQQFriendApi } from '@/ntqqapi/api'
import { CategoryFriend } from '@/ntqqapi/types' import { CategoryFriend } from '@/ntqqapi/types'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -14,6 +15,9 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList actionName = ActionName.GetFriendList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (+qqPkgInfo.buildVersion >= 26702) {
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true'))
}
if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
const _friends = await NTQQFriendApi.getFriends(true) const _friends = await NTQQFriendApi.getFriends(true)
// log('强制刷新好友列表,结果: ', _friends) // log('强制刷新好友列表,结果: ', _friends)

View File

@@ -19,7 +19,7 @@ export default class SendLike extends BaseAction<Payload, null> {
const friend = await getFriend(qq) const friend = await getFriend(qq)
let uid: string let uid: string
if (!friend) { if (!friend) {
uid = getUidByUin(qq) uid = getUidByUin(qq)!
} else { } else {
uid = friend.uid uid = friend.uid
} }

View File

@@ -15,6 +15,7 @@ import {
FaceIndex, FaceIndex,
GrayTipElementSubType, GrayTipElementSubType,
Group, Group,
Peer,
GroupMember, GroupMember,
PicType, PicType,
RawMessage, RawMessage,
@@ -23,8 +24,9 @@ import {
TipGroupElementType, TipGroupElementType,
User, User,
VideoElement, VideoElement,
FriendV2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap } from '../common/data' import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db' import { dbUtil } from '../common/db'
@@ -34,9 +36,10 @@ import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNotice
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { NTQQUserApi } from '../ntqqapi/api/user' import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQFileApi } from '../ntqqapi/api/file' import { NTQQFileApi } from '../ntqqapi/api/file'
import { NTQQMsgApi } from '../ntqqapi/api/msg'
import { calcQQLevel } from '../common/utils/qqlevel' import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { sleep } from '../common/utils/helper' import { isNull, sleep } from '../common/utils/helper'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
@@ -47,6 +50,9 @@ import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent' import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent' import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
let lastRKeyUpdateTime = 0 let lastRKeyUpdateTime = 0
@@ -61,14 +67,14 @@ export class OB11Constructor {
const message_type = msg.chatType == ChatType.group ? 'group' : 'private' const message_type = msg.chatType == ChatType.group ? 'group' : 'private'
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin), self_id: parseInt(selfInfo.uin),
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin!),
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId, message_id: msg.msgShortId!,
real_id: msg.msgShortId, real_id: msg.msgShortId!,
message_seq: msg.msgShortId, message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private', message_type: msg.chatType == ChatType.group ? 'group' : 'private',
sender: { sender: {
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName, nickname: msg.sendNickName,
card: msg.sendMemberName || '', card: msg.sendMemberName || '',
}, },
@@ -85,7 +91,7 @@ export class OB11Constructor {
if (msg.chatType == ChatType.group) { if (msg.chatType == ChatType.group) {
resMsg.sub_type = 'normal' resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin) resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin) const member = await getGroupMember(msg.peerUin, msg.senderUin!)
if (member) { if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role) resMsg.sender.role = OB11Constructor.groupMemberRole(member.role)
resMsg.sender.nickname = member.nick resMsg.sender.nickname = member.nick
@@ -93,7 +99,7 @@ export class OB11Constructor {
} }
else if (msg.chatType == ChatType.friend) { else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin) const friend = await getFriend(msg.senderUin!)
if (friend) { if (friend) {
resMsg.sender.nickname = friend.nick resMsg.sender.nickname = friend.nick
} }
@@ -107,33 +113,40 @@ export class OB11Constructor {
} }
for (let element of msg.elements) { for (let element of msg.elements) {
let message_data: OB11MessageData | any = { let message_data: OB11MessageData = {
data: {}, data: {} as any,
type: 'unknown', type: 'unknown' as any,
} }
if (element.textElement && element.textElement?.atType !== AtType.notAt) { if (element.textElement && element.textElement?.atType !== AtType.notAt) {
message_data['type'] = OB11MessageDataType.at let qq: string
let name: string | undefined
if (element.textElement.atType == AtType.atAll) { if (element.textElement.atType == AtType.atAll) {
// message_data["data"]["mention"] = "all" qq = 'all'
message_data['data']['qq'] = 'all'
} }
else { else {
let atUid = element.textElement.atNtUid const { atNtUid, content } = element.textElement
let atQQ = element.textElement.atUid let atQQ = element.textElement.atUid
if (!atQQ || atQQ === '0') { if (!atQQ || atQQ === '0') {
const atMember = await getGroupMember(msg.peerUin, atUid) const atMember = await getGroupMember(msg.peerUin, atNtUid)
if (atMember) { if (atMember) {
atQQ = atMember.uin atQQ = atMember.uin
} }
} }
if (atQQ) { if (atQQ) {
// message_data["data"]["mention"] = atQQ qq = atQQ
message_data['data']['qq'] = atQQ name = content.replace('@', '')
}
}
message_data = {
type: OB11MessageDataType.at,
data: {
qq: qq!,
name
} }
} }
} }
else if (element.textElement) { else if (element.textElement) {
message_data['type'] = 'text' message_data['type'] = OB11MessageDataType.text
let text = element.textElement.content let text = element.textElement.content
if (!text.trim()) { if (!text.trim()) {
continue continue
@@ -141,30 +154,32 @@ export class OB11Constructor {
message_data['data']['text'] = text message_data['data']['text'] = text
} }
else if (element.replyElement) { else if (element.replyElement) {
message_data['type'] = 'reply' message_data['type'] = OB11MessageDataType.reply
// log("收到回复消息", element.replyElement.replayMsgSeq) // log("收到回复消息", element.replyElement.replayMsgSeq)
try { try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq) const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) // log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) { if (replyMsg) {
message_data['data']['id'] = replyMsg.msgShortId.toString() message_data['data']['id'] = replyMsg.msgShortId?.toString()
} }
else { else {
continue continue
} }
} catch (e) { } catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
} }
} }
else if (element.picElement) { else if (element.picElement) {
message_data['type'] = 'image' message_data['type'] = OB11MessageDataType.image
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
let fileName = element.picElement.fileName let fileName = element.picElement.fileName
const sourcePath = element.picElement.sourcePath const sourcePath = element.picElement.sourcePath
if (element.picElement.picType === PicType.gif && !fileName.endsWith('.gif')) { const isGif = element.picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif' fileName += '.gif'
} }
message_data['data']['file'] = fileName message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
@@ -206,12 +221,12 @@ export class OB11Constructor {
) )
} }
dbUtil dbUtil
.addFileCache(videoOrFileElement.fileUuid, { .addFileCache(videoOrFileElement.fileUuid!, {
msgId: msg.msgId, msgId: msg.msgId,
elementId: element.elementId, elementId: element.elementId,
fileName: videoOrFileElement.fileName, fileName: videoOrFileElement.fileName,
filePath: videoOrFileElement.filePath, filePath: videoOrFileElement.filePath,
fileSize: videoOrFileElement.fileSize, fileSize: videoOrFileElement.fileSize!,
downloadFunc: async () => { downloadFunc: async () => {
await NTQQFileApi.downloadMedia( await NTQQFileApi.downloadMedia(
msg.msgId, msg.msgId,
@@ -219,7 +234,7 @@ export class OB11Constructor {
msg.peerUid, msg.peerUid,
element.elementId, element.elementId,
ob11MessageDataType == OB11MessageDataType.video ob11MessageDataType == OB11MessageDataType.video
? (videoOrFileElement as VideoElement).thumbPath.get(0) ? (videoOrFileElement as VideoElement).thumbPath?.get(0)
: null, : null,
videoOrFileElement.filePath, videoOrFileElement.filePath,
) )
@@ -245,9 +260,9 @@ export class OB11Constructor {
// log("收到语音消息", msg) // log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text); // console.log("语音转文字结果", text)
// }).catch(err => { // }).catch(err => {
// console.log("语音转文字失败", err); // console.log("语音转文字失败", err)
// }) // })
} }
else if (element.arkElement) { else if (element.arkElement) {
@@ -282,7 +297,7 @@ export class OB11Constructor {
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId message_data['data']['emoji_id'] = element.marketFaceElement.emojiId
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId) message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId)
message_data['data']['key'] = element.marketFaceElement.key message_data['data']['key'] = element.marketFaceElement.key
mFaceCache.set(md5, element.marketFaceElement.faceName) mFaceCache.set(md5, element.marketFaceElement.faceName!)
} }
else if (element.markdownElement) { else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown message_data['type'] = OB11MessageDataType.markdown
@@ -292,10 +307,10 @@ export class OB11Constructor {
message_data['type'] = OB11MessageDataType.forward message_data['type'] = OB11MessageDataType.forward
message_data['data']['id'] = msg.msgId message_data['data']['id'] = msg.msgId
} }
if (message_data.type !== 'unknown' && message_data.data) { if ((message_data.type as string) !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data) const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') { if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode (resMsg.message as string) += cqCode
} }
else (resMsg.message as OB11MessageData[]).push(message_data) else (resMsg.message as OB11MessageData[]).push(message_data)
@@ -306,7 +321,36 @@ export class OB11Constructor {
return resMsg return resMsg
} }
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> { static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent | void> {
if (msg.chatType !== ChatType.friend) {
return
}
for (const element of msg.elements) {
if (element.grayTipElement) {
if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr)
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
}
}
}
// 好友增加事件
if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin))
return event
}
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | void> {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.group) {
return return
} }
@@ -316,14 +360,14 @@ export class OB11Constructor {
const event = new OB11GroupCardEvent( const event = new OB11GroupCardEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(msg.senderUin), parseInt(msg.senderUin),
msg.sendMemberName, msg.sendMemberName!,
member.cardName, member.cardName,
) )
member.cardName = msg.sendMemberName member.cardName = msg.sendMemberName!
return event return event
} }
} }
// log("group msg", msg); // log("group msg", msg)
for (let element of msg.elements) { for (let element of msg.elements) {
const grayTipElement = element.grayTipElement const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement const groupElement = grayTipElement?.groupElement
@@ -349,10 +393,10 @@ export class OB11Constructor {
} }
else if (groupElement.type === TipGroupElementType.ban) { else if (groupElement.type === TipGroupElementType.ban) {
log('收到群群员禁言提示', groupElement) log('收到群群员禁言提示', groupElement)
const memberUid = groupElement.shutUp.member.uid const memberUid = groupElement.shutUp?.member.uid
const adminUid = groupElement.shutUp.admin.uid const adminUid = groupElement.shutUp?.admin.uid
let memberUin: string = '' let memberUin: string = ''
let duration = parseInt(groupElement.shutUp.duration) let duration = parseInt(groupElement.shutUp?.duration!)
let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban' let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban'
if (memberUid) { if (memberUid) {
memberUin = memberUin =
@@ -366,7 +410,7 @@ export class OB11Constructor {
} }
} }
const adminUin = const adminUin =
(await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid))?.uin (await getGroupMember(msg.peerUid, adminUid!))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid!))?.uin
if (memberUin && adminUin) { if (memberUin && adminUin) {
return new OB11GroupBanEvent( return new OB11GroupBanEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
@@ -399,8 +443,8 @@ export class OB11Constructor {
} }
} }
else if (element.fileElement) { else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), { return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin!), {
id: element.fileElement.fileUuid, id: element.fileElement.fileUuid!,
name: element.fileElement.fileName, name: element.fileElement.fileName,
size: parseInt(element.fileElement.fileSize), size: parseInt(element.fileElement.fileSize),
busid: element.fileElement.fileBizId || 0, busid: element.fileElement.fileBizId || 0,
@@ -432,13 +476,13 @@ export class OB11Constructor {
if (!msg) { if (!msg) {
return return
} }
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId, [ return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [
{ {
emoji_id: emojiId, emoji_id: emojiId,
count: 1, count: 1,
}, },
]) ])
} catch (e) { } catch (e: any) {
log('解析表情回应消息失败', e.stack) log('解析表情回应消息失败', e.stack)
} }
} }
@@ -451,8 +495,8 @@ export class OB11Constructor {
if (xmlElement?.content) { if (xmlElement?.content) {
const regex = /jp="(\d+)"/g const regex = /jp="(\d+)"/g
let matches = [] const matches: string[] = []
let match = null let match: RegExpExecArray | null = null
while ((match = regex.exec(xmlElement.content)) !== null) { while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1]) matches.push(match[1])
@@ -490,27 +534,51 @@ export class OB11Constructor {
} }
* */ * */
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
}
}
if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
log('收到群精华消息', json)
const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode')
const Businessid = searchParams.get('businessid')
const Peer: Peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
}
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin
}
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin))
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
const memberUin = json.items[1].param[0] const memberUin = json.items[1].param[0]
const title = json.items[3].txt const title = json.items[3].txt
log('收到群成员新头衔消息', json) log('收到群成员新头衔消息', json)
getGroupMember(msg.peerUid, memberUin).then((member) => { getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNull(member)) {
member.memberSpecialTitle = title member.memberSpecialTitle = title
}
}) })
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title) return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
} }
} }
} }
} }
static async FriendAddEvent(msg: RawMessage): Promise<OB11FriendAddNoticeEvent | undefined> {
if (msg.chatType !== ChatType.friend) {
return
}
if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin))
return event
}
return
} }
static async RecallEvent( static async RecallEvent(
@@ -526,16 +594,16 @@ export class OB11Constructor {
const revokeElement = msgElement.grayTipElement.revokeElement const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) { if (isGroup) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid) const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid) const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent( return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(sender.uin), parseInt(sender?.uin!),
parseInt(operator.uin), parseInt(operator?.uin!),
msg.msgShortId, msg.msgShortId!,
) )
} }
else { else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin), msg.msgShortId) return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!)
} }
} }
@@ -544,7 +612,7 @@ export class OB11Constructor {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),
nickname: friend.nick, nickname: friend.nick,
remark: friend.remark, remark: friend.remark,
sex: OB11Constructor.sex(friend.sex), sex: OB11Constructor.sex(friend.sex!),
level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0, level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0,
} }
} }
@@ -560,6 +628,25 @@ export class OB11Constructor {
return friends.map(OB11Constructor.friend) return friends.map(OB11Constructor.friend)
} }
static friendsV2(friends: FriendV2[]): OB11User[] {
const data: OB11User[] = []
for (const friend of friends) {
const sexValue = this.sex(friend.baseInfo.sex!)
data.push({
...friend.baseInfo,
...friend.coreInfo,
user_id: parseInt(friend.coreInfo.uin),
nickname: friend.coreInfo.nick,
remark: friend.coreInfo.nick,
sex: sexValue,
level: 0,
categroyName: friend.categroyName,
categoryId: friend.categoryId
})
}
return data
}
static groupMemberRole(role: number): OB11GroupMemberRole | undefined { static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return { return {
4: OB11GroupMemberRole.owner, 4: OB11GroupMemberRole.owner,
@@ -583,7 +670,7 @@ export class OB11Constructor {
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName, card: member.cardName,
sex: OB11Constructor.sex(member.sex), sex: OB11Constructor.sex(member.sex!),
age: 0, age: 0,
area: '', area: '',
level: 0, level: 0,
@@ -605,7 +692,7 @@ export class OB11Constructor {
...user, ...user,
user_id: parseInt(user.uin), user_id: parseInt(user.uin),
nickname: user.nick, nickname: user.nick,
sex: OB11Constructor.sex(user.sex), sex: OB11Constructor.sex(user.sex!),
age: 0, age: 0,
qid: user.qid, qid: user.qid,
login_days: 0, login_days: 0,

View File

@@ -60,7 +60,15 @@ export function encodeCQCode(data: OB11MessageData) {
let result = '[CQ:' + data.type let result = '[CQ:' + data.type
for (const name in data.data) { for (const name in data.data) {
const value = data.data[name] const value = data.data[name]
result += `,${name}=${CQCodeEscape(value)}` if (value === undefined) {
continue
}
try {
const text = value.toString()
result += `,${name}=${CQCodeEscape(text)}`
} catch (error) {
// If it can't be converted, skip this name-value pair
}
} }
result += ']' result += ']'
return result return result

View File

@@ -11,5 +11,5 @@ export enum EventType {
export abstract class OB11BaseEvent { export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000) time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin) self_id = parseInt(selfInfo.uin)
post_type: EventType abstract post_type: EventType
} }

View File

@@ -2,5 +2,5 @@ import { EventType, OB11BaseEvent } from '../OB11BaseEvent'
export abstract class OB11BaseMetaEvent extends OB11BaseEvent { export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META post_type = EventType.META
meta_event_type: string abstract meta_event_type: string
} }

View File

@@ -2,5 +2,14 @@ import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent { export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin' notice_type = 'group_admin'
sub_type: 'set' | 'unset' // "set" | "unset" sub_type: 'set' | 'unset'
group_id: number
user_id: number
constructor(subType: 'set' | 'unset', groupId: number, userId: number) {
super()
this.sub_type = subType
this.group_id = groupId
this.user_id = userId
}
} }

View File

@@ -5,6 +5,8 @@ export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
operator_id: number operator_id: number
duration: number duration: number
sub_type: 'ban' | 'lift_ban' sub_type: 'ban' | 'lift_ban'
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: 'ban' | 'lift_ban') { constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: 'ban' | 'lift_ban') {
super() super()

View File

@@ -4,6 +4,8 @@ export class OB11GroupCardEvent extends OB11GroupNoticeEvent {
notice_type = 'group_card' notice_type = 'group_card'
card_new: string card_new: string
card_old: string card_old: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, cardNew: string, cardOld: string) { constructor(groupId: number, userId: number, cardNew: string, cardOld: string) {
super() super()

View File

@@ -6,6 +6,8 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease' notice_type = 'group_decrease'
sub_type: GroupDecreaseSubType = 'leave' // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me") sub_type: GroupDecreaseSubType = 'leave' // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operator_id: number operator_id: number
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') { constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super() super()

View File

@@ -0,0 +1,16 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
notice_type = 'essence'
message_id: number
sender_id: number
sub_type: 'add' | 'delete' = 'add'
group_id: number
user_id: number = 0
constructor(groupId: number, message_id: number, sender_id: number) {
super()
this.group_id = groupId
this.message_id = message_id
this.sender_id = sender_id
}
}

View File

@@ -1,10 +1,14 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent' import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
type GroupIncreaseSubType = 'approve' | 'invite' type GroupIncreaseSubType = 'approve' | 'invite'
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent { export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_increase' notice_type = 'group_increase'
operator_id: number operator_id: number
sub_type: GroupIncreaseSubType sub_type: GroupIncreaseSubType
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = 'approve') { constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = 'approve') {
super() super()
this.group_id = groupId this.group_id = groupId

View File

@@ -1,6 +1,7 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent { export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number abstract group_id: number
user_id: number abstract user_id: number
abstract notice_type: string
} }

View File

@@ -4,6 +4,8 @@ export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_recall' notice_type = 'group_recall'
operator_id: number operator_id: number
message_id: number message_id: number
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, messageId: number) { constructor(groupId: number, userId: number, operatorId: number, messageId: number) {
super() super()

View File

@@ -4,6 +4,8 @@ export class OB11GroupTitleEvent extends OB11GroupNoticeEvent {
notice_type = 'notify' notice_type = 'notify'
sub_type = 'title' sub_type = 'title'
title: string title: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, title: string) { constructor(groupId: number, userId: number, title: string) {
super() super()

View File

@@ -10,6 +10,8 @@ export interface GroupUploadFile {
export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent { export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_upload' notice_type = 'group_upload'
file: GroupUploadFile file: GroupUploadFile
group_id: number
user_id: number
constructor(groupId: number, userId: number, file: GroupUploadFile) { constructor(groupId: number, userId: number, file: GroupUploadFile) {
super() super()

View File

@@ -8,14 +8,17 @@ export interface MsgEmojiLike {
export class OB11GroupMsgEmojiLikeEvent extends OB11GroupNoticeEvent { export class OB11GroupMsgEmojiLikeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_msg_emoji_like' notice_type = 'group_msg_emoji_like'
message_id: number message_id: number
sub_type: 'ban' | 'lift_ban' sub_type?: 'ban' | 'lift_ban'
likes: MsgEmojiLike[] likes: MsgEmojiLike[]
group_id: number
user_id: number
constructor(groupId: number, userId: number, messageId: number, likes: MsgEmojiLike[]) { constructor(groupId: number, userId: number, messageId: number, likes: MsgEmojiLike[], sub_type?: 'ban' | 'lift_ban') {
super() super()
this.group_id = groupId this.group_id = groupId
this.user_id = userId // 可为空表示是对别人的消息操作如果是对bot自己的消息则不为空 this.user_id = userId // 可为空表示是对别人的消息操作如果是对bot自己的消息则不为空
this.message_id = messageId this.message_id = messageId
this.likes = likes this.likes = likes
this.sub_type = sub_type
} }
} }

View File

@@ -1,30 +1,32 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
import { selfInfo } from '../../../common/data'
import { OB11BaseEvent } from '../OB11BaseEvent'
class OB11PokeEvent extends OB11BaseNoticeEvent { abstract class OB11PokeEvent extends OB11BaseNoticeEvent {
notice_type = 'notify' notice_type = 'notify'
sub_type = 'poke' sub_type = 'poke'
target_id = parseInt(selfInfo.uin) target_id = 0
user_id: number abstract user_id: number
raw_message: any
} }
export class OB11FriendPokeEvent extends OB11PokeEvent { export class OB11FriendPokeEvent extends OB11PokeEvent {
sender_id: number user_id: number
constructor(user_id: number) {
super() constructor(user_id: number, target_id: number, raw_message: any) {
this.user_id = user_id super();
this.sender_id = user_id this.target_id = target_id;
this.user_id = user_id;
this.raw_message = raw_message;
} }
} }
export class OB11GroupPokeEvent extends OB11PokeEvent { export class OB11GroupPokeEvent extends OB11PokeEvent {
user_id: number
group_id: number group_id: number
constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) {
constructor(group_id: number, user_id: number = 0) {
super() super()
this.group_id = group_id this.group_id = group_id
this.target_id = user_id this.target_id = target_id
this.user_id = user_id this.user_id = user_id
this.raw_message = raw_message
} }
} }

View File

@@ -4,7 +4,15 @@ import { EventType } from '../OB11BaseEvent'
export class OB11FriendRequestEvent extends OB11BaseNoticeEvent { export class OB11FriendRequestEvent extends OB11BaseNoticeEvent {
post_type = EventType.REQUEST post_type = EventType.REQUEST
user_id: number user_id: number
request_type: 'friend' = 'friend' request_type: 'friend'
comment: string comment: string
flag: string flag: string
constructor(userId: number, comment: string, flag: string, requestType: 'friend' = 'friend') {
super()
this.user_id = userId
this.comment = comment
this.flag = flag
this.request_type = requestType
}
} }

View File

@@ -1,11 +1,24 @@
import { OB11GroupNoticeEvent } from '../notice/OB11GroupNoticeEvent' import { OB11BaseNoticeEvent } from '../notice/OB11BaseNoticeEvent'
import { EventType } from '../OB11BaseEvent' import { EventType } from '../OB11BaseEvent'
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent { export class OB11GroupRequestEvent extends OB11BaseNoticeEvent {
post_type = EventType.REQUEST post_type = EventType.REQUEST
request_type: 'group' = 'group' request_type: 'group'
sub_type: 'add' | 'invite' = 'add' sub_type: 'add' | 'invite'
invitor_id: number | undefined = undefined invitor_id: number | undefined
comment: string comment?: string
flag: string flag: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, flag: string, comment?: string, invitorId?: number, subType: 'add' | 'invite' = 'add', requestType: 'group' = 'group') {
super()
this.group_id = groupId
this.user_id = userId
this.comment = comment
this.flag = flag
this.request_type = requestType
this.sub_type = subType
this.invitor_id = invitorId
}
} }

View File

@@ -0,0 +1,65 @@
import { PostEventType } from './post-ob11-event'
interface HttpEventType {
seq: number
event: PostEventType
}
interface HttpUserType {
lastAccessTime: number
userSeq: number
}
let curentSeq: number = 0
const eventList: HttpEventType[] = []
const httpUser: Record<string, HttpUserType> = {}
export function postHttpEvent(event: PostEventType) {
curentSeq += 1
eventList.push({
seq: curentSeq,
event: event
});
while (eventList.length > 100) {
eventList.shift()
}
}
export async function getHttpEvent(userKey: string, timeout = 0) {
const toRetEvent: PostEventType[] = []
// 清除过时的user5分钟没访问过的user将被删除
const now = Date.now();
for (let key in httpUser) {
let user = httpUser[key]
if (now - user.lastAccessTime > 1000 * 60 * 5) {
delete httpUser[key]
}
}
// 增加新的user
if (!httpUser[userKey]) {
httpUser[userKey] = {
lastAccessTime: now,
userSeq: curentSeq
}
}
const user = httpUser[userKey]
// 等待数据到来,暂时先这么写吧......
while (curentSeq == user.userSeq && Date.now() - now < timeout) {
await new Promise(resolve => setTimeout(resolve, 10))
}
// 取数据
for (let i = 0; i < eventList.length; i++) {
let evt = eventList[i]
if (evt.seq > user.userSeq) {
toRetEvent.push(evt.event)
}
}
// 更新user数据
user.lastAccessTime = Date.now()
user.userSeq = curentSeq
return toRetEvent
}

View File

@@ -40,7 +40,7 @@ class HTTPHeart {
} }
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
// ws的心跳是ws自己维护的 // ws的心跳是ws自己维护的
postOb11Event(new OB11HeartbeatEvent(selfInfo.online, true, heartInterval), false, false) postOb11Event(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!), false, false)
}, heartInterval) }, heartInterval)
} }

View File

@@ -8,6 +8,7 @@ import { log } from '@/common/utils'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import crypto from 'crypto' import crypto from 'crypto'
import { handleQuickOperation, QuickOperationEvent } from '../action/quick-operation' import { handleQuickOperation, QuickOperationEvent } from '../action/quick-operation'
import { postHttpEvent } from './event-for-http'
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
@@ -42,7 +43,7 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
} }
if (config.ob11.enableHttpPost) { if (config.ob11.enableHttpPost) {
const msgStr = JSON.stringify(msg) const msgStr = JSON.stringify(msg)
const hmac = crypto.createHmac('sha1', config.ob11.httpSecret) const hmac = crypto.createHmac('sha1', config.ob11.httpSecret!)
hmac.update(msgStr) hmac.update(msgStr)
const sig = hmac.digest('hex') const sig = hmac.digest('hex')
let headers = { let headers = {
@@ -78,4 +79,8 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
if (postWs) { if (postWs) {
postWsEvent(msg) postWsEvent(msg)
} }
if (!(msg.post_type == 'meta_event' && (msg as OB11BaseMetaEvent).meta_event_type == 'heartbeat')) {
// 不上报心跳
postHttpEvent(msg)
}
} }

View File

@@ -15,7 +15,7 @@ import { version } from '../../../version'
export let rwsList: ReverseWebsocket[] = [] export let rwsList: ReverseWebsocket[] = []
export class ReverseWebsocket { export class ReverseWebsocket {
public websocket: WebSocketClass public websocket?: WebSocketClass
public url: string public url: string
private running: boolean = false private running: boolean = false
@@ -27,38 +27,38 @@ export class ReverseWebsocket {
public stop() { public stop() {
this.running = false this.running = false
this.websocket.close() this.websocket?.close()
} }
public onopen() { public onopen() {
wsReply(this.websocket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) wsReply(this.websocket!, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} }
public async onmessage(msg: string) { public async onmessage(msg: string) {
let receiveData: { action: ActionName; params: any; echo?: any } = { action: null, params: {} } let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} }
let echo = null let echo = null
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
echo = receiveData.echo echo = receiveData.echo
log('收到反向Websocket消息', receiveData) log('收到反向Websocket消息', receiveData)
} catch (e) { } catch (e) {
return wsReply(this.websocket, OB11Response.error('json解析失败请检查数据格式', 1400, echo)) return wsReply(this.websocket!, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
} }
const action: BaseAction<any, any> = actionMap.get(receiveData.action) const action: BaseAction<any, any> = actionMap.get(receiveData.action!)!
if (!action) { if (!action) {
return wsReply(this.websocket, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo)) return wsReply(this.websocket!, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo))
} }
try { try {
let handleResult = await action.websocketHandle(receiveData.params, echo) let handleResult = await action.websocketHandle(receiveData.params, echo)
wsReply(this.websocket, handleResult) wsReply(this.websocket!, handleResult)
} catch (e) { } catch (e) {
wsReply(this.websocket, OB11Response.error(`api处理出错:${e}`, 1200, echo)) wsReply(this.websocket!, OB11Response.error(`api处理出错:${e}`, 1200, echo))
} }
} }
public onclose = function () { public onclose = () => {
log('反向ws断开', this.url) log('反向ws断开', this.url)
unregisterWsEventSender(this.websocket) unregisterWsEventSender(this.websocket!)
if (this.running) { if (this.running) {
this.reconnect() this.reconnect()
} }
@@ -104,7 +104,7 @@ export class ReverseWebsocket {
this.websocket.on('error', log) this.websocket.on('error', log)
const wsClientInterval = setInterval(() => { const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online, true, heartInterval)) postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包
this.websocket.on('close', () => { this.websocket.on('close', () => {
clearInterval(wsClientInterval) clearInterval(wsClientInterval)
@@ -121,7 +121,7 @@ class OB11ReverseWebsockets {
new Promise(() => { new Promise(() => {
try { try {
rwsList.push(new ReverseWebsocket(url)) rwsList.push(new ReverseWebsocket(url))
} catch (e) { } catch (e: any) {
log(e.stack) log(e.stack)
} }
}).then() }).then()
@@ -132,7 +132,7 @@ class OB11ReverseWebsockets {
for (let rws of rwsList) { for (let rws of rwsList) {
try { try {
rws.stop() rws.stop()
} catch (e) { } catch (e: any) {
log('反向ws关闭:', e.stack) log('反向ws关闭:', e.stack)
} }
} }

View File

@@ -13,15 +13,13 @@ import { selfInfo } from '../../../common/data'
import { log } from '../../../common/utils/log' import { log } from '../../../common/utils/log'
import { getConfigUtil } from '../../../common/config' import { getConfigUtil } from '../../../common/config'
let heartbeatRunning = false
class OB11WebsocketServer extends WebsocketServerBase { class OB11WebsocketServer extends WebsocketServerBase {
authorizeFailed(wsClient: WebSocket) { authorizeFailed(wsClient: WebSocket) {
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败'))) wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')))
} }
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) { async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> = actionMap.get(actionName) const action: BaseAction<any, any> = actionMap.get(actionName)!
if (!action) { if (!action) {
return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo)) return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo))
} }
@@ -29,7 +27,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
let handleResult = await action.websocketHandle(params, echo) let handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo handleResult.echo = echo
wsReply(wsClient, handleResult) wsReply(wsClient, handleResult)
} catch (e) { } catch (e: any) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))
} }
} }
@@ -37,7 +35,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
if (url == '/api' || url == '/api/' || url == '/') { if (url == '/api' || url == '/api/' || url == '/') {
wsClient.on('message', async (msg) => { wsClient.on('message', async (msg) => {
let receiveData: { action: ActionName; params: any; echo?: any } = { action: null, params: {} } let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} }
let echo = null let echo = null
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
@@ -46,7 +44,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} catch (e) { } catch (e) {
return wsReply(wsClient, OB11Response.error('json解析失败请检查数据格式', 1400, echo)) return wsReply(wsClient, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
} }
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then() this.handleAction(wsClient, receiveData.action!, receiveData.params, receiveData.echo).then()
}) })
} }
if (url == '/event' || url == '/event/' || url == '/') { if (url == '/event' || url == '/event/' || url == '/') {
@@ -61,7 +59,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} }
const { heartInterval } = getConfigUtil().getConfig() const { heartInterval } = getConfigUtil().getConfig()
const wsClientInterval = setInterval(() => { const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online, true, heartInterval)) postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包
wsClient.on('close', () => { wsClient.on('close', () => {
log('event上报ws客户端已断开') log('event上报ws客户端已断开')

View File

@@ -12,7 +12,7 @@ export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEvent
} }
wsClient.send(JSON.stringify(packet)) wsClient.send(JSON.stringify(packet))
log('ws 消息上报', wsClient.url || '', data) log('ws 消息上报', wsClient.url || '', data)
} catch (e) { } catch (e: any) {
log('websocket 回复失败', e.stack, data) log('websocket 回复失败', e.stack, data)
} }
} }

View File

@@ -11,6 +11,8 @@ export interface OB11User {
age?: number age?: number
qid?: string qid?: string
login_days?: number login_days?: number
categroyName?: string
categoryId?: number
} }
export enum OB11UserSex { export enum OB11UserSex {
@@ -195,6 +197,7 @@ export interface OB11MessageAt {
type: OB11MessageDataType.at type: OB11MessageDataType.at
data: { data: {
qq: string | 'all' qq: string | 'all'
name?: string
} }
} }
@@ -241,6 +244,20 @@ export interface OB11MessageJson {
data: { data: string /* , config: { token: string } */ } data: { data: string /* , config: { token: string } */ }
} }
export interface OB11MessageMarkdown {
type: OB11MessageDataType.markdown
data: {
data: string
}
}
export interface OB11MessageForward {
type: OB11MessageDataType.forward
data: {
id: string
}
}
export type OB11MessageData = export type OB11MessageData =
| OB11MessageText | OB11MessageText
| OB11MessageFace | OB11MessageFace
@@ -258,6 +275,8 @@ export type OB11MessageData =
| OB11MessagePoke | OB11MessagePoke
| OB11MessageDice | OB11MessageDice
| OB11MessageRPS | OB11MessageRPS
| OB11MessageMarkdown
| OB11MessageForward
export interface OB11PostSendMsg { export interface OB11PostSendMsg {
message_type?: 'private' | 'group' message_type?: 'private' | 'group'

View File

@@ -1,6 +1,6 @@
export const SettingItem = ( export const SettingItem = (
title: string, title: string,
subtitle?: string, subtitle?: string | null,
action?: string, action?: string,
id?: string, id?: string,
visible: boolean = true, visible: boolean = true,

View File

@@ -30,11 +30,11 @@ window.customElements.define(
super() super()
this.attachShadow({ mode: 'open' }) this.attachShadow({ mode: 'open' })
this.shadowRoot.append(SelectTemplate.content.cloneNode(true)) this.shadowRoot?.append(SelectTemplate.content.cloneNode(true))
this._button = this.shadowRoot.querySelector('div[part="button"]') this._button = this.shadowRoot?.querySelector('div[part="button"]')!
this._text = this.shadowRoot.querySelector('input[part="current-text"]') this._text = this.shadowRoot?.querySelector('input[part="current-text"]')!
this._context = this.shadowRoot.querySelector('ul[part="option-list"]') this._context = this.shadowRoot?.querySelector('ul[part="option-list"]')!
const buttonClick = () => { const buttonClick = () => {
const isHidden = this._context.classList.toggle('hidden') const isHidden = this._context.classList.toggle('hidden')
@@ -46,7 +46,8 @@ window.customElements.define(
} }
this._button.addEventListener('click', buttonClick) this._button.addEventListener('click', buttonClick)
this._context.addEventListener('click', ({ target }: MouseEventExtend) => { this._context.addEventListener('click', e => {
const { target } = e as MouseEventExtend
if (target.tagName !== 'SETTING-OPTION') return if (target.tagName !== 'SETTING-OPTION') return
buttonClick() buttonClick()
@@ -55,7 +56,7 @@ window.customElements.define(
this.querySelectorAll('setting-option[is-selected]').forEach((dom) => dom.toggleAttribute('is-selected')) this.querySelectorAll('setting-option[is-selected]').forEach((dom) => dom.toggleAttribute('is-selected'))
target.toggleAttribute('is-selected') target.toggleAttribute('is-selected')
this._text.value = target.textContent this._text.value = target.textContent!
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('selected', { new CustomEvent('selected', {
bubbles: true, bubbles: true,
@@ -68,7 +69,7 @@ window.customElements.define(
) )
}) })
this._text.value = this.querySelector('setting-option[is-selected]').textContent this._text.value = this.querySelector('setting-option[is-selected]')?.textContent!
} }
}, },
) )

View File

@@ -4,6 +4,7 @@ import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect }
// @ts-ignore // @ts-ignore
import StyleRaw from './style.css?raw' import StyleRaw from './style.css?raw'
import { iconSvg } from './icon' import { iconSvg } from './icon'
import { version } from '../version'
// 打开设置界面时触发 // 打开设置界面时触发
@@ -11,16 +12,16 @@ function aprilFoolsEgg(node: Element) {
let today = new Date() let today = new Date()
if (today.getDate() === 1) { if (today.getDate() === 1) {
console.log('超时空猫猫!!!') console.log('超时空猫猫!!!')
node.querySelector('.name').innerHTML = 'ChronoCat' node.querySelector('.name')!.innerHTML = 'ChronoCat'
} }
} }
function initSideBar() { function initSideBar() {
document.querySelectorAll('.nav-item.liteloader').forEach((node) => { document.querySelectorAll('.nav-item.liteloader').forEach((node) => {
if (node.textContent.startsWith('LLOneBot')) { if (node.textContent?.startsWith('LLOneBot')) {
aprilFoolsEgg(node) aprilFoolsEgg(node)
let iconEle = node.querySelector('.q-icon') let iconEle = node.querySelector('.q-icon')
iconEle.innerHTML = iconSvg iconEle!.innerHTML = iconSvg
} }
}) })
} }
@@ -62,6 +63,13 @@ async function onSettingWindowCreated(view: Element) {
SettingButton('请稍候', 'llonebot-update-button', 'secondary'), SettingButton('请稍候', 'llonebot-update-button', 'secondary'),
), ),
]), ]),
SettingList([
SettingItem(
'是否启用 LLOneBot, 重启QQ后生效',
null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)]
),
SettingList([ SettingList([
SettingItem( SettingItem(
'启用 HTTP 服务', '启用 HTTP 服务',
@@ -179,11 +187,6 @@ async function onSettingWindowCreated(view: Element) {
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')), SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]), ]),
SettingList([ SettingList([
SettingItem(
'戳一戳消息, 暂时只支持Windows版的LLOneBot',
`重启QQ后生效如果导致QQ崩溃请勿开启此项, 群戳一戳只能收到群号`,
SettingSwitch('enablePoke', config.enablePoke),
),
SettingItem( SettingItem(
'使用 Base64 编码获取文件', '使用 Base64 编码获取文件',
'调用 /get_image、/get_record、/get_file 时,没有 url 时添加 Base64 字段', '调用 /get_image、/get_record、/get_file 时,没有 url 时添加 Base64 字段',
@@ -231,29 +234,29 @@ async function onSettingWindowCreated(view: Element) {
await new Promise((res) => setTimeout(() => res(true), 1000)) await new Promise((res) => setTimeout(() => res(true), 1000))
const errDom = document.querySelector('#llonebot-error') || doc.querySelector('#llonebot-error') const errDom = document.querySelector('#llonebot-error') || doc.querySelector('#llonebot-error')
const errCodeDom = errDom.querySelector('code') const errCodeDom = errDom?.querySelector('code')
const errMsg = await window.llonebot.getError() const errMsg = await window.llonebot.getError()
if (!errMsg) { if (!errMsg) {
errDom.classList.remove('show') errDom?.classList.remove('show')
} else { } else {
errDom.classList.add('show') errDom?.classList.add('show')
} }
errCodeDom.innerHTML = errMsg errCodeDom!.innerHTML = errMsg
} }
showError().then() showError().then()
// 外链按钮 // 外链按钮
doc.querySelector('#open-github').addEventListener('click', () => { doc.querySelector('#open-github')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://github.com/LLOneBot/LLOneBot') window.LiteLoader.api.openExternal('https://github.com/LLOneBot/LLOneBot')
}) })
doc.querySelector('#open-telegram').addEventListener('click', () => { doc.querySelector('#open-telegram')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://t.me/+nLZEnpne-pQ1OWFl') window.LiteLoader.api.openExternal('https://t.me/+nLZEnpne-pQ1OWFl')
}) })
doc.querySelector('#open-qq-group').addEventListener('click', () => { doc.querySelector('#open-qq-group')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://qm.qq.com/q/bDnHRG38aI') window.LiteLoader.api.openExternal('https://qm.qq.com/q/bDnHRG38aI')
}) })
doc.querySelector('#open-docs').addEventListener('click', () => { doc.querySelector('#open-docs')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://llonebot.github.io/') window.LiteLoader.api.openExternal('https://llonebot.github.io/')
}) })
// 生成反向地址列表 // 生成反向地址列表
@@ -300,14 +303,14 @@ async function onSettingWindowCreated(view: Element) {
} }
const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => { const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`) const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr)) hostContainerDom?.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr))
ob11Config[type].push('') ob11Config[type].push('')
} }
const initReverseHost = (type: string, doc: Document = document) => { const initReverseHost = (type: string, doc: Document = document) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`) const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
;[...hostContainerDom.childNodes].forEach((dom) => dom.remove()) ;[...hostContainerDom?.childNodes!].forEach((dom) => dom.remove())
buildHostList(ob11Config[type], type).forEach((dom) => { buildHostList(ob11Config[type], type).forEach((dom) => {
hostContainerDom.appendChild(dom) hostContainerDom?.appendChild(dom)
}) })
} }
initReverseHost('httpHosts', doc) initReverseHost('httpHosts', doc)
@@ -315,42 +318,43 @@ async function onSettingWindowCreated(view: Element) {
doc doc
.querySelector('#config-ob11-httpHosts-add') .querySelector('#config-ob11-httpHosts-add')
.addEventListener('click', () => ?.addEventListener('click', () =>
addReverseHost('httpHosts', document, { placeholder: '如http://127.0.0.1:5140/onebot' }), addReverseHost('httpHosts', document, { placeholder: '如http://127.0.0.1:5140/onebot' }),
) )
doc doc
.querySelector('#config-ob11-wsHosts-add') .querySelector('#config-ob11-wsHosts-add')
.addEventListener('click', () => ?.addEventListener('click', () =>
addReverseHost('wsHosts', document, { placeholder: '如ws://127.0.0.1:5140/onebot' }), addReverseHost('wsHosts', document, { placeholder: '如ws://127.0.0.1:5140/onebot' }),
) )
doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => { doc.querySelector('#config-ffmpeg-select')?.addEventListener('click', () => {
window.llonebot.selectFile().then((path) => { window.llonebot.selectFile().then((path) => {
if (!isEmpty(path)) { if (!isEmpty(path)) {
setConfig('ffmpeg', path) setConfig('ffmpeg', path)
document.querySelector('#config-ffmpeg-path-text').innerHTML = path document.querySelector('#config-ffmpeg-path-text')!.innerHTML = path
} }
}) })
}) })
doc.querySelector('#config-open-log-path').addEventListener('click', () => { doc.querySelector('#config-open-log-path')?.addEventListener('click', () => {
window.LiteLoader.api.openPath(window.LiteLoader.plugins['LLOneBot'].path.data) window.LiteLoader.api.openPath(window.LiteLoader.plugins['LLOneBot'].path.data)
}) })
// 开关 // 开关
doc.querySelectorAll('setting-switch[data-config-key]').forEach((dom: HTMLElement) => { doc.querySelectorAll('setting-switch[data-config-key]').forEach(element => {
const dom = element as HTMLElement
dom.addEventListener('click', () => { dom.addEventListener('click', () => {
const active = dom.getAttribute('is-active') === null const active = dom.getAttribute('is-active') === null
setConfig(dom.dataset.configKey, active) setConfig(dom.dataset.configKey!, active)
if (active) dom.setAttribute('is-active', '') if (active) dom.setAttribute('is-active', '')
else dom.removeAttribute('is-active') else dom.removeAttribute('is-active')
if (!isEmpty(dom.dataset.controlDisplayId)) { if (!isEmpty(dom.dataset.controlDisplayId)) {
const displayDom = document.querySelector(`#${dom.dataset.controlDisplayId}`) const displayDom = document.querySelector(`#${dom.dataset.controlDisplayId}`)
if (active) displayDom.removeAttribute('is-hidden') if (active) displayDom?.removeAttribute('is-hidden')
else displayDom.setAttribute('is-hidden', '') else displayDom?.setAttribute('is-hidden', '')
} }
}) })
}) })
@@ -358,28 +362,31 @@ async function onSettingWindowCreated(view: Element) {
// 输入框 // 输入框
doc doc
.querySelectorAll('setting-item .q-input input.q-input__inner[data-config-key]') .querySelectorAll('setting-item .q-input input.q-input__inner[data-config-key]')
.forEach((dom: HTMLInputElement) => { .forEach(element => {
const dom = element as HTMLInputElement
dom.addEventListener('input', () => { dom.addEventListener('input', () => {
const Type = dom.getAttribute('type') const Type = dom.getAttribute('type')
const configKey = dom.dataset.configKey const configKey = dom.dataset.configKey
const configValue = Type === 'number' ? (parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1) : dom.value const configValue = Type === 'number' ? (parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1) : dom.value
setConfig(configKey, configValue) setConfig(configKey!, configValue)
}) })
}) })
// 下拉框 // 下拉框
doc.querySelectorAll('ob-setting-select[data-config-key]').forEach((dom: HTMLElement) => { doc?.querySelectorAll('ob-setting-select[data-config-key]').forEach(element => {
dom.addEventListener('selected', (e: CustomEvent) => { const dom = element as HTMLElement
dom?.addEventListener('selected', e => {
const { detail } = e as CustomEvent
const configKey = dom.dataset.configKey const configKey = dom.dataset.configKey
const configValue = e.detail.value const configValue = detail.value
setConfig(configKey, configValue) setConfig(configKey!, configValue)
}) })
}) })
// 保存按钮 // 保存按钮
doc.querySelector('#config-ob11-save').addEventListener('click', () => { doc.querySelector('#config-ob11-save')?.addEventListener('click', () => {
config.ob11 = ob11Config config.ob11 = ob11Config
window.llonebot.setConfig(false, config) window.llonebot.setConfig(false, config)
@@ -397,7 +404,7 @@ async function onSettingWindowCreated(view: Element) {
const buttonDom = view.querySelector<HTMLButtonElement>('#llonebot-update-button')! const buttonDom = view.querySelector<HTMLButtonElement>('#llonebot-update-button')!
if (ResultVersion.version === '') { if (ResultVersion.version === '') {
titleDom.innerHTML = '检查更新失败' titleDom.innerHTML = `当前版本为 v${version}检查更新失败`
buttonDom.innerHTML = '点击重试' buttonDom.innerHTML = '点击重试'
buttonDom.addEventListener('click', async () => { buttonDom.addEventListener('click', async () => {
@@ -407,10 +414,10 @@ async function onSettingWindowCreated(view: Element) {
return return
} }
if (!ResultVersion.result) { if (!ResultVersion.result) {
titleDom.innerHTML = '当前已是最新版本 v' + ResultVersion.version titleDom.innerHTML = '当前已是最新版本 v' + version
buttonDom.innerHTML = '无需更新' buttonDom.innerHTML = '无需更新'
} else { } else {
titleDom.innerHTML = '已检测到最新版本 v' + ResultVersion.version titleDom.innerHTML = `当前版本为 v${version}最新版本 v${ResultVersion.version}`
buttonDom.innerHTML = '点击更新' buttonDom.innerHTML = '点击更新'
buttonDom.dataset.type = 'primary' buttonDom.dataset.type = 'primary'
@@ -443,7 +450,7 @@ function init() {
} }
if (location.hash === '#/blank') { if (location.hash === '#/blank') {
;(window as any).navigation.addEventListener('navigatesuccess', init, { once: true }) globalThis.navigation.addEventListener('navigatesuccess', init, { once: true })
} else { } else {
init() init()
} }

View File

@@ -1 +1 @@
export const version = '3.26.7' export const version = '3.28.0'

View File

@@ -3,7 +3,8 @@
"target": "ESNext", "target": "ESNext",
"module": "commonjs", "module": "commonjs",
"outDir": "./dist", "outDir": "./dist",
"strict": false, "strict": true,
"noImplicitAny": false,
"esModuleInterop": true, "esModuleInterop": true,
"allowJs": true, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
@@ -23,6 +24,12 @@
}, },
"noEmit": true "noEmit": true
}, },
"include": ["src/*", "src/**/*", "scripts/*"], "include": [
"exclude": ["node_modules"] "src/*",
"src/**/*",
"scripts/*"
],
"exclude": [
"node_modules"
]
} }