Compare commits

...

106 Commits

Author SHA1 Message Date
idranme
5c68d4de84 Merge pull request #404 from LLOneBot/dev
3.31.10
2024-09-07 21:55:04 +08:00
idranme
c1c22e872e chore: v3.31.10 2024-09-07 21:52:34 +08:00
idranme
709c0b6f7b fix 2024-09-07 21:51:17 +08:00
idranme
dd34286b43 refactor 2024-09-07 17:45:36 +08:00
idranme
80487e31f5 chore 2024-09-07 03:03:33 +08:00
idranme
bdf1c7fd5f chore 2024-09-07 02:56:59 +08:00
idranme
285fc1d33d Merge pull request #398 from LLOneBot/dev
3.31.9
2024-09-06 23:12:47 +08:00
idranme
e14ba108fc chore: v3.31.9 2024-09-06 23:06:43 +08:00
idranme
e0be0bcc77 fix 2024-09-06 23:04:17 +08:00
idranme
c24fa6cea1 chore: improve code quality
chore: improve code quality
2024-09-06 23:04:17 +08:00
idranme
04b2a323a7 chore: improve code quality 2024-09-06 23:03:04 +08:00
idranme
970f1a98ec chore: improve code quality
chore: improve code quality
2024-09-06 23:03:04 +08:00
idranme
3064a6eb7c chore: improve code quality 2024-09-06 22:59:14 +08:00
idranme
f93e2b5a95 chore: improve code quality
chore: improve code quality
2024-09-06 22:59:14 +08:00
idranme
e185e700b7 chore: improve code quality
chore: improve code quality

chore: improve code quality
2024-09-06 22:58:00 +08:00
idranme
eae6e09e22 optimize 2024-09-05 17:16:42 +08:00
idranme
e204bb0957 Merge pull request #395 from LLOneBot/dev
3.31.8
2024-09-05 15:00:57 +08:00
idranme
ed546ace3d chore: v3.31.8 2024-09-05 15:00:05 +08:00
idranme
3c79cffa42 optimize 2024-09-05 14:58:52 +08:00
idranme
acce444dee optimize 2024-09-05 02:26:42 +08:00
idranme
f359e3ea9d fix 2024-09-05 02:20:23 +08:00
idranme
fe99da985f Merge pull request #394 from LLOneBot/dev
3.31.7
2024-09-04 20:35:08 +08:00
idranme
58d5de572c chore: v3.31.7 2024-09-04 20:32:44 +08:00
idranme
b2088824cc feat 2024-09-04 17:15:41 +08:00
idranme
fffa664400 fix: reply message segment 2024-09-04 16:18:48 +08:00
idranme
02e5222f92 feat: SendGroupNotice 2024-09-04 15:42:10 +08:00
idranme
18d253edf6 fix: GroupMsgEmojiLikeEvent 2024-09-04 13:13:49 +08:00
idranme
da8b5e2429 chore 2024-09-04 13:12:39 +08:00
idranme
502be69bc5 feat: SetOnlineStatus 2024-09-04 01:23:25 +08:00
idranme
273d4133eb refactor 2024-09-04 00:44:41 +08:00
idranme
44bfc5aab9 optimize 2024-09-03 21:46:26 +08:00
idranme
050c9d9b54 fix 2024-09-03 21:43:18 +08:00
idranme
7904f45c20 Merge pull request #392 from LLOneBot/dev
3.31.6
2024-09-03 18:38:07 +08:00
idranme
1afdad1452 chore: v3.31.6 2024-09-03 18:34:30 +08:00
idranme
cd930c43b6 feat: GetGroupRootFiles 2024-09-03 15:14:05 +08:00
idranme
b7efbdf239 fix: ws 2024-09-03 13:16:25 +08:00
idranme
56706f3838 chore 2024-09-03 01:24:21 +08:00
idranme
387c9dcb52 refactor 2024-09-03 01:04:16 +08:00
idranme
a7bb55b31c chore 2024-09-02 19:53:18 +08:00
idranme
fbf09e1db4 chore 2024-09-02 19:48:17 +08:00
idranme
9b98f8f33d optimize 2024-09-02 19:30:23 +08:00
idranme
727f399de6 fix: GetGroupMsgHistory 2024-09-02 19:24:27 +08:00
idranme
e03b82fb44 optimize: ci 2024-09-02 18:28:21 +08:00
idranme
ba413b9581 Merge pull request #390 from LLOneBot/dev
3.31.5
2024-09-02 16:42:35 +08:00
idranme
abcec99ce0 chore: v3.31.5 2024-09-02 16:39:36 +08:00
idranme
a7da7ab598 optimize 2024-09-02 01:58:31 +08:00
idranme
5cc8a2b96e fix 2024-09-02 01:46:08 +08:00
idranme
f0d8c851d4 optimize 2024-09-02 01:24:15 +08:00
idranme
828b20e0e8 optimize 2024-09-02 01:05:58 +08:00
idranme
3570349fcd optimize 2024-09-02 00:42:35 +08:00
idranme
ad74854e42 fix 2024-09-01 20:28:12 +08:00
idranme
15e7afed62 Merge pull request #385 from LLOneBot/dev
3.31.4
2024-09-01 18:50:38 +08:00
idranme
bf71328650 chore: v3.31.4 2024-09-01 18:50:09 +08:00
idranme
b3299ba1e3 chore 2024-09-01 15:39:37 +08:00
idranme
d36ea93e63 refactor 2024-09-01 15:26:34 +08:00
idranme
0bd3f8f1a2 feat 2024-09-01 15:26:11 +08:00
idranme
4bf79e021e Merge pull request #383 from LLOneBot/dev
3.31.3
2024-09-01 00:36:41 +08:00
idranme
2dac109e58 chore: v3.31.3 2024-09-01 00:34:08 +08:00
idranme
2637a5da6d chore 2024-08-31 22:59:42 +08:00
idranme
f8b2be246f optimize 2024-08-31 22:55:26 +08:00
idranme
44921e85ad chore 2024-08-31 19:46:35 +08:00
idranme
388e016365 optimize 2024-08-31 19:41:48 +08:00
idranme
a2056a43f3 fix 2024-08-31 01:29:44 +08:00
idranme
a249e0b581 Merge pull request #381 from LLOneBot/dev
3.31.2
2024-08-30 12:47:18 +08:00
idranme
f7343332d7 chore: v3.31.2 2024-08-30 12:46:03 +08:00
idranme
bf17d46157 fix 2024-08-30 12:38:39 +08:00
idranme
3e3f792035 optimize 2024-08-30 03:09:34 +08:00
idranme
d7cc5d68a7 refactor 2024-08-30 02:52:21 +08:00
idranme
64a8efb8df optimize 2024-08-30 02:51:56 +08:00
idranme
6af31c48c4 fix 2024-08-29 20:48:08 +08:00
idranme
6954551cb7 feat 2024-08-29 18:06:53 +08:00
idranme
c71885a29e refactor 2024-08-28 23:57:11 +08:00
idranme
183eab2cf4 optimize 2024-08-28 17:13:26 +08:00
idranme
c0b682606c Merge pull request #378 from LLOneBot/dev
3.31.1
2024-08-28 16:09:35 +08:00
idranme
8564630c4d Update manifest.json 2024-08-28 16:07:58 +08:00
idranme
abd5a12708 chore: v3.31.1 2024-08-28 16:07:31 +08:00
idranme
234167f305 fix 2024-08-28 16:06:40 +08:00
idranme
da75f59d0d fix 2024-08-28 15:40:08 +08:00
idranme
eaf96ac3fc Merge pull request #376 from LLOneBot/dev
fix
2024-08-28 10:45:50 +08:00
idranme
2491de9af8 fix 2024-08-28 02:45:17 +00:00
idranme
01f8987e1e Merge pull request #375 from LLOneBot/dev
3.31.0
2024-08-28 10:28:27 +08:00
idranme
4a9bebbc9c chore: v3.31.0 2024-08-28 10:27:05 +08:00
idranme
6be6151d73 fix 2024-08-28 10:25:17 +08:00
idranme
738b0a96a0 chore 2024-08-28 06:52:29 +08:00
idranme
7cb94cb8b8 refactor 2024-08-28 06:49:46 +08:00
idranme
5501980ab3 refactor 2024-08-28 04:48:07 +08:00
idranme
bc3c8b1259 Merge pull request #374 from LLOneBot/main
merge
2024-08-28 04:45:33 +08:00
idranme
61e63efbd8 Merge pull request #373 from itzdrli/main
Fix typo in LICENSE file
2024-08-27 22:01:30 +08:00
itzdrli
28770d5995 Fix typo in LICENSE file 2024-08-27 13:01:14 +00:00
idranme
67d3dfb3cf Merge pull request #367 from LLOneBot/dev
3.30.5
2024-08-25 23:09:44 +08:00
idranme
afe8392a1e chore: v3.30.5 2024-08-25 23:07:33 +08:00
idranme
c1f5c5cd58 fix 2024-08-25 20:00:13 +08:00
idranme
85001a40da Merge pull request #366 from LLOneBot/dev
3.30.4
2024-08-23 17:05:03 +08:00
idranme
867a05c85a chore: v3.30.4 2024-08-23 17:03:58 +08:00
idranme
d8a63f6561 fix 2024-08-23 17:02:31 +08:00
idranme
e9fb9d1b30 Update publish.yml 2024-08-23 16:08:59 +08:00
idranme
b4fc987537 Merge pull request #365 from LLOneBot/dev
3.30.3
2024-08-23 13:40:59 +08:00
idranme
d0ccf53d88 chore: v3.30.3 2024-08-23 13:39:26 +08:00
idranme
d5ca94569d fix 2024-08-23 13:32:58 +08:00
idranme
bf72685501 Merge pull request #363 from LLOneBot/dev
3.30.2
2024-08-23 00:30:48 +08:00
idranme
c07467b670 chore: v3.30.2 2024-08-23 00:08:52 +08:00
idranme
ea164fb048 fix: friend list 2024-08-22 23:47:15 +08:00
idranme
0c0ad9a616 Merge pull request #362 from LLOneBot/dev
3.30.1
2024-08-22 20:41:32 +08:00
idranme
7bb4808e2d chore: v3.30.1 2024-08-22 20:18:16 +08:00
idranme
3f7592d06d opt 2024-08-22 20:17:28 +08:00
idranme
2f341fcf43 fix 2024-08-22 18:16:08 +08:00
148 changed files with 5894 additions and 6955 deletions

View File

@@ -1,4 +1,4 @@
name: 'publish'
name: Publish
on:
push:
tags:
@@ -8,31 +8,36 @@ jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v4
- name: setup node
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: install dependenies
- name: Install dependenies
run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
npm install
- name: build
- name: Build
run: npm run build
- name: zip
- name: Compress
run: |
sudo apt install zip -y
cd ./dist/
zip -r ../LLOneBot.zip ./*
- name: publish
- name: Extract version from tag
id: get-version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
- name: Release
uses: ncipollo/release-action@v1
with:
artifacts: 'LLOneBot.zip'
draft: true
token: ${{ secrets.RELEASE_TOKEN }}
name: LLOneBot v${{ steps.get-version.outputs.VERSION }}

View File

@@ -1,4 +1,4 @@
MIT Without Public Sicial Media Promotion License
MIT Without Public Social Media Promotion License
Copyright (c) 2024 LLOneBot

View File

@@ -3,7 +3,7 @@
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**
> 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息
TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
@@ -15,10 +15,6 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例
<img src="./doc/image/example.jpg" width="500px" alt="HTTP调用示例"/>
## 支持的 API
<https://llonebot.github.io/zh-CN/develop/api>
@@ -31,10 +27,10 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [chronocat](https://github.com/chrononeko/chronocat)
- [Chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm)
## 友链
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core): An Implementation of NTQQ Protocol

View File

@@ -1,6 +1,7 @@
import cp from 'vite-plugin-cp'
import path from 'node:path'
import './scripts/gen-manifest'
import type { ElectronViteConfig } from 'electron-vite'
const external = [
'silk-wasm',
@@ -12,7 +13,7 @@ function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
}
let config = {
const config: ElectronViteConfig = {
main: {
build: {
outDir: 'dist/main',
@@ -39,9 +40,6 @@ let config = {
...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' },
{ src: './icon.webp', dest: 'dist' },
// { 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-linux-x64.node', dest: 'dist/main/' },
],
}),
],

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.30.0",
"version": "3.31.10",
"icon": "./icon.webp",
"authors": [
{

View File

@@ -22,24 +22,24 @@
"cors": "^2.8.5",
"cosmokit": "^1.6.2",
"express": "^4.19.2",
"fast-xml-parser": "^4.4.1",
"fast-xml-parser": "^4.5.0",
"file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.0",
"minato": "^3.5.1",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.25",
"@types/fluent-ffmpeg": "^2.1.26",
"@types/node": "^20.14.15",
"@types/ws": "^8.5.12",
"electron": "^31.4.0",
"electron-vite": "^2.3.0",
"typescript": "^5.5.4",
"vite": "^5.4.2",
"vite": "^5.4.3",
"vite-plugin-cp": "^4.0.8"
},
"packageManager": "yarn@4.4.0"
"packageManager": "yarn@4.4.1"
}

View File

@@ -1,5 +1,6 @@
export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
export const CHANNEL_SET_CONFIG_CONFIRMED = 'llonebot_set_config_confirmed'
export const CHANNEL_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update'

View File

@@ -1,11 +1,8 @@
import fs from 'node:fs'
import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper'
import path from 'node:path'
import { getSelfUin } from './data'
import { DATA_DIR } from './utils'
//export const HOOK_LOG = false
import { Config, OB11Config } from './types'
import { selfInfo, DATA_DIR } from './globalVars'
import { mergeNewProperties } from './utils/misc'
export class ConfigUtil {
private readonly configPath: string
@@ -24,7 +21,7 @@ export class ConfigUtil {
}
reloadConfig(): Config {
let ob11Default: OB11Config = {
const ob11Default: OB11Config = {
httpPort: 3000,
httpHosts: [],
httpSecret: '',
@@ -36,9 +33,10 @@ export class ConfigUtil {
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
enableQOAutoQuote: false
enableQOAutoQuote: false,
listenLocalhost: false
}
let defaultConfig: Config = {
const defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default,
heartInterval: 60000,
@@ -69,7 +67,6 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
// console.log("get config", jsonData);
this.config = jsonData
return this.config
}
@@ -81,21 +78,21 @@ export class ConfigUtil {
}
private checkOldConfig(
currentConfig: Config | OB11Config,
oldConfig: Config | OB11Config,
currentKey: string,
oldKey: string,
currentConfig: OB11Config,
oldConfig: Config,
currentKey: 'httpPort' | 'httpHosts' | 'wsPort',
oldKey: 'http' | 'hosts' | 'wsPort',
) {
// 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey]
if (oldValue) {
currentConfig[currentKey] = oldValue
Object.assign(currentConfig, { [currentKey]: oldValue })
delete oldConfig[oldKey]
}
}
}
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`)
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}

View File

@@ -1,128 +0,0 @@
import {
type Friend,
type GroupMember,
type SelfInfo,
} from '../ntqqapi/types'
import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export let friends: Friend[] = []
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
}
// 群号 -> 群成员map(uid=>GroupMember)
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
const filterKey: 'uin' | 'uid' = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
const filterValue = uinOrUid
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend && getBuildVersion() < 26702) {
try {
const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
if (friend) {
friends.push(friend)
}
} catch (e: any) {
log('刷新好友列表失败', e.stack.toString())
}
}
return friend
}
export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString()
const memberUinOrUidStr = memberUinOrUid.toString()
let members = groupMembers.get(groupCodeStr)
if (!members) {
try {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表
groupMembers.set(groupCodeStr, members)
}
catch (e) {
return null
}
}
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else {
member = members!.get(memberUinOrUidStr)
}
return member
}
let member = getMember()
if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember()
}
return member
}
const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export async function getSelfNick(force = false): Promise<string> {
if ((!selfInfo.nick || force) && selfInfo.uid) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
selfInfo.nick = userInfo.nick
return userInfo.nick
}
}
return selfInfo.nick
}
export function getSelfInfo() {
return selfInfo
}
export function setSelfInfo(data: Partial<SelfInfo>) {
Object.assign(selfInfo, data)
}
export function getSelfUid() {
return selfInfo['uid']
}
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
}
const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
export function getMsgCache(msgId: string) {
return messages.get(msgId)
}

22
src/common/globalVars.ts Normal file
View File

@@ -0,0 +1,22 @@
import { LLOneBotError } from './types'
import { SelfInfo } from '../ntqqapi/types'
import path from 'node:path'
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
}
export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
export const LOG_DIR = path.join(DATA_DIR, 'logs')
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}

View File

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

View File

@@ -11,16 +11,19 @@ export interface OB11Config {
messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
listenLocalhost: boolean
}
export interface CheckVersion {
result: boolean
version: string
}
export interface Config {
enableLLOB: boolean
ob11: OB11Config
token?: string
heartInterval?: number // ms
heartInterval: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean
reportSelfMessage?: boolean
@@ -32,6 +35,12 @@ export interface Config {
ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
/** @deprecated */
http?: string
/** @deprecated */
hosts?: string[]
/** @deprecated */
wsPort?: string
}
export interface LLOneBotError {

View File

@@ -1,234 +0,0 @@
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
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
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}
public initialised = false
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
this.initialised = true
}
createEventFunction<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
}
}
createListenerFunction<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.createEventFunction<(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.createEventFunction<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.createListenerFunction(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.createListenerFunction(ListenerMainName)
const EventFunc = this.createEventFunction<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

@@ -1,163 +0,0 @@
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from './table'
import { DATA_DIR } from './index'
import Database, { Tables } from 'minato'
import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file_v2: FileCacheV2
}
interface MsgIdAndPeerByShortId {
MsgId: string
Peer: Peer
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
}
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const heads = this.msgIdMap.getHeads(size)
if (!heads) {
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number | undefined {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId)
}
async getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId)
if (!shortId) return undefined
return await this.getMsgIdAndPeerByShortId(shortId)
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -1,52 +0,0 @@
import path from 'node:path'
import os from 'node:os'
import { systemPlatform } from './system'
export const exePath = process.execPath
function getPKGPath() {
let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json')
if (systemPlatform === 'darwin') {
p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json')
}
return p
}
export const pkgInfoPath = getPKGPath()
let configVersionInfoPath: string
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json')
}
else {
const userPath = os.homedir()
const appDataPath = path.resolve(userPath, './.config/QQ')
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json')
}
if (typeof configVersionInfoPath !== 'string') {
throw new Error('Something went wrong when load QQ info path')
}
export { configVersionInfoPath }
type QQPkgInfo = {
version: string
buildVersion: string
platform: string
eleArch: string
}
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B',
// appid: '537213764',
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
export function getBuildVersion(): number {
return +qqPkgInfo.buildVersion
}

View File

@@ -2,11 +2,10 @@ import path from 'node:path'
import ffmpeg from 'fluent-ffmpeg'
import fsPromise from 'node:fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import { log } from './log'
import { TEMP_DIR } from './index'
import { getConfigUtil } from '../config'
import { TEMP_DIR } from '../globalVars'
import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream'
import { Context } from 'cordis'
interface FFmpegOptions {
input?: string[]
@@ -15,14 +14,14 @@ interface FFmpegOptions {
type Input = string | Readable
function convert(input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer> | Promise<string> {
return new Promise<any>((resolve, reject) => {
function convert(ctx: Context, input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer | string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
let command = ffmpeg(input)
.on('error', err => {
log(`FFmpeg处理转换出错: `, err.message)
ctx.logger.error(`FFmpeg处理转换出错: `, err.message)
reject(err)
})
.on('end', () => {
@@ -38,7 +37,7 @@ function convert(input: Input, options: FFmpegOptions, outputPath?: string): Pro
if (options.output) {
command = command.outputOptions(options.output)
}
const ffmpegPath = getConfigUtil().getConfig().ffmpeg
const ffmpegPath: string | undefined = ctx.config.ffmpeg
if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath)
}
@@ -53,17 +52,17 @@ function convert(input: Input, options: FFmpegOptions, outputPath?: string): Pro
})
}
export async function encodeSilk(filePath: string) {
export async function encodeSilk(ctx: Context, filePath: string) {
try {
const file = await fsPromise.readFile(filePath)
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`)
ctx.logger.info(`语音文件${filePath}需要转换成silk`)
let result: EncodeResult
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
result = await encode(file, 0)
} else {
const input = await convert(filePath, {
const input = await convert(ctx, filePath, {
output: [
'-ar 24000',
'-ac 1',
@@ -74,7 +73,7 @@ export async function encodeSilk(filePath: string) {
}
const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
@@ -85,8 +84,8 @@ export async function encodeSilk(filePath: string) {
let duration = 1
try {
duration = getDuration(silk) / 1000
} catch (e: any) {
log('获取语音文件时长失败, 默认为1秒', filePath, e.stack)
} catch (e) {
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
}
return {
converted: false,
@@ -94,22 +93,22 @@ export async function encodeSilk(filePath: string) {
duration,
}
}
} catch (error: any) {
log('convert silk failed', error.stack)
} catch (err) {
ctx.logger.error('convert silk failed', (err as Error).stack)
return {}
}
}
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') {
export async function decodeSilk(ctx: Context, inputFilePath: string, outFormat: OutFormat = 'mp3') {
const silk = await fsPromise.readFile(inputFilePath)
const { data } = await decode(silk, 24000)
const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath))
const outFilePath = tmpPath + `.${outFormat}`
const pcmFilePath = tmpPath + '.pcm'
await fsPromise.writeFile(pcmFilePath, data)
return convert(pcmFilePath, {
return convert(ctx, pcmFilePath, {
input: [
'-f s16le',
'-ar 24000',

View File

@@ -1,7 +1,7 @@
import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import path from 'node:path'
import { TEMP_DIR } from './index'
import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
@@ -56,34 +56,6 @@ export function calculateFileMD5(filePath: string): Promise<string> {
})
}
export interface HttpDownloadOptions {
url: string
headers?: Record<string, string> | string
}
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let url: string
let headers: Record<string, string> = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
}
if (typeof options === 'string') {
url = options
} else {
url = options.url
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers)
} else {
headers = options.headers
}
}
}
const fetchRes = await fetch(url, { headers })
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
return Buffer.from(await fetchRes.arrayBuffer())
}
export enum FileUriType {
Unknown = 0,
FileURL = 1,
@@ -117,10 +89,11 @@ interface FetchFileRes {
url: string
}
async function fetchFile(url: string): Promise<FetchFileRes> {
export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> {
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname
'Host': new URL(url).hostname,
...headersInit
}
const raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
@@ -169,8 +142,8 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
const filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data)
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e: any) {
const errMsg = `${uri}下载失败,` + e.toString()
} catch (e) {
const errMsg = `${uri} 下载失败, ${(e as Error).message}`
return { success: false, errMsg, fileName: '', path: '', isLocal: false }
}
}
@@ -202,7 +175,7 @@ export async function copyFolder(sourcePath: string, destPath: string) {
try {
const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true })
await fsPromise.mkdir(destPath, { recursive: true })
for (let entry of entries) {
for (const entry of entries) {
const srcPath = path.join(sourcePath, entry.name)
const dstPath = path.join(destPath, entry.name)
if (entry.isDirectory()) {

View File

@@ -1,169 +0,0 @@
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export function isNumeric(str: string) {
return /^\d+$/.test(str)
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]
}
}
})
}
export function isNull(value: unknown) {
return value === undefined || value === null
}
/**
* 将字符串按最大长度分割并添加换行符
* @param str 原始字符串
* @param maxLength 每行的最大字符数
* @returns 处理后的字符串,超过长度的地方将会换行
*/
export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果
let result: string = ''
// 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n'
result += str.substring(i, i + maxLength)
}
return result
}
/**
* 函数缓存装饰器根据方法名、参数、自定义key生成缓存键在一定时间内返回缓存结果
* @param ttl 超时时间,单位毫秒
* @param customKey 自定义缓存键前缀,可为空,防止方法名参数名一致时导致缓存键冲突
* @returns 处理后缓存或调用原方法的结果
*/
export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>()
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value
const className = target.constructor.name // 获取类名
const methodName = propertyKey // 获取方法名
descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`
const cached = cache.get(cacheKey)
if (cached && cached.expiry > Date.now()) {
return cached.value
} else {
const result = await originalMethod.apply(this, args)
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl })
return result
}
}
return descriptor
}
}
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
if (!checker(...args, result)) {
return result //丢弃缓存
}
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
}

View File

@@ -1,14 +1,7 @@
import path from 'node:path'
export * from './file'
export * from './helper'
export * from './log'
export * from './qqlevel'
export * from './QQBasicInfo'
export * from './misc'
export * from './legacyLog'
export * from './misc'
export * from './upgrade'
export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
export { getVideoInfo } from './video'
export { checkFfmpeg } from './video'
export { getVideoInfo, checkFfmpeg } from './video'
export { encodeSilk } from './audio'

View File

@@ -0,0 +1,42 @@
import fs from 'fs'
import path from 'node:path'
import { getConfigUtil } from '../config'
import { LOG_DIR } from '../globalVars'
import { Dict } from 'cosmokit'
function truncateString(obj: Dict | null, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
export function log(...msg: unknown[]) {
if (!getConfigUtil().getConfig().log) {
return
}
let logMsg = ''
for (const msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === 'object') {
logMsg += JSON.stringify(truncateString(msgItem)) + ' '
continue
}
logMsg += msgItem + ' '
}
const currentDateTime = new Date().toLocaleString()
logMsg = `${currentDateTime} ${logMsg}\n\n`
fs.appendFile(path.join(LOG_DIR, logFileName), logMsg, () => { })
}

View File

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

View File

@@ -0,0 +1,163 @@
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import Database, { Tables } from 'minato'
import SQLite from '@minatojs/driver-sqlite'
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from './table'
import { DATA_DIR } from '../globalVars'
import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file_v2: FileCacheV2
}
interface MsgIdAndPeerByShortId {
MsgId: string
Peer: Peer
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
}
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const heads = this.msgIdMap.getHeads(size)
if (!heads) {
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId)
}
async getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId)
if (!shortId) return undefined
return await this.getMsgIdAndPeerByShortId(shortId)
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

35
src/common/utils/misc.ts Normal file
View File

@@ -0,0 +1,35 @@
import { QQLevel } from '@/ntqqapi/types'
import { Dict } from 'cosmokit'
export function isNumeric(str: string) {
return /^\d+$/.test(str)
}
export function calcQQLevel(level: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
}
/** QQ Build Version */
export function getBuildVersion(): number {
const version: string = globalThis.LiteLoader.versions.qqnt
return +version.split('-')[1]
}
/** 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 */
export function mergeNewProperties(newObj: Dict, oldObj: Dict) {
Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]
}
}
})
}

View File

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

View File

@@ -1,107 +1,104 @@
import https from 'node:https';
import http from 'node:http';
import { log } from '@/common/utils/log'
import https from 'node:https'
import http from 'node:http'
import { Dict } from 'cosmokit'
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
const client = url.startsWith('https') ? https : http
return new Promise((resolve, reject) => {
client.get(url, (res) => {
let cookies: { [key: string]: string } = {};
let cookies: { [key: string]: string } = {}
const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location);
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
const redirectUrl = new URL(res.headers.location, url)
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies
//log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies };
resolve(cookies);
});
cookies = { ...cookies, ...redirectCookies }
resolve(cookies)
})
} else {
resolve(cookies);
resolve(cookies)
}
} else {
resolve(cookies);
resolve(cookies)
}
};
res.on('data', () => { }); // Necessary to consume the stream
}
res.on('data', () => { }) // Necessary to consume the stream
res.on('end', () => {
handleRedirect(res);
});
handleRedirect(res)
})
if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']);
//log('set-cookie', url, res.headers['set-cookie']);
//log('set-cookie', url, res.headers['set-cookie'])
res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
const parts = cookie.split(';')[0].split('=')
const key = parts[0]
const value = parts[1]
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
cookies[key] = value
}
});
})
}
}).on('error', (err) => {
reject(err);
});
});
reject(err)
})
})
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
let option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: unknown, headers: Record<string, string> = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
const option = new URL(url)
const protocol = url.startsWith('https://') ? https : http
const options = {
hostname: option.hostname,
port: option.port,
path: option.href,
method: method,
headers: headers
};
}
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: any) => {
let responseBody = '';
const req = protocol.request(options, (res: Dict) => {
let responseBody = ''
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
responseBody += chunk.toString()
})
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
if (isJsonRet) {
const responseJson = JSON.parse(responseBody);
resolve(responseJson as T);
const responseJson = JSON.parse(responseBody)
resolve(responseJson as T)
} else {
resolve(responseBody as T);
resolve(responseBody as T)
}
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
reject(new Error(`Unexpected status code: ${res.statusCode}`))
}
} catch (parseError) {
reject(parseError);
reject(parseError)
}
});
});
})
})
req.on('error', (error: any) => {
reject(error);
});
req.on('error', (error) => {
reject(error)
})
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (isArgJson) {
req.write(JSON.stringify(data));
req.write(JSON.stringify(data))
} else {
req.write(data);
req.write(data)
}
}
req.end();
});
req.end()
})
}
// 请求返回都是原始内容
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
static async HttpGetText(url: string, method: string = 'GET', data?: unknown, headers: Record<string, string> = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false)
}
}

View File

@@ -1,4 +1,4 @@
import { log } from './log'
import { Context } from 'cordis'
export interface IdMusicSignPostData {
type: 'qq' | '163'
@@ -19,7 +19,7 @@ export type MusicSignPostData = IdMusicSignPostData | CustomMusicSignPostData
export class MusicSign {
private readonly url: string
constructor(url: string) {
constructor(protected ctx: Context, url: string) {
this.url = url
}
@@ -31,7 +31,7 @@ export class MusicSign {
})
if (!resp.ok) throw new Error(resp.statusText)
const data = await resp.text()
log('音乐消息生成成功', data)
this.ctx.logger.info('音乐消息生成成功', data)
return data
}
}

View File

@@ -1,10 +0,0 @@
import os from 'node:os';
import path from 'node:path';
export const systemPlatform = os.platform();
export const cpuArch = os.arch();
export const systemVersion = os.release();
// export const hostname = os.hostname(); // win7不支持
const homeDir = os.homedir();
export const downloadsPath = path.join(homeDir, 'Downloads');
export const systemName = os.type();

View File

@@ -1,8 +1,9 @@
import { version } from '../../version'
import * as path from 'node:path'
import * as fs from 'node:fs'
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.'
import path from 'node:path'
import compressing from 'compressing'
import { writeFile } from 'node:fs/promises'
import { version } from '../../version'
import { copyFolder, log, fetchFile } from '.'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/']
const checkVersionMirrorHosts = ['https://kkgithub.com']
@@ -10,10 +11,10 @@ const checkVersionMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion()
const latestVersion = latestVersionText.split('.')
log('llonebot last version', latestVersion)
//log('llonebot last version', latestVersion)
const currentVersion: string[] = version.split('.')
log('llonebot current version', currentVersion)
for (let k of [0, 1, 2]) {
//log('llonebot current version', currentVersion)
for (const k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) {
log('')
return { result: true, version: latestVersionText }
@@ -33,8 +34,8 @@ export async function upgradeLLOneBot() {
// 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) {
try {
const buffer = await httpDownload(mirrorGithub + downloadUrl)
fs.writeFileSync(filePath, buffer)
const res = await fetchFile(mirrorGithub + downloadUrl)
await writeFile(filePath, res.data)
downloadSuccess = true
break
} catch (e) {
@@ -46,14 +47,14 @@ export async function upgradeLLOneBot() {
return false
}
const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion)
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
const uncompressedPromise = async function () {
return new Promise<boolean>(resolve => {
compressing.zip
.uncompress(filePath, temp_ver_dir)
.then(() => {
resolve(true)
})
.catch((reason: any) => {
.catch(reason => {
log('llonebot upgrade failed, ', reason)
if (reason?.errno == -4082) {
resolve(true)
@@ -74,8 +75,8 @@ export async function upgradeLLOneBot() {
export async function getRemoteVersion() {
let Version = ''
for (let i = 0; i < checkVersionMirrorHosts.length; i++) {
let mirrorGithub = checkVersionMirrorHosts[i]
let tVersion = await getRemoteVersionByMirror(mirrorGithub)
const mirrorGithub = checkVersionMirrorHosts[i]
const tVersion = await getRemoteVersionByMirror(mirrorGithub)
if (tVersion && tVersion != '') {
Version = tVersion
break
@@ -88,7 +89,7 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = 'error'
try {
releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
releasePage = (await fetchFile(mirrorGithub + '/LLOneBot/LLOneBot/releases')).data.toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]

File diff suppressed because one or more lines are too long

11
src/global.d.ts vendored
View File

@@ -1,8 +1,9 @@
import { type LLOneBot } from './preload'
import type { LLOneBot } from './preload'
import { Dict } from 'cosmokit'
declare global {
interface Window {
llonebot: LLOneBot
LiteLoader: Record<string, any>
}
var llonebot: LLOneBot
var LiteLoader: Dict
var authData: Dict | undefined
var navigation: Dict | undefined
}

View File

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

38
src/main/log.ts Normal file
View File

@@ -0,0 +1,38 @@
import path from 'node:path'
import { Context, Logger } from 'cordis'
import { appendFile } from 'node:fs'
import { LOG_DIR, selfInfo } from '@/common/globalVars'
import { noop } from 'cosmokit'
interface Config {
enable: boolean
filename: string
}
export default class Log {
static name = 'logger'
constructor(ctx: Context, cfg: Config) {
Logger.targets.splice(0, Logger.targets.length)
if (!cfg.enable) {
return
}
const file = path.join(LOG_DIR, cfg.filename)
/*const refreshNick = ctx.debounce(() => {
const ntUserApi = ctx.get('ntUserApi')
if (ntUserApi && !selfInfo.nick) {
ntUserApi.getSelfNick(true)
}
}, 1000)*/
const target: Logger.Target = {
colors: 0,
record: (record: Logger.Record) => {
const dateTime = new Date(record.timestamp).toLocaleString()
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ''
const content = `${dateTime} [${record.type}] ${userInfo} | ${record.name} ${record.content}\n\n`
appendFile(file, content, noop)
},
}
Logger.targets.push(target)
}
}

View File

@@ -1,9 +1,10 @@
// 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron'
import path from 'node:path'
import fs from 'node:fs'
import { Config } from '../common/types'
import Log from './log'
import Core from '../ntqqapi/core'
import OneBot11Adapter from '../onebot11/adapter'
import { BrowserWindow, dialog, ipcMain } from 'electron'
import { Config as LLOBConfig } from '../common/types'
import {
CHANNEL_CHECK_VERSION,
CHANNEL_ERROR,
@@ -12,55 +13,55 @@ import {
CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG,
CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED
} from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR, getBuildVersion, TEMP_DIR } from '../common/utils'
import {
llonebotError,
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin,
addMsgCache
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
FriendRequestNotify,
GroupNotify,
GroupNotifyTypes,
RawMessage,
BuddyReqType,
} from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import { MessageUnique } from '../common/utils/MessageUnique'
import { setConfig } from './setConfig'
import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { getBuildVersion } from '../common/utils'
import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/wrapper'
import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
import { Peer } from '../ntqqapi/types'
import { getSession } from '../ntqqapi/wrapper'
import { Context } from 'cordis'
import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars'
import { log, logFileName } from '../common/utils/legacyLog'
import {
NTQQFileApi,
NTQQFileCacheApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQMsgApi,
NTQQUserApi,
NTQQWebApi,
NTQQWindowApi
} from '../ntqqapi/api'
declare module 'cordis' {
interface Events {
'llonebot/config-updated': (input: LLOBConfig) => void
}
}
let mainWindow: BrowserWindow | null = null
// 加载插件时触发
function onLoad() {
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true })
}
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR)
}
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
return checkNewVersion()
})
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => {
ipcMain.handle(CHANNEL_UPDATE, async () => {
return upgradeLLOneBot()
})
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
ipcMain.handle(CHANNEL_SELECT_FILE, async () => {
const selectPath = new Promise<string>((resolve, reject) => {
dialog
.showOpenDialog({
@@ -73,11 +74,9 @@ function onLoad() {
if (!result.canceled) {
const _selectPath = path.join(result.filePaths[0])
resolve(_selectPath)
// let config = getConfigUtil().getConfig()
// config.ffmpeg = path.join(result.filePaths[0]);
// getConfigUtil().setConfig(config);
}
} else {
resolve('')
}
})
.catch((err) => {
reject(err)
@@ -90,30 +89,30 @@ function onLoad() {
return ''
}
})
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true })
}
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
ipcMain.handle(CHANNEL_ERROR, async () => {
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常'
let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
const { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace('\n\n', '\n')
error = error.trim()
log('查询llonebot错误信息', error)
log('查询 LLOneBot 错误信息', error)
return error
})
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
ipcMain.handle(CHANNEL_GET_CONFIG, async () => {
const config = getConfigUtil().getConfig()
return config
})
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => {
ipcMain.handle(CHANNEL_SET_CONFIG, (_event, ask: boolean, config: LLOBConfig) => {
return new Promise<boolean>(resolve => {
if (!ask) {
setConfig(config)
.then()
.catch((e) => {
log('保存设置失败', e.stack)
})
getConfigUtil().setConfig(config)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
resolve(true)
return
}
dialog
@@ -127,250 +126,24 @@ function onLoad() {
})
.then((result) => {
if (result.response === 0) {
setConfig(config)
.then()
.catch((e) => {
log('保存设置失败', e.stack)
})
}
else {
getConfigUtil().setConfig(config)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
resolve(true)
}
})
.catch((err) => {
log('保存设置询问弹窗错误', err)
resolve(false)
})
})
})
ipcMain.on(CHANNEL_LOG, (event, arg) => {
ipcMain.on(CHANNEL_LOG, (_event, arg) => {
log(arg)
})
async function postReceiveMsg(msgList: RawMessage[]) {
const { debug, reportSelfMessage } = getConfigUtil().getConfig()
for (let message of msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < startTime / 1000) {
continue
}
// log("收到新消息", message.msgId, message.msgSeq)
const peer: Peer = {
chatType: message.chatType,
peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
addMsgCache(message)
OB11Constructor.message(message)
.then((msg) => {
if (!debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === getSelfUin()
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
postOb11Event(msg)
// log("post msg", msg)
})
.catch((e) => log('constructMessage error: ', e.stack.toString()))
OB11Constructor.GroupEvent(message).then((groupEvent) => {
if (groupEvent) {
// log("post group event", groupEvent);
postOb11Event(groupEvent)
}
})
OB11Constructor.PrivateEvent(message).then((privateEvent) => {
//log(message)
if (privateEvent) {
// log("post private event", privateEvent);
postOb11Event(privateEvent)
}
})
}
}
async function startReceiveHook() {
startHook()
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList)
} catch (e: any) {
log('report message error: ', e.stack.toString())
}
})
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
if (!oriMessageId) {
continue
}
OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => {
if (recallEvent) {
//log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
})
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const { reportSelfMessage } = getConfigUtil().getConfig()
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord])
} catch (e: any) {
log('report self message error: ', e.stack.toString())
}
})
const processedGroupNotify: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notifies: GroupNotify[]
try {
notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
// log("获取群通知详情失败", e);
return
}
for (const notify of notifies) {
try {
notify.time = Date.now()
const notifyTime = parseInt(notify.seq) / 1000
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
continue
}
processedGroupNotify.push(flag)
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))!
let operatorId = member1Uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid)
if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick'
}
const groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
}
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求')
let requestQQ = ''
try {
// uid-->uin
requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
}
} catch (e) {
log('获取加群人QQ号失败 Uid:', notify.user1.uid, e)
}
let invitorId: string
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite'
try {
// uid-->uin
invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid))
if (isNaN(parseInt(invitorId))) {
invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin
}
} catch (e) {
invitorId = ''
log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
}
}
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0,
flag,
notify.postscript,
invitorId! === undefined ? undefined : +invitorId,
'add'
)
postOb11Event(groupRequestEvent)
}
else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || ''
const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId),
flag,
undefined,
undefined,
'invite'
)
postOb11Event(groupInviteEvent)
}
} catch (e: any) {
log('解析群通知失败', e.stack.toString())
}
}
}
else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
continue
}
if (+req.reqTime < startTime / 1000) {
continue
}
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
postOb11Event(friendRequestEvent)
}
})
}
let startTime = 0 // 毫秒
async function start(uid: string, uin: string) {
async function start() {
log('process pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
@@ -379,75 +152,75 @@ function onLoad() {
return
}
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
fs.mkdirSync(TEMP_DIR)
}
llonebotError.otherError = ''
startTime = Date.now()
const WrapperSession = getSession()
if (WrapperSession) {
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession })
}
MessageUnique.init(uin)
//log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort)
}
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start()
}
if (config.ob11.enableHttpHeart) {
httpHeart.start()
}
log('LLOneBot start')
const ctx = new Context()
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
})
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config)
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config)
})
}
const buildVersion = getBuildVersion()
const intervalId = setInterval(() => {
const current = getSelfInfo()
if (!current.uin) {
setSelfInfo({
const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
nick: current.uin,
online: true
})
}
if (current.uin && (buildVersion >= 27187 || getSession())) {
if (self.uin && (buildVersion >= 27187 || getSession())) {
clearInterval(intervalId)
start(current.uid, current.uin)
start()
}
}, 600)
}
// 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) {
if (window.id !== 2) {
if (![2, 4].includes(window.id)) {
return
}
if (window.id === 2) {
mainWindow = window
log('window create', window.webContents.getURL().toString())
}
//log('window create', window.webContents.getURL().toString())
try {
hookNTQQApiCall(window)
hookNTQQApiReceive(window)
} catch (e: any) {
log('LLOneBot hook error: ', e.toString())
hookNTQQApiCall(window, window.id !== 2)
hookNTQQApiReceive(window, window.id !== 2)
} catch (e) {
log('LLOneBot hook error: ', String(e))
}
}
try {
onLoad()
} catch (e: any) {
console.log(e.toString())
} catch (e) {
console.log(e)
}
// 这两个函数都是可选的

View File

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

View File

@@ -5,7 +5,6 @@ import {
CacheFileListItem,
CacheFileType,
CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic,
ChatType,
ElementType,
@@ -16,36 +15,68 @@ import {
import path from 'node:path'
import fs from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/helper/rkey'
import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit'
import { Service, Context } from 'cordis'
import { TEMP_DIR } from '@/common/globalVars'
export class NTQQFileApi {
/** 27187 TODO */
static async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
declare module 'cordis' {
interface Context {
ntFileApi: NTQQFileApi
ntFileCacheApi: NTQQFileCacheApi
}
}
export class NTQQFileApi extends Service {
private rkeyManager: RkeyManager
constructor(protected ctx: Context) {
super(ctx, 'ntFileApi', true)
this.rkeyManager = new RkeyManager(ctx, 'http://napcat-sign.wumiao.wang:2082/rkey')
}
async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession()
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
if (session) {
return (await session.getRichMediaService().getVideoPlayUrlV2(
peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url
{ downSourceType: 1, triggerType: 1 }
)).urlResult.domainUrl[0]?.url
} else {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer,
msgId,
elemId: elementId,
videoCodecFormat: 0,
exParams: {
downSourceType: 1,
triggerType: 1
},
}, null])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
}
return data.urlResult.domainUrl[0]?.url
}
}
static async getFileType(filePath: string) {
async getFileType(filePath: string) {
return fileTypeFromFile(filePath)
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
let ext = (await this.getFileType(filePath))?.ext || ''
if (ext) {
ext = '.' + ext
}
@@ -67,10 +98,7 @@ export class NTQQFileApi {
file_uuid: ''
})
} else {
mediaPath = await invoke<string>({
methodName: NTMethod.MEDIA_FILE_PATH,
args: [
{
mediaPath = await invoke(NTMethod.MEDIA_FILE_PATH, [{
path_info: {
md5HexStr: fileMd5,
fileName: fileName,
@@ -81,9 +109,7 @@ export class NTQQFileApi {
downloadType: 1,
file_uuid: '',
},
},
],
})
}])
}
await fsPromise.copyFile(filePath, mediaPath)
const fileSize = (await fsPromise.stat(filePath)).size
@@ -96,7 +122,7 @@ export class NTQQFileApi {
}
}
static async downloadMedia(
async downloadMedia(
msgId: string,
chatType: ChatType,
peerUid: string,
@@ -116,52 +142,9 @@ export class NTQQFileApi {
return sourcePath
}
}
let filePath: string
if (NTEventDispatch.initialised) {
const data = await NTEventDispatch.CallNormalEvent<
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
}
)
filePath = data[1].filePath
} else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>({
methodName: NTMethod.DOWNLOAD_MEDIA,
args: [
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
'nodeIKernelMsgService/downloadRichMedia',
[
{
getReq: {
fileModelId: '0',
@@ -178,12 +161,13 @@ export class NTQQFileApi {
},
null,
],
{
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId,
timeout
})
filePath = data.notifyInfo.filePath
}
)
let filePath = data.notifyInfo.filePath
if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath)
@@ -192,15 +176,17 @@ export class NTQQFileApi {
return filePath
}
static async getImageSize(filePath: string) {
return await invoke<{ width: number; height: number }>({
async getImageSize(filePath: string) {
return await invoke<{ width: number; height: number }>(
NTMethod.IMAGE_SIZE,
[filePath],
{
className: NTClass.FS_API,
methodName: NTMethod.IMAGE_SIZE,
args: [filePath],
})
}
)
}
static async getImageUrl(element: PicElement) {
async getImageUrl(element: PicElement) {
if (!element) {
return ''
}
@@ -209,17 +195,17 @@ export class NTQQFileApi {
const fileMd5 = element.md5HexStr
if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = UrlParse.searchParams.get('appid')
const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = parsedUrl.searchParams.get('appid')
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey')
if (UrlRkey) {
let rkey = parsedUrl.searchParams.get('rkey')
if (rkey) {
return IMAGE_HTTP_HOST_NT + url
}
const rkeyData = await rkeyManager.getRkey()
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
const rkeyData = await this.rkeyManager.getRkey()
rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
return IMAGE_HTTP_HOST_NT + url + rkey
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url
@@ -228,134 +214,58 @@ export class NTQQFileApi {
// 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
}
log('图片url获取失败', element)
this.ctx.logger.error('图片url获取失败', element)
return ''
}
}
export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_SET_SILENCE,
args: [
{
isSilent,
},
null,
],
})
export class NTQQFileCacheApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntFileCacheApi', true)
}
static getCacheSessionPathList() {
async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }, null])
}
getCacheSessionPathList() {
return invoke<
{
key: string
value: string
}[]
>({
className: NTClass.OS_API,
methodName: NTMethod.CACHE_PATH_SESSION,
})
>(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
}
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return invoke<any>({
// TODO: 目前还不知道真正的返回值是什么
methodName: NTMethod.CACHE_CLEAR,
args: [
{
keys: cacheKeys,
},
null,
],
})
scanCache() {
invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { classNameIsRegister: true })
return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second })
}
static addCacheScannedPaths(pathMap: object = {}) {
return invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [
{
pathMap: { ...pathMap },
},
null,
],
})
getHotUpdateCachePath() {
return invoke<string>(NTMethod.CACHE_PATH_HOT_UPDATE, [], { className: NTClass.HOTUPDATE_API })
}
static scanCache() {
invoke<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then()
return invoke<CacheScanResult>({
methodName: NTMethod.CACHE_SCAN,
args: [null, null],
timeout: 300 * Time.second,
})
getDesktopTmpPath() {
return invoke<string>(NTMethod.CACHE_PATH_DESKTOP_TEMP, [], { className: NTClass.BUSINESS_API })
}
static getHotUpdateCachePath() {
return invoke<string>({
className: NTClass.HOTUPDATE_API,
methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
})
}
static getDesktopTmpPath() {
return invoke<string>({
className: NTClass.BUSINESS_API,
methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP,
})
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
invoke<ChatCacheList>({
methodName: NTMethod.CACHE_CHAT_GET,
args: [
{
chatType: type,
pageSize,
order: 1,
pageIndex,
},
null,
],
})
.then((list) => res(list))
.catch((e) => rej(e))
})
}
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return invoke<CacheFileList>({
methodName: NTMethod.CACHE_FILE_GET,
args: [
{
return invoke<CacheFileList>(NTMethod.CACHE_FILE_GET, [{
fileType: fileType,
restart: true,
pageSize: pageSize,
order: 1,
lastRecord: _lastRecord,
},
null,
],
})
}, null])
}
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_CHAT_CLEAR,
args: [
{
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{
chats,
fileKeys,
},
null,
],
})
}, null])
}
}

View File

@@ -2,13 +2,23 @@ import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { LimitedHashTable } from '@/common/utils/table'
import { BuddyListReqType } from '../services'
import { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis'
declare module 'cordis' {
interface Context {
ntFriendApi: NTQQFriendApi
}
}
export class NTQQFriendApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntFriendApi', true)
}
export class NTQQFriendApi {
/** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) {
async getFriends() {
const data = await invoke<{
data: {
categoryId: number
@@ -16,20 +26,23 @@ export class NTQQFriendApi {
categroyMbCount: number
buddyList: Friend[]
}[]
}>({
methodName: NTMethod.FRIENDS,
args: [{ force_update: forced }, undefined],
}>(
'getBuddyList',
[],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
let _friends: Friend[] = []
for (const fData of data.data) {
_friends.push(...fData.buddyList)
}
)
const _friends: Friend[] = []
for (const item of data.data) {
_friends.push(...item.buddyList)
}
return _friends
}
static async handleFriendRequest(flag: string, accept: boolean) {
async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|')
if (data.length < 2) {
return
@@ -44,96 +57,97 @@ export class NTQQFriendApi {
accept
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_FRIEND_REQUEST,
args: [
{
return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
approvalInfo: {
friendUid,
reqTime,
accept,
},
},
],
})
}])
}
}
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const session = getSession()
if (session) {
const uids: string[] = []
const buddyService = session.getBuddyService()
const buddyListV2 = 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
)
const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
return Array.from(data.values())
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Map<string, SimpleInfo>
}>({
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
return Array.from(data.userSimpleInfos.values())
}
)
const uids = data.buddyCategory.flatMap(item => item.buddyUids)
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
}
}
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
/** uid => uin */
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map()
const session = getSession()
if (session) {
const uids: string[] = []
const buddyService = session?.getBuddyService()
const buddyService = session.getBuddyService()
const buddyListV2 = 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
)
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
for (const [, item] of data) {
if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
}
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Map<string, SimpleInfo>
}>({
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
data.userSimpleInfos.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
}
)
for (const item of Object.values(data.userSimpleInfos)) {
if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
}
}
return retMap
}
static async getBuddyV2ExWithCate(refresh = false) {
async getBuddyV2ExWithCate(refresh = false) {
const session = getSession()
if (session) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const categoryMap: Map<string, Dict> = new Map()
const buddyService = session.getBuddyService()
const buddyListV2 = (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data
uids.push(
...buddyListV2?.flatMap(item => {
...buddyListV2.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
}))
const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
@@ -141,39 +155,38 @@ export class NTQQFriendApi {
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Map<string, SimpleInfo>
}>({
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
return Array.from(data.userSimpleInfos).map(([key, value]) => {
if (value.baseInfo) {
}
)
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
}
return Object.values(data.userSimpleInfos)
.filter(v => v.baseInfo && category.get(v.baseInfo.categoryId)?.buddyUids.includes(v.uid!))
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: data.buddyCategory.find(e => e.categoryId === value.baseInfo.categoryId)?.categroyName
categroyName: category.get(value.baseInfo.categoryId)?.categroyName
}
}
return value
})
}
}
static async isBuddy(uid: string): Promise<boolean> {
async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke<boolean>({
methodName: 'nodeIKernelBuddyService/isBuddy',
args: [
{ uid },
null,
],
})
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
}
}
}

View File

@@ -1,71 +1,55 @@
import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GetFileListParam } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { NTQQWindowApi, NTQQWindows } from './window'
import { NTQQWindows } from './window'
import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners'
import { OnGroupFileInfoUpdateParams } from '../listeners'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
export class NTQQGroupApi {
static async getGroups(forced = false): Promise<Group[]> {
if (NTEventDispatch.initialised) {
type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
const [, , groupList] = await NTEventDispatch.CallNormalEvent
<(force: boolean) => Promise<any>, ListenerType>
(
'NodeIKernelGroupService/getGroupList',
'NodeIKernelGroupListener/onGroupListUpdate',
1,
5000,
() => true,
forced
)
return groupList
} else {
declare module 'cordis' {
interface Context {
ntGroupApi: NTQQGroupApi
}
}
export class NTQQGroupApi extends Service {
static inject = ['ntWindowApi']
public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true)
}
async getGroups(): Promise<Group[]> {
const result = await invoke<{
updateType: number
groupList: Group[]
}>({
}>(
'getGroupList',
[],
{
className: NTClass.NODE_STORE_API,
methodName: 'getGroupList',
cbCmd: ReceiveCmdS.GROUPS_STORE,
afterFirstCmd: false,
})
}
)
return result.groupList
}
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession()
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
if (session) {
const groupService = session.getGroupService()
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow')
const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow')
result = await groupService.getNextMemberList(sceneId, undefined, num)
} else {
const sceneId = await invoke<string>({
methodName: NTMethod.GROUP_MEMBER_SCENE,
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
})
result = await invoke<
ReturnType<NodeIKernelGroupService['getNextMemberList']>
>({
methodName: NTMethod.GROUP_MEMBERS,
args: [
{
sceneId,
num,
},
null,
],
})
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
}
if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg)
@@ -73,47 +57,60 @@ export class NTQQGroupApi {
return result.result.infos
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getSingleScreenNotifies(14)
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString()
const memberUinOrUidStr = memberUinOrUid.toString()
if (!this.groupMembers.has(groupCodeStr)) {
try {
// 更新群成员列表
this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr))
}
catch (e) {
return null
}
}
let members = this.groupMembers.get(groupCodeStr)!
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members.values()).find(member => member.uin === memberUinOrUidStr)
} else {
member = members.get(memberUinOrUidStr)
}
return member
}
let member = getMember()
if (!member) {
this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr))
members = this.groupMembers.get(groupCodeStr)!
member = getMember()
}
return member
}
async getGroupIgnoreNotifies() {
await this.getSingleScreenNotifies(14)
return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow,
[],
ReceiveCmdS.GROUP_NOTIFY,
)
}
static async getSingleScreenNotifies(num: number) {
if (NTEventDispatch.initialised) {
const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
(
'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1,
5000,
() => true,
false,
'',
num,
)
return notifies
} else {
return (await invoke<GroupNotifies>({
methodName: NTMethod.GET_GROUP_NOTICE,
async getSingleScreenNotifies(num: number) {
invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true })
return (await invoke<GroupNotifies>(
'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt: false, startSeq: '', number: num }, null],
{
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: num }, null],
})).notifies
}
)).notifies
}
/** 27187 TODO */
static async delGroupFile(groupCode: string, files: string[]) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)
}
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|')
const groupCode = flagitem[0]
const seq = flagitem[1]
@@ -132,10 +129,7 @@ export class NTQQGroupApi {
}
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_GROUP_REQUEST,
args: [
{
return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
doubt: false,
operateMsg: {
operateType,
@@ -146,142 +140,80 @@ export class NTQQGroupApi {
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
},
},
null,
],
})
}, null])
}
}
static async quitGroup(groupQQ: string) {
async quitGroup(groupCode: string) {
const session = getSession()
if (session) {
return session.getGroupService().quitGroup(groupQQ)
return session.getGroupService().quitGroup(groupCode)
} else {
return await invoke({
methodName: NTMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }, null])
}
}
static async kickMember(
groupQQ: string,
async kickMember(
groupCode: string,
kickUids: string[],
refuseForever = false,
kickReason = '',
) {
const session = getSession()
if (session) {
return session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
} else {
return await invoke({
methodName: NTMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
}
}
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
const session = getSession()
if (session) {
return session.getGroupService().setMemberShutUp(groupQQ, memList)
return session.getGroupService().setMemberShutUp(groupCode, memList)
} else {
return await invoke({
methodName: NTMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
}
}
static async banGroup(groupQQ: string, shutUp: boolean) {
async banGroup(groupCode: string, shutUp: boolean) {
const session = getSession()
if (session) {
return session.getGroupService().setGroupShutUp(groupQQ, shutUp)
return session.getGroupService().setGroupShutUp(groupCode, shutUp)
} else {
return await invoke({
methodName: NTMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }, null])
}
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
const session = getSession()
if (session) {
return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
return session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName)
} else {
return await invoke({
methodName: NTMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }, null])
}
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
const session = getSession()
if (session) {
return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
return session.getGroupService().modifyMemberRole(groupCode, memberUid, role)
} else {
return await invoke({
methodName: NTMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }, null])
}
}
static async setGroupName(groupQQ: string, groupName: string) {
async setGroupName(groupCode: string, groupName: string) {
const session = getSession()
if (session) {
return session.getGroupService().modifyGroupName(groupQQ, groupName, false)
return session.getGroupService().modifyGroupName(groupCode, groupName, false)
} else {
return await invoke({
methodName: NTMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }, null])
}
}
static async getGroupAtAllRemainCount(groupCode: string) {
async getGroupRemainAtTimes(groupCode: string) {
return await invoke<
GeneralCallResult & {
atInfo: {
@@ -292,44 +224,68 @@ export class NTQQGroupApi {
canNotAtAllMsg: ''
}
}
>({
methodName: NTMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [
{
groupCode,
},
null,
],
})
>(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null])
}
/** 27187 TODO */
static async removeGroupEssence(GroupCode: string, msgId: string) {
async removeGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
// 代码没测过
// 需要 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!)
const data = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const param = {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param)
}
/** 27187 TODO */
static async addGroupEssence(GroupCode: string, msgId: string) {
async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
// 代码没测过
// 需要 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!)
const data = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const param = {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
}
async createGroupFileFolder(groupId: string, folderName: string) {
return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }, null])
}
async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null])
}
async deleteGroupFile(groupId: string, fileIdList: string[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList: [102], fileIdList }, null])
}
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { classNameIsRegister: true })
const data = await invoke<{ fileInfo: OnGroupFileInfoUpdateParams }>(
'nodeIKernelRichMediaService/getGroupFileList',
[
{
groupId,
fileListForm
},
null,
],
{
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false,
cmdCB: (payload, result) => payload.fileInfo.reqId === result
}
)
return data.fileInfo.item
}
}

View File

@@ -1,9 +1,15 @@
import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { getSelfNick, getSelfUid } from '../../common/data'
import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
declare module 'cordis' {
interface Context {
ntMsgApi: NTQQMsgApi
}
}
function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000)
@@ -15,177 +21,92 @@ function generateMsgId() {
return msgId
}
export class NTQQMsgApi {
/** 27187 TODO */
static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession()
return session?.getMsgService().getTempChatInfo(chatType, peerUid)
export class NTQQMsgApi extends Service {
static inject = ['ntUserApi']
constructor(protected ctx: Context) {
super(ctx, 'ntMsgApi', true)
}
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession()
if (session) {
return session.getMsgService().getTempChatInfo(chatType, peerUid)
} else {
return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
}
}
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
const session = getSession()
const emojiType = emojiId.length > 3 ? '2' : '1'
if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiType, setEmoji)
} else {
return await invoke({
methodName: NTMethod.EMOJI_LIKE,
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
}
}
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession()
if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }, null])
}
}
static async activateChat(peer: Peer) {
return await invoke<GeneralCallResult>({
methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null],
})
async activateChat(peer: Peer) {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 20 }, null])
}
static async activateChatAndGetHistory(peer: Peer) {
return await invoke<GeneralCallResult>({
methodName: NTMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样
args: [{ peer, cnt: 20 }, null],
})
async activateChatAndGetHistory(peer: Peer) {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null])
}
static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null])
}
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession()
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsByMsgId',
args: [
{
peer,
msgIds,
},
null,
],
})
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
}
}
static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) {
const session = getSession()
// 消息时间从旧到新
if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.HISTORY_MSG,
args: [
{
peer,
msgId,
cnt: count,
queryOrder: isReverseOrder,
},
null,
],
})
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
}
}
static async recallMsg(peer: Peer, msgIds: string[]) {
async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession()
if (session) {
return session.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds)
return session.getMsgService().recallMsg(peer, msgIds)
} else {
return await invoke({
methodName: NTMethod.RECALL_MSG,
args: [
{
peer,
msgIds,
},
null,
],
})
return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
}
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
const msgId = generateMsgId()
peer.guildId = msgId
let msgList: RawMessage[]
if (NTEventDispatch.initialised) {
const data = await NTEventDispatch.CallNormalEvent<
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/sendMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
timeout,
(msgRecords: RawMessage[]) => {
for (const msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
'0',
peer,
msgElements,
new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/sendMsg',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
args: [
const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/sendMsg',
[
{
msgId: '0',
peer,
@@ -194,84 +115,47 @@ export class NTQQMsgApi {
},
null
],
timeout
})
msgList = data.msgList
}
const retMsg = msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
})
return retMsg!
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession()
if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else {
return await invoke<GeneralCallResult>({
methodName: NTMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
}
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await getSelfNick()
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
})
const selfUid = getSelfUid()
let msgList: RawMessage[]
if (NTEventDispatch.initialised) {
const data = await NTEventDispatch.CallNormalEvent<
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/multiForwardMsgWithComment',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
msgInfos,
srcPeer,
destPeer,
[],
new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/multiForwardMsgWithComment',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
args: [
timeout
}
)
return data.msgList.find(msgRecord => msgRecord.guildId === msgId)
}
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession()
if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else {
return await invoke(NTMethod.FORWARD_MSG, [{
msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
}, null])
}
}
async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await this.ctx.ntUserApi.getSelfNick(true)
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
})
const selfUid = selfInfo.uid
const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/multiForwardMsgWithComment',
[
{
msgInfos,
srcContact: srcPeer,
@@ -281,15 +165,25 @@ export class NTQQMsgApi {
},
null,
],
})
msgList = data.msgList
{
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
for (const msg of msgList) {
}
return false
},
}
)
for (const msg of data.msgList) {
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
continue
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
const forwardData = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
continue
}
@@ -300,61 +194,62 @@ export class NTQQMsgApi {
throw new Error('转发消息超时')
}
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
async getMsgsBySeqAndCount(peer: Peer, msgSeq: string, count: number, desc: boolean, z: boolean) {
const session = getSession()
if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)
return await session.getMsgService().getMsgsBySeqAndCount(peer, msgSeq, count, desc, z)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsBySeqAndCount',
args: [
{
return await invoke('nodeIKernelMsgService/getMsgsBySeqAndCount', [{
peer,
cnt: count,
msgSeq: seq,
msgSeq,
queryOrder: desc
},
null,
],
})
}, null])
}
}
/** 27187 TODO */
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
async getSingleMsg(peer: Peer, msgSeq: string) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
if (session) {
return await session.getMsgService().getSingleMsg(peer, msgSeq)
} else {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
}
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: count,
})
return ret
pageLimit: 1,
}
}, null])
}
static async getSingleMsg(peer: Peer, seq: string) {
const session = getSession()
if (session) {
return await session.getMsgService().getSingleMsg(peer, seq)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getSingleMsg',
args: [
{
peer,
msgSeq: seq,
},
null,
],
})
}
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[]) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
filterMsgToTime: filterMsgTime,
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
}

View File

@@ -1,56 +1,43 @@
import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { invoke } from '../ntcall'
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { friends, groupMembers, getSelfUin } from '@/common/data'
import { CacheClassFuncAsync, getBuildVersion } from '@/common/utils'
import { getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NTQQFriendApi } from './friend'
import { UserDetailSource, ProfileBizType } from '../services'
import { Time } from 'cosmokit'
import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
export class NTQQUserApi {
static async setQQAvatar(filePath: string) {
return await invoke<GeneralCallResult>({
methodName: NTMethod.SET_QQ_AVATAR,
args: [
{
path: filePath,
},
null,
],
timeout: 10 * Time.second, // 10秒不一定够
})
declare module 'cordis' {
interface Context {
ntUserApi: NTQQUserApi
}
}
static async fetchUserDetailInfo(uid: string) {
let info: UserDetailInfoListenerArg
if (NTEventDispatch.initialised) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
1,
5000,
(profile) => profile.uid === uid,
'BuddyProfileStore',
[uid],
UserDetailSource.KSERVER,
[ProfileBizType.KALL]
export class NTQQUserApi extends Service {
static inject = ['ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context) {
super(ctx, 'ntUserApi', true)
}
async setQQAvatar(path: string) {
return await invoke(
'nodeIKernelProfileService/setHeader',
[
{ path },
null,
],
{
timeout: 10 * Time.second, // 10秒不一定够
}
)
info = profile
} else {
const result = await invoke<{ info: UserDetailInfoListenerArg }>({
methodName: 'nodeIKernelProfileService/fetchUserDetailInfo',
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
args: [
}
async fetchUserDetailInfo(uid: string) {
const result = await invoke<{ info: UserDetailInfoListenerArg }>(
'nodeIKernelProfileService/fetchUserDetailInfo',
[
{
callFrom: 'BuddyProfileStore',
uid: [uid],
@@ -59,9 +46,13 @@ export class NTQQUserApi {
},
null
],
})
info = result.info
{
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
}
)
const { info } = result
const ret: User = {
...info.simpleInfo.coreInfo,
...info.simpleInfo.status,
@@ -74,67 +65,51 @@ export class NTQQUserApi {
return ret
}
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
async getUserDetailInfo(uid: string) {
if (getBuildVersion() >= 26702) {
return NTQQUserApi.fetchUserDetailInfo(uid)
return this.fetchUserDetailInfo(uid)
}
if (NTEventDispatch.initialised) {
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile) => profile.uid === uid,
uid,
[0]
)
return profile
} else {
const result = await invoke<{ info: User }>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid,
args: [
const result = await invoke<{ info: User }>(
'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
[
{
uid,
bizList: [0]
},
null,
],
})
{
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid,
}
)
return result.info
}
}
static async getSkey(): Promise<string> {
const clientKeyData = await NTQQUserApi.forceFetchClientKey()
async getSkey(): Promise<string> {
const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败')
}
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin()
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey
+ '&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
}
@CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) {
const clientKeyData = await NTQQUserApi.forceFetchClientKey()
async getCookies(domain: string) {
const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败')
}
const uin = getSelfUin()
const uin = selfInfo.uin
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
const cookies: { [key: string]: string } = await RequestUtil.HttpsGetCookies(requestUrl)
return cookies
}
static genBkn(sKey: string) {
genBkn(sKey: string) {
sKey = sKey || ''
let hash = 5381
@@ -146,59 +121,62 @@ export class NTQQUserApi {
return (hash & 0x7fffffff).toString()
}
/** 27187 TODO */
static async getPSkey(domains: string[]): Promise<Map<string, string>> {
async like(uid: string, count = 1) {
const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true)
if (res?.result !== 0) {
throw new Error(`获取Pskey失败: ${res?.errMsg}`)
}
return res.domainPskeyMap
}
/** 27187 TODO */
static async like(uid: string, count = 1) {
const session = getSession()
return session?.getProfileLikeService().setBuddyProfileLike({
if (session) {
return session.getProfileLikeService().setBuddyProfileLike({
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})
} else {
return await invoke(
'nodeIKernelProfileLikeService/setBuddyProfileLike',
[
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
)
}
}
static async getUidByUinV1(Uin: string) {
async getUidByUinV1(uin: string) {
const session = getSession()
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
// Uid 好友转
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) {
friends.forEach((t) => {
if (t.uin == Uin) {
uid = t.uid
}
})
}
//Uid 群友列表转
if (!uid) {
for (let groupMembersList of groupMembers.values()) {
for (let GroupMember of groupMembersList.values()) {
if (GroupMember.uin == Uin) {
uid = GroupMember.uid
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
for (const member of membersList.values()) {
if (member.uin === uin) {
uid = member.uid
break
}
}
if (uid) break
}
}
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf('*') == -1) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid
}
}
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid
}
static async getUidByUinV2(uin: string) {
async getUidByUinV2(uin: string) {
const session = getSession()
if (session) {
let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin)
@@ -208,96 +186,54 @@ export class NTQQUserApi {
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
if (uid) return uid
} else {
let uid = (await invoke<{ uids: Map<string, string> }>({
methodName: 'nodeIKernelGroupService/getUidByUins',
args: [
{ uin: [uin] },
null,
],
})).uids.get(uin)
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
if (uid) return uid
uid = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUidByUin',
args: [
{
callFrom: 'FriendsServiceImpl',
uin: [uin],
},
null,
],
})).get(uin)
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid
uid = (await invoke<{ uidInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUid',
args: [
{ uin: [uin] },
null,
],
})).uidInfo.get(uin)
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid
}
const unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
}
static async getUidByUin(Uin: string) {
async getUidByUin(uin: string) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUidByUinV2(Uin)
return this.getUidByUinV2(uin)
}
return await NTQQUserApi.getUidByUinV1(Uin)
return this.getUidByUinV1(uin)
}
static async getUserDetailInfoByUinV2(uin: string) {
if (NTEventDispatch.initialised) {
return await NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUinV2>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
uin
)
} else {
return await invoke<UserDetailInfoByUinV2>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoByUin',
args: [
async getUserDetailInfoByUinV2(uin: string) {
return await invoke<UserDetailInfoByUinV2>(
'nodeIKernelProfileService/getUserDetailInfoByUin',
[
{ uin },
null,
],
})
}
}
static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
}
static async getUinByUidV1(Uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin',
5000,
[Uid]
async getUserDetailInfoByUin(uin: string) {
return await invoke<UserDetailInfoByUin>(
'nodeIKernelProfileService/getUserDetailInfoByUin',
[
{ uin },
null,
],
)
let uin = ret.uinInfo.get(Uid)
if (!uin) {
//从Buddy缓存获取Uin
friends.forEach((t) => {
if (t.uid == Uid) {
uin = t.uin
}
})
}
async getUinByUidV1(uid: string) {
const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])
let uin = ret.uinInfo.get(uid)
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
}
return uin
}
static async getUinByUidV2(uid: string) {
async getUinByUidV2(uid: string) {
const session = getSession()
if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
@@ -306,51 +242,54 @@ export class NTQQUserApi {
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
return uin
} else {
let uin = (await invoke<{ uins: Map<string, string> }>({
methodName: 'nodeIKernelGroupService/getUinByUids',
args: [
{ uid: [uid] },
null,
],
})).uins.get(uid)
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUinByUid',
args: [
{
callFrom: 'FriendsServiceImpl',
uid: [uid],
},
null,
],
})).get(uid)
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke<{ uinInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUin',
args: [
{ uid: [uid] },
null,
],
})).uinInfo.get(uid)
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(uid)
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin
uin = (await NTQQUserApi.getUserDetailInfo(uid)).uin //从QQ Native 转换
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
return uin
}
static async getUinByUid(Uid: string) {
async getUinByUid(uid: string) {
if (getBuildVersion() >= 26702) {
return (await NTQQUserApi.getUinByUidV2(Uid))!
return this.getUinByUidV2(uid)
}
return await NTQQUserApi.getUinByUidV1(Uid)
return this.getUinByUidV1(uid)
}
/** 27187 TODO */
static async forceFetchClientKey() {
async forceFetchClientKey() {
const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ domain: '' }, null])
}
}
async getSelfNick(refresh = false) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const userInfo = await this.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
}
return selfInfo.nick
}
async setSelfStatus(status: number, extStatus: number, batteryStatus: number) {
return await invoke('nodeIKernelMsgService/setStatus', [{
statusReq: {
status,
extStatus,
batteryStatus,
}
}, null])
}
}

View File

@@ -1,7 +1,12 @@
import { getSelfUin } from '@/common/data'
import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request'
import { Service, Context } from 'cordis'
import { Dict } from 'cosmokit'
declare module 'cordis' {
interface Context {
ntWebApi: NTQQWebApi
}
}
export enum WebHonorType {
ALL = 'all',
@@ -36,7 +41,7 @@ interface WebApiGroupMemberRet {
em: string
cache: number
adm_num: number
levelname: any
levelname: unknown
mems: WebApiGroupMember[]
count: number
svr_time: number
@@ -45,56 +50,6 @@ interface WebApiGroupMemberRet {
extmode: number
}
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
}
type: number
fn: number
cn: number
vn: number
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
}
read_num: number
is_read: number
is_all_confirm: number
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
}
sta: number,
gln: number
tst: number,
ui: any
server_time: number
svrt: number
ad: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
@@ -105,7 +60,7 @@ interface GroupEssenceMsg {
add_digest_uin: string
add_digest_nick: string
add_digest_time: number
msg_content: any[]
msg_content: unknown[]
can_be_removed: true
}
@@ -120,34 +75,48 @@ export interface GroupEssenceMsgRet {
}
}
export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> {
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'
let ret: GroupEssenceMsgRet
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue })
} catch {
return undefined
}
//console.log(url, CookieValue)
if (ret.retcode !== 0) {
return undefined
}
return ret
interface SetGroupNoticeParams {
groupCode: string
content: string
pinned: number
type: number
isShowEditCard: number
tipWindowType: number
confirmRequired: number
picId: string
imgWidth?: number
imgHeight?: number
}
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
interface SetGroupNoticeRet {
ec: number
em: string
id: number
ltsm: number
new_fid: string
read_only: number
role: number
srv_code: number
}
export class NTQQWebApi extends Service {
static inject = ['ntUserApi']
constructor(protected ctx: Context) {
super(ctx, 'ntWebApi', true)
}
async getGroupMembers(groupCode: string): Promise<WebApiGroupMember[]> {
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = this.cookieToString(cookieObject)
const retList: Promise<WebApiGroupMemberRet>[] = []
const params = new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: WebApi.genBkn(cookieObject.skey)
gc: groupCode,
bkn: this.genBkn(cookieObject.skey)
})
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
@@ -178,155 +147,162 @@ export class WebApi {
return memberData
}
static async setGroupNotice(GroupCode: string, Content: string = '') {
//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}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']
const _Skey = await NTQQUserApi.getSkey()
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin()
let ret: any = undefined
//console.log(CookieValue)
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined
}
const Bkn = WebApi.genBkn(_Skey)
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn
try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue })
return ret
} catch (e) {
return undefined
}
}
static genBkn(sKey: string) {
sKey = sKey || '';
let hash = 5381;
genBkn(sKey: string) {
sKey = sKey || ''
let hash = 5381
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code
}
return (hash & 0x7FFFFFFF).toString();
return (hash & 0x7FFFFFFF).toString()
}
//实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = '';
let resJson;
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
const url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString()
let resJson
try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
const res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr })
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/)
if (match) {
resJson = JSON.parse(match[1].trim());
resJson = JSON.parse(match[1].trim())
}
if (Internal_type === 1) {
return resJson?.talkativeList;
return resJson?.talkativeList
} else {
return resJson?.actorList;
return resJson?.actorList
}
} catch (e) {
log('获取当前群荣耀失败', url, e);
this.ctx.logger.error('获取当前群荣耀失败', url, e)
}
return undefined;
return undefined
}
let HonorInfo: any = { group_id: groupCode };
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies;
const honorInfo: Dict = { group_id: groupCode }
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = this.cookieToString(cookieObject)
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 1);
const RetInternal = await getDataInternal(groupCode, 1)
if (!RetInternal) {
throw new Error('获取龙王信息失败');
throw new Error('获取龙王信息失败')
}
HonorInfo.current_talkative = {
honorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name,
day_count: 0,
description: RetInternal[0]?.desc
}
HonorInfo.talkative_list = [];
honorInfo.talkative_list = [];
for (const talkative_ele of RetInternal) {
HonorInfo.talkative_list.push({
honorInfo.talkative_list.push({
user_id: talkative_ele?.uin,
avatar: talkative_ele?.avatar,
description: talkative_ele?.desc,
day_count: 0,
nickname: talkative_ele?.name
});
})
}
} catch (e) {
log(e);
this.ctx.logger.error(e)
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 2);
const RetInternal = await getDataInternal(groupCode, 2)
if (!RetInternal) {
throw new Error('获取群聊之火失败');
throw new Error('获取群聊之火失败')
}
HonorInfo.performer_list = [];
honorInfo.performer_list = []
for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({
honorInfo.performer_list.push({
user_id: performer_ele?.uin,
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc
});
})
}
} catch (e) {
log(e);
this.ctx.logger.error(e)
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 3);
const RetInternal = await getDataInternal(groupCode, 3)
if (!RetInternal) {
throw new Error('获取群聊炽焰失败');
throw new Error('获取群聊炽焰失败')
}
HonorInfo.legend_list = [];
honorInfo.legend_list = []
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
honorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description
});
})
}
} catch (e) {
log('获取群聊炽焰失败', e);
this.ctx.logger.error('获取群聊炽焰失败', e)
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 6);
const RetInternal = await getDataInternal(groupCode, 6)
if (!RetInternal) {
throw new Error('获取快乐源泉失败');
throw new Error('获取快乐源泉失败')
}
HonorInfo.emotion_list = [];
honorInfo.emotion_list = []
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
honorInfo.emotion_list.push({
user_id: emotion_ele?.uin,
nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar,
desc: emotion_ele?.description
});
})
}
} catch (e) {
log('获取快乐源泉失败', e);
this.ctx.logger.error('获取快乐源泉失败', e)
}
}
//冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = [];
honorInfo.strong_newbie_list = []
}
return HonorInfo;
return honorInfo
}
async setGroupNotice(params: SetGroupNoticeParams): Promise<SetGroupNoticeRet> {
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const settings = JSON.stringify({
is_show_edit_card: params.isShowEditCard,
tip_window_type: params.tipWindowType,
confirm_required: params.confirmRequired
})
return await RequestUtil.HttpGetJson<SetGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.genBkn(cookieObject.skey),
qid: params.groupCode,
text: params.content,
pinned: params.pinned.toString(),
type: params.type.toString(),
settings: settings,
...(params.picId !== '' && {
pic: params.picId,
imgWidth: params.imgWidth?.toString(),
imgHeight: params.imgHeight?.toString(),
})
})}`,
'POST',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
)
}
private cookieToString(cookieObject: Dict) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
}
}

View File

@@ -2,42 +2,55 @@ import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { ReceiveCmd } from '../hook'
import { BrowserWindow } from 'electron'
import { Service, Context } from 'cordis'
declare module 'cordis' {
interface Context {
ntWindowApi: NTQQWindowApi
}
}
export interface NTQQWindow {
windowName: string
windowUrlHash: string
}
export class NTQQWindows {
static GroupHomeWorkWindow: NTQQWindow = {
export namespace NTQQWindows {
export const GroupHomeWorkWindow: NTQQWindow = {
windowName: 'GroupHomeWorkWindow',
windowUrlHash: '#/group-home-work',
}
static GroupNotifyFilterWindow: NTQQWindow = {
export const GroupNotifyFilterWindow: NTQQWindow = {
windowName: 'GroupNotifyFilterWindow',
windowUrlHash: '#/group-notify-filter',
}
static GroupEssenceWindow: NTQQWindow = {
export const GroupEssenceWindow: NTQQWindow = {
windowName: 'GroupEssenceWindow',
windowUrlHash: '#/group-essence',
}
}
export class NTQQWindowApi {
export class NTQQWindowApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntWindowApi', true)
}
// 打开窗口并获取对应的下发事件
static async openWindow<R = GeneralCallResult>(
async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow,
args: any[],
args: unknown[],
cbCmd: ReceiveCmd | undefined,
autoCloseSeconds: number = 2,
) {
const result = await invoke<R>({
const result = await invoke<R>(
NTMethod.OPEN_EXTRA_WINDOW,
[ntQQWindow.windowName, ...args],
{
className: NTClass.WINDOW_API,
methodName: NTMethod.OPEN_EXTRA_WINDOW,
cbCmd,
afterFirstCmd: false,
args: [ntQQWindow.windowName, ...args],
})
}
)
setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())

236
src/ntqqapi/core.ts Normal file
View File

@@ -0,0 +1,236 @@
import fs from 'node:fs'
import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { MessageUnique } from '../common/utils/messageUnique'
import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall'
import {
RawMessage,
GroupNotify,
FriendRequestNotify,
FriendRequest,
GroupMember,
CategoryFriend,
SimpleInfo,
User,
ChatType
} from './types'
import { selfInfo } from '../common/globalVars'
import { version } from '../version'
declare module 'cordis' {
interface Context {
app: Core
}
interface Events {
'nt/message-created': (input: RawMessage[]) => void
'nt/message-deleted': (input: RawMessage[]) => void
'nt/message-sent': (input: RawMessage[]) => void
'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string; members: GroupMember[] }) => void
}
}
class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true)
}
public start() {
llonebotError.otherError = ''
MessageUnique.init(selfInfo.uin)
this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => {
Object.assign(this.config, input)
})
}
private registerListener() {
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = []
if ('userSimpleInfos' in payload) {
friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
return {
...v.coreInfo,
}
})
} else {
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
this.ctx.logger.info('好友列表变动', friendList.length)
for (const friend of friendList) {
this.ctx.ntMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend })
}
})
// 自动清理新消息文件
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
if (!this.config.autoDeleteFile) {
return
}
for (const message of payload.msgList) {
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...(msgElement.picElement?.thumbPath ?? []).values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath = [...(msgElement.videoElement?.thumbPath ?? []).values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
this.ctx.logger.info('删除文件成功', path)
})
}
}
}, this.config.autoDeleteFileSecond! * 1000)
}
}
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
Object.assign(selfInfo, { online: info.info.status !== 20 })
})
const activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const contact of recentContact.changedList) {
if (activatedPeerUids.includes(contact.id)) continue
activatedPeerUids.push(contact.id)
const peer = { peerUid: contact.id, chatType: contact.chatType }
if (contact.chatType === ChatType.temp) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => {
this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
const lastTempMsg = msgList.at(-1)
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) {
this.ctx.parallel('nt/message-created', [lastTempMsg!])
}
})
})
}
else {
this.ctx.ntMsgApi.activateChat(peer)
}
}
}
})
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
this.ctx.logger.info('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) {
chatType = ChatType.temp
}
const peer = { peerUid, chatType }
await this.ctx.sleep(1000)
this.ctx.ntMsgApi.activateChat(peer).then((r) => {
this.ctx.logger.info('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
this.ctx.parallel('nt/group-member-info-updated', { groupCode, members })
})
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
this.ctx.parallel('nt/message-created', payload.msgList)
})
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
const list = payload.msgList.filter(v => {
if (recallMsgIds.includes(v.msgId)) {
return false
}
recallMsgIds.push(v.msgId)
return true
})
this.ctx.parallel('nt/message-deleted', list)
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
const { msgId, chatType, peerUid } = payload.msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
if (!this.config.reportSelfMessage) {
return
}
this.ctx.parallel('nt/message-sent', [payload.msgRecord])
})
const groupNotifyFlags: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
let notifies: GroupNotify[]
try {
notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
return
}
const list = notifies.filter(v => {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type
if (groupNotifyFlags.includes(flag)) {
return false
}
groupNotifyFlags.push(flag)
return true
})
this.ctx.parallel('nt/group-notify', list)
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
})
}
}
namespace Core {
export interface Config extends LLOBConfig {
}
}
export default Core

View File

@@ -1,3 +1,5 @@
import ffmpeg from 'fluent-ffmpeg'
import faceConfig from './helper/face_config.json'
import {
AtType,
ElementType,
@@ -13,24 +15,17 @@ import {
SendTextElement,
SendVideoElement,
} from './types'
import { promises as fs } from 'node:fs'
import ffmpeg from 'fluent-ffmpeg'
import { NTQQFileApi } from './api/file'
import { stat, writeFile, copyFile, unlink } from 'node:fs/promises'
import { calculateFileMD5, isGIF } from '../common/utils/file'
import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils'
import faceConfig from './helper/face_config.json'
import { Context } from 'cordis'
import { isNullable } from 'cosmokit'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName
//export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export class SendMsgElementConstructor {
static poke(groupCode: string, uin: string) {
return null
}
static text(content: string): SendTextElement {
export namespace SendElementEntities {
export function text(content: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: '',
@@ -44,7 +39,7 @@ export class SendMsgElementConstructor {
}
}
static at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
export function at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: '',
@@ -58,7 +53,7 @@ export class SendMsgElementConstructor {
}
}
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return {
elementType: ElementType.REPLY,
elementId: '',
@@ -71,8 +66,8 @@ export class SendMsgElementConstructor {
}
}
static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType)
export async function pic(ctx: Context, picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(picPath, ElementType.PIC, subType)
if (fileSize === 0) {
throw '文件异常大小为0'
}
@@ -80,7 +75,7 @@ export class SendMsgElementConstructor {
if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
}
const imageSize = await NTQQFileApi.getImageSize(picPath)
const imageSize = await ctx.ntFileApi.getImageSize(picPath)
const picElement = {
md5HexStr: md5,
fileSize: fileSize.toString(),
@@ -96,7 +91,7 @@ export class SendMsgElementConstructor {
thumbFileSize: 0,
summary,
}
log('图片信息', picElement)
ctx.logger.info('图片信息', picElement)
return {
elementType: ElementType.PIC,
elementId: '',
@@ -104,8 +99,8 @@ export class SendMsgElementConstructor {
}
}
static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
export async function file(ctx: Context, filePath: string, fileName = '', folderId = ''): Promise<SendFileElement> {
const { fileName: _fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) {
throw '文件异常,大小为 0'
}
@@ -122,16 +117,16 @@ export class SendMsgElementConstructor {
return element
}
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
try {
await fs.stat(filePath)
await stat(filePath)
} catch (e) {
throw `文件${filePath}异常,不存在`
}
log('复制视频到QQ目录', filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO)
ctx.logger.info('复制视频到QQ目录', filePath)
const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.VIDEO)
log('复制视频到QQ目录完成', path)
ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) {
throw '文件异常大小为0'
}
@@ -152,21 +147,21 @@ export class SendMsgElementConstructor {
filePath,
}
try {
videoInfo = await getVideoInfo(path)
log('视频信息', videoInfo)
videoInfo = await getVideoInfo(ctx, path)
ctx.logger.info('视频信息', videoInfo)
} catch (e) {
log('获取视频信息失败', e)
ctx.logger.info('获取视频信息失败', e)
}
const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumbDir, thumbFileName)
log('开始生成视频缩略图', filePath)
ctx.logger.info('开始生成视频缩略图', filePath)
let completed = false
function useDefaultThumb() {
if (completed) return
log('获取视频封面失败,使用默认封面')
fs.writeFile(thumbPath, defaultVideoThumb)
ctx.logger.info('获取视频封面失败,使用默认封面')
writeFile(thumbPath, defaultVideoThumb)
.then(() => {
resolve(thumbPath)
})
@@ -175,9 +170,9 @@ export class SendMsgElementConstructor {
setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath)
.on('error', (err) => {
.on('error', () => {
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath)
copyFile(diyThumbPath, thumbPath)
.then(() => {
completed = true
resolve(thumbPath)
@@ -194,19 +189,19 @@ export class SendMsgElementConstructor {
size: videoInfo.width + 'x' + videoInfo.height,
})
.on('end', () => {
log('生成视频缩略图', thumbPath)
ctx.logger.info('生成视频缩略图', thumbPath)
completed = true
resolve(thumbPath)
})
})
let thumbPath = new Map()
const thumbPath = new Map()
const _thumbPath = await createThumb
log('生成视频缩略图', _thumbPath)
const thumbSize = (await fs.stat(_thumbPath)).size
ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await stat(_thumbPath)).size
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath)
let element: SendVideoElement = {
const element: SendVideoElement = {
elementType: ElementType.VIDEO,
elementId: '',
videoElement: {
@@ -232,22 +227,22 @@ export class SendMsgElementConstructor {
// sourceVideoCodecFormat: 2
},
}
log('videoElement', element)
ctx.logger.info('videoElement', element)
return element
}
static async ptt(pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath)
export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
if (!silkPath) {
throw '语音转换失败, 请检查语音文件是否正常'
}
// log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT)
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.PTT)
if (fileSize === 0) {
throw '文件异常大小为0'
}
if (converted) {
fs.unlink(silkPath).then()
unlink(silkPath)
}
return {
elementType: ElementType.PTT,
@@ -271,7 +266,7 @@ export class SendMsgElementConstructor {
}
}
static face(faceId: number): SendFaceElement {
export function face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji
@@ -300,23 +295,24 @@ export class SendMsgElementConstructor {
}
}
static mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement {
export function mface(emojiPackageId: number, emojiId: string, key: string, summary?: string): SendMarketFaceElement {
return {
elementType: ElementType.MFACE,
marketFaceElement: {
imageWidth: 300,
imageHeight: 300,
emojiPackageId,
emojiId,
key,
faceName: faceName || mFaceCache.get(emojiId) || '[商城表情]',
faceName: summary || '[商城表情]',
},
}
}
static dice(resultId: number | null): SendFaceElement {
export function dice(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果
// 随机1到6
if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return {
elementType: ElementType.FACE,
elementId: '',
@@ -328,7 +324,7 @@ export class SendMsgElementConstructor {
stickerId: '33',
sourceType: 1,
stickerType: 2,
resultId: resultId?.toString(),
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
@@ -336,9 +332,9 @@ export class SendMsgElementConstructor {
}
// 猜拳(石头剪刀布)表情
static rps(resultId: number | null): SendFaceElement {
export function rps(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果
if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return {
elementType: ElementType.FACE,
elementId: '',
@@ -350,14 +346,14 @@ export class SendMsgElementConstructor {
stickerId: '34',
sourceType: 1,
stickerType: 2,
resultId: resultId?.toString(),
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
}
}
static ark(data: string): SendArkElement {
export function ark(data: string): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: '',

View File

@@ -1,4 +1,4 @@
import { log } from '@/common/utils'
import { Context } from "cordis"
interface ServerRkeyData {
group_rkey: string
@@ -6,15 +6,15 @@ interface ServerRkeyData {
expired_time: number
}
class RkeyManager {
serverUrl: string = ''
export class RkeyManager {
private serverUrl: string = ''
private rkeyData: ServerRkeyData = {
group_rkey: '',
private_rkey: '',
expired_time: 0
}
constructor(serverUrl: string) {
constructor(protected ctx: Context, serverUrl: string) {
this.serverUrl = serverUrl
}
@@ -23,7 +23,7 @@ class RkeyManager {
try {
await this.refreshRkey()
} catch (e) {
log('获取rkey失败', e)
this.ctx.logger.error('获取rkey失败', e)
}
}
return this.rkeyData
@@ -35,7 +35,7 @@ class RkeyManager {
return now > this.rkeyData.expired_time
}
async refreshRkey(): Promise<any> {
async refreshRkey() {
//刷新rkey
this.rkeyData = await this.fetchServerRkey()
}
@@ -58,5 +58,3 @@ class RkeyManager {
})
}
}
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey')

View File

@@ -1,34 +1,12 @@
import type { BrowserWindow } from 'electron'
import { NTClass, NTMethod } from './ntcall'
import { NTQQMsgApi } from './api/msg'
import {
CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo, User,
} from './types'
import {
friends,
getFriend,
getGroupMember,
setSelfInfo
} from '@/common/data'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil } from '@/common/config'
import fs from 'node:fs'
import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { Dict } from 'cosmokit'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export const hookApiCallbacks: Record<string, (res: any) => void> = {}
export let ReceiveCmdS = {
export const ReceiveCmdS = {
RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2',
UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate',
@@ -53,7 +31,7 @@ export let ReceiveCmdS = {
export type ReceiveCmd = string
interface NTQQApiReturnData<Payload = unknown> extends Array<any> {
interface NTQQApiReturnData extends Array<unknown> {
0: {
type: 'request'
eventName: NTClass
@@ -62,7 +40,7 @@ interface NTQQApiReturnData<Payload = unknown> extends Array<any> {
1: {
cmdName: ReceiveCmd
cmdType: 'event'
payload: Payload
payload: unknown
}[]
}
@@ -79,7 +57,7 @@ const callHooks: Array<{
hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = []
export function hookNTQQApiReceive(window: BrowserWindow) {
export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
const originalSend = window.webContents.send
const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
try {
@@ -88,17 +66,18 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
log(`received ntqq api message: ${channel}`, args)
}
} catch { }
if (!onlyLog) {
if (args?.[1] instanceof Array) {
for (const receiveData of args?.[1]) {
for (const receiveData of args[1]) {
const ntQQApiMethodName = receiveData.cmdName
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (const hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
new Promise(resolve => {
try {
hook.hookFunc(receiveData.payload)
} catch (e: any) {
log('hook error', ntQQApiMethodName, e.stack.toString())
} catch (e) {
log('hook error', ntQQApiMethodName, (e as Error).stack?.toString())
}
resolve(undefined)
}).then()
@@ -110,22 +89,22 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
new Promise(resolve => {
hookApiCallbacks[callbackId](args[1])
resolve(undefined)
}).then()
delete hookApiCallbacks[callbackId]
}
}
}
originalSend.call(window.webContents, channel, ...args)
}
window.webContents.send = patchSend
}
export function hookNTQQApiCall(window: BrowserWindow) {
export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
// 监听调用NTQQApi
let webContents = window.webContents as any
const webContents = window.webContents as Dict
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
@@ -139,16 +118,17 @@ export function hookNTQQApiCall(window: BrowserWindow) {
try {
logHook && log('call NTQQ api', thisArg, args)
} catch (e) { }
if (!onlyLog) {
try {
const _args: unknown[] = args[3][1]
const cmdName: NTMethod = _args[0] as NTMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => {
new Promise(resolve => {
try {
hook.hookFunc(callParams)
} catch (e: any) {
} catch (e) {
log('hook call error', e, _args)
}
resolve(undefined)
@@ -157,6 +137,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
})
} catch (e) { }
}
}
return target.apply(thisArg, args)
},
})
@@ -169,14 +150,13 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
// console.log(args);
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs)
},
})
let ret = target.apply(thisArg, args)
const ret = target.apply(thisArg, args)
/*try {
HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { }*/
@@ -223,299 +203,3 @@ export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex((h) => h.id === id)
receiveHooks.splice(index, 1)
}
//let activatedGroups: string[] = []
/*async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) {
log('update group', group.groupCode)
if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode)
continue
}
//log('update group', group)
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log)
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
Object.assign(existGroup, group)
} else {
groups.push(group)
existGroup = group
}
if (needUpdate) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
if (members) {
existGroup.members = Array.from(members.values())
}
}
}
}*/
/*async function processGroupEvent(payload: { groupList: Group[] }) {
try {
const newGroupList = payload.groupList
for (const group of newGroupList) {
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
if (existGroup.memberCount > group.memberCount) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`)
const oldMembers = existGroup.members
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = Array.from(newMembers.values())
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度
for (const member of newMembers) {
newMembersSet.add(member[1].uin)
}
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
const selfUin = getSelfUin()
const bot = await getGroupMember(group.groupCode, selfUin)
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfUin) {
postOb11Event(
new OB11GroupDecreaseEvent(
parseInt(group.groupCode),
parseInt(member.uin),
parseInt(member.uin),
'leave',
),
)
break
}
}
}
if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode)
}
}
}
updateGroups(newGroupList, false).then()
} catch (e: any) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
}*/
export async function startHook() {
// 群列表变动
/*registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动, store", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})*/
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
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)
}
}
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
// 好友列表变动
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
// log("onBuddyListChange", payload)
// let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = [];
if ((payload as any).userSimpleInfos) {
// friendListV2 = payload as any
friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
return {
...v.coreInfo,
}
})
}
else {
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
log('好友列表变动', friendList.length)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
}
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const { msgId, chatType, peerUid } = msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
setSelfInfo({
online: info.info.status !== 20
})
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) {
OB11Constructor.message(lastTempMsg!).then((r) => postOb11Event(r))
}
})
})
}
else {
NTQQMsgApi.activateChat(peer).then()
}
}
}
})
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}

View File

@@ -1,6 +1,6 @@
import { Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/ntqqapi/types'
interface IGroupListener {
export interface IGroupListener {
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): void
onGroupExtListUpdate(...args: unknown[]): void
@@ -52,189 +52,7 @@ interface IGroupListener {
onJoinGroupNoVerifyFlag(...args: unknown[]): void
onGroupArkInviteStateResult(...args: unknown[]): void
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void
}
export interface NodeIKernelGroupListener extends IGroupListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IGroupListener): NodeIKernelGroupListener
}
export class GroupListener implements IGroupListener {
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void {
}
onGetGroupBulletinListResult(...args: unknown[]) {
}
onGroupAllInfoChange(...args: unknown[]) {
}
onGroupBulletinChange(...args: unknown[]) {
}
onGroupBulletinRemindNotify(...args: unknown[]) {
}
onGroupArkInviteStateResult(...args: unknown[]) {
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
}
onGroupConfMemberChange(...args: unknown[]) {
}
onGroupDetailInfoChange(...args: unknown[]) {
}
onGroupExtListUpdate(...args: unknown[]) {
}
onGroupFirstBulletinNotify(...args: unknown[]) {
}
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
}
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
}
onGroupsMsgMaskResult(...args: unknown[]) {
}
onGroupStatisticInfoChange(...args: unknown[]) {
}
onJoinGroupNotify(...args: unknown[]) {
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
}
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean,
hasRobot: boolean
}) {
}
onSearchMemberChange(...args: unknown[]) {
}
onShutUpMemberListChanged(...args: unknown[]) {
}
}
export class DebugGroupListener implements IGroupListener {
onGroupMemberLevelInfoChange(...args: unknown[]): void {
console.log('onGroupMemberLevelInfoChange:', ...args)
}
onGetGroupBulletinListResult(...args: unknown[]) {
console.log('onGetGroupBulletinListResult:', ...args)
}
onGroupAllInfoChange(...args: unknown[]) {
console.log('onGroupAllInfoChange:', ...args)
}
onGroupBulletinChange(...args: unknown[]) {
console.log('onGroupBulletinChange:', ...args)
}
onGroupBulletinRemindNotify(...args: unknown[]) {
console.log('onGroupBulletinRemindNotify:', ...args)
}
onGroupArkInviteStateResult(...args: unknown[]) {
console.log('onGroupArkInviteStateResult:', ...args)
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
console.log('onGroupBulletinRichMediaDownloadComplete:', ...args)
}
onGroupConfMemberChange(...args: unknown[]) {
console.log('onGroupConfMemberChange:', ...args)
}
onGroupDetailInfoChange(...args: unknown[]) {
console.log('onGroupDetailInfoChange:', ...args)
}
onGroupExtListUpdate(...args: unknown[]) {
console.log('onGroupExtListUpdate:', ...args)
}
onGroupFirstBulletinNotify(...args: unknown[]) {
console.log('onGroupFirstBulletinNotify:', ...args)
}
onGroupListUpdate(...args: unknown[]) {
console.log('onGroupListUpdate:', ...args)
}
onGroupNotifiesUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUpdated:', ...args)
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
console.log('onGroupBulletinRichMediaProgressUpdate:', ...args)
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUnreadCountUpdated:', ...args)
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
console.log('onGroupSingleScreenNotifies:')
}
onGroupsMsgMaskResult(...args: unknown[]) {
console.log('onGroupsMsgMaskResult:', ...args)
}
onGroupStatisticInfoChange(...args: unknown[]) {
console.log('onGroupStatisticInfoChange:', ...args)
}
onJoinGroupNotify(...args: unknown[]) {
console.log('onJoinGroupNotify:', ...args)
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
console.log('onJoinGroupNoVerifyFlag:', ...args)
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
console.log('onMemberInfoChange:', groupCode, changeType, members)
}
onMemberListChange(...args: unknown[]) {
console.log('onMemberListChange:', ...args)
}
onSearchMemberChange(...args: unknown[]) {
console.log('onSearchMemberChange:', ...args)
}
onShutUpMemberListChanged(...args: unknown[]) {
console.log('onShutUpMemberListChanged:', ...args)
}
}

View File

@@ -23,15 +23,55 @@ export interface OnRichMediaDownloadCompleteParams {
userUsedSpacePerDay: unknown | null
}
export interface onGroupFileInfoUpdateParamType {
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: Array<any>
allFileCount: string
nextIndex: string
reqId: string
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
}
// {
@@ -82,7 +122,7 @@ export interface IKernelMsgListener {
onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType): void
onGroupFileInfoUpdate(groupFileListResult: OnGroupFileInfoUpdateParams): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
@@ -225,290 +265,4 @@ export interface IKernelMsgListener {
// 第一次发现于Win 9.9.9 23159
onBroadcastHelperProgerssUpdate(...args: unknown[]): void
}
export interface NodeIKernelMsgListener extends IKernelMsgListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelMsgListener): NodeIKernelMsgListener
}
export class MsgListener implements IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage) {
}
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
}
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
}
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
}
onContactUnreadCntUpdate(hashMap: unknown) {
}
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
}
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
}
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
}
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
}
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFileMsgCome(arrayList: unknown) {
}
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFirstViewGroupGuildMapping(arrayList: unknown) {
}
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
}
onGroupFileInfoAdd(groupItem: unknown) {
}
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType) {
}
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
}
onGroupTransferInfoAdd(groupItem: unknown) {
}
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
}
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
}
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
}
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
}
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
}
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
}
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
}
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
}
onInputStatusPush(inputStatusInfo: unknown) {
}
onKickedOffLine(kickedInfo: unknown) {
}
onLineDev(arrayList: unknown) {
}
onLogLevelChanged(j2: unknown) {
}
onMsgAbstractUpdate(arrayList: unknown) {
}
onMsgBoxChanged(arrayList: unknown) {
}
onMsgDelete(contact: unknown, arrayList: unknown) {
}
onMsgEventListUpdate(hashMap: unknown) {
}
onMsgInfoListAdd(arrayList: unknown) {
}
onMsgInfoListUpdate(msgList: RawMessage[]) {
}
onMsgQRCodeStatusChanged(i2: unknown) {
}
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
}
onMsgSecurityNotify(msgRecord: unknown) {
}
onMsgSettingUpdate(msgSetting: unknown) {
}
onNtFirstViewMsgSyncEnd() {
}
onNtMsgSyncEnd() {
}
onNtMsgSyncStart() {
}
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onRecvGroupGuildFlag(i2: unknown) {
}
onRecvMsg(arrayList: RawMessage[]) {
}
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
}
onRecvOnlineFileMsg(arrayList: unknown) {
}
onRecvS2CMsg(arrayList: unknown) {
}
onRecvSysMsg(arrayList: unknown) {
}
onRecvUDCFlag(i2: unknown) {
}
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
}
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
}
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
}
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
}
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
}
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
}
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
}
onUnreadCntAfterFirstView(hashMap: unknown) {
}
onUnreadCntUpdate(hashMap: unknown) {
}
onUserChannelTabStatusChanged(z: unknown) {
}
onUserOnlineStatusChanged(z: unknown) {
}
onUserTabStatusChanged(arrayList: unknown) {
}
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]) {
}
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
}
onRedTouchChanged(...args: unknown[]) {
}
// 第一次发现于Win 9.9.9-23159
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
}
}

View File

@@ -1,6 +1,6 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
interface IProfileListener {
export interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void
@@ -13,32 +13,3 @@ interface IProfileListener {
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

@@ -1,8 +1,21 @@
import { ipcMain } from 'electron'
import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log'
import { log } from '../common/utils/legacyLog'
import { randomUUID } from 'node:crypto'
import { GeneralCallResult } from './services'
import {
GeneralCallResult,
NodeIKernelBuddyService,
NodeIKernelProfileService,
NodeIKernelGroupService,
NodeIKernelProfileLikeService,
NodeIKernelMsgService,
NodeIKernelMSFService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelSearchService,
} from './services'
export enum NTClass {
NT_API = 'ns-ntApi',
@@ -19,121 +32,113 @@ export enum NTClass {
}
export enum NTMethod {
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
FILE_TYPE = 'getFileType',
FILE_MD5 = 'getFileMd5',
FILE_COPY = 'copyFile',
IMAGE_SIZE = 'getImageSizeFromPath',
FILE_SIZE = 'getFileSize',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
SEND_MSG = 'nodeIKernelMsgService/sendMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp',
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName',
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole',
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
ACTIVATE_MEMBER_LIST_CHANGE = 'nodeIKernelGroupListener/onMemberListChange',
ACTIVATE_MEMBER_INFO_CHANGE = 'nodeIKernelGroupListener/onMemberInfoChange',
GET_MSG_BOX_INFO = 'nodeIKernelMsgService/getABatchOfContactMsgBoxInfo',
GET_GROUP_ALL_INFO = 'nodeIKernelGroupService/getGroupAllInfo',
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
}
export enum NTChannel {
IPC_UP_1 = 'IPC_UP_1',
IPC_UP_2 = 'IPC_UP_2',
IPC_UP_3 = 'IPC_UP_3',
IPC_UP_1 = 'IPC_UP_1',
IPC_UP_4 = 'IPC_UP_4'
}
interface InvokeParams<ReturnType> {
methodName: string
interface NTService {
nodeIKernelBuddyService: NodeIKernelBuddyService
nodeIKernelProfileService: NodeIKernelProfileService
nodeIKernelGroupService: NodeIKernelGroupService
nodeIKernelProfileLikeService: NodeIKernelProfileLikeService
nodeIKernelMsgService: NodeIKernelMsgService
nodeIKernelMSFService: NodeIKernelMSFService
nodeIKernelUixConvertService: NodeIKernelUixConvertService
nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelSearchService: NodeIKernelSearchService
}
interface InvokeOptions<ReturnType> {
className?: NTClass
channel?: NTChannel
classNameIsRegister?: boolean
args?: unknown[]
cbCmd?: string | string[]
cmdCB?: (payload: ReturnType) => boolean
cmdCB?: (payload: ReturnType, result: unknown) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeout?: number
}
export function invoke<ReturnType>(params: InvokeParams<ReturnType>) {
const className = params.className ?? NTClass.NT_API
const channel = params.channel ?? NTChannel.IPC_UP_2
const timeout = params.timeout ?? 5000
const afterFirstCmd = params.afterFirstCmd ?? true
const uuid = randomUUID()
export function invoke<
R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>,
S extends keyof NTService = any,
M extends keyof NTService[S] & string = any
>(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const className = options.className ?? NTClass.NT_API
const channel = options.channel ?? NTChannel.IPC_UP_2
const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true
let eventName = className + '-' + channel[channel.length - 1]
if (params.classNameIsRegister) {
if (options.classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [params.methodName, ...(params.args ?? [])]
//log('callNTQQApi', channel, eventName, apiArgs, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => {
return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args]
const callbackId = randomUUID()
let success = false
if (!params.cbCmd) {
if (!options.cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => {
hookApiCallbacks[callbackId] = res => {
success = true
resolve(r)
resolve(res)
}
}
else {
let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(params.cbCmd!, (payload) => {
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!params.cmdCB) {
if (params.cmdCB(payload)) {
if (options.cmdCB) {
if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId)
success = true
resolve(payload)
@@ -147,21 +152,21 @@ export function invoke<ReturnType>(params: InvokeParams<ReturnType>) {
})
}
!afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
if (result?.result === 0 || result === undefined) {
log(`${params.methodName} callback`, result)
hookApiCallbacks[callbackId] = (res: GeneralCallResult) => {
result = res
if (res?.result === 0 || ['undefined', 'number'].includes(typeof res)) {
afterFirstCmd && secondCallback()
}
else {
log('ntqq api call failed', result)
reject(`ntqq api call failed, ${result.errMsg}`)
log('ntqq api call failed,', method, res)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`)
}
}
}
setTimeout(() => {
if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}, ${apiArgs}`)
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}
}, timeout)
@@ -169,11 +174,11 @@ export function invoke<ReturnType>(params: InvokeParams<ReturnType>) {
channel,
{
sender: {
send: (..._args: unknown[]) => {
send: () => {
},
},
},
{ type: 'request', callbackId: uuid, eventName },
{ type: 'request', callbackId, eventName },
apiArgs,
)
})

View File

@@ -29,7 +29,7 @@ export interface NodeIKernelBuddyService {
buddyUids: Array<string>//Uids
}>>
addKernelBuddyListener(listener: any): number
addKernelBuddyListener(listener: unknown): number
getAllBuddyCount(): number

View File

@@ -1,14 +1,12 @@
import { NodeIKernelGroupListener } from '@/ntqqapi/listeners'
import {
GroupExtParam,
GroupMember,
GroupMemberRole,
GroupNotifyTypes,
GroupNotifyType,
GroupRequestOperateTypes,
} from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
import { Dict } from 'cosmokit'
export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: {
@@ -29,8 +27,10 @@ export interface NodeIKernelGroupService {
onlineFlag: string,
realSpecialTitleFlag: number
}): Promise<unknown>
//26702
getGroupMemberLevelInfo(groupCode: string): Promise<unknown>
//26702
getGroupHonorList(groupCodes: Array<string>): unknown
@@ -45,6 +45,7 @@ export interface NodeIKernelGroupService {
errMsg: string,
uids: Map<string, string>
}>
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
@@ -70,12 +71,16 @@ export interface NodeIKernelGroupService {
brief: string
}
}): Promise<unknown>
//26702(其实更早 但是我不知道)
isEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
queryCachedEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
fetchGroupEssenceList(Req: { groupCode: string, pageStart: number, pageLimit: number }, Arg: unknown): Promise<unknown>
//26702
getAllMemberList(groupCode: string, forceFetch: boolean): Promise<{
errCode: number,
@@ -85,7 +90,7 @@ export interface NodeIKernelGroupService {
uid: string,
index: number//0
}>,
infos: {},
infos: Dict,
finish: true,
hasRobot: false
}
@@ -93,7 +98,7 @@ export interface NodeIKernelGroupService {
setHeader(uid: string, path: string): unknown
addKernelGroupListener(listener: NodeIKernelGroupListener): number
addKernelGroupListener(listener: unknown): number
removeKernelGroupListener(listenerId: unknown): void
@@ -171,7 +176,7 @@ export interface NodeIKernelGroupService {
clearGroupNotifies(groupCode: string): void
getGroupNotifiesUnreadCount(unknown: Boolean): Promise<GeneralCallResult>
getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>
clearGroupNotifiesUnreadCount(groupCode: string): void
@@ -181,7 +186,7 @@ export interface NodeIKernelGroupService {
operateType: GroupRequestOperateTypes, // 2 拒绝
targetMsg: {
seq: string, // 通知序列号
type: GroupNotifyTypes,
type: GroupNotifyType,
groupCode: string,
postscript: string
}
@@ -193,7 +198,7 @@ export interface NodeIKernelGroupService {
deleteGroupBulletin(groupCode: string, seq: string): void
publishGroupBulletin(groupCode: string, pskey: string, data: any): Promise<GeneralCallResult>
publishGroupBulletin(groupCode: string, pskey: string, data: unknown): Promise<GeneralCallResult>
publishInstructionForNewcomers(groupCode: string, arg: unknown): void

View File

@@ -1,6 +1,6 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { NodeIKernelMsgListener } from '@/ntqqapi/listeners/NodeIKernelMsgListener'
import { GeneralCallResult } from './common'
import { Dict } from 'cosmokit'
export interface QueryMsgsParams {
chatInfo: Peer,
@@ -29,16 +29,15 @@ export interface TmpChatInfo {
}
export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number
addKernelMsgListener(nodeIKernelMsgListener: unknown): number
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<GeneralCallResult>
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<unknown, unknown>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
addKernelMsgImportToolListener(arg: Object): unknown
addKernelMsgImportToolListener(arg: Dict): unknown
removeKernelMsgListener(args: unknown): unknown
@@ -52,7 +51,7 @@ export interface NodeIKernelMsgService {
getOnLineDev(): void
kickOffLine(DevInfo: Object): unknown
kickOffLine(DevInfo: Dict): unknown
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
@@ -81,11 +80,11 @@ export interface NodeIKernelMsgService {
// this.voipToken = bArr2
// this.profileId = str
setToken(arg: Object): unknown
setToken(arg: Dict): unknown
switchForeGround(): unknown
switchBackGround(arg: Object): unknown
switchBackGround(arg: Dict): unknown
//hex
setTokenForMqq(token: string): unknown
@@ -110,13 +109,12 @@ export interface NodeIKernelMsgService {
resendMsg(...args: unknown[]): unknown
recallMsg(...args: unknown[]): unknown
reeditRecallMsg(...args: unknown[]): unknown
//调用请检查除开commentElements其余参数不能为null
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsgWithComment(...args: unknown[]): unknown
forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
forwardSubMsgWithComment(...args: unknown[]): unknown
@@ -144,9 +142,9 @@ export interface NodeIKernelMsgService {
addLocalTofuRecordMsg(...args: unknown[]): unknown
addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<any> | number, front: boolean): Promise<unknown>
addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<unknown> | number, front: boolean): Promise<unknown>
deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<any>
deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<unknown>
updateElementExtBufForUI(...args: unknown[]): unknown
@@ -170,9 +168,10 @@ export interface NodeIKernelMsgService {
getLastMessageList(peer: Peer[]): Promise<unknown>
getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown
getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
//deprecated 从9.9.15-26702版本开始该接口已经废弃请使用getMsgsEx
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
@@ -371,8 +370,9 @@ export interface NodeIKernelMsgService {
getFileThumbSavePathForSend(...args: unknown[]): unknown
getFileThumbSavePath(...args: unknown[]): unknown
//猜测居多
translatePtt2Text(MsgId: string, Peer: {}, MsgElement: {}): unknown
translatePtt2Text(MsgId: string, Peer: Dict, MsgElement: Dict): unknown
setPttPlayedState(...args: unknown[]): unknown
// NodeIQQNTWrapperSession fetchFavEmojiList [
@@ -449,7 +449,7 @@ export interface NodeIKernelMsgService {
getEmojiResourcePath(...args: unknown[]): unknown
JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: any/*joinDragonGroupEmojiReq*/): unknown
JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: unknown): unknown
getMsgAbstracts(...args: unknown[]): unknown
@@ -624,7 +624,6 @@ export interface NodeIKernelMsgService {
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>
//chattype,uid->Promise<any>
getTempChatInfo(ChatType: number, Uid: string): Promise<TmpChatInfoApi>
setContactLocalTop(...args: unknown[]): unknown
@@ -655,7 +654,7 @@ export interface NodeIKernelMsgService {
recordEmoji(...args: unknown[]): unknown
fetchGetHitEmotionsByWord(args: Object): Promise<unknown>//表情推荐?
fetchGetHitEmotionsByWord(args: Dict): Promise<unknown>//表情推荐?
deleteAllRoamMsgs(...args: unknown[]): unknown//漫游消息?

View File

@@ -9,10 +9,10 @@ export interface NodeIKernelProfileLikeService {
setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
'info': {
'userLikeInfos': Array<any>,
'friendMaxVotes': number,
'start': number
info: {
userLikeInfos: Array<unknown>,
friendMaxVotes: number,
start: number
}
}>

View File

@@ -33,7 +33,7 @@ export interface NodeIKernelProfileService {
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
addKernelProfileListener(listener: any): number
addKernelProfileListener(listener: unknown): number
removeKernelProfileListener(listenerId: number): void
@@ -64,7 +64,7 @@ export interface NodeIKernelProfileService {
modifySelfProfile(...args: unknown[]): Promise<unknown>
modifyDesktopMiniProfile(param: any): Promise<GeneralCallResult>
modifyDesktopMiniProfile(param: unknown): Promise<GeneralCallResult>
setNickName(NickName: string): Promise<unknown>
@@ -74,7 +74,7 @@ export interface NodeIKernelProfileService {
setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown>
setHeader(arg: string): Promise<GeneralCallResult>
setRecommendImgFlag(...args: unknown[]): Promise<unknown>
@@ -82,9 +82,9 @@ export interface NodeIKernelProfileService {
getUserDetailInfo(uid: string): Promise<unknown>
getUserDetailInfoWithBizInfo(uid: string, Biz: any[]): Promise<GeneralCallResult>
getUserDetailInfoWithBizInfo(uid: string, Biz: unknown[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<any>
getUserDetailInfoByUin(uin: string): Promise<unknown>
getZplanAvatarInfos(args: string[]): Promise<unknown>
@@ -99,7 +99,7 @@ export interface NodeIKernelProfileService {
getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown>
//profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList)
getCoreInfo(name: string, arg: any[]): unknown
getCoreInfo(name: string, arg: unknown[]): unknown
//m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>())
isNull(): boolean

View File

@@ -169,18 +169,14 @@ export interface NodeIKernelRichMediaService {
downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: unknown, groupItem: Array<unknown> } }>
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown
createGroupFolder(arg1: unknown, arg2: unknown): unknown
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown
@@ -226,9 +222,9 @@ export interface NodeIKernelRichMediaService {
deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
transGroupFileResult: {
result: any
successFileIdList: Array<any>
failFileIdList: Array<any>
result: unknown
successFileIdList: Array<unknown>
failFileIdList: Array<unknown>
}
}>

View File

@@ -1,75 +1,75 @@
import { ChatType } from '../types'
export interface NodeIKernelSearchService {
addKernelSearchListener(...args: any[]): unknown// needs 1 arguments
addKernelSearchListener(...args: unknown[]): unknown// needs 1 arguments
removeKernelSearchListener(...args: any[]): unknown// needs 1 arguments
removeKernelSearchListener(...args: unknown[]): unknown// needs 1 arguments
searchStranger(...args: any[]): unknown// needs 3 arguments
searchStranger(...args: unknown[]): unknown// needs 3 arguments
searchGroup(...args: any[]): unknown// needs 1 arguments
searchGroup(...args: unknown[]): unknown// needs 1 arguments
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown
cancelSearchLocalInfo(...args: any[]): unknown// needs 3 arguments
cancelSearchLocalInfo(...args: unknown[]): unknown// needs 3 arguments
searchBuddyChatInfo(...args: any[]): unknown// needs 2 arguments
searchBuddyChatInfo(...args: unknown[]): unknown// needs 2 arguments
searchMoreBuddyChatInfo(...args: any[]): unknown// needs 1 arguments
searchMoreBuddyChatInfo(...args: unknown[]): unknown// needs 1 arguments
cancelSearchBuddyChatInfo(...args: any[]): unknown// needs 3 arguments
cancelSearchBuddyChatInfo(...args: unknown[]): unknown// needs 3 arguments
searchContact(...args: any[]): unknown// needs 2 arguments
searchContact(...args: unknown[]): unknown// needs 2 arguments
searchMoreContact(...args: any[]): unknown// needs 1 arguments
searchMoreContact(...args: unknown[]): unknown// needs 1 arguments
cancelSearchContact(...args: any[]): unknown// needs 3 arguments
cancelSearchContact(...args: unknown[]): unknown// needs 3 arguments
searchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
searchGroupChatInfo(...args: unknown[]): unknown// needs 3 arguments
resetSearchGroupChatInfoSortType(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoSortType(...args: unknown[]): unknown// needs 3 arguments
resetSearchGroupChatInfoFilterMembers(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoFilterMembers(...args: unknown[]): unknown// needs 3 arguments
searchMoreGroupChatInfo(...args: any[]): unknown// needs 1 arguments
searchMoreGroupChatInfo(...args: unknown[]): unknown// needs 1 arguments
cancelSearchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
cancelSearchGroupChatInfo(...args: unknown[]): unknown// needs 3 arguments
searchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchChatsWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchMoreChatsWithKeywords(...args: any[]): unknown// needs 1 arguments
searchMoreChatsWithKeywords(...args: unknown[]): unknown// needs 1 arguments
cancelSearchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
cancelSearchChatsWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchChatMsgs(...args: any[]): unknown// needs 2 arguments
searchChatMsgs(...args: unknown[]): unknown// needs 2 arguments
searchMoreChatMsgs(...args: any[]): unknown// needs 1 arguments
searchMoreChatMsgs(...args: unknown[]): unknown// needs 1 arguments
cancelSearchChatMsgs(...args: any[]): unknown// needs 3 arguments
cancelSearchChatMsgs(...args: unknown[]): unknown// needs 3 arguments
searchMsgWithKeywords(...args: any[]): unknown// needs 2 arguments
searchMsgWithKeywords(...args: unknown[]): unknown// needs 2 arguments
searchMoreMsgWithKeywords(...args: any[]): unknown// needs 1 arguments
searchMoreMsgWithKeywords(...args: unknown[]): unknown// needs 1 arguments
cancelSearchMsgWithKeywords(...args: any[]): unknown// needs 3 arguments
cancelSearchMsgWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<string>// needs 2 arguments
searchMoreFileWithKeywords(...args: any[]): unknown// needs 1 arguments
searchMoreFileWithKeywords(...args: unknown[]): unknown// needs 1 arguments
cancelSearchFileWithKeywords(...args: any[]): unknown// needs 3 arguments
cancelSearchFileWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchAtMeChats(...args: unknown[]): unknown// needs 3 arguments
searchMoreAtMeChats(...args: any[]): unknown// needs 1 arguments
searchMoreAtMeChats(...args: unknown[]): unknown// needs 1 arguments
cancelSearchAtMeChats(...args: any[]): unknown// needs 3 arguments
cancelSearchAtMeChats(...args: unknown[]): unknown// needs 3 arguments
searchChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
searchChatAtMeMsgs(...args: unknown[]): unknown// needs 1 arguments
searchMoreChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
searchMoreChatAtMeMsgs(...args: unknown[]): unknown// needs 1 arguments
cancelSearchChatAtMeMsgs(...args: any[]): unknown// needs 3 arguments
cancelSearchChatAtMeMsgs(...args: unknown[]): unknown// needs 3 arguments
addSearchHistory(param: {
type: number,//4
@@ -120,9 +120,9 @@ export interface NodeIKernelSearchService {
id?: number
}>
removeSearchHistory(...args: any[]): unknown// needs 1 arguments
removeSearchHistory(...args: unknown[]): unknown// needs 1 arguments
searchCache(...args: any[]): unknown// needs 3 arguments
searchCache(...args: unknown[]): unknown// needs 3 arguments
clearSearchCache(...args: any[]): unknown// needs 1 arguments
clearSearchCache(...args: unknown[]): unknown// needs 1 arguments
}

View File

@@ -36,6 +36,7 @@ export interface Group {
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
}
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string
}
export enum GroupMemberRole {

View File

@@ -275,7 +275,7 @@ export interface PicElement {
thumbPath: Map<number, string>
picWidth: number
picHeight: number
fileSize: number
fileSize: string
fileName: string
fileUuid: string
md5HexStr?: string
@@ -335,6 +335,8 @@ export interface MarketFaceElement {
faceName?: string
emojiId: string
key: string
imageWidth?: number
imageHeight?: number
}
export interface VideoElement {
@@ -350,7 +352,7 @@ export interface VideoElement {
thumbHeight?: number
busiType?: 0 // 未知
subBusiType?: 0 // 未知
thumbPath?: Map<number, any>
thumbPath?: Map<number, string>
transferStatus?: 0 // 未知
progress?: 0 // 下载进度?
invalidState?: 0 // 未知
@@ -478,6 +480,8 @@ export interface RawMessage {
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
senderUidStr: string
replyMsgTime: string
}
textElement: {
atType: AtType
@@ -519,22 +523,22 @@ export interface MessageElement {
grayTipElement?: GrayTipElement
arkElement?: ArkElement
fileElement?: FileElement
liveGiftElement?: null
liveGiftElement?: unknown
markdownElement?: MarkdownElement
structLongMsgElement?: any
structLongMsgElement?: unknown
multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: any
walletElement?: null
giphyElement?: unknown
walletElement?: unknown
inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: null //????
calendarElement?: any
yoloGameResultElement?: any
avRecordElement?: any
structMsgElement?: null
faceBubbleElement?: any
shareLocationElement?: any
tofuRecordElement?: any
taskTopMsgElement?: any
recommendedMsgElement?: any
actionBarElement?: any
textGiftElement?: unknown //????
calendarElement?: unknown
yoloGameResultElement?: unknown
avRecordElement?: unknown
structMsgElement?: unknown
faceBubbleElement?: unknown
shareLocationElement?: unknown
tofuRecordElement?: unknown
taskTopMsgElement?: unknown
recommendedMsgElement?: unknown
actionBarElement?: unknown
}

View File

@@ -1,13 +1,19 @@
export enum GroupNotifyTypes {
INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST_BY_INVITED = 5, // 有人邀请了别人入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12, // 我被取消管理员
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员
export enum GroupNotifyType {
INVITED_BY_MEMBER = 1,
REFUSE_INVITED,
REFUSED_BY_ADMINI_STRATOR,
AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群
INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群
AGREED_TO_JOIN_BY_ADMINI_STRATOR,
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS,
SET_ADMIN,
KICK_MEMBER_NOTIFY_ADMIN,
KICK_MEMBER_NOTIFY_KICKED,
MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出
CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员
CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员
TRANSFER_GROUP_NOTIFY_OLDOWNER,
TRANSFER_GROUP_NOTIFY_ADMIN
}
export interface GroupNotifies {
@@ -17,17 +23,18 @@ export interface GroupNotifies {
}
export enum GroupNotifyStatus {
IGNORE = 0,
WAIT_HANDLE = 1,
APPROVE = 2,
REJECT = 3,
KINIT, // 初始化
KUNHANDLE, // 未处理
KAGREED, // 同意
KREFUSED, // 拒绝
KIGNORED // 忽略
}
export interface GroupNotify {
time: number // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string // 唯一标识符转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes
status: GroupNotifyStatus // 0是已忽略1是未处理2是已同意
type: GroupNotifyType
status: GroupNotifyStatus
group: { groupCode: string; groupName: string }
user1: { uid: string; nickName: string } // 被设置管理员的人
user2: { uid: string; nickName: string } // 操作者

View File

@@ -82,7 +82,8 @@ export interface CategoryFriend {
categroyName: string
categroyMbCount: number
onlineCount: number
buddyList: User[]
buddyList: User[] // V1
buddyUids: string[]
}
export interface CoreInfo {
@@ -123,7 +124,7 @@ interface VideoInfo {
interface ExtOnlineBusinessInfo {
buf: string
customStatus: any
customStatus: unknown
videoBizInfo: VideoBizInfo
videoInfo: VideoInfo
}
@@ -141,7 +142,7 @@ interface UserStatus {
termType: number
netType: number
iconType: number
customStatus: any
customStatus: unknown
setTime: string
specialFlag: number
abiFlag: number
@@ -155,8 +156,8 @@ interface UserStatus {
interface PrivilegeIcon {
jumpUrl: string
openIconList: any[]
closeIconList: any[]
openIconList: unknown[]
closeIconList: unknown[]
}
interface VasInfo {
@@ -179,7 +180,7 @@ interface VasInfo {
fontEffect: number
newLoverDiamondFlag: number
extendNameplateId: number
diyNameplateIDs: any[]
diyNameplateIDs: unknown[]
vipStartFlag: number
vipDataFlag: number
gameNameplateId: string
@@ -199,8 +200,8 @@ export interface SimpleInfo {
status: UserStatus | null
vasInfo: VasInfo | null
relationFlags: RelationFlags | null
otherFlags: any | null
intimate: any | null
otherFlags: unknown | null
intimate: unknown | null
}
interface RelationFlags {
@@ -240,7 +241,7 @@ interface CommonExt {
address: string
regTime: number
interest: string
labels: any[]
labels: string[]
qqLevel: QQLevel
}
@@ -322,12 +323,12 @@ export interface UserDetailInfoByUin {
regTime: number
interest: string
termType: number
labels: any[]
labels: unknown[]
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number }
isHideQQLevel: number
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] }
privilegeIcon: { jumpUrl: string, openIconList: unknown[], closeIconList: unknown[] }
isHidePrivilegeIcon: number
photoWall: { picList: any[] }
photoWall: { picList: unknown[] }
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean

View File

@@ -11,11 +11,11 @@ import {
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os'
import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
[key: string]: any
getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
@@ -34,20 +34,7 @@ export interface WrapperApi {
}
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
[key: string]: unknown
}
const wrapperApi: WrapperApi = {}
@@ -72,11 +59,11 @@ const constructor = [
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
for (const export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
construct: (target, args) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret

View File

@@ -1,11 +1,16 @@
import { ActionName, BaseCheckResult } from './types'
import { OB11Response } from './OB11Response'
import { OB11Return } from '../types'
import { log } from '../../common/utils/log'
import { Context } from 'cordis'
import type Adapter from '../adapter'
abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName
protected ctx: Context
constructor(protected adapter: Adapter) {
this.ctx = adapter.ctx
}
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
@@ -21,13 +26,13 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData)
} catch (e: any) {
log('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
} catch (e) {
this.ctx.logger.error('发生错误', e)
return OB11Response.error(e?.toString() || (e as Error)?.stack?.toString() || '未知错误,可能操作超时', 200)
}
}
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
public async websocketHandle(payload: PayloadType, echo: unknown): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload)
if (!result.valid) {
return OB11Response.error(result.message, 1400)
@@ -35,9 +40,9 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo)
} catch (e: any) {
log('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
} catch (e) {
this.ctx.logger.error('发生错误', e)
return OB11Response.error((e as Error)?.stack?.toString() || String(e), 1200, echo)
}
}

View File

@@ -1,6 +1,5 @@
import { OB11Return } from '../types'
import { isNull } from '../../common/utils/helper'
import { isNullable } from 'cosmokit'
export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ''): OB11Return<T> {
@@ -14,17 +13,17 @@ export class OB11Response {
}
}
static ok<T>(data: T, echo: any = null) {
let res = OB11Response.res<T>(data, 'ok', 0)
if (!isNull(echo)) {
static ok<T>(data: T, echo?: unknown) {
const res = OB11Response.res<T>(data, 'ok', 0)
if (!isNullable(echo)) {
res.echo = echo
}
return res
}
static error(err: string, retcode: number, echo: any = null) {
let res = OB11Response.res(null, 'failed', retcode, err)
if (!isNull(echo)) {
static error(err: string, retcode: number, echo?: unknown) {
const res = OB11Response.res(null, 'failed', retcode, err)
if (!isNullable(echo)) {
res.echo = echo
}
return res

View File

@@ -1,11 +1,8 @@
import BaseAction from '../BaseAction'
import fsPromise from 'node:fs/promises'
import { getConfigUtil } from '@/common/config'
import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
import { UUIDConverter } from '@/common/utils/helper'
import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { Peer, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
export interface GetFilePayload {
file: string // 文件名或者fileUuid
@@ -22,7 +19,7 @@ export interface GetFileResponse {
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = getConfigUtil().getConfig()
const { enableLocalFile2Url } = this.adapter.config
let fileCache = await MessageUnique.getFileCacheById(String(payload.file))
if (!fileCache?.length) {
@@ -30,7 +27,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
}
if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia(
const downloadPath = await this.ctx.ntFileApi.downloadMedia(
fileCache[0].msgId,
fileCache[0].chatType,
fileCache[0].peerUid,
@@ -50,7 +47,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
guildId: ''
}
if (fileCache[0].elementType === ElementType.PIC) {
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
}
@@ -59,11 +56,11 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
if (!findEle) {
throw new Error('element not found')
}
res.url = await NTQQFileApi.getImageUrl(findEle.picElement)
res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement)
} else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
}
if (enableLocalFile2Url && downloadPath && res.file === res.url) {
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) {

View File

@@ -1,7 +1,6 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types'
import {decodeSilk} from "@/common/utils/audio";
import { getConfigUtil } from '@/common/config'
import { decodeSilk } from '@/common/utils/audio'
import path from 'node:path'
import fs from 'node:fs'
@@ -13,11 +12,11 @@ export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = await super._handle(payload)
res.file = await decodeSilk(res.file!, payload.out_format)
const res = await super._handle(payload)
res.file = await decodeSilk(this.ctx, res.file!, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){
if (this.adapter.config.enableLocalFile2Url) {
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res

View File

@@ -0,0 +1,17 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
name: string
parent_id?: '/'
}
export class CreateGroupFileFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder
async _handle(payload: Payload) {
await this.ctx.ntGroupApi.createGroupFileFolder(payload.group_id.toString(), payload.name)
return null
}
}

View File

@@ -1,17 +1,15 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
}
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg;
export class DelEssenceMsg extends BaseAction<Payload, unknown> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg
protected async _handle(payload: Payload): Promise<any> {
protected async _handle(payload: Payload) {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
@@ -19,7 +17,7 @@ export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
if (!msg) {
throw new Error('msg not found')
}
return await NTQQGroupApi.removeGroupEssence(
return await this.ctx.ntGroupApi.removeGroupEssence(
msg.Peer.peerUid,
msg.MsgId,
)

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload {
group_id: string | number
@@ -8,10 +7,11 @@ interface Payload {
busid?: 102
}
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> {
export class DelGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) {
await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id])
await this.ctx.ntGroupApi.deleteGroupFile(payload.group_id.toString(), [payload.file_id])
return null
}
}

View File

@@ -0,0 +1,16 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
folder_id: string
}
export class DelGroupFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFolder
async _handle(payload: Payload) {
await this.ctx.ntGroupApi.deleteGroupFileFolder(payload.group_id.toString(), payload.folder_id)
return null
}
}

View File

@@ -1,10 +1,12 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import fs from 'fs'
import fsPromise from 'fs/promises'
import path from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils'
import { ActionName } from '../types'
import { calculateFileMD5, fetchFile } from '@/common/utils'
import { TEMP_DIR } from '@/common/globalVars'
import { randomUUID } from 'node:crypto'
import { Dict } from 'cosmokit'
interface Payload {
thread_count?: number
@@ -18,7 +20,7 @@ interface FileResponse {
file: string
}
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile
protected async _handle(payload: Payload): Promise<FileResponse> {
@@ -30,8 +32,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
await fsPromise.writeFile(filePath, payload.base64, 'base64')
} else if (payload.url) {
const headers = this.getHeaders(payload.headers)
const buffer = await httpDownload({ url: payload.url, headers: headers })
await fsPromise.writeFile(filePath, buffer)
const res = await fetchFile(payload.url, headers)
await fsPromise.writeFile(filePath, res.data)
} else {
throw new Error('不存在任何文件, 无法下载')
}
@@ -50,7 +52,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
}
getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {}
const headers: Dict = {}
if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]')
}

View File

@@ -1,9 +1,8 @@
import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { OB11Constructor } from '../../constructor'
import { OB11ForwardMessage } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: string // long msg idgocq
@@ -11,12 +10,12 @@ interface Payload {
}
interface Response {
messages: (OB11Message & { content: OB11MessageData })[]
messages: OB11ForwardMessage[]
}
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
export class GetForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> {
protected async _handle(payload: Payload) {
const msgId = payload.id || payload.message_id
if (!msgId) {
throw Error('message_id不能为空')
@@ -26,26 +25,27 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
if (!rootMsg) {
throw Error('msg not found')
}
const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId)
const data = await this.ctx.ntMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId)
if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg)
}
const msgList = data.msgList
const messages = await Promise.all(
msgList.map(async (msg) => {
const resMsg = await OB11Constructor.message(msg)
const resMsg = await OB11Entities.message(this.ctx, msg)
resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)!
}, msg.msgId)
return resMsg
}),
)
messages.map(v => {
const forwardMessages = messages.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
return msg as OB11ForwardMessage
})
return { messages }
return { messages: forwardMessages }
}
}

View File

@@ -0,0 +1,25 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Response {
can_at_all: boolean
remain_at_all_count_for_group: number
remain_at_all_count_for_uin: number
}
export class GetGroupAtAllRemain extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupRemainAtTimes(payload.group_id.toString())
return {
can_at_all: data.atInfo.canAtAll,
remain_at_all_count_for_group: data.atInfo.RemainAtAllCountForGroup,
remain_at_all_count_for_uin: data.atInfo.RemainAtAllCountForUin
}
}
}

View File

@@ -2,15 +2,14 @@ import BaseAction from '../BaseAction'
import { OB11Message } from '../../types'
import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { OB11Constructor } from '../../constructor'
import { OB11Entities } from '../../entities'
import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
group_id: number | string
message_seq?: number
count?: number
message_seq?: number | string
count?: number | string
reverseOrder?: boolean
}
@@ -18,7 +17,7 @@ interface Response {
messages: OB11Message[]
}
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> {
@@ -28,20 +27,20 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
let msgList: RawMessage[] | undefined
// 包含 message_seq 0
if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count))?.msgList
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在`
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq))?.MsgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
}
if (!msgList?.length) throw '未找到消息'
if (!msgList?.length) throw new Error('未找到消息')
if (isReverseOrder) msgList.reverse()
await Promise.all(
msgList.map(async msg => {
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
})
)
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg)))
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Entities.message(this.ctx, msg)))
return { messages: ob11MsgList }
}
}

View File

@@ -0,0 +1,62 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
interface Payload {
group_id: string | number
file_count: string | number
}
interface Response {
files: OB11GroupFile[]
folders: OB11GroupFileFolder[]
}
export class GetGroupRootFiles extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupRootFiles
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +(payload.file_count ?? 50),
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
this.ctx.logger.info(data)
return {
files: data.filter(item => item.fileInfo)
.map(item => {
const file = item.fileInfo!
return {
group_id: +item.peerId,
file_id: file.fileId,
file_name: file.fileName,
busid: file.busId,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,
download_times: file.downloadTimes,
uploader: +file.uploaderUin,
uploader_name: file.uploaderName
}
}),
folders: data.filter(item => item.folderInfo)
.map(item => {
const folder = item.folderInfo!
return {
group_id: +item.peerId,
folder_id: folder.folderId,
folder_name: folder.folderName,
create_time: folder.createTime,
creator: +folder.createUin,
creator_name: folder.creatorName,
total_file_count: folder.totalFileCount
}
})
}
}
}

View File

@@ -0,0 +1,59 @@
import BaseAction from '../BaseAction'
import { GroupNotifyStatus } from '@/ntqqapi/types'
import { ActionName } from '../types'
interface Response {
invited_requests: {
request_id: number
invitor_uin: number
invitor_nick: string
group_id: number
group_name: string
checked: boolean
actor: number
}[]
join_requests: {
request_id: number
requester_uin: number
requester_nick: string
message: string
group_id: number
group_name: string
checked: boolean
actor: number
}[]
}
export class GetGroupSystemMsg extends BaseAction<void, Response> {
actionName = ActionName.GoCQHTTP_GetGroupSystemMsg
async _handle() {
const singleScreenNotifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(10)
const data: Response = { invited_requests: [], join_requests: [] }
for (const notify of singleScreenNotifies) {
if (notify.type == 1) {
data.invited_requests.push({
request_id: +notify.seq,
invitor_uin: Number(await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)),
invitor_nick: notify.user1.nickName,
group_id: +notify.group.groupCode,
group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.KUNHANDLE,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
})
} else if (notify.type == 7) {
data.join_requests.push({
request_id: +notify.seq,
requester_uin: Number(await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)),
requester_nick: notify.user1.nickName,
message: notify.postscript,
group_id: +notify.group.groupCode,
group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.KUNHANDLE,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
})
}
}
return data
}
}

View File

@@ -1,24 +1,23 @@
import BaseAction from '../BaseAction'
import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { getBuildVersion } from '@/common/utils'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
import { calcQQLevel } from '@/common/utils/misc'
interface Payload {
user_id: number | string
}
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: Payload): Promise<OB11User> {
if (!(getBuildVersion() >= 26702)) {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUin(user_id)
const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
@@ -33,12 +32,12 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Entities.stranger(data)
} else {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
@@ -52,8 +51,8 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Entities.stranger(data)
}
}
}

View File

@@ -5,10 +5,10 @@ interface Payload {
message_id: number
}
export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null> {
export class MarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead
protected async _handle(payload: Payload): Promise<null> {
protected async _handle() {
return null
}
}

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../quick-operation'
import { log } from '@/common/utils'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../../helper/quickOperation'
import { ActionName } from '../types'
interface Payload {
@@ -8,10 +7,10 @@ interface Payload{
operation: QuickOperation
}
export class GoCQHTTHandleQuickOperation extends BaseAction<Payload, null>{
export class HandleQuickOperation extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(payload.context, payload.operation).then().catch(log);
handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e))
return null
}
}

View File

@@ -1,8 +1,9 @@
import SendMsg, { convertMessage2List } from '../msg/SendMsg'
import SendMsg from '../msg/SendMsg'
import { OB11PostSendMsg } from '../../types'
import { ActionName } from '../types'
import { convertMessage2List } from '../../helper/createMessage'
export class GoCQHTTPSendForwardMsg extends SendMsg {
export class SendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg
protected async check(payload: OB11PostSendMsg) {
@@ -11,10 +12,10 @@ export class GoCQHTTPSendForwardMsg extends SendMsg {
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
export class SendPrivateForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
export class SendGroupForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg
}

View File

@@ -0,0 +1,37 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
content: string
image?: string
pinned?: number | string //扩展
confirm_required?: number | string //扩展
}
export class SendGroupNotice extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupNotice
async _handle(payload: Payload) {
const type = 1
const isShowEditCard = 0
const tipWindowType = 0
const pinned = Number(payload.pinned ?? 0)
const confirmRequired = Number(payload.confirm_required ?? 1)
const result = await this.ctx.ntWebApi.setGroupNotice({
groupCode: payload.group_id.toString(),
content: payload.content,
pinned,
type,
isShowEditCard,
tipWindowType,
confirmRequired,
picId: ''
})
if (result.ec !== 0) {
throw new Error(`设置群公告失败, 错误信息: ${result.em}`)
}
return null
}
}

View File

@@ -1,16 +1,15 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
}
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg;
export class SetEssenceMsg extends BaseAction<Payload, unknown> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg
protected async _handle(payload: Payload): Promise<any> {
protected async _handle(payload: Payload) {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
@@ -18,7 +17,7 @@ export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
if (!msg) {
throw new Error('msg not found')
}
return await NTQQGroupApi.addGroupEssence(
return await this.ctx.ntGroupApi.addGroupEssence(
msg.Peer.peerUid,
msg.MsgId
)

View File

@@ -1,26 +1,23 @@
import fs from 'node:fs'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types'
import { SendElementEntities } from '@/ntqqapi/entities'
import { SendFileElement } from '@/ntqqapi/types'
import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg'
import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
interface Payload {
user_id: number | string
group_id?: number | string
interface UploadGroupFilePayload {
group_id: number | string
file: string
name: string
folder?: string
folder_id?: string
}
export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
export class UploadGroupFile extends BaseAction<UploadGroupFilePayload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
protected async _handle(payload: UploadGroupFilePayload): Promise<null> {
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
@@ -29,32 +26,24 @@ export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await sendMsg({
chatType: ChatType.group,
peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true)
const sendFileEle = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name, payload.folder_id)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [sendFileEle], [])
return null
}
}
export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
interface UploadPrivateFilePayload {
user_id: number | string
file: string
name: string
}
export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null> {
actionName = ActionName.GoCQHTTP_UploadPrivateFile
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw `私聊${payload.user_id}不存在`
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid)
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
}
throw '缺少参数 user_id'
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload)
protected async _handle(payload: UploadPrivateFilePayload): Promise<null> {
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
@@ -63,8 +52,8 @@ export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name)
await sendMsg(peer, [sendFileEle], [], true)
const sendFileEle: SendFileElement = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name)
await sendMsg(this.ctx, peer, [sendFileEle], [])
return null
}
}

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor'
import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload {
group_id: number | string
@@ -12,9 +11,9 @@ class GetGroupInfo extends BaseAction<Payload, OB11Group> {
actionName = ActionName.GetGroupInfo
protected async _handle(payload: Payload) {
const group = (await NTQQGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString())
const group = (await this.ctx.ntGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString())
if (group) {
return OB11Constructor.group(group)
return OB11Entities.group(group)
} else {
throw `${payload.group_id}不存在`
}

View File

@@ -1,8 +1,7 @@
import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor'
import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api'
interface Payload {
no_cache: boolean | string
@@ -11,9 +10,9 @@ interface Payload {
class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) {
const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true')
return OB11Constructor.groups(groupList)
protected async _handle() {
const groupList = await this.ctx.ntGroupApi.getGroups()
return OB11Entities.groups(groupList)
}
}

View File

@@ -1,10 +1,9 @@
import { OB11GroupMember } from '../../types'
import { getGroupMember, getSelfUid } from '@/common/data'
import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction'
import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { isNull } from '@/common/utils/helper'
import { selfInfo } from '@/common/globalVars'
import { isNullable } from 'cosmokit'
interface Payload {
group_id: number | string
@@ -15,18 +14,18 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: Payload) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) {
if (isNull(member.sex)) {
if (isNullable(member.sex)) {
//log('获取群成员详细信息')
const info = await NTQQUserApi.getUserDetailInfo(member.uid, true)
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
//log('群成员详细信息结果', info)
Object.assign(member, info)
}
const ret = OB11Constructor.groupMember(payload.group_id.toString(), member)
const self = await getGroupMember(payload.group_id.toString(), getSelfUid())
const ret = OB11Entities.groupMember(payload.group_id.toString(), member)
const self = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), selfInfo.uid)
if (self?.role === 3 || self?.role === 4) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString())
const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id)
if (target) {
ret.join_time = target.join_time

View File

@@ -1,9 +1,8 @@
import { OB11GroupMember } from '../../types'
import { OB11Constructor } from '../../constructor'
import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi, WebApi } from '@/ntqqapi/api'
import { getSelfUid } from '@/common/data'
import { selfInfo } from '@/common/globalVars'
interface Payload {
group_id: number | string
@@ -14,11 +13,11 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList
protected async _handle(payload: Payload) {
const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
const groupMembers = await this.ctx.ntGroupApi.getGroupMembers(payload.group_id.toString())
const groupMembersArr = Array.from(groupMembers.values())
let _groupMembers = groupMembersArr.map(item => {
return OB11Constructor.groupMember(payload.group_id.toString(), item)
return OB11Entities.groupMember(payload.group_id.toString(), item)
})
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
@@ -31,11 +30,11 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i])
}
const selfRole = groupMembers.get(getSelfUid())?.role
const selfRole = groupMembers.get(selfInfo.uid)?.role
const isPrivilege = selfRole === 3 || selfRole === 4
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString())
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) {
continue

View File

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

View File

@@ -1,7 +1,6 @@
import BaseAction from '../BaseAction'
import { GroupRequestOperateTypes } from '../../../ntqqapi/types'
import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload {
flag: string
@@ -15,7 +14,7 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString()
const approve = payload.approve?.toString() !== 'false'
await NTQQGroupApi.handleGroupRequest(flag,
await this.ctx.ntGroupApi.handleGroupRequest(flag,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason || ''
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload {
group_id: number
@@ -11,7 +10,7 @@ export default class SetGroupName extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupName
protected async _handle(payload: Payload): Promise<null> {
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name)
await this.ctx.ntGroupApi.setGroupName(payload.group_id.toString(), payload.group_name)
return null
}
}

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload {
group_id: number
@@ -12,7 +11,7 @@ export default class SetGroupWholeBan extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
const enable = payload.enable.toString() === 'true'
await NTQQGroupApi.banGroup(payload.group_id.toString(), enable)
await this.ctx.ntGroupApi.banGroup(payload.group_id.toString(), enable)
return null
}
}

View File

@@ -1,3 +1,4 @@
import type Adapter from '../adapter'
import GetMsg from './msg/GetMsg'
import GetLoginInfo from './system/GetLoginInfo'
import { GetFriendList, GetFriendWithCategory } from './user/GetFriendList'
@@ -15,11 +16,11 @@ import CanSendRecord from './system/CanSendRecord'
import CanSendImage from './system/CanSendImage'
import GetStatus from './system/GetStatus'
import {
GoCQHTTPSendForwardMsg,
GoCQHTTPSendGroupForwardMsg,
GoCQHTTPSendPrivateForwardMsg,
SendForwardMsg,
SendGroupForwardMsg,
SendPrivateForwardMsg,
} from './go-cqhttp/SendForwardMsg'
import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo'
import { GetStrangerInfo } from './go-cqhttp/GetStrangerInfo'
import SendLike from './user/SendLike'
import SetGroupAddRequest from './group/SetGroupAddRequest'
import SetGroupLeave from './group/SetGroupLeave'
@@ -34,92 +35,104 @@ import SetGroupAdmin from './group/SetGroupAdmin'
import SetGroupCard from './group/SetGroupCard'
import GetImage from './file/GetImage'
import GetRecord from './file/GetRecord'
import GoCQHTTPMarkMsgAsRead from './msg/MarkMsgAsRead'
import { MarkMsgAsRead } from './go-cqhttp/MarkMsgAsRead'
import CleanCache from './system/CleanCache'
import { GoCQHTTPUploadGroupFile, GoCQHTTPUploadPrivateFile } from './go-cqhttp/UploadFile'
import { UploadGroupFile, UploadPrivateFile } from './go-cqhttp/UploadFile'
import { GetConfigAction, SetConfigAction } from './llonebot/Config'
import GetGroupAddRequest from './llonebot/GetGroupAddRequest'
import SetQQAvatar from './llonebot/SetQQAvatar'
import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile'
import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory'
import { DownloadFile } from './go-cqhttp/DownloadFile'
import { GetGroupMsgHistory } from './go-cqhttp/GetGroupMsgHistory'
import GetFile from './file/GetFile'
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetForwardMsg } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import { HandleQuickOperation } from './go-cqhttp/QuickOperation'
import { SetEssenceMsg } from './go-cqhttp/SetEssenceMsg'
import { DelEssenceMsg } from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile'
import { DelGroupFile } from './go-cqhttp/DelGroupFile'
import { GetGroupSystemMsg } from './go-cqhttp/GetGroupSystemMsg'
import { CreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder'
import { DelGroupFolder } from './go-cqhttp/DelGroupFolder'
import { GetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'
import { GetGroupRootFiles } from './go-cqhttp/GetGroupRootFiles'
import { SetOnlineStatus } from './llonebot/SetOnlineStatus'
import { SendGroupNotice } from './go-cqhttp/SendGroupNotice'
export const actionHandlers = [
new GetFile(),
new Debug(),
new GetConfigAction(),
new SetConfigAction(),
new GetGroupAddRequest(),
new SetQQAvatar(),
new GetFriendWithCategory(),
new GetEvent(),
export function initActionMap(adapter: Adapter) {
const actionHandlers = [
new GetFile(adapter),
new Debug(adapter),
new GetConfigAction(adapter),
new SetConfigAction(adapter),
new GetGroupAddRequest(adapter),
new SetQQAvatar(adapter),
new GetFriendWithCategory(adapter),
new GetEvent(adapter),
new SetOnlineStatus(adapter),
// onebot11
new SendLike(),
new GetMsg(),
new GetLoginInfo(),
new GetFriendList(),
new GetGroupList(),
new GetGroupInfo(),
new GetGroupMemberList(),
new GetGroupMemberInfo(),
new SendGroupMsg(),
new SendPrivateMsg(),
new SendMsg(),
new DeleteMsg(),
new SetGroupAddRequest(),
new SetFriendAddRequest(),
new SetGroupLeave(),
new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage(),
new GetStatus(),
new SetGroupWholeBan(),
new SetGroupBan(),
new SetGroupKick(),
new SetGroupAdmin(),
new SetGroupName(),
new SetGroupCard(),
new GetImage(),
new GetRecord(),
new CleanCache(),
new GetCookies(),
new SetMsgEmojiLike(),
new ForwardFriendSingleMsg(),
new ForwardGroupSingleMsg(),
new SendLike(adapter),
new GetMsg(adapter),
new GetLoginInfo(adapter),
new GetFriendList(adapter),
new GetGroupList(adapter),
new GetGroupInfo(adapter),
new GetGroupMemberList(adapter),
new GetGroupMemberInfo(adapter),
new SendGroupMsg(adapter),
new SendPrivateMsg(adapter),
new SendMsg(adapter),
new DeleteMsg(adapter),
new SetGroupAddRequest(adapter),
new SetFriendAddRequest(adapter),
new SetGroupLeave(adapter),
new GetVersionInfo(adapter),
new CanSendRecord(adapter),
new CanSendImage(adapter),
new GetStatus(adapter),
new SetGroupWholeBan(adapter),
new SetGroupBan(adapter),
new SetGroupKick(adapter),
new SetGroupAdmin(adapter),
new SetGroupName(adapter),
new SetGroupCard(adapter),
new GetImage(adapter),
new GetRecord(adapter),
new CleanCache(adapter),
new GetCookies(adapter),
new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(adapter),
//以下为go-cqhttp api
new GetGroupEssence(),
new GetGroupHonorInfo(),
new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GoCQHTTPDownloadFile(),
new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(),
new GoCQHTTPUploadPrivateFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(),
new GoCQHTTHandleQuickOperation(),
new GoCQHTTPSetEssenceMsg(),
new GoCQHTTPDelEssenceMsg(),
new GoCQHTTPDelGroupFile()
new GetGroupEssence(adapter),
new GetGroupHonorInfo(adapter),
new SendForwardMsg(adapter),
new SendGroupForwardMsg(adapter),
new SendPrivateForwardMsg(adapter),
new GetStrangerInfo(adapter),
new DownloadFile(adapter),
new GetGuildList(adapter),
new MarkMsgAsRead(adapter),
new UploadGroupFile(adapter),
new UploadPrivateFile(adapter),
new GetGroupMsgHistory(adapter),
new GetForwardMsg(adapter),
new HandleQuickOperation(adapter),
new SetEssenceMsg(adapter),
new DelEssenceMsg(adapter),
new DelGroupFile(adapter),
new GetGroupSystemMsg(adapter),
new CreateGroupFileFolder(adapter),
new DelGroupFolder(adapter),
new GetGroupAtAllRemain(adapter),
new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter)
]
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>()
const actionMap = new Map<string, BaseAction<unknown, unknown>>()
for (const action of actionHandlers) {
actionMap.set(action.actionName, action)
actionMap.set(action.actionName + '_async', action)
@@ -128,5 +141,3 @@ function initActionMap() {
return actionMap
}
export const actionMap = initActionMap()

View File

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

View File

@@ -1,33 +1,22 @@
import BaseAction from '../BaseAction'
// import * as ntqqApi from "../../../ntqqapi/api";
import {
NTQQMsgApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQUserApi,
NTQQFileApi,
NTQQFileCacheApi,
NTQQWindowApi,
} from '../../../ntqqapi/api'
import { ActionName } from '../types'
import { log } from '../../../common/utils/log'
interface Payload {
method: string
args: any[]
args: unknown[]
}
export default class Debug extends BaseAction<Payload, any> {
export default class Debug extends BaseAction<Payload, unknown> {
actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> {
log('debug call ntqq api', payload)
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi]
protected async _handle(payload: Payload) {
this.ctx.logger.info('debug call ntqq api', payload)
const { ntMsgApi, ntFileApi, ntFileCacheApi, ntFriendApi, ntGroupApi, ntUserApi, ntWindowApi } = this.ctx
const ntqqApi = [ntMsgApi, ntFriendApi, ntGroupApi, ntUserApi, ntFileApi, ntFileCacheApi, ntWindowApi]
for (const ntqqApiClass of ntqqApi) {
//log('ntqqApiClass', ntqqApiClass)
const method = ntqqApiClass[payload.method]
if (method) {
const result = method(...payload.args)
const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass]
if (method && method instanceof Function) {
const result = method.apply(ntqqApiClass, payload.args)
if (method.constructor.name === 'AsyncFunction') {
return await result
}
@@ -35,8 +24,5 @@ export default class Debug extends BaseAction<Payload, any> {
}
}
throw `${payload.method}方法 不存在`
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

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