Compare commits

..

52 Commits

Author SHA1 Message Date
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
75 changed files with 1613 additions and 1408 deletions

View File

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

View File

@@ -15,10 +15,6 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
<img src="./doc/image/setting.png" width="400px" alt="设置界面"/> <img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例
<img src="./doc/image/example.jpg" width="500px" alt="HTTP调用示例"/>
## 支持的 API ## 支持的 API
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
@@ -31,10 +27,10 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ) - [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) - [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) - [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm) - [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 cp from 'vite-plugin-cp'
import path from 'node:path' import path from 'node:path'
import './scripts/gen-manifest' import './scripts/gen-manifest'
import type { ElectronViteConfig } from 'electron-vite'
const external = [ const external = [
'silk-wasm', 'silk-wasm',
@@ -12,7 +13,7 @@ function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false } return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
} }
let config = { const config: ElectronViteConfig = {
main: { main: {
build: { build: {
outDir: 'dist/main', outDir: 'dist/main',
@@ -39,9 +40,6 @@ let config = {
...external.map(genCpModule), ...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, { src: './manifest.json', dest: 'dist' },
{ src: './icon.webp', 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", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.31.1", "version": "3.31.7",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -22,7 +22,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.2", "cosmokit": "^1.6.2",
"express": "^4.19.2", "express": "^4.19.2",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.5.0",
"file-type": "^19.4.1", "file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.1", "minato": "^3.5.1",
@@ -38,7 +38,7 @@
"electron": "^31.4.0", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.2", "vite": "^5.4.3",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.1" "packageManager": "yarn@4.4.1"

View File

@@ -1,25 +1,8 @@
import fs from 'node:fs' import fs from 'node:fs'
import { Config, OB11Config } from './types'
import path from 'node:path' import path from 'node:path'
import { Config, OB11Config } from './types'
import { selfInfo, DATA_DIR } from './globalVars' import { selfInfo, DATA_DIR } from './globalVars'
import { mergeNewProperties } from './utils/misc'
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
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 class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string
@@ -38,7 +21,7 @@ export class ConfigUtil {
} }
reloadConfig(): Config { reloadConfig(): Config {
let ob11Default: OB11Config = { const ob11Default: OB11Config = {
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: '', httpSecret: '',
@@ -50,9 +33,10 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
enableQOAutoQuote: false enableQOAutoQuote: false,
listenLocalhost: false
} }
let defaultConfig: Config = { const defaultConfig: Config = {
enableLLOB: true, enableLLOB: true,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
@@ -83,7 +67,6 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http') this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts') this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort') this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
// console.log("get config", jsonData);
this.config = jsonData this.config = jsonData
return this.config return this.config
} }
@@ -95,15 +78,15 @@ export class ConfigUtil {
} }
private checkOldConfig( private checkOldConfig(
currentConfig: Config | OB11Config, currentConfig: OB11Config,
oldConfig: Config | OB11Config, oldConfig: Config,
currentKey: string, currentKey: 'httpPort' | 'httpHosts' | 'wsPort',
oldKey: string, oldKey: 'http' | 'hosts' | 'wsPort',
) { ) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]
if (oldValue) { if (oldValue) {
currentConfig[currentKey] = oldValue currentConfig[currentKey] = oldValue as any
delete oldConfig[oldKey] delete oldConfig[oldKey]
} }
} }

View File

@@ -11,6 +11,7 @@ export interface OB11Config {
messagePostFormat?: 'array' | 'string' messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息 enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
listenLocalhost: boolean
} }
export interface CheckVersion { export interface CheckVersion {
@@ -34,6 +35,12 @@ export interface Config {
ignoreBeforeLoginMsg?: boolean ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */ /** 单位为秒 */
msgCacheExpire?: number msgCacheExpire?: number
/** @deprecated */
http?: string
/** @deprecated */
hosts?: string[]
/** @deprecated */
wsPort?: string
} }
export interface LLOneBotError { export interface LLOneBotError {

View File

@@ -82,7 +82,7 @@ class MessageUniqueWrapper {
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined) return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
} }
createMsg(peer: Peer, msgId: string): number | undefined { createMsg(peer: Peer, msgId: string): number {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}` const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest() const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数 //设置第一个bit为0 保证shortId为正数

View File

@@ -14,3 +14,21 @@ export function getBuildVersion(): number {
const version: string = globalThis.LiteLoader.versions.qqnt const version: string = globalThis.LiteLoader.versions.qqnt
return +version.split('-')[1] return +version.split('-')[1]
} }
/** 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 */
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]
}
}
})
}

7
src/global.d.ts vendored
View File

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

View File

@@ -18,18 +18,15 @@ export default class Log {
return return
} }
const file = path.join(LOG_DIR, cfg.filename) const file = path.join(LOG_DIR, cfg.filename)
const refreshNick = ctx.debounce(() => { /*const refreshNick = ctx.debounce(() => {
const ntUserApi = ctx.get('ntUserApi') const ntUserApi = ctx.get('ntUserApi')
if (ntUserApi && !selfInfo.nick) { if (ntUserApi && !selfInfo.nick) {
ntUserApi.getSelfNick(true) ntUserApi.getSelfNick(true)
} }
}, 1000) }, 1000)*/
const target: Logger.Target = { const target: Logger.Target = {
colors: 0, colors: 0,
record: (record: Logger.Record) => { record: (record: Logger.Record) => {
if (!selfInfo.nick) {
refreshNick()
}
const dateTime = new Date(record.timestamp).toLocaleString() const dateTime = new Date(record.timestamp).toLocaleString()
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ''
const content = `${dateTime} [${record.type}] ${userInfo} | ${record.name} ${record.content}\n\n` const content = `${dateTime} [${record.type}] ${userInfo} | ${record.name} ${record.content}\n\n`

View File

@@ -199,14 +199,16 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (window.id !== 2) { if (![2, 4].includes(window.id)) {
return return
} }
mainWindow = window if (window.id === 2) {
log('window create', window.webContents.getURL().toString()) mainWindow = window
}
//log('window create', window.webContents.getURL().toString())
try { try {
hookNTQQApiCall(window) hookNTQQApiCall(window, window.id !== 2)
hookNTQQApiReceive(window) hookNTQQApiReceive(window, window.id !== 2)
} catch (e: any) { } catch (e: any) {
log('LLOneBot hook error: ', e.toString()) log('LLOneBot hook error: ', e.toString())
} }

View File

@@ -5,7 +5,6 @@ import {
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult, CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType, ElementType,
@@ -43,14 +42,32 @@ export class NTQQFileApi extends Service {
this.rkeyManager = new RkeyManager(ctx, 'http://napcat-sign.wumiao.wang:2082/rkey') this.rkeyManager = new RkeyManager(ctx, 'http://napcat-sign.wumiao.wang:2082/rkey')
} }
/** 27187 TODO */
async getVideoUrl(peer: Peer, msgId: string, elementId: string) { async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession() const session = getSession()
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, if (session) {
msgId, return (await session.getRichMediaService().getVideoPlayUrlV2(
elementId, peer,
0, msgId,
{ downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url elementId,
0,
{ 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
}
} }
async getFileType(filePath: string) { async getFileType(filePath: string) {
@@ -82,23 +99,18 @@ export class NTQQFileApi extends Service {
file_uuid: '' file_uuid: ''
}) })
} else { } else {
mediaPath = await invoke<string>({ mediaPath = await invoke(NTMethod.MEDIA_FILE_PATH, [{
methodName: NTMethod.MEDIA_FILE_PATH, path_info: {
args: [ md5HexStr: fileMd5,
{ fileName: fileName,
path_info: { elementType: elementType,
md5HexStr: fileMd5, elementSubType,
fileName: fileName, thumbSize: 0,
elementType: elementType, needCreate: true,
elementSubType, downloadType: 1,
thumbSize: 0, file_uuid: '',
needCreate: true, },
downloadType: 1, }])
file_uuid: '',
},
},
],
})
} }
await fsPromise.copyFile(filePath, mediaPath) await fsPromise.copyFile(filePath, mediaPath)
const fileSize = (await fsPromise.stat(filePath)).size const fileSize = (await fsPromise.stat(filePath)).size
@@ -174,9 +186,9 @@ export class NTQQFileApi extends Service {
) )
filePath = data[1].filePath filePath = data[1].filePath
} else { } else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>({ const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
methodName: NTMethod.DOWNLOAD_MEDIA, 'nodeIKernelMsgService/downloadRichMedia',
args: [ [
{ {
getReq: { getReq: {
fileModelId: '0', fileModelId: '0',
@@ -193,10 +205,12 @@ export class NTQQFileApi extends Service {
}, },
null, null,
], ],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, {
cmdCB: payload => payload.notifyInfo.msgId === msgId, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
timeout cmdCB: payload => payload.notifyInfo.msgId === msgId,
}) timeout
}
)
filePath = data.notifyInfo.filePath filePath = data.notifyInfo.filePath
} }
if (filePath.startsWith('\\')) { if (filePath.startsWith('\\')) {
@@ -208,11 +222,13 @@ export class NTQQFileApi extends Service {
} }
async getImageSize(filePath: string) { async getImageSize(filePath: string) {
return await invoke<{ width: number; height: number }>({ return await invoke<{ width: number; height: number }>(
className: NTClass.FS_API, NTMethod.IMAGE_SIZE,
methodName: NTMethod.IMAGE_SIZE, [filePath],
args: [filePath], {
}) className: NTClass.FS_API,
}
)
} }
async getImageUrl(element: PicElement) { async getImageUrl(element: PicElement) {
@@ -254,15 +270,7 @@ export class NTQQFileCacheApi extends Service {
} }
async setCacheSilentScan(isSilent: boolean = true) { async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }, null])
methodName: NTMethod.CACHE_SET_SILENCE,
args: [
{
isSilent,
},
null,
],
})
} }
getCacheSessionPathList() { getCacheSessionPathList() {
@@ -271,110 +279,38 @@ export class NTQQFileCacheApi extends Service {
key: string key: string
value: string value: string
}[] }[]
>({ >(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
className: NTClass.OS_API,
methodName: NTMethod.CACHE_PATH_SESSION,
})
}
clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return invoke<any>({
// TODO: 目前还不知道真正的返回值是什么
methodName: NTMethod.CACHE_CLEAR,
args: [
{
keys: cacheKeys,
},
null,
],
})
}
addCacheScannedPaths(pathMap: object = {}) {
return invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [
{
pathMap: { ...pathMap },
},
null,
],
})
} }
scanCache() { scanCache() {
invoke<GeneralCallResult>({ invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { classNameIsRegister: true })
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second })
classNameIsRegister: true,
}).then()
return invoke<CacheScanResult>({
methodName: NTMethod.CACHE_SCAN,
args: [null, null],
timeout: 300 * Time.second,
})
} }
getHotUpdateCachePath() { getHotUpdateCachePath() {
return invoke<string>({ return invoke<string>(NTMethod.CACHE_PATH_HOT_UPDATE, [], { className: NTClass.HOTUPDATE_API })
className: NTClass.HOTUPDATE_API,
methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
})
} }
getDesktopTmpPath() { getDesktopTmpPath() {
return invoke<string>({ return invoke<string>(NTMethod.CACHE_PATH_DESKTOP_TEMP, [], { className: NTClass.BUSINESS_API })
className: NTClass.BUSINESS_API,
methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP,
})
}
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))
})
} }
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType } const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return invoke<CacheFileList>({ return invoke<CacheFileList>(NTMethod.CACHE_FILE_GET, [{
methodName: NTMethod.CACHE_FILE_GET, fileType: fileType,
args: [ restart: true,
{ pageSize: pageSize,
fileType: fileType, order: 1,
restart: true, lastRecord: _lastRecord,
pageSize: pageSize, }, null])
order: 1,
lastRecord: _lastRecord,
},
null,
],
})
} }
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{
methodName: NTMethod.CACHE_CHAT_CLEAR, chats,
args: [ fileKeys,
{ }, null])
chats,
fileKeys,
},
null,
],
})
} }
} }

View File

@@ -4,7 +4,6 @@ import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/eventTask' import { NTEventDispatch } from '@/common/utils/eventTask'
import { LimitedHashTable } from '@/common/utils/table'
import { pick } from 'cosmokit' import { pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@@ -28,12 +27,15 @@ export class NTQQFriendApi extends Service {
categroyMbCount: number categroyMbCount: number
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>({ }>(
className: NTClass.NODE_STORE_API, 'getBuddyList',
methodName: 'getBuddyList', [],
cbCmd: ReceiveCmdS.FRIENDS, {
afterFirstCmd: false, className: NTClass.NODE_STORE_API,
}) cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const _friends: Friend[] = [] const _friends: Friend[] = []
for (const item of data.data) { for (const item of data.data) {
_friends.push(...item.buddyList) _friends.push(...item.buddyList)
@@ -56,18 +58,13 @@ export class NTQQFriendApi extends Service {
accept accept
}) })
} else { } else {
return await invoke({ return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
methodName: NTMethod.HANDLE_FRIEND_REQUEST, approvalInfo: {
args: [ friendUid,
{ reqTime,
approvalInfo: { accept,
friendUid, },
reqTime, }])
accept,
},
},
],
})
} }
} }
@@ -86,13 +83,15 @@ export class NTQQFriendApi extends Service {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
}>({ }>(
className: NTClass.NODE_STORE_API, 'getBuddyList',
methodName: 'getBuddyList', [refresh],
args: [refresh], {
cbCmd: ReceiveCmdS.FRIENDS, className: NTClass.NODE_STORE_API,
afterFirstCmd: false, cbCmd: ReceiveCmdS.FRIENDS,
}) afterFirstCmd: false,
}
)
const categoryUids: Map<number, string[]> = new Map() const categoryUids: Map<number, string[]> = new Map()
for (const item of data.buddyCategory) { for (const item of data.buddyCategory) {
categoryUids.set(item.categoryId, item.buddyUids) categoryUids.set(item.categoryId, item.buddyUids)
@@ -101,8 +100,9 @@ export class NTQQFriendApi extends Service {
} }
} }
async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> { /** uid => uin */
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000) async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map()
const session = getSession() const session = getSession()
if (session) { if (session) {
const uids: string[] = [] const uids: string[] = []
@@ -112,22 +112,30 @@ export class NTQQFriendApi extends Service {
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
) )
data.forEach((value, key) => { for (const [, item] of data) {
retMap.set(value.uin!, value.uid!) if (retMap.size > 5000) {
}) break
}
retMap.set(item.uid!, item.uin!)
}
} else { } else {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
}>({ }>(
className: NTClass.NODE_STORE_API, 'getBuddyList',
methodName: 'getBuddyList', [refresh],
args: [refresh], {
cbCmd: ReceiveCmdS.FRIENDS, className: NTClass.NODE_STORE_API,
afterFirstCmd: false, cbCmd: ReceiveCmdS.FRIENDS,
}) afterFirstCmd: false,
}
)
for (const item of Object.values(data.userSimpleInfos)) { for (const item of Object.values(data.userSimpleInfos)) {
retMap.set(item.uin!, item.uid!) if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
} }
} }
return retMap return retMap
@@ -158,13 +166,15 @@ export class NTQQFriendApi extends Service {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
}>({ }>(
className: NTClass.NODE_STORE_API, 'getBuddyList',
methodName: 'getBuddyList', [refresh],
args: [refresh], {
cbCmd: ReceiveCmdS.FRIENDS, className: NTClass.NODE_STORE_API,
afterFirstCmd: false, cbCmd: ReceiveCmdS.FRIENDS,
}) afterFirstCmd: false,
}
)
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map() const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) { for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName'])) category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
@@ -186,13 +196,7 @@ export class NTQQFriendApi extends Service {
if (session) { if (session) {
return session.getBuddyService().isBuddy(uid) return session.getBuddyService().isBuddy(uid)
} else { } else {
return await invoke<boolean>({ return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
methodName: 'nodeIKernelBuddyService/isBuddy',
args: [
{ uid },
null,
],
})
} }
} }
} }

View File

@@ -1,11 +1,11 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify, GetFileListParam } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window' import { NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/eventTask' import { NTEventDispatch } from '@/common/utils/eventTask'
import { NodeIKernelGroupListener } from '../listeners' import { NodeIKernelGroupListener, OnGroupFileInfoUpdateParams } from '../listeners'
import { NodeIKernelGroupService } from '../services' import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc' import { isNumeric } from '@/common/utils/misc'
@@ -17,7 +17,9 @@ declare module 'cordis' {
} }
export class NTQQGroupApi extends Service { export class NTQQGroupApi extends Service {
private groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>() static inject = ['ntWindowApi']
public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true) super(ctx, 'ntGroupApi', true)
@@ -41,45 +43,29 @@ export class NTQQGroupApi extends Service {
const result = await invoke<{ const result = await invoke<{
updateType: number updateType: number
groupList: Group[] groupList: Group[]
}>({ }>(
className: NTClass.NODE_STORE_API, 'getGroupList',
methodName: 'getGroupList', [],
cbCmd: ReceiveCmdS.GROUPS_STORE, {
afterFirstCmd: false, className: NTClass.NODE_STORE_API,
}) cbCmd: ReceiveCmdS.GROUPS_STORE,
afterFirstCmd: false,
}
)
return result.groupList return result.groupList
} }
} }
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession() const session = getSession()
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>> let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
if (session) { if (session) {
const groupService = session.getGroupService() 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) result = await groupService.getNextMemberList(sceneId, undefined, num)
} else { } else {
const sceneId = await invoke<string>({ const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
methodName: NTMethod.GROUP_MEMBER_SCENE, result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
})
result = await invoke<
ReturnType<NodeIKernelGroupService['getNextMemberList']>
>({
methodName: NTMethod.GROUP_MEMBERS,
args: [
{
sceneId,
num,
},
null,
],
})
} }
if (result.errCode !== 0) { if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg) throw ('获取群成员列表出错,' + result.errMsg)
@@ -144,23 +130,17 @@ export class NTQQGroupApi extends Service {
) )
return notifies return notifies
} else { } else {
invoke({ invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true })
methodName: ReceiveCmdS.GROUP_NOTIFY, return (await invoke<GroupNotifies>(
classNameIsRegister: true, 'nodeIKernelGroupService/getSingleScreenNotifies',
}) [{ doubt: false, startSeq: '', number: num }, null],
return (await invoke<GroupNotifies>({ {
methodName: NTMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: num }, null],
})).notifies
}
}
/** 27187 TODO */ cbCmd: ReceiveCmdS.GROUP_NOTIFY,
async delGroupFile(groupCode: string, files: string[]) { afterFirstCmd: false,
const session = getSession() }
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files) )).notifies
}
} }
async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
@@ -182,156 +162,91 @@ export class NTQQGroupApi extends Service {
} }
}) })
} else { } else {
return await invoke({ return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
methodName: NTMethod.HANDLE_GROUP_REQUEST, doubt: false,
args: [ operateMsg: {
{ operateType,
doubt: false, targetMsg: {
operateMsg: { seq,
operateType, type,
targetMsg: { groupCode,
seq, postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
},
}, },
null, },
], }, null])
})
} }
} }
async quitGroup(groupQQ: string) { async quitGroup(groupCode: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().quitGroup(groupQQ) return session.getGroupService().quitGroup(groupCode)
} else { } else {
return await invoke({ return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }, null])
methodName: NTMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
} }
} }
async kickMember( async kickMember(
groupQQ: string, groupCode: string,
kickUids: string[], kickUids: string[],
refuseForever = false, refuseForever = false,
kickReason = '', kickReason = '',
) { ) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason) return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
} else { } else {
return await invoke({ return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
methodName: NTMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
} }
} }
async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().setMemberShutUp(groupQQ, memList) return session.getGroupService().setMemberShutUp(groupCode, memList)
} else { } else {
return await invoke({ return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
methodName: NTMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
} }
} }
async banGroup(groupQQ: string, shutUp: boolean) { async banGroup(groupCode: string, shutUp: boolean) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().setGroupShutUp(groupQQ, shutUp) return session.getGroupService().setGroupShutUp(groupCode, shutUp)
} else { } else {
return await invoke({ return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }, null])
methodName: NTMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
} }
} }
async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName) return session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName)
} else { } else {
return await invoke({ return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }, null])
methodName: NTMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
} }
} }
async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role) return session.getGroupService().modifyMemberRole(groupCode, memberUid, role)
} else { } else {
return await invoke({ return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }, null])
methodName: NTMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
} }
} }
async setGroupName(groupQQ: string, groupName: string) { async setGroupName(groupCode: string, groupName: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().modifyGroupName(groupQQ, groupName, false) return session.getGroupService().modifyGroupName(groupCode, groupName, false)
} else { } else {
return await invoke({ return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }, null])
methodName: NTMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
} }
} }
async getGroupAtAllRemainCount(groupCode: string) { async getGroupRemainAtTimes(groupCode: string) {
return await invoke< return await invoke<
GeneralCallResult & { GeneralCallResult & {
atInfo: { atInfo: {
@@ -342,15 +257,7 @@ export class NTQQGroupApi extends Service {
canNotAtAllMsg: '' canNotAtAllMsg: ''
} }
} }
>({ >(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null])
methodName: NTMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [
{
groupCode,
},
null,
],
})
} }
/** 27187 TODO */ /** 27187 TODO */
@@ -382,4 +289,36 @@ export class NTQQGroupApi extends Service {
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数 // GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param) 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,5 +1,5 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult, TmpChatInfoApi } from '../services' import { GeneralCallResult } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/eventTask' import { NTEventDispatch } from '@/common/utils/eventTask'
@@ -23,6 +23,8 @@ function generateMsgId() {
} }
export class NTQQMsgApi extends Service { export class NTQQMsgApi extends Service {
static inject = ['ntUserApi']
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntMsgApi', true) super(ctx, 'ntMsgApi', true)
} }
@@ -32,41 +34,20 @@ export class NTQQMsgApi extends Service {
if (session) { if (session) {
return session.getMsgService().getTempChatInfo(chatType, peerUid) return session.getMsgService().getTempChatInfo(chatType, peerUid)
} else { } else {
return await invoke<TmpChatInfoApi>({ return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
methodName: 'nodeIKernelMsgService/getTempChatInfo',
args: [
{
chatType,
peerUid,
},
null,
],
})
} }
} }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { 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//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // 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 // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
const session = getSession() const session = getSession()
const emojiType = emojiId.length > 3 ? '2' : '1'
if (session) { 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 { } else {
return await invoke({ return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
methodName: NTMethod.EMOJI_LIKE,
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
} }
} }
@@ -75,33 +56,20 @@ export class NTQQMsgApi extends Service {
if (session) { if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId) return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else { } else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({ return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }, null])
methodName: NTMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
} }
} }
async activateChat(peer: Peer) { async activateChat(peer: Peer) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 20 }, null])
methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null],
})
} }
async activateChatAndGetHistory(peer: Peer) { async activateChatAndGetHistory(peer: Peer) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null])
methodName: NTMethod.ACTIVE_CHAT_HISTORY, }
// 参数似乎不是这样
args: [{ peer, cnt: 20 }, null], async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
}) return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null])
} }
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
@@ -111,60 +79,26 @@ export class NTQQMsgApi extends Service {
if (session) { if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds) return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else { } else {
return await invoke<GeneralCallResult & { return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsByMsgId',
args: [
{
peer,
msgIds,
},
null,
],
})
} }
} }
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() const session = getSession()
// 消息时间从旧到新 // 消息时间从旧到新
if (session) { if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder) return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else { } else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({ return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
methodName: NTMethod.HISTORY_MSG,
args: [
{
peer,
msgId,
cnt: count,
queryOrder: isReverseOrder,
},
null,
],
})
} }
} }
async recallMsg(peer: Peer, msgIds: string[]) { async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getMsgService().recallMsg({ return session.getMsgService().recallMsg(peer, msgIds)
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds)
} else { } else {
return await invoke({ return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
methodName: NTMethod.RECALL_MSG,
args: [
{
peer,
msgIds,
},
null,
],
})
} }
} }
@@ -196,19 +130,9 @@ export class NTQQMsgApi extends Service {
) )
msgList = data[1] msgList = data[1]
} else { } else {
const data = await invoke<{ msgList: RawMessage[] }>({ const data = await invoke<{ msgList: RawMessage[] }>(
methodName: 'nodeIKernelMsgService/sendMsg', '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: [
{ {
msgId: '0', msgId: '0',
peer, peer,
@@ -217,16 +141,23 @@ export class NTQQMsgApi extends Service {
}, },
null null
], ],
timeout {
}) cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
timeout
}
)
msgList = data.msgList msgList = data.msgList
} }
const retMsg = msgList.find(msgRecord => { return msgList.find(msgRecord => msgRecord.guildId === msgId)
if (msgRecord.guildId === msgId) {
return true
}
})
return retMsg!
} }
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
@@ -234,19 +165,13 @@ export class NTQQMsgApi extends Service {
if (session) { if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], []) return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else { } else {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.FORWARD_MSG, [{
methodName: NTMethod.FORWARD_MSG, msgIds,
args: [ srcContact: srcPeer,
{ dstContacts: [destPeer],
msgIds: msgIds, commentElements: [],
srcContact: srcPeer, msgAttributeInfos: new Map(),
dstContacts: [destPeer], }, null])
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
} }
} }
@@ -282,19 +207,9 @@ export class NTQQMsgApi extends Service {
) )
msgList = data[1] msgList = data[1]
} else { } else {
const data = await invoke<{ msgList: RawMessage[] }>({ const data = await invoke<{ msgList: RawMessage[] }>(
methodName: 'nodeIKernelMsgService/multiForwardMsgWithComment', 'nodeIKernelMsgService/multiForwardMsgWithComment',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', [
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
args: [
{ {
msgInfos, msgInfos,
srcContact: srcPeer, srcContact: srcPeer,
@@ -304,7 +219,19 @@ export class NTQQMsgApi extends Service {
}, },
null, null,
], ],
}) {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
}
)
msgList = data.msgList msgList = data.msgList
} }
for (const msg of msgList) { for (const msg of msgList) {
@@ -323,61 +250,62 @@ export class NTQQMsgApi extends Service {
throw new Error('转发消息超时') throw new Error('转发消息超时')
} }
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() const session = getSession()
if (session) { if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z) return await session.getMsgService().getMsgsBySeqAndCount(peer, msgSeq, count, desc, z)
} else { } else {
return await invoke<GeneralCallResult & { return await invoke('nodeIKernelMsgService/getMsgsBySeqAndCount', [{
msgList: RawMessage[] peer,
}>({ cnt: count,
methodName: 'nodeIKernelMsgService/getMsgsBySeqAndCount', msgSeq,
args: [ queryOrder: desc
{ }, null])
peer,
cnt: count,
msgSeq: seq,
queryOrder: desc
},
null,
],
})
} }
} }
/** 27187 TODO */ async getSingleMsg(peer: Peer, msgSeq: string) {
async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: count,
})
return ret
}
async getSingleMsg(peer: Peer, seq: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return await session.getMsgService().getSingleMsg(peer, seq) return await session.getMsgService().getSingleMsg(peer, msgSeq)
} else { } else {
return await invoke<GeneralCallResult & { return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getSingleMsg',
args: [
{
peer,
msgSeq: seq,
},
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: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, 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,10 +1,9 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners' import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/eventTask' import { NTEventDispatch } from '@/common/utils/eventTask'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
@@ -18,21 +17,23 @@ declare module 'cordis' {
} }
export class NTQQUserApi extends Service { export class NTQQUserApi extends Service {
static inject = ['ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntUserApi', true) super(ctx, 'ntUserApi', true)
} }
async setQQAvatar(filePath: string) { async setQQAvatar(path: string) {
return await invoke<GeneralCallResult>({ return await invoke(
methodName: NTMethod.SET_QQ_AVATAR, 'nodeIKernelProfileService/setHeader',
args: [ [
{ { path },
path: filePath,
},
null, null,
], ],
timeout: 10 * Time.second, // 10秒不一定够 {
}) timeout: 10 * Time.second, // 10秒不一定够
}
)
} }
async fetchUserDetailInfo(uid: string) { async fetchUserDetailInfo(uid: string) {
@@ -55,12 +56,9 @@ export class NTQQUserApi extends Service {
) )
info = profile info = profile
} else { } else {
const result = await invoke<{ info: UserDetailInfoListenerArg }>({ const result = await invoke<{ info: UserDetailInfoListenerArg }>(
methodName: 'nodeIKernelProfileService/fetchUserDetailInfo', 'nodeIKernelProfileService/fetchUserDetailInfo',
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged', [
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
args: [
{ {
callFrom: 'BuddyProfileStore', callFrom: 'BuddyProfileStore',
uid: [uid], uid: [uid],
@@ -69,7 +67,12 @@ export class NTQQUserApi extends Service {
}, },
null null
], ],
}) {
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
}
)
info = result.info info = result.info
} }
const ret: User = { const ret: User = {
@@ -104,19 +107,21 @@ export class NTQQUserApi extends Service {
) )
return profile return profile
} else { } else {
const result = await invoke<{ info: User }>({ const result = await invoke<{ info: User }>(
methodName: 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo', 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', [
afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid,
args: [
{ {
uid, uid,
bizList: [0] bizList: [0]
}, },
null, null,
], ],
}) {
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid,
}
)
return result.info return result.info
} }
} }
@@ -139,7 +144,7 @@ export class NTQQUserApi extends Service {
} }
const uin = selfInfo.uin 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 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 return cookies
} }
@@ -165,9 +170,9 @@ export class NTQQUserApi extends Service {
doLikeTollCount: 0 doLikeTollCount: 0
}) })
} else { } else {
return await invoke<GeneralCallResult & { succCounts: number }>({ return await invoke(
methodName: 'nodeIKernelProfileLikeService/setBuddyProfileLike', 'nodeIKernelProfileLikeService/setBuddyProfileLike',
args: [ [
{ {
doLikeUserInfo: { doLikeUserInfo: {
friendUid: uid, friendUid: uid,
@@ -178,20 +183,35 @@ export class NTQQUserApi extends Service {
}, },
null, null,
], ],
}) )
} }
} }
async getUidByUinV1(Uin: string) { async getUidByUinV1(uin: string) {
const session = getSession() const session = getSession()
// 通用转换开始尝试 // 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin) let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) { if (!uid) {
let unveifyUid = (await this.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三 for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
if (unveifyUid.indexOf('*') == -1) { for (const member of membersList.values()) {
if (member.uin === uin) {
uid = member.uid
break
}
}
if (uid) break
}
}
if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid uid = unveifyUid
} }
} }
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid return uid
} }
@@ -205,43 +225,22 @@ export class NTQQUserApi extends Service {
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin) uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
if (uid) return uid if (uid) return uid
} else { } else {
let uid = (await invoke<{ uids: Map<string, string> }>({ let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
methodName: 'nodeIKernelGroupService/getUidByUins',
args: [
{ uin: [uin] },
null,
],
})).uids.get(uin)
if (uid) return uid if (uid) return uid
uid = (await invoke<Map<string, string>>({ uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
methodName: 'nodeIKernelProfileService/getUidByUin',
args: [
{
callFrom: 'FriendsServiceImpl',
uin: [uin],
},
null,
],
})).get(uin)
if (uid) return uid if (uid) return uid
uid = (await invoke<{ uidInfo: Map<string, string> }>({ uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
methodName: 'nodeIKernelUixConvertService/getUid',
args: [
{ uin: [uin] },
null,
],
})).uidInfo.get(uin)
if (uid) return uid if (uid) return uid
} }
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换 const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid if (unveifyUid.indexOf('*') == -1) return unveifyUid
} }
async getUidByUin(Uin: string) { async getUidByUin(uin: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return await this.getUidByUinV2(Uin) return this.getUidByUinV2(uin)
} }
return await this.getUidByUinV1(Uin) return this.getUidByUinV1(uin)
} }
async getUserDetailInfoByUinV2(uin: string) { async getUserDetailInfoByUinV2(uin: string) {
@@ -253,35 +252,35 @@ export class NTQQUserApi extends Service {
uin uin
) )
} else { } else {
return await invoke<UserDetailInfoByUinV2>({ return await invoke<UserDetailInfoByUinV2>(
methodName: 'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
args: [ [
{ uin }, { uin },
null, null,
], ],
}) )
} }
} }
async getUserDetailInfoByUin(Uin: string) { async getUserDetailInfoByUin(uin: string) {
return NTEventDispatch.CallNoListenerEvent return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>( <(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin', 'NodeIKernelProfileService/getUserDetailInfoByUin',
5000, 5000,
Uin uin
) )
} }
async getUinByUidV1(Uid: string) { async getUinByUidV1(uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>( <(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin', 'NodeIKernelUixConvertService/getUin',
5000, 5000,
[Uid] [uid]
) )
let uin = ret.uinInfo.get(Uid) let uin = ret.uinInfo.get(uid)
if (!uin) { if (!uin) {
uin = (await this.getUserDetailInfo(Uid)).uin //从QQ Native 转换 uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
} }
return uin return uin
} }
@@ -295,46 +294,25 @@ export class NTQQUserApi extends Service {
if (uin) return uin if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid) uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin if (uin) return uin
return uin
} else { } else {
let uin = (await invoke<{ uins: Map<string, string> }>({ let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
methodName: 'nodeIKernelGroupService/getUinByUids',
args: [
{ uid: [uid] },
null,
],
})).uins.get(uid)
if (uin) return uin if (uin) return uin
uin = (await invoke<Map<string, string>>({ uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
methodName: 'nodeIKernelProfileService/getUinByUid',
args: [
{
callFrom: 'FriendsServiceImpl',
uid: [uid],
},
null,
],
})).get(uid)
if (uin) return uin if (uin) return uin
uin = (await invoke<{ uinInfo: Map<string, string> }>({ uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
methodName: 'nodeIKernelUixConvertService/getUin',
args: [
{ uid: [uid] },
null,
],
})).uinInfo.get(uid)
if (uin) return uin if (uin) return uin
} }
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).getKey(uid) let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
return uin
} }
async getUinByUid(Uid: string) { async getUinByUid(uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return (await this.getUinByUidV2(Uid))! return this.getUinByUidV2(uid)
} }
return await this.getUinByUidV1(Uid) return this.getUinByUidV1(uid)
} }
async forceFetchClientKey() { async forceFetchClientKey() {
@@ -342,12 +320,7 @@ export class NTQQUserApi extends Service {
if (session) { if (session) {
return await session.getTicketService().forceFetchClientKey('') return await session.getTicketService().forceFetchClientKey('')
} else { } else {
return await invoke<forceFetchClientKeyRetType>({ return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ domain: '' }, null])
methodName: 'nodeIKernelTicketService/forceFetchClientKey',
args: [{
domain: ''
}, null],
})
} }
} }
@@ -361,4 +334,14 @@ export class NTQQUserApi extends Service {
} }
return selfInfo.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,5 +1,6 @@
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { Dict } from 'cosmokit'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@@ -49,56 +50,6 @@ interface WebApiGroupMemberRet {
extmode: number 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 { interface GroupEssenceMsg {
group_code: string group_code: string
msg_seq: number msg_seq: number
@@ -124,7 +75,33 @@ export interface GroupEssenceMsgRet {
} }
} }
interface SetGroupNoticeParams {
groupCode: string
content: string
pinned: number
type: number
isShowEditCard: number
tipWindowType: number
confirmRequired: number
picId: string
imgWidth?: number
imgHeight?: number
}
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 { export class NTQQWebApi extends Service {
static inject = ['ntUserApi']
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntWebApi', true) super(ctx, 'ntWebApi', true)
} }
@@ -132,7 +109,7 @@ export class NTQQWebApi extends Service {
async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com') const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') const cookieStr = this.cookieToString(cookieObject)
const retList: Promise<WebApiGroupMemberRet>[] = [] const retList: Promise<WebApiGroupMemberRet>[] = []
const params = new URLSearchParams({ const params = new URLSearchParams({
st: '0', st: '0',
@@ -171,49 +148,47 @@ export class NTQQWebApi extends Service {
} }
genBkn(sKey: string) { genBkn(sKey: string) {
sKey = sKey || ''; sKey = sKey || ''
let hash = 5381; let hash = 5381
for (let i = 0; i < sKey.length; i++) { for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i); const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code; hash = hash + (hash << 5) + code
} }
return (hash & 0x7FFFFFFF).toString()
return (hash & 0x7FFFFFFF).toString();
} }
//实现未缓存 考虑2h缓存 //实现未缓存 考虑2h缓存
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => { const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString(); let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString()
let res = ''; let res = ''
let resJson; let resJson
try { try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr }); res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr })
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); const match = res.match(/window\.__INITIAL_STATE__=(.*?);/)
if (match) { if (match) {
resJson = JSON.parse(match[1].trim()); resJson = JSON.parse(match[1].trim())
} }
if (Internal_type === 1) { if (Internal_type === 1) {
return resJson?.talkativeList; return resJson?.talkativeList
} else { } else {
return resJson?.actorList; return resJson?.actorList
} }
} catch (e) { } catch (e) {
this.ctx.logger.error('获取当前群荣耀失败', url, e); this.ctx.logger.error('获取当前群荣耀失败', url, e)
} }
return undefined; return undefined
} }
let HonorInfo: any = { group_id: groupCode }; let HonorInfo: any = { group_id: groupCode }
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com') const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') const cookieStr = this.cookieToString(cookieObject)
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 1); let RetInternal = await getDataInternal(groupCode, 1)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取龙王信息失败'); throw new Error('获取龙王信息失败')
} }
HonorInfo.current_talkative = { HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin, user_id: RetInternal[0]?.uin,
@@ -230,17 +205,17 @@ export class NTQQWebApi extends Service {
description: talkative_ele?.desc, description: talkative_ele?.desc,
day_count: 0, day_count: 0,
nickname: talkative_ele?.name nickname: talkative_ele?.name
}); })
} }
} catch (e) { } catch (e) {
this.ctx.logger.error(e); this.ctx.logger.error(e)
} }
} }
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 2); let RetInternal = await getDataInternal(groupCode, 2)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取群聊之火失败'); throw new Error('获取群聊之火失败')
} }
HonorInfo.performer_list = []; HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) { for (const performer_ele of RetInternal) {
@@ -249,54 +224,86 @@ export class NTQQWebApi extends Service {
nickname: performer_ele?.name, nickname: performer_ele?.name,
avatar: performer_ele?.avatar, avatar: performer_ele?.avatar,
description: performer_ele?.desc description: performer_ele?.desc
}); })
} }
} catch (e) { } catch (e) {
this.ctx.logger.error(e); this.ctx.logger.error(e)
} }
} }
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 3); let RetInternal = await getDataInternal(groupCode, 3)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取群聊炽焰失败'); throw new Error('获取群聊炽焰失败')
} }
HonorInfo.legend_list = []; HonorInfo.legend_list = []
for (const legend_ele of RetInternal) { for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({ HonorInfo.legend_list.push({
user_id: legend_ele?.uin, user_id: legend_ele?.uin,
nickname: legend_ele?.name, nickname: legend_ele?.name,
avatar: legend_ele?.avatar, avatar: legend_ele?.avatar,
desc: legend_ele?.description desc: legend_ele?.description
}); })
} }
} catch (e) { } catch (e) {
this.ctx.logger.error('获取群聊炽焰失败', e); this.ctx.logger.error('获取群聊炽焰失败', e)
} }
} }
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 6); let RetInternal = await getDataInternal(groupCode, 6)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取快乐源泉失败'); throw new Error('获取快乐源泉失败')
} }
HonorInfo.emotion_list = []; HonorInfo.emotion_list = []
for (const emotion_ele of RetInternal) { for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({ HonorInfo.emotion_list.push({
user_id: emotion_ele?.uin, user_id: emotion_ele?.uin,
nickname: emotion_ele?.name, nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar, avatar: emotion_ele?.avatar,
desc: emotion_ele?.description desc: emotion_ele?.description
}); })
} }
} catch (e) { } catch (e) {
this.ctx.logger.error('获取快乐源泉失败', e); this.ctx.logger.error('获取快乐源泉失败', e)
} }
} }
//冒尖小春笋好像已经被tx扬了 //冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { 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

@@ -42,13 +42,15 @@ export class NTQQWindowApi extends Service {
cbCmd: ReceiveCmd | undefined, cbCmd: ReceiveCmd | undefined,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await invoke<R>({ const result = await invoke<R>(
className: NTClass.WINDOW_API, NTMethod.OPEN_EXTRA_WINDOW,
methodName: NTMethod.OPEN_EXTRA_WINDOW, [ntQQWindow.windowName, ...args],
cbCmd, {
afterFirstCmd: false, className: NTClass.WINDOW_API,
args: [ntQQWindow.windowName, ...args], cbCmd,
}) afterFirstCmd: false,
}
)
setTimeout(() => { setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) { for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL()) // log("close window", w.webContents.getURL())

View File

@@ -33,12 +33,11 @@ declare module 'cordis' {
'nt/group-notify': (input: GroupNotify[]) => void 'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest[]) => void 'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string; members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string; members: GroupMember[] }) => void
'nt/friend-list-updated': (input: { groupCode: string; members: GroupMember[] }) => void
} }
} }
class Core extends Service { class Core extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi'] static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context, public config: Core.Config) { constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true) super(ctx, 'app', true)

View File

@@ -1,3 +1,5 @@
import ffmpeg from 'fluent-ffmpeg'
import faceConfig from './helper/face_config.json'
import { import {
AtType, AtType,
ElementType, ElementType,
@@ -13,22 +15,16 @@ import {
SendTextElement, SendTextElement,
SendVideoElement, SendVideoElement,
} from './types' } from './types'
import { promises as fs } from 'node:fs' import { stat, writeFile, copyFile, unlink } from 'node:fs/promises'
import ffmpeg from 'fluent-ffmpeg'
import { calculateFileMD5, isGIF } from '../common/utils/file' import { calculateFileMD5, isGIF } from '../common/utils/file'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import faceConfig from './helper/face_config.json'
import { Context } from 'cordis' import { Context } from 'cordis'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName //export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export namespace SendMsgElementConstructor {
export function poke(groupCode: string, uin: string) {
return null
}
export namespace SendElementEntities {
export function text(content: string): SendTextElement { export function text(content: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
@@ -123,7 +119,7 @@ export namespace SendMsgElementConstructor {
export async function video(ctx: Context, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
try { try {
await fs.stat(filePath) await stat(filePath)
} catch (e) { } catch (e) {
throw `文件${filePath}异常,不存在` throw `文件${filePath}异常,不存在`
} }
@@ -165,7 +161,7 @@ export namespace SendMsgElementConstructor {
function useDefaultThumb() { function useDefaultThumb() {
if (completed) return if (completed) return
ctx.logger.info('获取视频封面失败,使用默认封面') ctx.logger.info('获取视频封面失败,使用默认封面')
fs.writeFile(thumbPath, defaultVideoThumb) writeFile(thumbPath, defaultVideoThumb)
.then(() => { .then(() => {
resolve(thumbPath) resolve(thumbPath)
}) })
@@ -176,7 +172,7 @@ export namespace SendMsgElementConstructor {
ffmpeg(filePath) ffmpeg(filePath)
.on('error', (err) => { .on('error', (err) => {
if (diyThumbPath) { if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath) copyFile(diyThumbPath, thumbPath)
.then(() => { .then(() => {
completed = true completed = true
resolve(thumbPath) resolve(thumbPath)
@@ -201,7 +197,7 @@ export namespace SendMsgElementConstructor {
let thumbPath = new Map() let thumbPath = new Map()
const _thumbPath = await createThumb const _thumbPath = await createThumb
ctx.logger.info('生成视频缩略图', _thumbPath) ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await fs.stat(_thumbPath)).size const thumbSize = (await stat(_thumbPath)).size
// log("生成缩略图", _thumbPath) // log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
@@ -246,7 +242,7 @@ export namespace SendMsgElementConstructor {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
if (converted) { if (converted) {
fs.unlink(silkPath).then() unlink(silkPath)
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
@@ -299,7 +295,7 @@ export namespace SendMsgElementConstructor {
} }
} }
export function mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement { export function mface(emojiPackageId: number, emojiId: string, key: string, summary?: string): SendMarketFaceElement {
return { return {
elementType: ElementType.MFACE, elementType: ElementType.MFACE,
marketFaceElement: { marketFaceElement: {
@@ -308,14 +304,13 @@ export namespace SendMsgElementConstructor {
emojiPackageId, emojiPackageId,
emojiId, emojiId,
key, key,
faceName: faceName || mFaceCache.get(emojiId) || '[商城表情]', faceName: summary || '[商城表情]',
}, },
} }
} }
export function dice(resultId: number | null): SendFaceElement { export function dice(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果 // 实际测试并不能控制结果
// 随机1到6 // 随机1到6
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1 if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return { return {
@@ -329,7 +324,7 @@ export namespace SendMsgElementConstructor {
stickerId: '33', stickerId: '33',
sourceType: 1, sourceType: 1,
stickerType: 2, stickerType: 2,
resultId: resultId?.toString(), resultId: resultId.toString(),
surpriseId: '', surpriseId: '',
// "randomType": 1, // "randomType": 1,
}, },
@@ -337,7 +332,7 @@ export namespace SendMsgElementConstructor {
} }
// 猜拳(石头剪刀布)表情 // 猜拳(石头剪刀布)表情
export function rps(resultId: number | null): SendFaceElement { export function rps(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果 // 实际测试并不能控制结果
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1 if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return { return {
@@ -351,7 +346,7 @@ export namespace SendMsgElementConstructor {
stickerId: '34', stickerId: '34',
sourceType: 1, sourceType: 1,
stickerType: 2, stickerType: 2,
resultId: resultId?.toString(), resultId: resultId.toString(),
surpriseId: '', surpriseId: '',
// "randomType": 1, // "randomType": 1,
}, },

View File

@@ -26,7 +26,7 @@ export const ReceiveCmdS = {
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate', SKEY_UPDATE: 'onSkeyUpdate',
} } as const
export type ReceiveCmd = string export type ReceiveCmd = string
@@ -56,7 +56,7 @@ const callHooks: Array<{
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
const originalSend = window.webContents.send const originalSend = window.webContents.send
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
try { try {
@@ -65,34 +65,36 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
log(`received ntqq api message: ${channel}`, args) log(`received ntqq api message: ${channel}`, args)
} }
} catch { } } catch { }
if (args?.[1] instanceof Array) { if (!onlyLog) {
for (const receiveData of args?.[1]) { if (args?.[1] instanceof Array) {
const ntQQApiMethodName = receiveData.cmdName for (const receiveData of args?.[1]) {
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) const ntQQApiMethodName = receiveData.cmdName
for (const hook of receiveHooks) { // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
if (hook.method.includes(ntQQApiMethodName)) { for (const hook of receiveHooks) {
new Promise((resolve, reject) => { if (hook.method.includes(ntQQApiMethodName)) {
try { new Promise((resolve, reject) => {
hook.hookFunc(receiveData.payload) try {
} catch (e: any) { hook.hookFunc(receiveData.payload)
log('hook error', ntQQApiMethodName, e.stack.toString()) } catch (e: any) {
} log('hook error', ntQQApiMethodName, e.stack.toString())
resolve(undefined) }
}).then() resolve(undefined)
}).then()
}
} }
} }
} }
} if (args[0]?.callbackId) {
if (args[0]?.callbackId) { // log("hookApiCallback", hookApiCallbacks, args)
// log("hookApiCallback", hookApiCallbacks, args) const callbackId = args[0].callbackId
const callbackId = args[0].callbackId if (hookApiCallbacks[callbackId]) {
if (hookApiCallbacks[callbackId]) { // log("callback found")
// log("callback found") new Promise((resolve, reject) => {
new Promise((resolve, reject) => { hookApiCallbacks[callbackId](args[1])
hookApiCallbacks[callbackId](args[1]) resolve(undefined)
resolve(undefined) }).then()
}).then() delete hookApiCallbacks[callbackId]
delete hookApiCallbacks[callbackId] }
} }
} }
originalSend.call(window.webContents, channel, ...args) originalSend.call(window.webContents, channel, ...args)
@@ -100,7 +102,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
window.webContents.send = patchSend window.webContents.send = patchSend
} }
export function hookNTQQApiCall(window: BrowserWindow) { export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
// 监听调用NTQQApi // 监听调用NTQQApi
let webContents = window.webContents as any let webContents = window.webContents as any
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message'] const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
@@ -116,23 +118,25 @@ export function hookNTQQApiCall(window: BrowserWindow) {
try { try {
logHook && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', thisArg, args)
} catch (e) { } } catch (e) { }
try { if (!onlyLog) {
const _args: unknown[] = args[3][1] try {
const cmdName: NTMethod = _args[0] as NTMethod const _args: unknown[] = args[3][1]
const callParams = _args.slice(1) const cmdName: NTMethod = _args[0] as NTMethod
callHooks.forEach((hook) => { const callParams = _args.slice(1)
if (hook.method.includes(cmdName)) { callHooks.forEach((hook) => {
new Promise((resolve, reject) => { if (hook.method.includes(cmdName)) {
try { new Promise((resolve, reject) => {
hook.hookFunc(callParams) try {
} catch (e: any) { hook.hookFunc(callParams)
log('hook call error', e, _args) } catch (e: any) {
} log('hook call error', e, _args)
resolve(undefined) }
}).then() resolve(undefined)
} }).then()
}) }
} catch (e) { } })
} catch (e) { }
}
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
}, },

View File

@@ -23,15 +23,55 @@ export interface OnRichMediaDownloadCompleteParams {
userUsedSpacePerDay: unknown | null userUsedSpacePerDay: unknown | null
} }
export interface onGroupFileInfoUpdateParamType { export interface OnGroupFileInfoUpdateParams {
retCode: number retCode: number
retMsg: string retMsg: string
clientWording: string clientWording: string
isEnd: boolean isEnd: boolean
item: Array<any> item: {
allFileCount: string peerId: string
nextIndex: string type: number
reqId: string 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 onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType): void onGroupFileInfoUpdate(groupFileListResult: OnGroupFileInfoUpdateParams): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
@@ -295,7 +335,7 @@ export class MsgListener implements IKernelMsgListener {
} }
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType) { onGroupFileInfoUpdate(groupFileListResult: OnGroupFileInfoUpdateParams) {
} }

View File

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

View File

@@ -3,7 +3,7 @@ import {
GroupExtParam, GroupExtParam,
GroupMember, GroupMember,
GroupMemberRole, GroupMemberRole,
GroupNotifyTypes, GroupNotifyType,
GroupRequestOperateTypes, GroupRequestOperateTypes,
} from '@/ntqqapi/types' } from '@/ntqqapi/types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
@@ -181,7 +181,7 @@ export interface NodeIKernelGroupService {
operateType: GroupRequestOperateTypes, // 2 拒绝 operateType: GroupRequestOperateTypes, // 2 拒绝
targetMsg: { targetMsg: {
seq: string, // 通知序列号 seq: string, // 通知序列号
type: GroupNotifyTypes, type: GroupNotifyType,
groupCode: string, groupCode: string,
postscript: string postscript: string
} }

View File

@@ -110,8 +110,6 @@ export interface NodeIKernelMsgService {
resendMsg(...args: unknown[]): unknown resendMsg(...args: unknown[]): unknown
recallMsg(...args: unknown[]): unknown
reeditRecallMsg(...args: unknown[]): unknown reeditRecallMsg(...args: unknown[]): unknown
//调用请检查除开commentElements其余参数不能为null //调用请检查除开commentElements其余参数不能为null
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult> forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
@@ -170,9 +168,10 @@ export interface NodeIKernelMsgService {
getLastMessageList(peer: Peer[]): Promise<unknown> 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> getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & { getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {

View File

@@ -74,7 +74,7 @@ export interface NodeIKernelProfileService {
setGander(...args: unknown[]): Promise<unknown> setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown> setHeader(arg: string): Promise<GeneralCallResult>
setRecommendImgFlag(...args: unknown[]): Promise<unknown> setRecommendImgFlag(...args: unknown[]): Promise<unknown>

View File

@@ -173,14 +173,10 @@ export interface NodeIKernelRichMediaService {
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): 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 downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(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 deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown

View File

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

View File

@@ -275,7 +275,7 @@ export interface PicElement {
thumbPath: Map<number, string> thumbPath: Map<number, string>
picWidth: number picWidth: number
picHeight: number picHeight: number
fileSize: number fileSize: string
fileName: string fileName: string
fileUuid: string fileUuid: string
md5HexStr?: string md5HexStr?: string
@@ -480,6 +480,8 @@ export interface RawMessage {
sourceMsgIsIncPic: boolean // 原消息是否有图片 sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
senderUidStr: string
replyMsgTime: string
} }
textElement: { textElement: {
atType: AtType atType: AtType

View File

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

View File

@@ -11,7 +11,8 @@ import {
NodeIKernelTipOffService, NodeIKernelTipOffService,
NodeIKernelSearchService NodeIKernelSearchService
} from './services' } from './services'
import os from 'node:os' import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process') const Process = require('node:process')
export interface NodeIQQNTWrapperSession { export interface NodeIQQNTWrapperSession {
@@ -72,7 +73,7 @@ const constructor = [
Process.dlopenOrig = Process.dlopen 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) const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) { for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], { module.exports[export_name] = new Proxy(module.exports[export_name], {

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

@@ -6,7 +6,7 @@ interface Payload {
message_id: number | string message_id: number | string
} }
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> { export class DelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg; actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {

View File

@@ -2,15 +2,16 @@ import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: string | number group_id: string | number
file_id: string file_id: string
busid?: 102 busid?: 102
} }
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> { export class DelGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFile actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) { async _handle(payload: Payload) {
await this.ctx.ntGroupApi.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

@@ -6,6 +6,7 @@ import { ActionName } from '../types'
import { calculateFileMD5, fetchFile } from '@/common/utils' import { calculateFileMD5, fetchFile } from '@/common/utils'
import { TEMP_DIR } from '@/common/globalVars' import { TEMP_DIR } from '@/common/globalVars'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Dict } from 'cosmokit'
interface Payload { interface Payload {
thread_count?: number thread_count?: number
@@ -19,7 +20,7 @@ interface FileResponse {
file: string file: string
} }
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> { export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile actionName = ActionName.GoCQHTTP_DownloadFile
protected async _handle(payload: Payload): Promise<FileResponse> { protected async _handle(payload: Payload): Promise<FileResponse> {
@@ -51,7 +52,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
} }
getHeaders(headersIn?: string | string[]): Record<string, string> { getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {} const headers: Dict = {}
if (typeof headersIn == 'string') { if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]') headersIn = headersIn.split('[\\r\\n]')
} }

View File

@@ -1,6 +1,6 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types' import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
@@ -13,7 +13,7 @@ interface Response {
messages: (OB11Message & { content: OB11MessageData })[] messages: (OB11Message & { content: OB11MessageData })[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> { export class GetForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const msgId = payload.id || payload.message_id const msgId = payload.id || payload.message_id
@@ -32,7 +32,7 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
const msgList = data.msgList const msgList = data.msgList
const messages = await Promise.all( const messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
const resMsg = await OB11Constructor.message(this.ctx, msg) const resMsg = await OB11Entities.message(this.ctx, msg)
resMsg.message_id = MessageUnique.createMsg({ resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,

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,14 +2,14 @@ import BaseAction from '../BaseAction'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { RawMessage } from '@/ntqqapi/types' import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
message_seq?: number message_seq?: number | string
count?: number count?: number | string
reverseOrder?: boolean reverseOrder?: boolean
} }
@@ -17,7 +17,7 @@ interface Response {
messages: OB11Message[] messages: OB11Message[]
} }
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> { export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
@@ -27,20 +27,20 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
let msgList: RawMessage[] | undefined let msgList: RawMessage[] | undefined
// 包含 message_seq 0 // 包含 message_seq 0
if (!payload.message_seq) { if (!payload.message_seq) {
msgList = (await this.ctx.ntMsgApi.getLastestMsgByUids(peer, count))?.msgList msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else { } else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在` if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, count)).msgList msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
} }
if (!msgList?.length) throw '未找到消息' if (!msgList?.length) throw new Error('未找到消息')
if (isReverseOrder) msgList.reverse() if (isReverseOrder) msgList.reverse()
await Promise.all( await Promise.all(
msgList.map(async msg => { msgList.map(async msg => {
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId) msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
}) })
) )
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(this.ctx, msg))) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Entities.message(this.ctx, msg)))
return { messages: ob11MsgList } 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(payload: void) {
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,6 +1,6 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { OB11UserSex } from '../../types' import { OB11UserSex } from '../../types'
@@ -10,7 +10,7 @@ interface Payload {
user_id: number | string user_id: number | string
} }
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> { export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: Payload): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
@@ -33,7 +33,7 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
return ret return ret
} }
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) } const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data) return OB11Entities.stranger(data)
} else { } else {
const user_id = payload.user_id.toString() const user_id = payload.user_id.toString()
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id) const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id)
@@ -52,7 +52,7 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
return ret return ret
} }
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) } const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data) return OB11Entities.stranger(data)
} }
} }
} }

View File

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

View File

@@ -7,7 +7,7 @@ interface Payload {
operation: QuickOperation operation: QuickOperation
} }
export class GoCQHTTHandleQuickOperation extends BaseAction<Payload, null> { export class HandleQuickOperation extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_HandleQuickOperation actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e)) handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e))

View File

@@ -3,7 +3,7 @@ import { OB11PostSendMsg } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { convertMessage2List } from '../../helper/createMessage' import { convertMessage2List } from '../../helper/createMessage'
export class GoCQHTTPSendForwardMsg extends SendMsg { export class SendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg actionName = ActionName.GoCQHTTP_SendForwardMsg
protected async check(payload: OB11PostSendMsg) { protected async check(payload: OB11PostSendMsg) {
@@ -12,10 +12,10 @@ export class GoCQHTTPSendForwardMsg extends SendMsg {
} }
} }
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg { export class SendPrivateForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
} }
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg { export class SendGroupForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg 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

@@ -6,7 +6,7 @@ interface Payload {
message_id: number | string message_id: number | string
} }
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> { export class SetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg; actionName = ActionName.GoCQHTTP_SetEssenceMsg;
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {

View File

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

View File

@@ -1,5 +1,5 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -13,7 +13,7 @@ class GetGroupInfo extends BaseAction<Payload, OB11Group> {
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const group = (await this.ctx.ntGroupApi.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) { if (group) {
return OB11Constructor.group(group) return OB11Entities.group(group)
} else { } else {
throw `${payload.group_id}不存在` throw `${payload.group_id}不存在`
} }

View File

@@ -1,5 +1,5 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -12,7 +12,7 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const groupList = await this.ctx.ntGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true') const groupList = await this.ctx.ntGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true')
return OB11Constructor.groups(groupList) return OB11Entities.groups(groupList)
} }
} }

View File

@@ -1,6 +1,6 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
@@ -22,7 +22,7 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
//log('群成员详细信息结果', info) //log('群成员详细信息结果', info)
Object.assign(member, info) Object.assign(member, info)
} }
const ret = OB11Constructor.groupMember(payload.group_id.toString(), member) const ret = OB11Entities.groupMember(payload.group_id.toString(), member)
const self = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), selfInfo.uid) const self = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), selfInfo.uid)
if (self?.role === 3 || self?.role === 4) { if (self?.role === 3 || self?.role === 4) {
const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString()) const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString())

View File

@@ -1,5 +1,5 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -17,7 +17,7 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
const groupMembersArr = Array.from(groupMembers.values()) const groupMembersArr = Array.from(groupMembers.values())
let _groupMembers = groupMembersArr.map(item => { 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>() const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()

View File

@@ -1,3 +1,4 @@
import type Adapter from '../adapter'
import GetMsg from './msg/GetMsg' import GetMsg from './msg/GetMsg'
import GetLoginInfo from './system/GetLoginInfo' import GetLoginInfo from './system/GetLoginInfo'
import { GetFriendList, GetFriendWithCategory } from './user/GetFriendList' import { GetFriendList, GetFriendWithCategory } from './user/GetFriendList'
@@ -15,11 +16,11 @@ import CanSendRecord from './system/CanSendRecord'
import CanSendImage from './system/CanSendImage' import CanSendImage from './system/CanSendImage'
import GetStatus from './system/GetStatus' import GetStatus from './system/GetStatus'
import { import {
GoCQHTTPSendForwardMsg, SendForwardMsg,
GoCQHTTPSendGroupForwardMsg, SendGroupForwardMsg,
GoCQHTTPSendPrivateForwardMsg, SendPrivateForwardMsg,
} from './go-cqhttp/SendForwardMsg' } from './go-cqhttp/SendForwardMsg'
import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo' import { GetStrangerInfo } from './go-cqhttp/GetStrangerInfo'
import SendLike from './user/SendLike' import SendLike from './user/SendLike'
import SetGroupAddRequest from './group/SetGroupAddRequest' import SetGroupAddRequest from './group/SetGroupAddRequest'
import SetGroupLeave from './group/SetGroupLeave' import SetGroupLeave from './group/SetGroupLeave'
@@ -34,27 +35,33 @@ import SetGroupAdmin from './group/SetGroupAdmin'
import SetGroupCard from './group/SetGroupCard' import SetGroupCard from './group/SetGroupCard'
import GetImage from './file/GetImage' import GetImage from './file/GetImage'
import GetRecord from './file/GetRecord' import GetRecord from './file/GetRecord'
import GoCQHTTPMarkMsgAsRead from './msg/MarkMsgAsRead' import { MarkMsgAsRead } from './go-cqhttp/MarkMsgAsRead'
import CleanCache from './system/CleanCache' 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 { GetConfigAction, SetConfigAction } from './llonebot/Config'
import GetGroupAddRequest from './llonebot/GetGroupAddRequest' import GetGroupAddRequest from './llonebot/GetGroupAddRequest'
import SetQQAvatar from './llonebot/SetQQAvatar' import SetQQAvatar from './llonebot/SetQQAvatar'
import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile' import { DownloadFile } from './go-cqhttp/DownloadFile'
import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory' import { GetGroupMsgHistory } from './go-cqhttp/GetGroupMsgHistory'
import GetFile from './file/GetFile' import GetFile from './file/GetFile'
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg' import { GetForwardMsg } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie' import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike' import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg' import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence' import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo' import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation' import { HandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg' import { SetEssenceMsg } from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import { DelEssenceMsg } from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent' import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile' import { DelGroupFile } from './go-cqhttp/DelGroupFile'
import type Adapter from '../adapter' 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 function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -66,6 +73,7 @@ export function initActionMap(adapter: Adapter) {
new SetQQAvatar(adapter), new SetQQAvatar(adapter),
new GetFriendWithCategory(adapter), new GetFriendWithCategory(adapter),
new GetEvent(adapter), new GetEvent(adapter),
new SetOnlineStatus(adapter),
// onebot11 // onebot11
new SendLike(adapter), new SendLike(adapter),
new GetMsg(adapter), new GetMsg(adapter),
@@ -102,21 +110,27 @@ export function initActionMap(adapter: Adapter) {
//以下为go-cqhttp api //以下为go-cqhttp api
new GetGroupEssence(adapter), new GetGroupEssence(adapter),
new GetGroupHonorInfo(adapter), new GetGroupHonorInfo(adapter),
new GoCQHTTPSendForwardMsg(adapter), new SendForwardMsg(adapter),
new GoCQHTTPSendGroupForwardMsg(adapter), new SendGroupForwardMsg(adapter),
new GoCQHTTPSendPrivateForwardMsg(adapter), new SendPrivateForwardMsg(adapter),
new GoCQHTTPGetStrangerInfo(adapter), new GetStrangerInfo(adapter),
new GoCQHTTPDownloadFile(adapter), new DownloadFile(adapter),
new GetGuildList(adapter), new GetGuildList(adapter),
new GoCQHTTPMarkMsgAsRead(adapter), new MarkMsgAsRead(adapter),
new GoCQHTTPUploadGroupFile(adapter), new UploadGroupFile(adapter),
new GoCQHTTPUploadPrivateFile(adapter), new UploadPrivateFile(adapter),
new GoCQHTTPGetGroupMsgHistory(adapter), new GetGroupMsgHistory(adapter),
new GoCQHTTGetForwardMsgAction(adapter), new GetForwardMsg(adapter),
new GoCQHTTHandleQuickOperation(adapter), new HandleQuickOperation(adapter),
new GoCQHTTPSetEssenceMsg(adapter), new SetEssenceMsg(adapter),
new GoCQHTTPDelEssenceMsg(adapter), new DelEssenceMsg(adapter),
new GoCQHTTPDelGroupFile(adapter) new DelGroupFile(adapter),
new GetGroupSystemMsg(adapter),
new CreateGroupFileFolder(adapter),
new DelGroupFolder(adapter),
new GetGroupAtAllRemain(adapter),
new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter)
] ]
const actionMap = new Map<string, BaseAction<any, any>>() const actionMap = new Map<string, BaseAction<any, any>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@@ -14,8 +14,8 @@ export default class Debug extends BaseAction<Payload, any> {
const { ntMsgApi, ntFileApi, ntFileCacheApi, ntFriendApi, ntGroupApi, ntUserApi, ntWindowApi } = this.ctx const { ntMsgApi, ntFileApi, ntFileCacheApi, ntFriendApi, ntGroupApi, ntUserApi, ntWindowApi } = this.ctx
const ntqqApi = [ntMsgApi, ntFriendApi, ntGroupApi, ntUserApi, ntFileApi, ntFileCacheApi, ntWindowApi] const ntqqApi = [ntMsgApi, ntFriendApi, ntGroupApi, ntUserApi, ntFileApi, ntFileCacheApi, ntWindowApi]
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
const method = ntqqApiClass[payload.method] as Function const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass]
if (method) { if (method && method instanceof Function) {
const result = method.apply(ntqqApiClass, payload.args) const result = method.apply(ntqqApiClass, payload.args)
if (method.constructor.name === 'AsyncFunction') { if (method.constructor.name === 'AsyncFunction') {
return await result return await result

View File

@@ -1,5 +1,5 @@
import { GroupNotify, GroupNotifyStatus } from '@/ntqqapi/types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { GroupNotify, GroupNotifyStatus } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
@@ -13,7 +13,7 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> { protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies() const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies()
const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.KUNHANDLE)
const returnData: OB11GroupRequestNotify[] = [] const returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)

View File

@@ -0,0 +1,25 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
status: number | string
ext_status: number | string
battery_status: number | string
}
export class SetOnlineStatus extends BaseAction<Payload, null> {
actionName = ActionName.SetOnlineStatus
async _handle(payload: Payload) {
const ret = await this.ctx.ntUserApi.setSelfStatus(
Number(payload.status),
Number(payload.ext_status),
Number(payload.battery_status),
)
if (ret.result !== 0) {
this.ctx.logger.error(ret)
throw new Error('设置在线状态失败')
}
return null
}
}

View File

@@ -11,13 +11,17 @@ class DeleteMsg extends BaseAction<Payload, void> {
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (!payload.message_id) { if (!payload.message_id) {
throw Error('message_id不能为空') throw new Error('参数message_id不能为空')
} }
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw `消息${payload.message_id}不存在` throw new Error(`消息${payload.message_id}不存在`)
}
const data = await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId])
if (data.result !== 0) {
this.ctx.logger.error('delete_msg', payload.message_id, data)
throw new Error(`消息撤回失败`)
} }
await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId])
} }
} }

View File

@@ -1,6 +1,6 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
@@ -28,7 +28,7 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
chatType: msgIdWithPeer.Peer.chatType chatType: msgIdWithPeer.Peer.chatType
} }
const msg = this.adapter.getMsgCache(msgIdWithPeer.MsgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0] const msg = this.adapter.getMsgCache(msgIdWithPeer.MsgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const retMsg = await OB11Constructor.message(this.ctx, msg) const retMsg = await OB11Entities.message(this.ctx, msg)
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)! retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_seq = retMsg.message_id retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id retMsg.real_id = retMsg.message_id

View File

@@ -21,45 +21,15 @@ import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostD
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg } from '../../helper/createMessage' import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
export interface ReturnDataType { interface ReturnData {
message_id: number message_id: number
} }
export enum ContextMode { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
Normal = 0,
Private = 1,
Group = 2
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
private async createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
const isBuddy = await this.ctx.ntFriendApi.isBuddy(Uid!)
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!,
guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
}
}
throw '请指定 group_id 或 user_id'
}
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message) const messages = convertMessage2List(payload.message)
const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node) const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node)
@@ -84,13 +54,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
let contextMode = ContextMode.Normal let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') { if (payload.message_type === 'group') {
contextMode = ContextMode.Group contextMode = CreatePeerMode.Group
} else if (payload.message_type === 'private') { } else if (payload.message_type === 'private') {
contextMode = ContextMode.Private contextMode = CreatePeerMode.Private
} }
const peer = await this.createContext(payload, contextMode) const peer = await createPeer(this.ctx, payload, contextMode)
const messages = convertMessage2List( const messages = convertMessage2List(
payload.message, payload.message,
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',
@@ -160,6 +130,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
const returnMsg = await sendMsg(this.ctx, peer, sendElements, deleteAfterSentFiles) const returnMsg = await sendMsg(this.ctx, peer, sendElements, deleteAfterSentFiles)
if (!returnMsg) {
throw new Error('消息发送失败')
}
return { message_id: returnMsg.msgShortId! } return { message_id: returnMsg.msgShortId! }
} }
@@ -251,9 +224,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// log("分割后的转发节点", sendElementsSplit) // log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) { for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true) const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true)
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push(nodeMsg.msgId) nodeMsgIds.push(nodeMsg.msgId)
await this.ctx.sleep(400) await this.ctx.sleep(400)
this.ctx.logger.info('转发节点生成成功', nodeMsg.msgId)
} }
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
})) }))

View File

@@ -8,7 +8,7 @@ interface ReturnType {
export default class CanSendRecord extends BaseAction<any, ReturnType> { export default class CanSendRecord extends BaseAction<any, ReturnType> {
actionName = ActionName.CanSendRecord actionName = ActionName.CanSendRecord
protected async _handle(payload): Promise<ReturnType> { protected async _handle(payload: void): Promise<ReturnType> {
return { return {
yes: true, yes: true,
} }

View File

@@ -23,6 +23,7 @@ export enum ActionName {
GetFile = 'get_file', GetFile = 'get_file',
GetFriendsWithCategory = 'get_friends_with_category', GetFriendsWithCategory = 'get_friends_with_category',
GetEvent = 'get_event', GetEvent = 'get_event',
SetOnlineStatus = 'set_online_status',
// onebot 11 // onebot 11
SendLike = 'send_like', SendLike = 'send_like',
GetLoginInfo = 'get_login_info', GetLoginInfo = 'get_login_info',
@@ -74,4 +75,10 @@ export enum ActionName {
GoCQHTTP_SetEssenceMsg = 'set_essence_msg', GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg', GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
GoCQHTTP_DelGroupFile = 'delete_group_file', GoCQHTTP_DelGroupFile = 'delete_group_file',
GoCQHTTP_GetGroupSystemMsg = 'get_group_system_msg',
GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder',
GoCQHTTP_DelGroupFolder = 'delete_group_folder',
GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_SendGroupNotice = '_send_group_notice',
} }

View File

@@ -1,6 +1,6 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
@@ -14,9 +14,9 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const refresh = payload?.no_cache === true || payload?.no_cache === 'true' const refresh = payload?.no_cache === true || payload?.no_cache === 'true'
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return OB11Constructor.friendsV2(await this.ctx.ntFriendApi.getBuddyV2(refresh)) return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2(refresh))
} }
return OB11Constructor.friends(await this.ctx.ntFriendApi.getFriends(refresh)) return OB11Entities.friends(await this.ctx.ntFriendApi.getFriends(refresh))
} }
} }
@@ -27,7 +27,7 @@ export class GetFriendWithCategory extends BaseAction<void, any> {
protected async _handle(payload: void) { protected async _handle(payload: void) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
//全新逻辑 //全新逻辑
return OB11Constructor.friendsV2(await this.ctx.ntFriendApi.getBuddyV2ExWithCate(true)) return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2ExWithCate(true))
} else { } else {
throw new Error('this ntqq version not support, must be 26702 or later') throw new Error('this ntqq version not support, must be 26702 or later')
} }

View File

@@ -1,14 +1,15 @@
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { OB11Constructor } from './constructor' import { OB11Entities } from './entities'
import { import {
GroupNotify, GroupNotify,
GroupNotifyTypes, GroupNotifyType,
RawMessage, RawMessage,
BuddyReqType, BuddyReqType,
Peer, Peer,
FriendRequest, FriendRequest,
GroupMember, GroupMember,
GroupMemberRole GroupMemberRole,
GroupNotifyStatus
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest'
@@ -34,7 +35,7 @@ declare module 'cordis' {
} }
class OneBot11Adapter extends Service { class OneBot11Adapter extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi'] static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi']
public messages: Map<string, RawMessage> = new Map() public messages: Map<string, RawMessage> = new Map()
public startTime = 0 public startTime = 0
@@ -49,7 +50,8 @@ class OneBot11Adapter extends Service {
this.ob11Http = new OB11Http(ctx, { this.ob11Http = new OB11Http(ctx, {
port: config.httpPort, port: config.httpPort,
token: config.token, token: config.token,
actionMap actionMap,
listenLocalhost: config.listenLocalhost
}) })
this.ob11HttpPost = new OB11HttpPost(ctx, { this.ob11HttpPost = new OB11HttpPost(ctx, {
hosts: config.httpHosts, hosts: config.httpHosts,
@@ -61,7 +63,8 @@ class OneBot11Adapter extends Service {
port: config.wsPort, port: config.wsPort,
heartInterval: config.heartInterval, heartInterval: config.heartInterval,
token: config.token, token: config.token,
actionMap actionMap,
listenLocalhost: config.listenLocalhost
}) })
this.ob11WebSocketReverseManager = new OB11WebSocketReverseManager(ctx, { this.ob11WebSocketReverseManager = new OB11WebSocketReverseManager(ctx, {
hosts: config.wsHosts, hosts: config.wsHosts,
@@ -108,15 +111,14 @@ class OneBot11Adapter extends Service {
private async handleGroupNotify(notifies: GroupNotify[]) { private async handleGroupNotify(notifies: GroupNotify[]) {
for (const notify of notifies) { for (const notify of notifies) {
try { try {
notify.time = Date.now()
const notifyTime = parseInt(notify.seq) / 1000 const notifyTime = parseInt(notify.seq) / 1000
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < this.startTime) { if (notifyTime < this.startTime) {
continue continue
} }
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) {
this.ctx.logger.info('有成员退出通知', notify) this.ctx.logger.info('有成员退出通知', notify)
const member1Uin = (await this.ctx.ntUserApi.getUinByUid(notify.user1.uid))! const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
let operatorId = member1Uin let operatorId = member1Uin
let subType: GroupDecreaseSubType = 'leave' let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) { if (notify.user2.uid) {
@@ -127,65 +129,51 @@ class OneBot11Adapter extends Service {
} }
subType = 'kick' subType = 'kick'
} }
const groupDecreaseEvent = new OB11GroupDecreaseEvent( const event = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(member1Uin), parseInt(member1Uin),
parseInt(operatorId), parseInt(operatorId),
subType, subType,
) )
this.dispatch(groupDecreaseEvent) this.dispatch(event)
} }
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { else if (notify.type === GroupNotifyType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('有加群请求') this.ctx.logger.info('有加群请求')
let requestQQ = '' const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
try { const event = new OB11GroupRequestEvent(
// uid-->uin
requestQQ = (await this.ctx.ntUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await this.ctx.ntUserApi.getUserDetailInfo(notify.user1.uid)).uin
}
} catch (e) {
this.ctx.logger.error('获取加群人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 this.ctx.ntUserApi.getUinByUid(notify.user2.uid))
if (isNaN(parseInt(invitorId))) {
invitorId = (await this.ctx.ntUserApi.getUserDetailInfo(notify.user2.uid)).uin
}
} catch (e) {
invitorId = ''
this.ctx.logger.error('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
}
}
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0, parseInt(requestUin) || 0,
flag, flag,
notify.postscript, notify.postscript,
invitorId! === undefined ? undefined : +invitorId,
'add'
) )
this.dispatch(groupRequestEvent) this.dispatch(event)
} }
else if (notify.type == GroupNotifyTypes.INVITE_ME) { else if (notify.type === GroupNotifyType.INVITED_BY_MEMBER && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到邀请我加群通知') this.ctx.logger.info('收到邀请我加群通知')
const userId = (await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) || '' const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const groupInviteEvent = new OB11GroupRequestEvent( const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(userId), parseInt(userId) || 0,
flag, flag,
undefined, notify.postscript,
undefined, undefined,
'invite' 'invite'
) )
this.dispatch(groupInviteEvent) this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
} }
} catch (e: any) { } catch (e: any) {
this.ctx.logger.error('解析群通知失败', e.stack.toString()) this.ctx.logger.error('解析群通知失败', e.stack)
} }
} }
} }
@@ -203,7 +191,7 @@ class OneBot11Adapter extends Service {
message.msgShortId = MessageUnique.createMsg(peer, message.msgId) message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
this.addMsgCache(message) this.addMsgCache(message)
OB11Constructor.message(this.ctx, message) OB11Entities.message(this.ctx, message)
.then((msg) => { .then((msg) => {
if (!this.config.debug && msg.message.length === 0) { if (!this.config.debug && msg.message.length === 0) {
return return
@@ -219,13 +207,13 @@ class OneBot11Adapter extends Service {
}) })
.catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString())) .catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString()))
OB11Constructor.GroupEvent(this.ctx, message).then((groupEvent) => { OB11Entities.groupEvent(this.ctx, message).then((groupEvent) => {
if (groupEvent) { if (groupEvent) {
this.dispatch(groupEvent) this.dispatch(groupEvent)
} }
}) })
OB11Constructor.PrivateEvent(this.ctx, message).then((privateEvent) => { OB11Entities.privateEvent(this.ctx, message).then((privateEvent) => {
if (privateEvent) { if (privateEvent) {
this.dispatch(privateEvent) this.dispatch(privateEvent)
} }
@@ -240,7 +228,7 @@ class OneBot11Adapter extends Service {
if (!oriMessageId) { if (!oriMessageId) {
continue continue
} }
OB11Constructor.RecallEvent(this.ctx, message, oriMessageId).then((recallEvent) => { OB11Entities.recallEvent(this.ctx, message, oriMessageId).then((recallEvent) => {
if (recallEvent) { if (recallEvent) {
this.dispatch(recallEvent) this.dispatch(recallEvent)
} }
@@ -306,7 +294,7 @@ class OneBot11Adapter extends Service {
} }
} }
// HTTP 端口变化,重启服务 // HTTP 端口变化,重启服务
if (config.ob11.httpPort !== old.httpPort) { if ((config.ob11.httpPort !== old.httpPort || config.ob11.listenLocalhost !== old.listenLocalhost) && config.ob11.enableHttp) {
await this.ob11Http.stop() await this.ob11Http.stop()
this.ob11Http.start() this.ob11Http.start()
} }
@@ -319,7 +307,7 @@ class OneBot11Adapter extends Service {
} }
} }
// 正向 WebSocket 端口变化,重启服务 // 正向 WebSocket 端口变化,重启服务
if (config.ob11.wsPort !== old.wsPort) { if ((config.ob11.wsPort !== old.wsPort || config.ob11.listenLocalhost !== old.listenLocalhost) && config.ob11.enableWs) {
await this.ob11WebSocket.stop() await this.ob11WebSocket.stop()
this.ob11WebSocket.start() this.ob11WebSocket.start()
llonebotError.wsServerError = '' llonebotError.wsServerError = ''

View File

@@ -10,6 +10,7 @@ import { OB11Message } from '../types'
import { OB11BaseEvent } from '../event/OB11BaseEvent' import { OB11BaseEvent } from '../event/OB11BaseEvent'
import { handleQuickOperation, QuickOperationEvent } from '../helper/quickOperation' import { handleQuickOperation, QuickOperationEvent } from '../helper/quickOperation'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { Dict } from 'cosmokit'
type RegisterHandler = (res: Response, payload: any) => Promise<any> type RegisterHandler = (res: Response, payload: any) => Promise<any>
@@ -50,8 +51,9 @@ class OB11Http {
this.expressAPP.get('/', (req: Request, res: Response) => { this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`LLOneBot server 已启动`) res.send(`LLOneBot server 已启动`)
}) })
this.server = this.expressAPP.listen(this.config.port, '0.0.0.0', () => { const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0'
this.ctx.logger.info(`HTTP server started 0.0.0.0:${this.config.port}`) this.server = this.expressAPP.listen(this.config.port, host, () => {
this.ctx.logger.info(`HTTP server started ${host}:${this.config.port}`)
}) })
llonebotError.httpServerError = '' llonebotError.httpServerError = ''
} catch (e: any) { } catch (e: any) {
@@ -135,6 +137,7 @@ namespace OB11Http {
port: number port: number
token?: string token?: string
actionMap: Map<string, BaseAction<any, any>> actionMap: Map<string, BaseAction<any, any>>
listenLocalhost: boolean
} }
} }
@@ -159,7 +162,7 @@ class OB11HttpPost {
public async emitEvent(event: OB11BaseEvent | OB11Message) { public async emitEvent(event: OB11BaseEvent | OB11Message) {
const msgStr = JSON.stringify(event) const msgStr = JSON.stringify(event)
const headers = { const headers: Dict = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-self-id': selfInfo.uin, 'x-self-id': selfInfo.uin,
} }

View File

@@ -14,16 +14,21 @@ import { version } from '../../version'
class OB11WebSocket { class OB11WebSocket {
private wsServer?: WebSocketServer private wsServer?: WebSocketServer
private wsClients: WebSocket[] = [] private wsClients: { socket: WebSocket; emitEvent: boolean }[] = []
constructor(protected ctx: Context, public config: OB11WebSocket.Config) { constructor(protected ctx: Context, public config: OB11WebSocket.Config) {
} }
public start() { public start() {
if (this.wsServer) return if (this.wsServer) return
this.ctx.logger.info(`WebSocket server started 0.0.0.0:${this.config.port}`) const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0'
this.ctx.logger.info(`WebSocket server started ${host}:${this.config.port}`)
try { try {
this.wsServer = new WebSocketServer({ port: this.config.port, maxPayload: 1024 * 1024 * 1024 }) this.wsServer = new WebSocketServer({
host,
port: this.config.port,
maxPayload: 1024 * 1024 * 1024
})
llonebotError.wsServerError = '' llonebotError.wsServerError = ''
} catch (e: any) { } catch (e: any) {
llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString() llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString()
@@ -31,7 +36,7 @@ class OB11WebSocket {
} }
this.wsServer?.on('connection', (socket, req) => { this.wsServer?.on('connection', (socket, req) => {
this.authorize(socket, req) this.authorize(socket, req)
this.connect(socket) this.connect(socket, req)
}) })
} }
@@ -53,8 +58,8 @@ class OB11WebSocket {
} }
public async emitEvent(event: OB11BaseEvent | OB11Message) { public async emitEvent(event: OB11BaseEvent | OB11Message) {
this.wsClients.forEach(socket => { this.wsClients.forEach(({ socket, emitEvent }) => {
if (socket.readyState === WebSocket.OPEN) { if (emitEvent && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(event)) socket.send(JSON.stringify(event))
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', event.post_type) this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', event.post_type)
} }
@@ -70,8 +75,8 @@ class OB11WebSocket {
return return
} }
socket.send(JSON.stringify(data)) socket.send(JSON.stringify(data))
if (data['post_type']) { if ('post_type' in data) {
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data['post_type']) this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data.post_type)
} }
} }
@@ -122,33 +127,40 @@ class OB11WebSocket {
} }
} }
private connect(socket: WebSocket) { private connect(socket: WebSocket, req: IncomingMessage) {
try { const url = req.url?.split('?').shift()
this.reply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) if (['/api', '/api/', '/', undefined].includes(url)) {
} catch (e) { socket.on('message', msg => {
this.ctx.logger.error('发送生命周期失败', e) this.handleAction(socket, msg.toString())
})
}
if (['/event', '/event/', '/', undefined].includes(url)) {
try {
this.reply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e) {
this.ctx.logger.error('发送生命周期失败', e)
}
const disposeHeartBeat = this.ctx.setInterval(() => {
this.reply(socket, new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval))
}, this.config.heartInterval)
socket.on('close', () => {
disposeHeartBeat()
this.ctx.logger.info('有一个 Websocket 连接断开')
})
} }
socket.on('error', err => this.ctx.logger.error(err.message)) socket.on('error', err => this.ctx.logger.error(err.message))
socket.on('message', msg => {
this.handleAction(socket, msg.toString())
})
socket.on('ping', () => { socket.on('ping', () => {
socket.pong() socket.pong()
}) })
const disposeHeartBeat = this.ctx.setInterval(() => { this.wsClients.push({
this.reply(socket, new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval)) socket,
}, this.config.heartInterval) emitEvent: ['/event', '/event/', '/', undefined].includes(url)
socket.on('close', () => {
disposeHeartBeat()
this.ctx.logger.info('有一个 Websocket 连接断开')
}) })
this.wsClients.push(socket)
} }
} }
@@ -158,6 +170,7 @@ namespace OB11WebSocket {
heartInterval: number heartInterval: number
token?: string token?: string
actionMap: Map<string, BaseAction<any, any>> actionMap: Map<string, BaseAction<any, any>>
listenLocalhost: boolean
} }
} }
@@ -192,8 +205,8 @@ class OB11WebSocketReverse {
return return
} }
socket.send(JSON.stringify(data)) socket.send(JSON.stringify(data))
if (data['post_type']) { if ('post_type' in data) {
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data['post_type']) this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data.post_type)
} }
} }
@@ -261,7 +274,7 @@ class OB11WebSocketReverse {
this.wsClient.on('close', () => { this.wsClient.on('close', () => {
disposeHeartBeat() disposeHeartBeat()
this.ctx.logger.info('The websocket connection: ' + this.config.url + ' closed, trying reconnecting...') this.ctx.logger.info(`The websocket connection: ${this.config.url} closed${this.running ? ', trying reconnecting...' : ''}`)
if (this.running) { if (this.running) {
this.ctx.setTimeout(() => this.tryConnect(), 3000) this.ctx.setTimeout(() => this.tryConnect(), 3000)
} }

View File

@@ -44,7 +44,7 @@ export function decodeCQCode(source: string): OB11MessageData[] {
return elements return elements
} }
export function encodeCQCode(data: OB11MessageData) { export function encodeCQCode(input: OB11MessageData) {
const CQCodeEscapeText = (text: string) => { const CQCodeEscapeText = (text: string) => {
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;') return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;')
} }
@@ -53,21 +53,20 @@ export function encodeCQCode(data: OB11MessageData) {
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;') return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;')
} }
if (data.type === 'text') { if (input.type === 'text') {
return CQCodeEscapeText(data.data.text) return CQCodeEscapeText(input.data.text)
} }
let result = '[CQ:' + data.type let result = '[CQ:' + input.type
for (const name in data.data) { for (const [key, value] of Object.entries(input.data)) {
const value = data.data[name]
if (value === undefined) { if (value === undefined) {
continue continue
} }
try { try {
const text = value.toString() const text = value.toString()
result += `,${name}=${CQCodeEscape(text)}` result += `,${key}=${CQCodeEscape(text)}`
} catch (error) { } catch (error) {
// If it can't be converted, skip this name-value pair // If it can't be converted, skip this key-value pair
} }
} }
result += ']' result += ']'

View File

@@ -37,18 +37,18 @@ import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent' import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent' import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent' import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit, isNullable } from 'cosmokit' import { omit, isNullable, pick } from 'cosmokit'
import { Context } from 'cordis' import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { pathToFileURL } from 'node:url'
export namespace OB11Constructor { export namespace OB11Entities {
export async function message(ctx: Context, msg: RawMessage): Promise<OB11Message> { export async function message(ctx: Context, msg: RawMessage): Promise<OB11Message> {
let config = getConfigUtil().getConfig() let config = getConfigUtil().getConfig()
const { const {
@@ -105,10 +105,7 @@ export namespace OB11Constructor {
} }
for (let element of msg.elements) { for (let element of msg.elements) {
let message_data: OB11MessageData = { let messageSegment: OB11MessageData | undefined
data: {} as any,
type: 'unknown' as any,
}
if (element.textElement && element.textElement?.atType !== AtType.notAt) { if (element.textElement && element.textElement?.atType !== AtType.notAt) {
let qq: string let qq: string
let name: string | undefined let name: string | undefined
@@ -129,7 +126,7 @@ export namespace OB11Constructor {
name = content.replace('@', '') name = content.replace('@', '')
} }
} }
message_data = { messageSegment = {
type: OB11MessageDataType.at, type: OB11MessageDataType.at,
data: { data: {
qq: qq!, qq: qq!,
@@ -138,58 +135,61 @@ export namespace OB11Constructor {
} }
} }
else if (element.textElement) { else if (element.textElement) {
message_data['type'] = OB11MessageDataType.text const text = element.textElement.content
let text = element.textElement.content
if (!text.trim()) { if (!text.trim()) {
continue continue
} }
message_data['data']['text'] = text messageSegment = {
type: OB11MessageDataType.text,
data: {
text
}
}
} }
else if (element.replyElement) { else if (element.replyElement) {
message_data['type'] = OB11MessageDataType.reply const { replyElement } = element
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: ''
}
try { try {
const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords) const { replayMsgSeq, replyMsgTime, senderUidStr } = replyElement
if (!records) throw new Error('找不到回复消息') const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement.sourceMsgIdInRecords)
let replyMsg = (await ctx.ntMsgApi.getMsgsBySeqAndCount({ if (!records || !replyMsgTime || !senderUidStr) {
peerUid: msg.peerUid, throw new Error('找不到回复消息')
guildId: '',
chatType: msg.chatType,
}, element.replyElement.replayMsgSeq, 1, true, true))?.msgList[0]
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
}
replyMsg = (await ctx.ntMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0]
} }
const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUidStr])
const replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom)
// 284840486: 合并消息内侧 消息具体定位不到 // 284840486: 合并消息内侧 消息具体定位不到
if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { if (!replyMsg && msg.peerUin !== '284840486') {
throw new Error('回复消息消息验证失败') ctx.logger.info('queryMsgs', msgList.map(e => pick(e, ['msgSeq', 'msgRandom'])))
throw new Error('回复消息验证失败')
}
messageSegment = {
type: OB11MessageDataType.reply,
data: {
id: MessageUnique.createMsg(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
}
} }
message_data['data']['id'] = replyMsg && MessageUnique.createMsg({
peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, replyMsg.msgId)?.toString()
} catch (e: any) { } catch (e: any) {
ctx.logger.error('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) ctx.logger.error('获取不到引用的消息', replyElement, e.stack)
continue continue
} }
} }
else if (element.picElement) { else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image
const { picElement } = element const { picElement } = element
/*let fileName = picElement.fileName const fileSize = picElement.fileSize ?? '0'
const isGif = picElement.picType === PicType.gif messageSegment = {
if (isGif && !fileName.endsWith('.gif')) { type: OB11MessageDataType.image,
fileName += '.gif' data: {
}*/ file: picElement.fileName,
message_data['data']['file'] = picElement.fileName subType: picElement.picSubType,
message_data['data']['subType'] = picElement.picSubType url: await ctx.ntFileApi.getImageUrl(picElement),
//message_data['data']['file_id'] = picElement.fileUuid file_size: fileSize,
message_data['data']['url'] = await ctx.ntFileApi.getImageUrl(picElement) }
message_data['data']['file_size'] = picElement.fileSize }
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
@@ -198,21 +198,26 @@ export namespace OB11Constructor {
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: picElement.fileName, fileName: picElement.fileName,
fileSize: String(picElement.fileSize || '0'), fileUuid: picElement.fileUuid,
fileUuid: picElement.fileUuid fileSize,
}) })
} }
else if (element.videoElement) { else if (element.videoElement) {
message_data['type'] = OB11MessageDataType.video
const { videoElement } = element const { videoElement } = element
message_data['data']['file'] = videoElement.fileName const videoUrl = await ctx.ntFileApi.getVideoUrl({
message_data['data']['path'] = videoElement.filePath
//message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_size'] = videoElement.fileSize
message_data['data']['url'] = await ctx.ntFileApi.getVideoUrl({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,
}, msg.msgId, element.elementId) }, msg.msgId, element.elementId)
const fileSize = videoElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.video,
data: {
file: videoElement.fileName,
url: videoUrl || pathToFileURL(videoElement.filePath).href,
path: videoElement.filePath,
file_size: fileSize,
}
}
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
@@ -221,17 +226,23 @@ export namespace OB11Constructor {
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: videoElement.fileName, fileName: videoElement.fileName,
fileSize: String(videoElement.fileSize || '0'), fileUuid: videoElement.fileUuid!,
fileUuid: videoElement.fileUuid! fileSize,
}) })
} }
else if (element.fileElement) { else if (element.fileElement) {
message_data['type'] = OB11MessageDataType.file
const { fileElement } = element const { fileElement } = element
message_data['data']['file'] = fileElement.fileName const fileSize = fileElement.fileSize ?? '0'
message_data['data']['path'] = fileElement.filePath messageSegment = {
message_data['data']['file_id'] = fileElement.fileUuid type: OB11MessageDataType.file,
message_data['data']['file_size'] = fileElement.fileSize data: {
file: fileElement.fileName,
url: pathToFileURL(fileElement.filePath).href,
file_id: fileElement.fileUuid,
path: fileElement.filePath,
file_size: fileSize,
}
}
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
@@ -240,17 +251,22 @@ export namespace OB11Constructor {
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: fileElement.fileName, fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'), fileUuid: fileElement.fileUuid!,
fileUuid: fileElement.fileUuid! fileSize,
}) })
} }
else if (element.pttElement) { else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice
const { pttElement } = element const { pttElement } = element
message_data['data']['file'] = pttElement.fileName const fileSize = pttElement.fileSize ?? '0'
message_data['data']['path'] = pttElement.filePath messageSegment = {
//message_data['data']['file_id'] = pttElement.fileUuid type: OB11MessageDataType.voice,
message_data['data']['file_size'] = pttElement.fileSize data: {
file: pttElement.fileName,
url: pathToFileURL(pttElement.filePath).href,
path: pttElement.filePath,
file_size: fileSize,
}
}
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
@@ -259,59 +275,91 @@ export namespace OB11Constructor {
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: pttElement.fileName, fileName: pttElement.fileName,
fileSize: String(pttElement.fileSize || '0'), fileUuid: pttElement.fileUuid,
fileUuid: pttElement.fileUuid fileSize,
}) })
} }
else if (element.arkElement) { else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json const { arkElement } = element
message_data['data']['data'] = element.arkElement.bytesData messageSegment = {
type: OB11MessageDataType.json,
data: {
data: arkElement.bytesData
}
}
} }
else if (element.faceElement) { else if (element.faceElement) {
const faceId = element.faceElement.faceIndex const { faceElement } = element
const faceId = faceElement.faceIndex
if (faceId === FaceIndex.dice) { if (faceId === FaceIndex.dice) {
message_data['type'] = OB11MessageDataType.dice messageSegment = {
message_data['data']['result'] = element.faceElement.resultId type: OB11MessageDataType.dice,
data: {
result: faceElement.resultId!
}
}
} }
else if (faceId === FaceIndex.RPS) { else if (faceId === FaceIndex.RPS) {
message_data['type'] = OB11MessageDataType.RPS messageSegment = {
message_data['data']['result'] = element.faceElement.resultId type: OB11MessageDataType.RPS,
data: {
result: faceElement.resultId!
}
}
} }
else { else {
message_data['type'] = OB11MessageDataType.face messageSegment = {
message_data['data']['id'] = element.faceElement.faceIndex.toString() type: OB11MessageDataType.face,
data: {
id: faceId.toString()
}
}
} }
} }
else if (element.marketFaceElement) { else if (element.marketFaceElement) {
message_data['type'] = OB11MessageDataType.mface const { marketFaceElement } = element
message_data['data']['summary'] = element.marketFaceElement.faceName const { emojiId } = marketFaceElement
const md5 = element.marketFaceElement.emojiId
// 取md5的前两位 // 取md5的前两位
const dir = md5.substring(0, 2) const dir = emojiId.substring(0, 2)
// 获取组装url // 获取组装url
// const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000` // const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000`
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${md5}/raw300.gif` const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`
message_data['data']['url'] = url messageSegment = {
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId type: OB11MessageDataType.mface,
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId) data: {
message_data['data']['key'] = element.marketFaceElement.key summary: marketFaceElement.faceName!,
mFaceCache.set(md5, element.marketFaceElement.faceName!) url,
emoji_id: emojiId,
emoji_package_id: marketFaceElement.emojiPackageId,
key: marketFaceElement.key
}
}
//mFaceCache.set(emojiId, element.marketFaceElement.faceName!)
} }
else if (element.markdownElement) { else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown const { markdownElement } = element
message_data['data']['data'] = element.markdownElement.content messageSegment = {
type: OB11MessageDataType.markdown,
data: {
data: markdownElement.content
}
}
} }
else if (element.multiForwardMsgElement) { else if (element.multiForwardMsgElement) {
message_data['type'] = OB11MessageDataType.forward messageSegment = {
message_data['data']['id'] = msg.msgId type: OB11MessageDataType.forward,
data: {
id: msg.msgId
}
}
} }
if ((message_data.type as string) !== 'unknown' && message_data.data) { if (messageSegment) {
const cqCode = encodeCQCode(message_data) const cqCode = encodeCQCode(messageSegment)
if (messagePostFormat === 'string') { if (messagePostFormat === 'string') {
(resMsg.message as string) += cqCode (resMsg.message as string) += cqCode
} else {
(resMsg.message as OB11MessageData[]).push(messageSegment)
} }
else (resMsg.message as OB11MessageData[]).push(message_data)
resMsg.raw_message += cqCode resMsg.raw_message += cqCode
} }
} }
@@ -319,7 +367,7 @@ export namespace OB11Constructor {
return resMsg return resMsg
} }
export async function PrivateEvent(ctx: Context, msg: RawMessage): Promise<OB11BaseNoticeEvent | void> { export async function privateEvent(ctx: Context, msg: RawMessage): Promise<OB11BaseNoticeEvent | void> {
if (msg.chatType !== ChatType.friend) { if (msg.chatType !== ChatType.friend) {
return return
} }
@@ -352,7 +400,7 @@ export namespace OB11Constructor {
} }
} }
export async function GroupEvent(ctx: Context, msg: RawMessage): Promise<OB11GroupNoticeEvent | void> { export async function groupEvent(ctx: Context, msg: RawMessage): Promise<OB11GroupNoticeEvent | void> {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.group) {
return return
} }
@@ -427,7 +475,7 @@ export namespace OB11Constructor {
ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
ctx.ntGroupApi.quitGroup(msg.peerUid) ctx.ntGroupApi.quitGroup(msg.peerUid)
try { try {
const adminUin = (await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await ctx.ntUserApi.getUidByUin(groupElement.adminUid)) const adminUin = (await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await ctx.ntUserApi.getUinByUid(groupElement.adminUid))
if (adminUin) { if (adminUin) {
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
@@ -473,29 +521,27 @@ export namespace OB11Constructor {
}).parse(xmlElement.content) }).parse(xmlElement.content)
ctx.logger.info('收到表情回应我的消息', emojiLikeData) ctx.logger.info('收到表情回应我的消息', emojiLikeData)
try { try {
const senderUin = emojiLikeData.gtip.qq.jp const senderUin: string = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq const msgSeq: string = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id const emojiId: string = emojiLikeData.gtip.face.id
const replyMsgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount({ const peer = {
chatType: ChatType.group, chatType: ChatType.group,
guildId: '', guildId: '',
peerUid: msg.peerUid, peerUid: msg.peerUid,
}, msgSeq, 1, true, true))?.msgList }
const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList
if (!replyMsgList?.length) { if (!replyMsgList?.length) {
return return
} }
const likes = [
{
emoji_id: emojiId,
count: 1,
},
]
const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId) const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent( return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(senderUin), parseInt(senderUin),
shortId!, shortId!,
likes [{
emoji_id: emojiId,
count: 1,
}]
) )
} catch (e: any) { } catch (e: any) {
ctx.logger.error('解析表情回应消息失败', e.stack) ctx.logger.error('解析表情回应消息失败', e.stack)
@@ -607,7 +653,7 @@ export namespace OB11Constructor {
} }
} }
export async function RecallEvent( export async function recallEvent(
ctx: Context, ctx: Context,
msg: RawMessage, msg: RawMessage,
shortId: number shortId: number
@@ -638,13 +684,13 @@ export namespace OB11Constructor {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),
nickname: friend.nick, nickname: friend.nick,
remark: friend.remark, remark: friend.remark,
sex: OB11Constructor.sex(friend.sex!), sex: sex(friend.sex!),
level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0, level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0,
} }
} }
export function friends(friends: User[]): OB11User[] { export function friends(friends: User[]): OB11User[] {
return friends.map(OB11Constructor.friend) return friends.map(friend)
} }
export function friendsV2(friends: FriendV2[]): OB11User[] { export function friendsV2(friends: FriendV2[]): OB11User[] {
@@ -689,7 +735,7 @@ export namespace OB11Constructor {
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName, card: member.cardName,
sex: OB11Constructor.sex(member.sex!), sex: sex(member.sex!),
age: 0, age: 0,
area: '', area: '',
level: '0', level: '0',
@@ -701,7 +747,7 @@ export namespace OB11Constructor {
card_changeable: true, card_changeable: true,
is_robot: member.isRobot, is_robot: member.isRobot,
shut_up_timestamp: member.shutUpTime, shut_up_timestamp: member.shutUpTime,
role: OB11Constructor.groupMemberRole(member.role), role: groupMemberRole(member.role),
title: member.memberSpecialTitle || '', title: member.memberSpecialTitle || '',
} }
} }
@@ -711,7 +757,7 @@ export namespace OB11Constructor {
...user, ...user,
user_id: parseInt(user.uin), user_id: parseInt(user.uin),
nickname: user.nick, nickname: user.nick,
sex: OB11Constructor.sex(user.sex!), sex: sex(user.sex!),
age: 0, age: 0,
qid: user.qid, qid: user.qid,
login_days: 0, login_days: 0,
@@ -719,20 +765,18 @@ export namespace OB11Constructor {
} }
} }
export function groupMembers(group: Group): OB11GroupMember[] {
return group.members.map((m) => OB11Constructor.groupMember(group.groupCode, m))
}
export function group(group: Group): OB11Group { export function group(group: Group): OB11Group {
return { return {
group_id: parseInt(group.groupCode), group_id: parseInt(group.groupCode),
group_name: group.groupName, group_name: group.groupName,
group_memo: group.remarkName,
group_create_time: +group.createTime,
member_count: group.memberCount, member_count: group.memberCount,
max_member_count: group.maxMember, max_member_count: group.maxMember,
} }
} }
export function groups(groups: Group[]): OB11Group[] { export function groups(groups: Group[]): OB11Group[] {
return groups.map(OB11Constructor.group) return groups.map(group)
} }
} }

View File

@@ -15,7 +15,7 @@ import {
} from '../types' } from '../types'
import { decodeCQCode } from '../cqcode' import { decodeCQCode } from '../cqcode'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { SendElementEntities } from '@/ntqqapi/entities'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
@@ -37,7 +37,7 @@ export async function createSendElements(
case OB11MessageDataType.text: { case OB11MessageDataType.text: {
const text = sendMsg.data?.text const text = sendMsg.data?.text
if (text) { if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text)) sendElements.push(SendElementEntities.text(sendMsg.data!.text))
} }
} }
break break
@@ -54,7 +54,7 @@ export async function createSendElements(
let isAdmin: boolean = true let isAdmin: boolean = true
if (groupCode) { if (groupCode) {
try { try {
remainAtAllCount = (await ctx.ntGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo remainAtAllCount = (await ctx.ntGroupApi.getGroupRemainAtTimes(groupCode)).atInfo
.RemainAtAllCountForUin .RemainAtAllCountForUin
ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount) ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin) const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin)
@@ -63,7 +63,7 @@ export async function createSendElements(
} }
} }
if (isAdmin && remainAtAllCount > 0) { if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '@全体成员')) sendElements.push(SendElementEntities.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
} }
} }
else if (peer.chatType === ChatType.group) { else if (peer.chatType === ChatType.group) {
@@ -71,14 +71,14 @@ export async function createSendElements(
if (atMember) { if (atMember) {
const display = `@${atMember.cardName || atMember.nick}` const display = `@${atMember.cardName || atMember.nick}`
sendElements.push( sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, display), SendElementEntities.at(atQQ, atMember.uid, AtType.atUser, display),
) )
} else { } else {
const atNmae = sendMsg.data?.name const atNmae = sendMsg.data?.name
const uid = await ctx.ntUserApi.getUidByUin(atQQ) || '' const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : '' const display = atNmae ? `@${atNmae}` : ''
sendElements.push( sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display), SendElementEntities.at(atQQ, uid, AtType.atUser, display),
) )
} }
} }
@@ -98,7 +98,7 @@ export async function createSendElements(
)).msgList[0] )).msgList[0]
if (replyMsg) { if (replyMsg) {
sendElements.push( sendElements.push(
SendMsgElementConstructor.reply( SendElementEntities.reply(
replyMsg.msgSeq, replyMsg.msgSeq,
replyMsg.msgId, replyMsg.msgId,
replyMsg.senderUin!, replyMsg.senderUin!,
@@ -112,13 +112,13 @@ export async function createSendElements(
case OB11MessageDataType.face: { case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id const faceId = sendMsg.data?.id
if (faceId) { if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId))) sendElements.push(SendElementEntities.face(parseInt(faceId)))
} }
} }
break break
case OB11MessageDataType.mface: { case OB11MessageDataType.mface: {
sendElements.push( sendElements.push(
SendMsgElementConstructor.mface( SendElementEntities.mface(
+sendMsg.data.emoji_package_id, +sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id, sendMsg.data.emoji_id,
sendMsg.data.key, sendMsg.data.key,
@@ -128,7 +128,7 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.image: { case OB11MessageDataType.image: {
const res = await SendMsgElementConstructor.pic( const res = await SendElementEntities.pic(
ctx, ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path, (await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '', sendMsg.data.summary || '',
@@ -140,7 +140,7 @@ export async function createSendElements(
break break
case OB11MessageDataType.file: { case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.file(ctx, path, fileName)) sendElements.push(await SendElementEntities.file(ctx, path, fileName))
} }
break break
case OB11MessageDataType.video: { case OB11MessageDataType.video: {
@@ -150,18 +150,18 @@ export async function createSendElements(
const uri2LocalRes = await uri2local(thumb) const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path if (uri2LocalRes.success) thumb = uri2LocalRes.path
} }
const res = await SendMsgElementConstructor.video(ctx, path, fileName, thumb) const res = await SendElementEntities.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath) deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.ptt(ctx, path)) sendElements.push(await SendElementEntities.ptt(ctx, path))
} }
break break
case OB11MessageDataType.json: { case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data)) sendElements.push(SendElementEntities.ark(sendMsg.data.data))
} }
break break
case OB11MessageDataType.poke: { case OB11MessageDataType.poke: {
@@ -170,12 +170,12 @@ export async function createSendElements(
break break
case OB11MessageDataType.dice: { case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.dice(resultId)) sendElements.push(SendElementEntities.dice(resultId))
} }
break break
case OB11MessageDataType.RPS: { case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.rps(resultId)) sendElements.push(SendElementEntities.rps(resultId))
} }
break break
} }
@@ -270,8 +270,40 @@ export async function sendMsg(
const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s ) const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
//log('设置消息超时时间', timeout) //log('设置消息超时时间', timeout)
const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) if (returnMsg) {
ctx.logger.info('消息发送', returnMsg.msgShortId) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path)) ctx.logger.info('消息发送', returnMsg.msgShortId)
return returnMsg deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg
}
}
export interface CreatePeerPayload {
group_id?: string | number
user_id?: string | number
}
export enum CreatePeerMode {
Normal = 0,
Private = 1,
Group = 2
}
export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode: CreatePeerMode): Promise<Peer> {
if ((mode === CreatePeerMode.Group || mode === CreatePeerMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((mode === CreatePeerMode.Private || mode === CreatePeerMode.Normal) && payload.user_id) {
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取用户信息')
const isBuddy = await ctx.ntFriendApi.isBuddy(uid)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: uid,
}
}
throw new Error('请指定 group_id 或 user_id')
} }

View File

@@ -1,12 +1,12 @@
import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types' import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg } from '../helper/createMessage' import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../helper/createMessage'
import { getConfigUtil } from '@/common/config'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
import { Context } from 'cordis' import { Context } from 'cordis'
import { OB11Config } from '@/common/types'
interface QuickOperationPrivateMessage { interface QuickOperationPrivateMessage {
reply?: string reply?: string
@@ -57,21 +57,14 @@ export async function handleQuickOperation(ctx: Context, event: QuickOperationEv
async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
const reply = quickAction.reply const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11 const ob11Config: OB11Config = ctx.config
const peer: Peer = { let contextMode = CreatePeerMode.Normal
chatType: ChatType.friend, if (msg.message_type === 'group') {
peerUid: msg.user_id.toString(), contextMode = CreatePeerMode.Group
} } else if (msg.message_type === 'private') {
if (msg.message_type == 'private') { contextMode = CreatePeerMode.Private
peer.peerUid = (await ctx.ntUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp
}
}
else {
peer.chatType = ChatType.group
peer.peerUid = msg.group_id?.toString()!
} }
const peer = await createPeer(ctx, msg, contextMode)
if (reply) { if (reply) {
let replyMessage: OB11MessageData[] = [] let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) { if (ob11Config.enableQOAutoQuote) {

View File

@@ -54,8 +54,10 @@ export interface OB11GroupMember {
export interface OB11Group { export interface OB11Group {
group_id: number group_id: number
group_name: string group_name: string
member_count?: number group_memo: string
max_member_count?: number group_create_time: number
member_count: number
max_member_count: number
} }
interface OB11Sender { interface OB11Sender {
@@ -133,20 +135,21 @@ export interface OB11MessageMFace {
emoji_package_id: number emoji_package_id: number
emoji_id: string emoji_id: string
key: string key: string
summary: string summary?: string
url?: string
} }
} }
export interface OB11MessageDice { export interface OB11MessageDice {
type: OB11MessageDataType.dice type: OB11MessageDataType.dice
data: { data: {
result: number result: number /* intended */ | string /* in fact */
} }
} }
export interface OB11MessageRPS { export interface OB11MessageRPS {
type: OB11MessageDataType.RPS type: OB11MessageDataType.RPS
data: { data: {
result: number result: number | string
} }
} }
@@ -171,6 +174,7 @@ export interface OB11MessageFileBase {
name?: string name?: string
file: string file: string
url?: string url?: string
file_size?: string //扩展
} }
} }
@@ -184,14 +188,24 @@ export interface OB11MessageImage extends OB11MessageFileBase {
export interface OB11MessageRecord extends OB11MessageFileBase { export interface OB11MessageRecord extends OB11MessageFileBase {
type: OB11MessageDataType.voice type: OB11MessageDataType.voice
data: OB11MessageFileBase['data'] & {
path?: string //扩展
}
} }
export interface OB11MessageFile extends OB11MessageFileBase { export interface OB11MessageFile extends OB11MessageFileBase {
type: OB11MessageDataType.file type: OB11MessageDataType.file
data: OB11MessageFileBase['data'] & {
file_id?: string
path?: string
}
} }
export interface OB11MessageVideo extends OB11MessageFileBase { export interface OB11MessageVideo extends OB11MessageFileBase {
type: OB11MessageDataType.video type: OB11MessageDataType.video
data: OB11MessageFileBase['data'] & {
path?: string //扩展
}
} }
export interface OB11MessageAt { export interface OB11MessageAt {
@@ -298,3 +312,27 @@ export interface OB11Status {
online: boolean | null online: boolean | null
good: boolean good: boolean
} }
export interface OB11GroupFile {
group_id: number
file_id: string
file_name: string
busid: number
file_size: number
upload_time: number
dead_time: number
modify_time: number
download_times: number
uploader: number
uploader_name: string
}
export interface OB11GroupFileFolder {
group_id: number
folder_id: string
folder_name: string
create_time: number
creator: number
creator_name: string
total_file_count: number
}

View File

@@ -38,11 +38,9 @@ window.customElements.define(
const buttonClick = () => { const buttonClick = () => {
const isHidden = this._context.classList.toggle('hidden') const isHidden = this._context.classList.toggle('hidden')
window[`${isHidden ? 'remove' : 'add'}EventListener`]('pointerdown', windowPointerDown) window[`${isHidden ? 'remove' : 'add'}EventListener`]('pointerdown', ({ target }) => {
} if (!this.contains(target as any)) buttonClick()
})
const windowPointerDown = ({ target }) => {
if (!this.contains(target)) buttonClick()
} }
this._button.addEventListener('click', buttonClick) this._button.addEventListener('click', buttonClick)

View File

@@ -1,8 +1,10 @@
import { CheckVersion } from '../common/types' import { CheckVersion, Config } from '../common/types'
import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components'
import { version } from '../version'
// @ts-ignore // @ts-ignore
import StyleRaw from './style.css?raw' import StyleRaw from './style.css?raw'
import { version } from '../version'
type HostsType = 'httpHosts' | 'wsHosts'
function isEmpty(value: unknown) { function isEmpty(value: unknown) {
return value === undefined || value === null || value === '' return value === undefined || value === null || value === ''
@@ -10,17 +12,20 @@ function isEmpty(value: unknown) {
async function onSettingWindowCreated(view: Element) { async function onSettingWindowCreated(view: Element) {
//window.llonebot.log('setting window created') //window.llonebot.log('setting window created')
let config = await window.llonebot.getConfig() const config = await window.llonebot.getConfig()
let ob11Config = { ...config.ob11 } const ob11Config = { ...config.ob11 }
const setConfig = (key: string, value: any) => { const setConfig = (key: string, value: any) => {
const configKey = key.split('.') const configKey = key.split('.')
if (key.indexOf('ob11') === 0) { if (key.startsWith('ob11')) {
if (configKey.length === 2) ob11Config[configKey[1]] = value if (configKey.length === 2) Object.assign(ob11Config, { [configKey[1]]: value })
else ob11Config[key] = value else Object.assign(ob11Config, { [key]: value })
} else { } else {
if (configKey.length === 2) config[configKey[0]][configKey[1]] = value if (configKey.length === 2) {
else config[key] = value Object.assign(config[configKey[0] as keyof Config[keyof Config]], { [configKey[1]]: value })
} else {
Object.assign(config, { [key]: value })
}
if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) { if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) {
window.llonebot.setConfig(false, config) window.llonebot.setConfig(false, config)
} }
@@ -161,7 +166,12 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'快速操作回复自动引用原消息', '快速操作回复自动引用原消息',
null, null,
SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote, { 'control-display-id': 'config-ob11-enableQOAutoQuote' }), SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote),
),
SettingItem(
'HTTP、正向 WebSocket 服务仅监听 127.0.0.1',
'而不是 0.0.0.0',
SettingSwitch('ob11.listenLocalhost', config.ob11.listenLocalhost),
), ),
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')), SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]), ]),
@@ -244,7 +254,7 @@ async function onSettingWindowCreated(view: Element) {
window.LiteLoader.api.openExternal('https://llonebot.github.io/') window.LiteLoader.api.openExternal('https://llonebot.github.io/')
}) })
// 生成反向地址列表 // 生成反向地址列表
const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any = {}) => { const buildHostListItem = (type: HostsType, host: string, index: number, inputAttrs: any = {}) => {
const dom = { const dom = {
container: document.createElement('setting-item'), container: document.createElement('setting-item'),
input: document.createElement('input'), input: document.createElement('input'),
@@ -276,7 +286,7 @@ async function onSettingWindowCreated(view: Element) {
return dom.container return dom.container
} }
const buildHostList = (hosts: string[], type: string, inputAttr: any = {}) => { const buildHostList = (hosts: string[], type: HostsType, inputAttr: any = {}) => {
const result: HTMLElement[] = [] const result: HTMLElement[] = []
hosts.forEach((host, index) => { hosts.forEach((host, index) => {
@@ -285,12 +295,12 @@ async function onSettingWindowCreated(view: Element) {
return result return result
} }
const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => { const addReverseHost = (type: HostsType, doc: Document = document, inputAttr: any = {}) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`) const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
hostContainerDom?.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr)) hostContainerDom?.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr))
ob11Config[type].push('') ob11Config[type].push('')
} }
const initReverseHost = (type: string, doc: Document = document) => { const initReverseHost = (type: HostsType, doc: Document = document) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`) const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
;[...hostContainerDom?.childNodes!].forEach((dom) => dom.remove()) ;[...hostContainerDom?.childNodes!].forEach((dom) => dom.remove())
buildHostList(ob11Config[type], type).forEach((dom) => { buildHostList(ob11Config[type], type).forEach((dom) => {
@@ -431,7 +441,7 @@ function init() {
} }
if (location.hash === '#/blank') { if (location.hash === '#/blank') {
globalThis.navigation.addEventListener('navigatesuccess', init, { once: true }) globalThis.navigation?.addEventListener('navigatesuccess', init, { once: true })
} else { } else {
init() init()
} }

View File

@@ -1 +1 @@
export const version = '3.31.1' export const version = '3.31.7'

View File

@@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "commonjs", "module": "CommonJS",
"outDir": "./dist", "outDir": "./dist",
"strict": true, "strict": true,
"noImplicitAny": false, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,