From 5501980ab362c4c58482cb247f41ccf62ded6591 Mon Sep 17 00:00:00 2001 From: idranme Date: Wed, 28 Aug 2024 04:48:07 +0800 Subject: [PATCH] refactor --- README.md | 2 +- package.json | 2 +- src/common/channels.ts | 1 + src/common/config.ts | 5 +- src/common/data.ts | 107 ----- src/common/globalVars.ts | 22 + src/common/server/http.ts | 119 ----- src/common/types.ts | 2 +- src/common/utils/LegacyLog.ts | 25 + src/common/utils/MessageUnique.ts | 12 +- src/common/utils/QQBasicInfo.ts | 3 +- src/common/utils/audio.ts | 28 +- src/common/utils/file.ts | 2 +- src/common/utils/index.ts | 7 +- src/common/utils/log.ts | 35 -- src/common/utils/request.ts | 5 +- src/common/utils/sign.ts | 6 +- src/common/utils/system.ts | 10 - src/common/utils/upgrade.ts | 9 +- src/common/utils/video.ts | 36 +- src/main/ipcsend.ts | 12 - src/main/log.ts | 42 ++ src/main/main.ts | 447 +++++------------- src/main/setConfig.ts | 67 --- src/ntqqapi/api/file.ts | 65 ++- src/ntqqapi/api/friend.ts | 25 +- src/ntqqapi/api/group.ts | 86 +++- src/ntqqapi/api/msg.ts | 47 +- src/ntqqapi/api/user.ts | 100 ++-- src/ntqqapi/api/webapi.ts | 44 +- src/ntqqapi/api/window.ts | 23 +- src/ntqqapi/constructor.ts | 63 ++- src/ntqqapi/core.ts | 240 ++++++++++ src/ntqqapi/helper/rkey.ts | 12 +- src/ntqqapi/hook.ts | 207 +------- src/ntqqapi/ntcall.ts | 4 +- src/onebot11/action/BaseAction.ts | 13 +- src/onebot11/action/OB11Response.ts | 7 +- src/onebot11/action/file/GetFile.ts | 12 +- src/onebot11/action/file/GetRecord.ts | 6 +- .../action/go-cqhttp/DelEssenceMsg.ts | 8 +- src/onebot11/action/go-cqhttp/DelGroupFile.ts | 3 +- src/onebot11/action/go-cqhttp/DownloadFile.ts | 3 +- .../action/go-cqhttp/GetForwardMsg.ts | 5 +- .../action/go-cqhttp/GetGroupMsgHistory.ts | 7 +- .../action/go-cqhttp/GetStrangerInfo.ts | 13 +- .../action/go-cqhttp/QuickOperation.ts | 9 +- .../action/go-cqhttp/SetEssenceMsg.ts | 3 +- src/onebot11/action/go-cqhttp/UploadFile.ts | 13 +- src/onebot11/action/group/GetGroupEssence.ts | 10 +- .../action/group/GetGroupHonorInfo.ts | 4 +- src/onebot11/action/group/GetGroupInfo.ts | 3 +- src/onebot11/action/group/GetGroupList.ts | 3 +- .../action/group/GetGroupMemberInfo.ts | 11 +- .../action/group/GetGroupMemberList.ts | 9 +- .../action/group/SetGroupAddRequest.ts | 5 +- src/onebot11/action/group/SetGroupAdmin.ts | 8 +- src/onebot11/action/group/SetGroupBan.ts | 6 +- src/onebot11/action/group/SetGroupCard.ts | 6 +- src/onebot11/action/group/SetGroupKick.ts | 6 +- src/onebot11/action/group/SetGroupLeave.ts | 6 +- src/onebot11/action/group/SetGroupName.ts | 3 +- src/onebot11/action/group/SetGroupWholeBan.ts | 3 +- src/onebot11/action/index.ts | 129 +++-- src/onebot11/action/llonebot/Config.ts | 7 +- src/onebot11/action/llonebot/Debug.ts | 20 +- src/onebot11/action/llonebot/GetEvent.ts | 16 +- .../action/llonebot/GetGroupAddRequest.ts | 8 +- src/onebot11/action/llonebot/SetQQAvatar.ts | 12 +- src/onebot11/action/msg/DeleteMsg.ts | 3 +- src/onebot11/action/msg/ForwardSingleMsg.ts | 5 +- src/onebot11/action/msg/GetMsg.ts | 6 +- src/onebot11/action/msg/SendMsg.ts | 138 +++--- src/onebot11/action/msg/SetMsgEmojiLike.ts | 5 +- src/onebot11/action/system/CleanCache.ts | 39 +- src/onebot11/action/system/GetLoginInfo.ts | 11 +- src/onebot11/action/system/GetStatus.ts | 4 +- src/onebot11/action/user/GetCookie.ts | 5 +- src/onebot11/action/user/GetFriendList.ts | 7 +- src/onebot11/action/user/SendLike.ts | 5 +- .../action/user/SetFriendAddRequest.ts | 2 +- src/onebot11/adapter.ts | 436 +++++++++++++++++ src/onebot11/connect/http.ts | 211 +++++++++ src/onebot11/connect/ws.ts | 329 +++++++++++++ src/onebot11/constructor.ts | 125 +++-- src/onebot11/event/OB11BaseEvent.ts | 4 +- .../{server => helper}/event-for-http.ts | 9 +- .../{action => helper}/quick-operation.ts | 61 ++- src/onebot11/server/http.ts | 54 --- src/onebot11/server/post-ob11-event.ts | 90 ---- src/onebot11/server/ws/ReverseWebsocket.ts | 153 ------ src/onebot11/server/ws/WebsocketServer.ts | 134 ------ src/onebot11/server/ws/reply.ts | 15 - src/preload.ts | 10 +- src/renderer/index.ts | 24 +- 95 files changed, 2115 insertions(+), 2069 deletions(-) delete mode 100644 src/common/data.ts create mode 100644 src/common/globalVars.ts delete mode 100644 src/common/server/http.ts create mode 100644 src/common/utils/LegacyLog.ts delete mode 100644 src/common/utils/log.ts delete mode 100644 src/common/utils/system.ts delete mode 100644 src/main/ipcsend.ts create mode 100644 src/main/log.ts delete mode 100644 src/main/setConfig.ts create mode 100644 src/ntqqapi/core.ts create mode 100644 src/onebot11/adapter.ts create mode 100644 src/onebot11/connect/http.ts create mode 100644 src/onebot11/connect/ws.ts rename src/onebot11/{server => helper}/event-for-http.ts (89%) rename src/onebot11/{action => helper}/quick-operation.ts (60%) delete mode 100644 src/onebot11/server/http.ts delete mode 100644 src/onebot11/server/post-ob11-event.ts delete mode 100644 src/onebot11/server/ws/ReverseWebsocket.ts delete mode 100644 src/onebot11/server/ws/WebsocketServer.ts delete mode 100644 src/onebot11/server/ws/reply.ts diff --git a/README.md b/README.md index ff0dd0b..bb3a83e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发 > [!CAUTION]\ -> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** +> 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息 TG 群: diff --git a/package.json b/package.json index 9c88a95..aa734f0 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/fluent-ffmpeg": "^2.1.25", + "@types/fluent-ffmpeg": "^2.1.26", "@types/node": "^20.14.15", "@types/ws": "^8.5.12", "electron": "^31.4.0", diff --git a/src/common/channels.ts b/src/common/channels.ts index 2119cd1..e183524 100644 --- a/src/common/channels.ts +++ b/src/common/channels.ts @@ -1,5 +1,6 @@ export const CHANNEL_GET_CONFIG = 'llonebot_get_config' export const CHANNEL_SET_CONFIG = 'llonebot_set_config' +export const CHANNEL_SET_CONFIG_CONFIRMED = 'llonebot_set_config_confirmed' export const CHANNEL_LOG = 'llonebot_log' export const CHANNEL_ERROR = 'llonebot_error' export const CHANNEL_UPDATE = 'llonebot_update' diff --git a/src/common/config.ts b/src/common/config.ts index 58833b6..6ddf484 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -2,8 +2,7 @@ import fs from 'node:fs' import { Config, OB11Config } from './types' import { mergeNewProperties } from './utils/helper' import path from 'node:path' -import { getSelfUin } from './data' -import { DATA_DIR } from './utils' +import { selfInfo, DATA_DIR } from './globalVars' export class ConfigUtil { private readonly configPath: string @@ -94,6 +93,6 @@ export class ConfigUtil { } export function getConfigUtil() { - const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`) + const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) return new ConfigUtil(configFilePath) } diff --git a/src/common/data.ts b/src/common/data.ts deleted file mode 100644 index 2138cbf..0000000 --- a/src/common/data.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - type GroupMember, - type SelfInfo, -} from '../ntqqapi/types' -import { type LLOneBotError } from './types' -import { NTQQGroupApi } from '../ntqqapi/api/group' -import { isNumeric } from './utils/helper' -import { NTQQUserApi } from '../ntqqapi/api' -import { RawMessage } from '../ntqqapi/types' -import { getConfigUtil } from './config' - -export const llonebotError: LLOneBotError = { - ffmpegError: '', - httpServerError: '', - wsServerError: '', - otherError: 'LLOneBot 未能正常启动,请检查日志查看错误', -} - -// 群号 -> 群成员map(uid=>GroupMember) -export const groupMembers: Map> = new Map>() - -export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { - const groupCodeStr = groupCode.toString() - const memberUinOrUidStr = memberUinOrUid.toString() - let members = groupMembers.get(groupCodeStr) - if (!members) { - try { - members = await NTQQGroupApi.getGroupMembers(groupCodeStr) - // 更新群成员列表 - groupMembers.set(groupCodeStr, members) - } - catch (e) { - return null - } - } - const getMember = () => { - let member: GroupMember | undefined = undefined - if (isNumeric(memberUinOrUidStr)) { - member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr) - } else { - member = members!.get(memberUinOrUidStr) - } - return member - } - let member = getMember() - if (!member) { - members = await NTQQGroupApi.getGroupMembers(groupCodeStr) - groupMembers.set(groupCodeStr, members) - member = getMember() - } - return member -} - -const selfInfo: SelfInfo = { - uid: '', - uin: '', - nick: '', - online: true, -} - -export async function getSelfNick(force = false): Promise { - if ((!selfInfo.nick || force) && selfInfo.uid) { - const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid) - if (userInfo) { - selfInfo.nick = userInfo.nick - return userInfo.nick - } - } - - return selfInfo.nick -} - -export function getSelfInfo() { - return selfInfo -} - -export function setSelfInfo(data: Partial) { - Object.assign(selfInfo, data) -} - -export function getSelfUid() { - return selfInfo['uid'] -} - -export function getSelfUin() { - return selfInfo['uin'] -} - -const messages: Map = new Map() - -/** 缓存近期消息内容 */ -export async function addMsgCache(msg: RawMessage) { - const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000 - if (expire === 0) { - return - } - const id = msg.msgId - messages.set(id, msg) - setTimeout(() => { - messages.delete(id) - }, expire) -} - -/** 获取近期消息内容 */ -export function getMsgCache(msgId: string) { - return messages.get(msgId) -} \ No newline at end of file diff --git a/src/common/globalVars.ts b/src/common/globalVars.ts new file mode 100644 index 0000000..51a543a --- /dev/null +++ b/src/common/globalVars.ts @@ -0,0 +1,22 @@ +import { LLOneBotError } from './types' +import { SelfInfo } from '../ntqqapi/types' +import path from 'node:path' + +export const llonebotError: LLOneBotError = { + ffmpegError: '', + httpServerError: '', + wsServerError: '', + otherError: 'LLOneBot 未能正常启动,请检查日志查看错误', +} + +export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data +export const TEMP_DIR: string = path.join(DATA_DIR, 'temp') +export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin +export const LOG_DIR = path.join(DATA_DIR, 'logs') + +export const selfInfo: SelfInfo = { + uid: '', + uin: '', + nick: '', + online: true, +} \ No newline at end of file diff --git a/src/common/server/http.ts b/src/common/server/http.ts deleted file mode 100644 index 3e5ba65..0000000 --- a/src/common/server/http.ts +++ /dev/null @@ -1,119 +0,0 @@ -import express, { Express, Request, Response } from 'express' -import http from 'node:http' -import cors from 'cors' -import { log } from '../utils/log' -import { getConfigUtil } from '../config' -import { llonebotError } from '../data' - -type RegisterHandler = (res: Response, payload: any) => Promise - -export abstract class HttpServerBase { - name: string = 'LLOneBot' - private readonly expressAPP: Express - private server: http.Server | null = null - - constructor() { - this.expressAPP = express() - // 添加 CORS 中间件 - this.expressAPP.use(cors()) - this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' })) - this.expressAPP.use((req, res, next) => { - // 兼容处理没有带content-type的请求 - // log("req.headers['content-type']", req.headers['content-type']) - req.headers['content-type'] = 'application/json' - const originalJson = express.json({ limit: '5000mb' }) - // 调用原始的express.json()处理器 - originalJson(req, res, (err) => { - if (err) { - log('Error parsing JSON:', err) - return res.status(400).send('Invalid JSON') - } - next() - }) - }) - } - - authorize(req: Request, res: Response, next: () => void) { - let serverToken = getConfigUtil().getConfig().token - let clientToken = '' - const authHeader = req.get('authorization') - if (authHeader) { - clientToken = authHeader.split('Bearer ').pop()! - log('receive http header token', clientToken) - } else if (req.query.access_token) { - if (Array.isArray(req.query.access_token)) { - clientToken = req.query.access_token[0].toString() - } else { - clientToken = req.query.access_token.toString() - } - log('receive http url token', clientToken) - } - - if (serverToken && clientToken != serverToken) { - return res.status(403).send(JSON.stringify({ message: 'token verify failed!' })) - } - next() - } - - start(port: number) { - try { - this.expressAPP.get('/', (req: Request, res: Response) => { - res.send(`${this.name} 已启动`) - }) - this.listen(port) - llonebotError.httpServerError = '' - } catch (e: any) { - log('HTTP服务启动失败', e.toString()) - llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString() - } - } - - stop() { - llonebotError.httpServerError = '' - if (this.server) { - this.server.close() - this.server = null - } - } - - restart(port: number) { - this.stop() - this.start(port) - } - - abstract handleFailed(res: Response, payload: any, err: any): void - - registerRouter(method: 'post' | 'get' | string, url: string, handler: RegisterHandler) { - if (!url.startsWith('/')) { - url = '/' + url - } - - if (!this.expressAPP[method]) { - const err = `${this.name} register router failed,${method} not exist` - log(err) - throw err - } - this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => { - let payload = req.body - if (method == 'get') { - payload = req.query - } else if (req.query) { - payload = { ...req.query, ...req.body } - } - log('收到 HTTP 请求', url, payload) - try { - res.send(await handler(res, payload)) - } catch (e: any) { - this.handleFailed(res, payload, e.stack.toString()) - } - }) - } - - protected listen(port: number) { - this.server = this.expressAPP.listen(port, '0.0.0.0', () => { - const info = `${this.name} started 0.0.0.0:${port}` - console.log(info) - log(info) - }) - } -} diff --git a/src/common/types.ts b/src/common/types.ts index eef438a..f1a2991 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -22,7 +22,7 @@ export interface Config { enableLLOB: boolean ob11: OB11Config token?: string - heartInterval?: number // ms + heartInterval: number // ms enableLocalFile2Url?: boolean // 开启后,本地文件路径图片会转成http链接, 语音会转成base64 debug?: boolean reportSelfMessage?: boolean diff --git a/src/common/utils/LegacyLog.ts b/src/common/utils/LegacyLog.ts new file mode 100644 index 0000000..1c335d2 --- /dev/null +++ b/src/common/utils/LegacyLog.ts @@ -0,0 +1,25 @@ +import fs from 'fs' +import path from 'node:path' +import { truncateString } from './index' +import { getConfigUtil } from '../config' +import { LOG_DIR } from '../globalVars' + +export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-') + +export function log(...msg: any[]) { + if (!getConfigUtil().getConfig().log) { + return + } + let logMsg = '' + for (const msgItem of msg) { + // 判断是否是对象 + if (typeof msgItem === 'object') { + logMsg += JSON.stringify(truncateString(msgItem)) + ' ' + continue + } + logMsg += msgItem + ' ' + } + const currentDateTime = new Date().toLocaleString() + logMsg = `${currentDateTime} ${logMsg}\n\n` + fs.appendFile(path.join(LOG_DIR, logFileName), logMsg, () => { }) +} diff --git a/src/common/utils/MessageUnique.ts b/src/common/utils/MessageUnique.ts index 27b882e..c733a76 100644 --- a/src/common/utils/MessageUnique.ts +++ b/src/common/utils/MessageUnique.ts @@ -1,12 +1,12 @@ -import { Peer } from '@/ntqqapi/types' -import { createHash } from 'node:crypto' -import { LimitedHashTable } from './table' -import { DATA_DIR } from './index' -import Database, { Tables } from 'minato' -import SQLite from '@minatojs/driver-sqlite' import fsPromise from 'node:fs/promises' import fs from 'node:fs' import path from 'node:path' +import Database, { Tables } from 'minato' +import SQLite from '@minatojs/driver-sqlite' +import { Peer } from '@/ntqqapi/types' +import { createHash } from 'node:crypto' +import { LimitedHashTable } from './table' +import { DATA_DIR } from '../globalVars' import { FileCacheV2 } from '../types' interface SQLiteTables extends Tables { diff --git a/src/common/utils/QQBasicInfo.ts b/src/common/utils/QQBasicInfo.ts index d0c8bb8..d4368a5 100644 --- a/src/common/utils/QQBasicInfo.ts +++ b/src/common/utils/QQBasicInfo.ts @@ -1,12 +1,11 @@ import path from 'node:path' import os from 'node:os' -import { systemPlatform } from './system' export const exePath = process.execPath function getPKGPath() { let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json') - if (systemPlatform === 'darwin') { + if (os.platform() === 'darwin') { p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json') } return p diff --git a/src/common/utils/audio.ts b/src/common/utils/audio.ts index a19996f..e6152aa 100644 --- a/src/common/utils/audio.ts +++ b/src/common/utils/audio.ts @@ -2,11 +2,11 @@ import path from 'node:path' import ffmpeg from 'fluent-ffmpeg' import fsPromise from 'node:fs/promises' import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm' -import { log } from './log' -import { TEMP_DIR } from './index' +import { TEMP_DIR } from '../globalVars' import { getConfigUtil } from '../config' import { randomUUID } from 'node:crypto' import { Readable } from 'node:stream' +import { Context } from 'cordis' interface FFmpegOptions { input?: string[] @@ -15,14 +15,14 @@ interface FFmpegOptions { type Input = string | Readable -function convert(input: Input, options: FFmpegOptions): Promise -function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise -function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise | Promise { +function convert(ctx: Context, input: Input, options: FFmpegOptions): Promise +function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath: string): Promise +function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?: string): Promise | Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = [] let command = ffmpeg(input) .on('error', err => { - log(`FFmpeg处理转换出错: `, err.message) + ctx.logger.error(`FFmpeg处理转换出错: `, err.message) reject(err) }) .on('end', () => { @@ -53,17 +53,17 @@ function convert(input: Input, options: FFmpegOptions, outputPath?: string): Pro }) } -export async function encodeSilk(filePath: string) { +export async function encodeSilk(ctx: Context, filePath: string) { try { const file = await fsPromise.readFile(filePath) if (!isSilk(file)) { - log(`语音文件${filePath}需要转换成silk`) + ctx.logger.info(`语音文件${filePath}需要转换成silk`) let result: EncodeResult const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) { result = await encode(file, 0) } else { - const input = await convert(filePath, { + const input = await convert(ctx, filePath, { output: [ '-ar 24000', '-ac 1', @@ -74,7 +74,7 @@ export async function encodeSilk(filePath: string) { } const pttPath = path.join(TEMP_DIR, randomUUID()) await fsPromise.writeFile(pttPath, result.data) - log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration) + ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration) return { converted: true, path: pttPath, @@ -86,7 +86,7 @@ export async function encodeSilk(filePath: string) { try { duration = getDuration(silk) / 1000 } catch (e: any) { - log('获取语音文件时长失败, 默认为1秒', filePath, e.stack) + ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, e.stack) } return { converted: false, @@ -95,21 +95,21 @@ export async function encodeSilk(filePath: string) { } } } catch (error: any) { - log('convert silk failed', error.stack) + ctx.logger.error('convert silk failed', error.stack) return {} } } type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' -export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') { +export async function decodeSilk(ctx: Context, inputFilePath: string, outFormat: OutFormat = 'mp3') { const silk = await fsPromise.readFile(inputFilePath) const { data } = await decode(silk, 24000) const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath)) const outFilePath = tmpPath + `.${outFormat}` const pcmFilePath = tmpPath + '.pcm' await fsPromise.writeFile(pcmFilePath, data) - return convert(pcmFilePath, { + return convert(ctx, pcmFilePath, { input: [ '-f s16le', '-ar 24000', diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index 752ebfa..fc08594 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import fsPromise from 'node:fs/promises' import path from 'node:path' -import { TEMP_DIR } from './index' +import { TEMP_DIR } from '../globalVars' import { randomUUID, createHash } from 'node:crypto' import { fileURLToPath } from 'node:url' diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 22f1c3c..8dc8ccd 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,14 +1,9 @@ -import path from 'node:path' - export * from './file' export * from './helper' -export * from './log' +export * from './LegacyLog' export * from './qqlevel' export * from './QQBasicInfo' export * from './upgrade' -export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data -export const TEMP_DIR: string = path.join(DATA_DIR, 'temp') -export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin export { getVideoInfo } from './video' export { checkFfmpeg } from './video' export { encodeSilk } from './audio' \ No newline at end of file diff --git a/src/common/utils/log.ts b/src/common/utils/log.ts deleted file mode 100644 index 537b3eb..0000000 --- a/src/common/utils/log.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getSelfInfo } from '../data' -import fs from 'fs' -import path from 'node:path' -import { DATA_DIR, truncateString } from './index' -import { getConfigUtil } from '../config' - -const date = new Date() -const logFileName = `llonebot-${date.toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-') -const logDir = path.join(DATA_DIR, 'logs') -if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }) -} - -export function log(...msg: any[]) { - if (!getConfigUtil().getConfig().log) { - return //console.log(...msg); - } - const selfInfo = getSelfInfo() - const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' - let logMsg = '' - for (let msgItem of msg) { - // 判断是否是对象 - if (typeof msgItem === 'object') { - let obj = JSON.parse(JSON.stringify(msgItem)) - logMsg += JSON.stringify(truncateString(obj)) + ' ' - continue - } - logMsg += msgItem + ' ' - } - let currentDateTime = new Date().toLocaleString() - logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` - // sendLog(...msg); - // console.log(msg) - fs.appendFile(path.join(logDir, logFileName), logMsg, () => {}) -} diff --git a/src/common/utils/request.ts b/src/common/utils/request.ts index 0c471d4..164b057 100644 --- a/src/common/utils/request.ts +++ b/src/common/utils/request.ts @@ -1,6 +1,5 @@ -import https from 'node:https'; -import http from 'node:http'; -import { log } from '@/common/utils/log' +import https from 'node:https' +import http from 'node:http' export class RequestUtil { // 适用于获取服务器下发cookies时获取,仅GET diff --git a/src/common/utils/sign.ts b/src/common/utils/sign.ts index 5fd3032..1c715fc 100644 --- a/src/common/utils/sign.ts +++ b/src/common/utils/sign.ts @@ -1,4 +1,4 @@ -import { log } from './log' +import { Context } from 'cordis' export interface IdMusicSignPostData { type: 'qq' | '163' @@ -19,7 +19,7 @@ export type MusicSignPostData = IdMusicSignPostData | CustomMusicSignPostData export class MusicSign { private readonly url: string - constructor(url: string) { + constructor(protected ctx: Context, url: string) { this.url = url } @@ -31,7 +31,7 @@ export class MusicSign { }) if (!resp.ok) throw new Error(resp.statusText) const data = await resp.text() - log('音乐消息生成成功', data) + this.ctx.logger.info('音乐消息生成成功', data) return data } } diff --git a/src/common/utils/system.ts b/src/common/utils/system.ts deleted file mode 100644 index 18e5d43..0000000 --- a/src/common/utils/system.ts +++ /dev/null @@ -1,10 +0,0 @@ -import os from 'node:os'; -import path from 'node:path'; - -export const systemPlatform = os.platform(); -export const cpuArch = os.arch(); -export const systemVersion = os.release(); -// export const hostname = os.hostname(); // win7不支持 -const homeDir = os.homedir(); -export const downloadsPath = path.join(homeDir, 'Downloads'); -export const systemName = os.type(); diff --git a/src/common/utils/upgrade.ts b/src/common/utils/upgrade.ts index 989a57d..844f07c 100644 --- a/src/common/utils/upgrade.ts +++ b/src/common/utils/upgrade.ts @@ -1,8 +1,9 @@ import { version } from '../../version' import * as path from 'node:path' import * as fs from 'node:fs' -import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.' +import { copyFolder, httpDownload, log } from '.' import compressing from 'compressing' +import { PLUGIN_DIR, TEMP_DIR } from '../globalVars' const downloadMirrorHosts = ['https://mirror.ghproxy.com/'] const checkVersionMirrorHosts = ['https://kkgithub.com'] @@ -10,9 +11,9 @@ const checkVersionMirrorHosts = ['https://kkgithub.com'] export async function checkNewVersion() { const latestVersionText = await getRemoteVersion() const latestVersion = latestVersionText.split('.') - log('llonebot last version', latestVersion) + //log('llonebot last version', latestVersion) const currentVersion: string[] = version.split('.') - log('llonebot current version', currentVersion) + //log('llonebot current version', currentVersion) for (let k of [0, 1, 2]) { if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) { log('') @@ -92,6 +93,6 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) { // log("releasePage", releasePage); if (releasePage === 'error') return '' return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0] - } catch {} + } catch { } return '' } diff --git a/src/common/utils/video.ts b/src/common/utils/video.ts index 9ddca67..e5b771e 100644 --- a/src/common/utils/video.ts +++ b/src/common/utils/video.ts @@ -1,6 +1,6 @@ -import { log } from './log' import ffmpeg from 'fluent-ffmpeg' -import fs from 'fs' +import fs from 'node:fs' +import { log } from './LegacyLog' import { getConfigUtil } from '../config' const defaultVideoThumbB64 = @@ -43,43 +43,19 @@ export async function getVideoInfo(filePath: string) { }) } -export async function encodeMp4(filePath: string) { - let videoInfo = await getVideoInfo(filePath) - log('视频信息', videoInfo) - if (videoInfo.format.indexOf('mp4') === -1) { - log('视频需要转换为MP4格式', filePath) - // 转成mp4 - const newPath: string = await new Promise((resolve, reject) => { - const newPath = filePath + '.mp4' - ffmpeg(filePath) - .toFormat('mp4') - .on('error', (err) => { - reject(`转换视频格式失败: ${err.message}`) - }) - .on('end', () => { - log('视频转换为MP4格式完成') - resolve(newPath) // 返回转换后的文件路径 - }) - .save(newPath) - }) - return await getVideoInfo(newPath) - } - return videoInfo -} - -export function checkFfmpeg(newPath: string | null = null): Promise { +export function checkFfmpeg(newPath?: string): Promise { return new Promise((resolve, reject) => { - log('开始检查ffmpeg', newPath) + log(`开始检查 FFmpeg ${newPath ?? ''}`) if (newPath) { ffmpeg.setFfmpegPath(newPath) } try { ffmpeg.getAvailableFormats((err, formats) => { if (err) { - log('ffmpeg is not installed or not found in PATH:', err) + log('FFmpeg is not installed or not found in PATH:', err) resolve(false) } else { - log('ffmpeg is installed.') + log('FFmpeg is installed.') resolve(true) } }) diff --git a/src/main/ipcsend.ts b/src/main/ipcsend.ts deleted file mode 100644 index 8efc722..0000000 --- a/src/main/ipcsend.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { webContents } from 'electron' - -function sendIPCMsg(channel: string, ...data: any) { - let contents = webContents.getAllWebContents() - for (const content of contents) { - try { - content.send(channel, ...data) - } catch (e) { - console.log('llonebot send ipc msg to render error:', e) - } - } -} diff --git a/src/main/log.ts b/src/main/log.ts new file mode 100644 index 0000000..a69a757 --- /dev/null +++ b/src/main/log.ts @@ -0,0 +1,42 @@ +import path from 'node:path' +import { Context, Logger } from 'cordis' +import { appendFile } from 'node:fs' +import { LOG_DIR, selfInfo } from '@/common/globalVars' +import { noop } from 'cosmokit' +import { getConfigUtil } from '../common/config' + +interface Config { + filename: string +} + +export default class Log { + static name = 'logger' + + constructor(ctx: Context, cfg: Config) { + // fetch data from the database + Logger.targets.splice(0, Logger.targets.length) + if (!getConfigUtil().getConfig().log) { + return + } + const file = path.join(LOG_DIR, cfg.filename) + const refreshNick = ctx.debounce(() => { + const ntUserApi = ctx.get('ntUserApi') + if (ntUserApi && !selfInfo.nick) { + ntUserApi.getSelfNick(true) + } + }, 1000) + const target: Logger.Target = { + colors: 0, + record: (record: Logger.Record) => { + if (!selfInfo.nick) { + refreshNick() + } + const dateTime = new Date(record.timestamp).toLocaleString() + const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' + const content = `${dateTime} [${record.type}] ${userInfo} | ${record.name} ${record.content}\n\n` + appendFile(file, content, noop) + }, + } + Logger.targets.push(target) + } +} \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 296734a..b57f3c6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,9 +1,10 @@ -// 运行在 Electron 主进程 下的插件入口 - -import { BrowserWindow, dialog, ipcMain } from 'electron' import path from 'node:path' import fs from 'node:fs' -import { Config } from '../common/types' +import Log from './log' +import Core from '../ntqqapi/core' +import OneBot11Adapter from '../onebot11/adapter' +import { BrowserWindow, dialog, ipcMain } from 'electron' +import { Config as LLOBConfig } from '../common/types' import { CHANNEL_CHECK_VERSION, CHANNEL_ERROR, @@ -12,54 +13,54 @@ import { CHANNEL_SELECT_FILE, CHANNEL_SET_CONFIG, CHANNEL_UPDATE, + CHANNEL_SET_CONFIG_CONFIRMED } from '../common/channels' -import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' -import { DATA_DIR, getBuildVersion, TEMP_DIR } from '../common/utils' -import { - llonebotError, - setSelfInfo, - getSelfInfo, - getSelfUid, - getSelfUin, - addMsgCache -} from '../common/data' -import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' -import { OB11Constructor } from '../onebot11/constructor' -import { - FriendRequestNotify, - GroupNotify, - GroupNotifyTypes, - RawMessage, - BuddyReqType, -} from '../ntqqapi/types' -import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' -import { postOb11Event } from '../onebot11/server/post-ob11-event' -import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' -import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest' -import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest' -import { MessageUnique } from '../common/utils/MessageUnique' -import { setConfig } from './setConfig' -import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api' +import { getBuildVersion } from '../common/utils' +import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' -import { log } from '../common/utils/log' import { getConfigUtil } from '../common/config' import { checkFfmpeg } from '../common/utils/video' -import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' -import '../ntqqapi/wrapper' -import { NTEventDispatch } from '../common/utils/EventTask' -import { wrapperConstructor, getSession } from '../ntqqapi/wrapper' -import { Peer } from '../ntqqapi/types' +import { getSession } from '../ntqqapi/wrapper' +import { Context } from 'cordis' +import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars' +import { log, logFileName } from '../common/utils/LegacyLog' +import { + NTQQFileApi, + NTQQFileCacheApi, + NTQQFriendApi, + NTQQGroupApi, + NTQQMsgApi, + NTQQUserApi, + NTQQWebApi, + NTQQWindowApi +} from '../ntqqapi/api' + +declare module 'cordis' { + interface Events { + 'llonebot/config-updated': (input: LLOBConfig) => void + } +} let mainWindow: BrowserWindow | null = null // 加载插件时触发 function onLoad() { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }) + } + + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }) + } + ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { return checkNewVersion() }) + ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => { return upgradeLLOneBot() }) + ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => { const selectPath = new Promise((resolve, reject) => { dialog @@ -73,11 +74,9 @@ function onLoad() { if (!result.canceled) { const _selectPath = path.join(result.filePaths[0]) resolve(_selectPath) - // let config = getConfigUtil().getConfig() - // config.ffmpeg = path.join(result.filePaths[0]); - // getConfigUtil().setConfig(config); + } else { + resolve('') } - resolve('') }) .catch((err) => { reject(err) @@ -90,9 +89,7 @@ function onLoad() { return '' } }) - if (!fs.existsSync(DATA_DIR)) { - fs.mkdirSync(DATA_DIR, { recursive: true }) - } + ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常' @@ -100,277 +97,53 @@ function onLoad() { let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` error = error.replace('\n\n', '\n') error = error.trim() - log('查询llonebot错误信息', error) + log('查询 LLOneBot 错误信息', error) return error }) + ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { const config = getConfigUtil().getConfig() return config }) - ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => { - if (!ask) { - setConfig(config) - .then() - .catch((e) => { - log('保存设置失败', e.stack) + + ipcMain.handle(CHANNEL_SET_CONFIG, (event, ask: boolean, config: LLOBConfig) => { + return new Promise(resolve => { + if (!ask) { + getConfigUtil().setConfig(config) + log('配置已更新', config) + checkFfmpeg(config.ffmpeg).then() + resolve(true) + return + } + dialog + .showMessageBox(mainWindow!, { + type: 'question', + buttons: ['确认', '取消'], + defaultId: 0, // 默认选中的按钮,0 代表第一个按钮,即 "确认" + title: '确认保存', + message: '是否保存?', + detail: 'LLOneBot配置已更改,是否保存?', }) - return - } - dialog - .showMessageBox(mainWindow!, { - type: 'question', - buttons: ['确认', '取消'], - defaultId: 0, // 默认选中的按钮,0 代表第一个按钮,即 "确认" - title: '确认保存', - message: '是否保存?', - detail: 'LLOneBot配置已更改,是否保存?', - }) - .then((result) => { - if (result.response === 0) { - setConfig(config) - .then() - .catch((e) => { - log('保存设置失败', e.stack) - }) - } - else { - } - }) - .catch((err) => { - log('保存设置询问弹窗错误', err) - }) + .then((result) => { + if (result.response === 0) { + getConfigUtil().setConfig(config) + log('配置已更新', config) + checkFfmpeg(config.ffmpeg).then() + resolve(true) + } + }) + .catch((err) => { + log('保存设置询问弹窗错误', err) + resolve(false) + }) + }) }) ipcMain.on(CHANNEL_LOG, (event, arg) => { log(arg) }) - async function postReceiveMsg(msgList: RawMessage[]) { - const { debug, reportSelfMessage } = getConfigUtil().getConfig() - for (let message of msgList) { - // 过滤启动之前的消息 - if (parseInt(message.msgTime) < startTime / 1000) { - continue - } - // log("收到新消息", message.msgId, message.msgSeq) - const peer: Peer = { - chatType: message.chatType, - peerUid: message.peerUid - } - message.msgShortId = MessageUnique.createMsg(peer, message.msgId) - addMsgCache(message) - - OB11Constructor.message(message) - .then((msg) => { - if (!debug && msg.message.length === 0) { - return - } - const isSelfMsg = msg.user_id.toString() === getSelfUin() - if (isSelfMsg && !reportSelfMessage) { - return - } - if (isSelfMsg) { - msg.target_id = parseInt(message.peerUin) - } - postOb11Event(msg) - // log("post msg", msg) - }) - .catch((e) => log('constructMessage error: ', e.stack.toString())) - OB11Constructor.GroupEvent(message).then((groupEvent) => { - if (groupEvent) { - // log("post group event", groupEvent); - postOb11Event(groupEvent) - } - }) - OB11Constructor.PrivateEvent(message).then((privateEvent) => { - //log(message) - if (privateEvent) { - // log("post private event", privateEvent); - postOb11Event(privateEvent) - } - }) - } - } - - async function startReceiveHook() { - startHook() - registerReceiveHook<{ - msgList: Array - }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => { - try { - await postReceiveMsg(payload.msgList) - } catch (e: any) { - log('report message error: ', e.stack.toString()) - } - }) - const recallMsgIds: string[] = [] // 避免重复上报 - registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { - for (const message of payload.msgList) { - if (message.recallTime != '0') { - if (recallMsgIds.includes(message.msgId)) { - continue - } - recallMsgIds.push(message.msgId) - const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId) - if (!oriMessageId) { - continue - } - OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => { - if (recallEvent) { - //log('post recall event', recallEvent) - postOb11Event(recallEvent) - } - }) - } - } - }) - registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { - const { reportSelfMessage } = getConfigUtil().getConfig() - if (!reportSelfMessage) { - return - } - // log("reportSelfMessage", payload) - try { - await postReceiveMsg([payload.msgRecord]) - } catch (e: any) { - log('report self message error: ', e.stack.toString()) - } - }) - const processedGroupNotify: string[] = [] - registerReceiveHook<{ - doubt: boolean - oldestUnreadSeq: string - unreadCount: number - }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { - if (payload.unreadCount) { - // log("开始获取群通知详情") - let notifies: GroupNotify[] - try { - notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount) - } catch (e) { - // log("获取群通知详情失败", e); - return - } - - for (const notify of notifies) { - try { - notify.time = Date.now() - const notifyTime = parseInt(notify.seq) / 1000 - const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type - if (notifyTime < startTime || processedGroupNotify.includes(flag)) { - continue - } - processedGroupNotify.push(flag) - if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { - log('有成员退出通知', notify) - const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))! - let operatorId = member1Uin - let subType: GroupDecreaseSubType = 'leave' - if (notify.user2.uid) { - // 是被踢的 - const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid) - if (member2Uin) { - operatorId = member2Uin - } - subType = 'kick' - } - const groupDecreaseEvent = new OB11GroupDecreaseEvent( - parseInt(notify.group.groupCode), - parseInt(member1Uin), - parseInt(operatorId), - subType, - ) - postOb11Event(groupDecreaseEvent, true) - } - else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { - log('有加群请求') - let requestQQ = '' - try { - // uid-->uin - requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid)) - if (isNaN(parseInt(requestQQ))) { - requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin - } - } catch (e) { - log('获取加群人QQ号失败 Uid:', notify.user1.uid, e) - } - let invitorId: string - if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) { - // groupRequestEvent.sub_type = 'invite' - try { - // uid-->uin - invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) - if (isNaN(parseInt(invitorId))) { - invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin - } - } catch (e) { - invitorId = '' - log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e) - } - } - const groupRequestEvent = new OB11GroupRequestEvent( - parseInt(notify.group.groupCode), - parseInt(requestQQ) || 0, - flag, - notify.postscript, - invitorId! === undefined ? undefined : +invitorId, - 'add' - ) - postOb11Event(groupRequestEvent) - } - else if (notify.type == GroupNotifyTypes.INVITE_ME) { - log('收到邀请我加群通知') - const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || '' - const groupInviteEvent = new OB11GroupRequestEvent( - parseInt(notify.group.groupCode), - parseInt(userId), - flag, - undefined, - undefined, - 'invite' - ) - postOb11Event(groupInviteEvent) - } - } catch (e: any) { - log('解析群通知失败', e.stack.toString()) - } - } - } - else if (payload.doubt) { - // 可能有群管理员变动 - } - }) - - registerReceiveHook(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { - for (const req of payload.data.buddyReqs) { - if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) { - continue - } - if (+req.reqTime < startTime / 1000) { - continue - } - let userId = 0 - try { - const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid) - userId = parseInt(requesterUin) - } catch (e) { - log('获取加好友者QQ号失败', e) - } - const flag = req.friendUid + '|' + req.reqTime - const comment = req.extWords - const friendRequestEvent = new OB11FriendRequestEvent( - userId, - comment, - flag - ) - postOb11Event(friendRequestEvent) - } - }) - } - - let startTime = 0 // 毫秒 - - async function start(uid: string, uin: string) { + async function start() { log('process pid', process.pid) const config = getConfigUtil().getConfig() if (!config.enableLLOB) { @@ -381,50 +154,44 @@ function onLoad() { if (!fs.existsSync(TEMP_DIR)) { fs.mkdirSync(TEMP_DIR, { recursive: true }) } - llonebotError.otherError = '' - startTime = Date.now() - const WrapperSession = getSession() - if (WrapperSession) { - NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession }) - } - MessageUnique.init(uin) - - //log('start activate group member info') - // 下面两个会导致CPU占用过高,QQ卡死 - // NTQQGroupApi.activateMemberInfoChange().then().catch(log) - // NTQQGroupApi.activateMemberListChange().then().catch(log) - startReceiveHook().then() - - if (config.ob11.enableHttp) { - ob11HTTPServer.start(config.ob11.httpPort) - } - if (config.ob11.enableWs) { - ob11WebsocketServer.start(config.ob11.wsPort) - } - if (config.ob11.enableWsReverse) { - ob11ReverseWebsockets.start() - } - if (config.ob11.enableHttpHeart) { - httpHeart.start() - } - - log('LLOneBot start') + const ctx = new Context() + ctx.plugin(Log, { + filename: logFileName + }) + ctx.plugin(NTQQFileApi) + ctx.plugin(NTQQFileCacheApi) + ctx.plugin(NTQQFriendApi) + ctx.plugin(NTQQGroupApi) + ctx.plugin(NTQQMsgApi) + ctx.plugin(NTQQUserApi) + ctx.plugin(NTQQWebApi) + ctx.plugin(NTQQWindowApi) + ctx.plugin(Core, config) + ctx.plugin(OneBot11Adapter, { + ...config.ob11, + heartInterval: config.heartInterval, + token: config.token!, + debug: config.debug!, + reportSelfMessage: config.reportSelfMessage!, + msgCacheExpire: config.msgCacheExpire!, + }) + ctx.start() + ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => { + ctx.parallel('llonebot/config-updated', config) + }) } const buildVersion = getBuildVersion() const intervalId = setInterval(() => { - const current = getSelfInfo() - if (!current.uin) { - setSelfInfo({ - uin: globalThis.authData?.uin, - uid: globalThis.authData?.uid, - nick: current.uin, - }) - } - if (current.uin && (buildVersion >= 27187 || getSession())) { + const self = Object.assign(selfInfo, { + uin: globalThis.authData?.uin, + uid: globalThis.authData?.uid, + online: true + }) + if (self.uin && (buildVersion >= 27187 || getSession())) { clearInterval(intervalId) - start(current.uid, current.uin) + start() } }, 600) } diff --git a/src/main/setConfig.ts b/src/main/setConfig.ts deleted file mode 100644 index 58baaf6..0000000 --- a/src/main/setConfig.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Config } from '../common/types' -import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' -import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' -import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' -import { llonebotError } from '../common/data' -import { getConfigUtil } from '../common/config' -import { checkFfmpeg, log } from '../common/utils' - -export async function setConfig(config: Config) { - let oldConfig = { ...getConfigUtil().getConfig() } - getConfigUtil().setConfig(config) - if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) { - ob11HTTPServer.restart(config.ob11.httpPort) - } - // 判断是否启用或关闭HTTP服务 - if (!config.ob11.enableHttp) { - ob11HTTPServer.stop() - } else { - ob11HTTPServer.start(config.ob11.httpPort) - } - // 正向ws端口变化,重启服务 - if (config.ob11.wsPort != oldConfig.ob11.wsPort) { - ob11WebsocketServer.restart(config.ob11.wsPort) - llonebotError.wsServerError = '' - } - // 判断是否启用或关闭正向ws - if (config.ob11.enableWs != oldConfig.ob11.enableWs) { - if (config.ob11.enableWs) { - ob11WebsocketServer.start(config.ob11.wsPort) - } else { - ob11WebsocketServer.stop() - } - } - // 判断是否启用或关闭反向ws - if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) { - if (config.ob11.enableWsReverse) { - ob11ReverseWebsockets.start() - } else { - ob11ReverseWebsockets.stop() - } - } - if (config.ob11.enableWsReverse) { - // 判断反向ws地址有变化 - if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) { - log('反向ws地址有变化, 重启反向ws服务') - ob11ReverseWebsockets.restart() - } else { - for (const newHost of config.ob11.wsHosts) { - if (!oldConfig.ob11.wsHosts.includes(newHost)) { - log('反向ws地址有变化, 重启反向ws服务') - ob11ReverseWebsockets.restart() - break - } - } - } - } - if (config.ob11.enableHttpHeart) { - // 启动http心跳 - httpHeart.start() - } else { - // 关闭http心跳 - httpHeart.stop() - } - log('old config', oldConfig) - log('配置已更新', config) - checkFfmpeg(config.ffmpeg).then() -} diff --git a/src/ntqqapi/api/file.ts b/src/ntqqapi/api/file.ts index b233621..8f64c04 100644 --- a/src/ntqqapi/api/file.ts +++ b/src/ntqqapi/api/file.ts @@ -16,8 +16,7 @@ import { import path from 'node:path' import fs from 'node:fs' import { ReceiveCmdS } from '../hook' -import { log, TEMP_DIR } from '@/common/utils' -import { rkeyManager } from '@/ntqqapi/helper/rkey' +import { RkeyManager } from '@/ntqqapi/helper/rkey' import { getSession } from '@/ntqqapi/wrapper' import { Peer } from '@/ntqqapi/types/msg' import { calculateFileMD5 } from '@/common/utils/file' @@ -26,10 +25,26 @@ import fsPromise from 'node:fs/promises' import { NTEventDispatch } from '@/common/utils/EventTask' import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' import { Time } from 'cosmokit' +import { Service, Context } from 'cordis' +import { TEMP_DIR } from '@/common/globalVars' + +declare module 'cordis' { + interface Context { + ntFileApi: NTQQFileApi + ntFileCacheApi: NTQQFileCacheApi + } +} + +export class NTQQFileApi extends Service { + private rkeyManager: RkeyManager + + constructor(protected ctx: Context) { + super(ctx, 'ntFileApi', true) + this.rkeyManager = new RkeyManager(ctx, 'http://napcat-sign.wumiao.wang:2082/rkey') + } -export class NTQQFileApi { /** 27187 TODO */ - static async getVideoUrl(peer: Peer, msgId: string, elementId: string) { + async getVideoUrl(peer: Peer, msgId: string, elementId: string) { const session = getSession() return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, msgId, @@ -38,14 +53,14 @@ export class NTQQFileApi { { downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url } - static async getFileType(filePath: string) { + async getFileType(filePath: string) { return fileTypeFromFile(filePath) } // 上传文件到QQ的文件夹 - static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { + async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { const fileMd5 = await calculateFileMD5(filePath) - let ext = (await NTQQFileApi.getFileType(filePath))?.ext || '' + let ext = (await this.getFileType(filePath))?.ext || '' if (ext) { ext = '.' + ext } @@ -96,7 +111,7 @@ export class NTQQFileApi { } } - static async downloadMedia( + async downloadMedia( msgId: string, chatType: ChatType, peerUid: string, @@ -192,7 +207,7 @@ export class NTQQFileApi { return filePath } - static async getImageSize(filePath: string) { + async getImageSize(filePath: string) { return await invoke<{ width: number; height: number }>({ className: NTClass.FS_API, methodName: NTMethod.IMAGE_SIZE, @@ -200,7 +215,7 @@ export class NTQQFileApi { }) } - static async getImageUrl(element: PicElement) { + async getImageUrl(element: PicElement) { if (!element) { return '' } @@ -217,7 +232,7 @@ export class NTQQFileApi { if (UrlRkey) { return IMAGE_HTTP_HOST_NT + url } - const rkeyData = await rkeyManager.getRkey() + const rkeyData = await this.rkeyManager.getRkey() UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}` } else { @@ -228,13 +243,17 @@ export class NTQQFileApi { // 没有url,需要自己拼接 return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` } - log('图片url获取失败', element) + this.ctx.logger.error('图片url获取失败', element) return '' } } -export class NTQQFileCacheApi { - static async setCacheSilentScan(isSilent: boolean = true) { +export class NTQQFileCacheApi extends Service { + constructor(protected ctx: Context) { + super(ctx, 'ntFileCacheApi', true) + } + + async setCacheSilentScan(isSilent: boolean = true) { return await invoke({ methodName: NTMethod.CACHE_SET_SILENCE, args: [ @@ -246,7 +265,7 @@ export class NTQQFileCacheApi { }) } - static getCacheSessionPathList() { + getCacheSessionPathList() { return invoke< { key: string @@ -258,7 +277,7 @@ export class NTQQFileCacheApi { }) } - static clearCache(cacheKeys: Array = ['tmp', 'hotUpdate']) { + clearCache(cacheKeys: Array = ['tmp', 'hotUpdate']) { return invoke({ // TODO: 目前还不知道真正的返回值是什么 methodName: NTMethod.CACHE_CLEAR, @@ -271,7 +290,7 @@ export class NTQQFileCacheApi { }) } - static addCacheScannedPaths(pathMap: object = {}) { + addCacheScannedPaths(pathMap: object = {}) { return invoke({ methodName: NTMethod.CACHE_ADD_SCANNED_PATH, args: [ @@ -283,7 +302,7 @@ export class NTQQFileCacheApi { }) } - static scanCache() { + scanCache() { invoke({ methodName: ReceiveCmdS.CACHE_SCAN_FINISH, classNameIsRegister: true, @@ -295,21 +314,21 @@ export class NTQQFileCacheApi { }) } - static getHotUpdateCachePath() { + getHotUpdateCachePath() { return invoke({ className: NTClass.HOTUPDATE_API, methodName: NTMethod.CACHE_PATH_HOT_UPDATE, }) } - static getDesktopTmpPath() { + getDesktopTmpPath() { return invoke({ className: NTClass.BUSINESS_API, methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP, }) } - static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { + getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { return new Promise((res, rej) => { invoke({ methodName: NTMethod.CACHE_CHAT_GET, @@ -328,7 +347,7 @@ export class NTQQFileCacheApi { }) } - static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { + getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { const _lastRecord = lastRecord ? lastRecord : { fileType: fileType } return invoke({ @@ -346,7 +365,7 @@ export class NTQQFileCacheApi { }) } - static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { + async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { return await invoke({ methodName: NTMethod.CACHE_CHAT_CLEAR, args: [ diff --git a/src/ntqqapi/api/friend.ts b/src/ntqqapi/api/friend.ts index 74972fd..bdc6142 100644 --- a/src/ntqqapi/api/friend.ts +++ b/src/ntqqapi/api/friend.ts @@ -6,10 +6,21 @@ import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { NTEventDispatch } from '@/common/utils/EventTask' import { LimitedHashTable } from '@/common/utils/table' import { pick } from 'cosmokit' +import { Service, Context } from 'cordis' + +declare module 'cordis' { + interface Context { + ntFriendApi: NTQQFriendApi + } +} + +export class NTQQFriendApi extends Service { + constructor(protected ctx: Context) { + super(ctx, 'ntFriendApi', true) + } -export class NTQQFriendApi { /** 大于或等于 26702 应使用 getBuddyV2 */ - static async getFriends(forced = false) { + async getFriends(forced = false) { const data = await invoke<{ data: { categoryId: number @@ -30,7 +41,7 @@ export class NTQQFriendApi { return _friends } - static async handleFriendRequest(flag: string, accept: boolean) { + async handleFriendRequest(flag: string, accept: boolean) { const data = flag.split('|') if (data.length < 2) { return @@ -60,7 +71,7 @@ export class NTQQFriendApi { } } - static async getBuddyV2(refresh = false): Promise { + async getBuddyV2(refresh = false): Promise { const session = getSession() if (session) { const uids: string[] = [] @@ -90,7 +101,7 @@ export class NTQQFriendApi { } } - static async getBuddyIdMap(refresh = false): Promise> { + async getBuddyIdMap(refresh = false): Promise> { const retMap: LimitedHashTable = new LimitedHashTable(5000) const session = getSession() if (session) { @@ -122,7 +133,7 @@ export class NTQQFriendApi { return retMap } - static async getBuddyV2ExWithCate(refresh = false) { + async getBuddyV2ExWithCate(refresh = false) { const session = getSession() if (session) { const uids: string[] = [] @@ -170,7 +181,7 @@ export class NTQQFriendApi { } } - static async isBuddy(uid: string): Promise { + async isBuddy(uid: string): Promise { const session = getSession() if (session) { return session.getBuddyService().isBuddy(uid) diff --git a/src/ntqqapi/api/group.ts b/src/ntqqapi/api/group.ts index d3fba0c..676c383 100644 --- a/src/ntqqapi/api/group.ts +++ b/src/ntqqapi/api/group.ts @@ -2,14 +2,28 @@ import { ReceiveCmdS } from '../hook' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types' import { invoke, NTClass, NTMethod } from '../ntcall' import { GeneralCallResult } from '../services' -import { NTQQWindowApi, NTQQWindows } from './window' +import { NTQQWindows } from './window' import { getSession } from '../wrapper' import { NTEventDispatch } from '@/common/utils/EventTask' import { NodeIKernelGroupListener } from '../listeners' import { NodeIKernelGroupService } from '../services' +import { Service, Context } from 'cordis' +import { isNumeric } from '@/common/utils/helper' -export class NTQQGroupApi { - static async getGroups(forced = false): Promise { +declare module 'cordis' { + interface Context { + ntGroupApi: NTQQGroupApi + } +} + +export class NTQQGroupApi extends Service { + private groupMembers: Map> = new Map>() + + constructor(protected ctx: Context) { + super(ctx, 'ntGroupApi', true) + } + + async getGroups(forced = false): Promise { if (NTEventDispatch.initialised) { type ListenerType = NodeIKernelGroupListener['onGroupListUpdate'] const [, , groupList] = await NTEventDispatch.CallNormalEvent @@ -37,7 +51,7 @@ export class NTQQGroupApi { } } - static async getGroupMembers(groupQQ: string, num = 3000): Promise> { + async getGroupMembers(groupQQ: string, num = 3000): Promise> { const session = getSession() let result: Awaited> if (session) { @@ -73,16 +87,48 @@ export class NTQQGroupApi { return result.result.infos } - static async getGroupIgnoreNotifies() { - await NTQQGroupApi.getSingleScreenNotifies(14) - return await NTQQWindowApi.openWindow( + async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { + const groupCodeStr = groupCode.toString() + const memberUinOrUidStr = memberUinOrUid.toString() + let members = this.groupMembers.get(groupCodeStr) + if (!members) { + try { + members = await this.getGroupMembers(groupCodeStr) + // 更新群成员列表 + this.groupMembers.set(groupCodeStr, members) + } + catch (e) { + return null + } + } + const getMember = () => { + let member: GroupMember | undefined = undefined + if (isNumeric(memberUinOrUidStr)) { + member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr) + } else { + member = members!.get(memberUinOrUidStr) + } + return member + } + let member = getMember() + if (!member) { + members = await this.getGroupMembers(groupCodeStr) + this.groupMembers.set(groupCodeStr, members) + member = getMember() + } + return member + } + + async getGroupIgnoreNotifies() { + await this.getSingleScreenNotifies(14) + return await this.ctx.ntWindowApi.openWindow( NTQQWindows.GroupNotifyFilterWindow, [], ReceiveCmdS.GROUP_NOTIFY, ) } - static async getSingleScreenNotifies(num: number) { + async getSingleScreenNotifies(num: number) { if (NTEventDispatch.initialised) { const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent <(arg1: boolean, arg2: string, arg3: number) => Promise, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void> @@ -112,12 +158,12 @@ export class NTQQGroupApi { } /** 27187 TODO */ - static async delGroupFile(groupCode: string, files: string[]) { + async delGroupFile(groupCode: string, files: string[]) { const session = getSession() return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files) } - static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { + async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { const flagitem = flag.split('|') const groupCode = flagitem[0] const seq = flagitem[1] @@ -157,7 +203,7 @@ export class NTQQGroupApi { } } - static async quitGroup(groupQQ: string) { + async quitGroup(groupQQ: string) { const session = getSession() if (session) { return session.getGroupService().quitGroup(groupQQ) @@ -169,7 +215,7 @@ export class NTQQGroupApi { } } - static async kickMember( + async kickMember( groupQQ: string, kickUids: string[], refuseForever = false, @@ -193,7 +239,7 @@ export class NTQQGroupApi { } } - static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { + async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { // timeStamp为秒数, 0为解除禁言 const session = getSession() if (session) { @@ -211,7 +257,7 @@ export class NTQQGroupApi { } } - static async banGroup(groupQQ: string, shutUp: boolean) { + async banGroup(groupQQ: string, shutUp: boolean) { const session = getSession() if (session) { return session.getGroupService().setGroupShutUp(groupQQ, shutUp) @@ -229,7 +275,7 @@ export class NTQQGroupApi { } } - static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { + async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { const session = getSession() if (session) { return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName) @@ -248,7 +294,7 @@ export class NTQQGroupApi { } } - static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { + async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { const session = getSession() if (session) { return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role) @@ -267,7 +313,7 @@ export class NTQQGroupApi { } } - static async setGroupName(groupQQ: string, groupName: string) { + async setGroupName(groupQQ: string, groupName: string) { const session = getSession() if (session) { return session.getGroupService().modifyGroupName(groupQQ, groupName, false) @@ -285,7 +331,7 @@ export class NTQQGroupApi { } } - static async getGroupAtAllRemainCount(groupCode: string) { + async getGroupAtAllRemainCount(groupCode: string) { return await invoke< GeneralCallResult & { atInfo: { @@ -308,7 +354,7 @@ export class NTQQGroupApi { } /** 27187 TODO */ - static async removeGroupEssence(GroupCode: string, msgId: string) { + async removeGroupEssence(GroupCode: string, msgId: string) { const session = getSession() // 代码没测过 // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom @@ -323,7 +369,7 @@ export class NTQQGroupApi { } /** 27187 TODO */ - static async addGroupEssence(GroupCode: string, msgId: string) { + async addGroupEssence(GroupCode: string, msgId: string) { const session = getSession() // 代码没测过 // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index bf23d3c..1b5e035 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -1,9 +1,16 @@ import { invoke, NTMethod } from '../ntcall' import { GeneralCallResult, TmpChatInfoApi } from '../services' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' -import { getSelfNick, getSelfUid } from '../../common/data' import { getSession } from '@/ntqqapi/wrapper' import { NTEventDispatch } from '@/common/utils/EventTask' +import { Service, Context } from 'cordis' +import { selfInfo } from '@/common/globalVars' + +declare module 'cordis' { + interface Context { + ntMsgApi: NTQQMsgApi + } +} function generateMsgId() { const timestamp = Math.floor(Date.now() / 1000) @@ -15,8 +22,12 @@ function generateMsgId() { return msgId } -export class NTQQMsgApi { - static async getTempChatInfo(chatType: ChatType2, peerUid: string) { +export class NTQQMsgApi extends Service { + constructor(protected ctx: Context) { + super(ctx, 'ntMsgApi', true) + } + + async getTempChatInfo(chatType: ChatType2, peerUid: string) { const session = getSession() if (session) { return session.getMsgService().getTempChatInfo(chatType, peerUid) @@ -34,7 +45,7 @@ export class NTQQMsgApi { } } - static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { + async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType @@ -59,7 +70,7 @@ export class NTQQMsgApi { } } - static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { + async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { const session = getSession() if (session) { return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId) @@ -78,14 +89,14 @@ export class NTQQMsgApi { } } - static async activateChat(peer: Peer) { + async activateChat(peer: Peer) { return await invoke({ methodName: NTMethod.ACTIVE_CHAT_PREVIEW, args: [{ peer, cnt: 20 }, null], }) } - static async activateChatAndGetHistory(peer: Peer) { + async activateChatAndGetHistory(peer: Peer) { return await invoke({ methodName: NTMethod.ACTIVE_CHAT_HISTORY, // 参数似乎不是这样 @@ -93,7 +104,7 @@ export class NTQQMsgApi { }) } - static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { + async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { if (!peer) throw new Error('peer is not allowed') if (!msgIds) throw new Error('msgIds is not allowed') const session = getSession() @@ -115,7 +126,7 @@ export class NTQQMsgApi { } } - static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { + async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { const session = getSession() // 消息时间从旧到新 if (session) { @@ -136,7 +147,7 @@ export class NTQQMsgApi { } } - static async recallMsg(peer: Peer, msgIds: string[]) { + async recallMsg(peer: Peer, msgIds: string[]) { const session = getSession() if (session) { return session.getMsgService().recallMsg({ @@ -157,7 +168,7 @@ export class NTQQMsgApi { } } - static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { + async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { const msgId = generateMsgId() peer.guildId = msgId let msgList: RawMessage[] @@ -218,7 +229,7 @@ export class NTQQMsgApi { return retMsg! } - static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { + async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { const session = getSession() if (session) { return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], []) @@ -239,12 +250,12 @@ export class NTQQMsgApi { } } - static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise { - const senderShowName = await getSelfNick() + async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise { + const senderShowName = await this.ctx.ntUserApi.getSelfNick(true) const msgInfos = msgIds.map(id => { return { msgId: id, senderShowName } }) - const selfUid = getSelfUid() + const selfUid = selfInfo.uid let msgList: RawMessage[] if (NTEventDispatch.initialised) { const data = await NTEventDispatch.CallNormalEvent< @@ -312,7 +323,7 @@ export class NTQQMsgApi { throw new Error('转发消息超时') } - static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { + async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { const session = getSession() if (session) { return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z) @@ -335,7 +346,7 @@ export class NTQQMsgApi { } /** 27187 TODO */ - static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) { + async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) { const session = getSession() const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { chatInfo: peer, @@ -350,7 +361,7 @@ export class NTQQMsgApi { return ret } - static async getSingleMsg(peer: Peer, seq: string) { + async getSingleMsg(peer: Peer, seq: string) { const session = getSession() if (session) { return await session.getMsgService().getSingleMsg(peer, seq) diff --git a/src/ntqqapi/api/user.ts b/src/ntqqapi/api/user.ts index f977cd5..04103d8 100644 --- a/src/ntqqapi/api/user.ts +++ b/src/ntqqapi/api/user.ts @@ -1,18 +1,28 @@ import { invoke, NTMethod } from '../ntcall' import { GeneralCallResult } from '../services' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types' -import { groupMembers, getSelfUin } from '@/common/data' -import { CacheClassFuncAsync, getBuildVersion } from '@/common/utils' +import { getBuildVersion } from '@/common/utils' import { getSession } from '@/ntqqapi/wrapper' import { RequestUtil } from '@/common/utils/request' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services' import { NodeIKernelProfileListener } from '../listeners' import { NTEventDispatch } from '@/common/utils/EventTask' -import { NTQQFriendApi } from './friend' import { Time } from 'cosmokit' +import { Service, Context } from 'cordis' +import { selfInfo } from '@/common/globalVars' -export class NTQQUserApi { - static async setQQAvatar(filePath: string) { +declare module 'cordis' { + interface Context { + ntUserApi: NTQQUserApi + } +} + +export class NTQQUserApi extends Service { + constructor(protected ctx: Context) { + super(ctx, 'ntUserApi', true) + } + + async setQQAvatar(filePath: string) { return await invoke({ methodName: NTMethod.SET_QQ_AVATAR, args: [ @@ -25,7 +35,7 @@ export class NTQQUserApi { }) } - static async fetchUserDetailInfo(uid: string) { + async fetchUserDetailInfo(uid: string) { let info: UserDetailInfoListenerArg if (NTEventDispatch.initialised) { type EventService = NodeIKernelProfileService['fetchUserDetailInfo'] @@ -74,9 +84,9 @@ export class NTQQUserApi { return ret } - static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { + async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { if (getBuildVersion() >= 26702) { - return NTQQUserApi.fetchUserDetailInfo(uid) + return this.fetchUserDetailInfo(uid) } if (NTEventDispatch.initialised) { type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] @@ -111,30 +121,29 @@ export class NTQQUserApi { } } - static async getSkey(): Promise { - const clientKeyData = await NTQQUserApi.forceFetchClientKey() + async getSkey(): Promise { + const clientKeyData = await this.forceFetchClientKey() if (clientKeyData?.result !== 0) { throw new Error('获取clientKey失败') } - const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin() + const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex return (await RequestUtil.HttpsGetCookies(url))?.skey } - @CacheClassFuncAsync(1800 * 1000) - static async getCookies(domain: string) { - const clientKeyData = await NTQQUserApi.forceFetchClientKey() + async getCookies(domain: string) { + const clientKeyData = await this.forceFetchClientKey() if (clientKeyData?.result !== 0) { throw new Error('获取clientKey失败') } - const uin = getSelfUin() + const uin = selfInfo.uin const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27' const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl) return cookies } - static genBkn(sKey: string) { + genBkn(sKey: string) { sKey = sKey || '' let hash = 5381 @@ -146,7 +155,7 @@ export class NTQQUserApi { return (hash & 0x7fffffff).toString() } - static async like(uid: string, count = 1) { + async like(uid: string, count = 1) { const session = getSession() if (session) { return session.getProfileLikeService().setBuddyProfileLike({ @@ -173,22 +182,12 @@ export class NTQQUserApi { } } - static async getUidByUinV1(Uin: string) { + async getUidByUinV1(Uin: string) { const session = getSession() // 通用转换开始尝试 let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin) - //Uid 群友列表转 if (!uid) { - for (let groupMembersList of groupMembers.values()) { - for (let GroupMember of groupMembersList.values()) { - if (GroupMember.uin == Uin) { - uid = GroupMember.uid - } - } - } - } - if (!uid) { - let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三 + let unveifyUid = (await this.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三 if (unveifyUid.indexOf('*') == -1) { uid = unveifyUid } @@ -196,7 +195,7 @@ export class NTQQUserApi { return uid } - static async getUidByUinV2(uin: string) { + async getUidByUinV2(uin: string) { const session = getSession() if (session) { let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin) @@ -234,18 +233,18 @@ export class NTQQUserApi { })).uidInfo.get(uin) if (uid) return uid } - const unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换 + const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换 if (unveifyUid.indexOf('*') == -1) return unveifyUid } - static async getUidByUin(Uin: string) { + async getUidByUin(Uin: string) { if (getBuildVersion() >= 26702) { - return await NTQQUserApi.getUidByUinV2(Uin) + return await this.getUidByUinV2(Uin) } - return await NTQQUserApi.getUidByUinV1(Uin) + return await this.getUidByUinV1(Uin) } - static async getUserDetailInfoByUinV2(uin: string) { + async getUserDetailInfoByUinV2(uin: string) { if (NTEventDispatch.initialised) { return await NTEventDispatch.CallNoListenerEvent <(Uin: string) => Promise>( @@ -264,7 +263,7 @@ export class NTQQUserApi { } } - static async getUserDetailInfoByUin(Uin: string) { + async getUserDetailInfoByUin(Uin: string) { return NTEventDispatch.CallNoListenerEvent <(Uin: string) => Promise>( 'NodeIKernelProfileService/getUserDetailInfoByUin', @@ -273,7 +272,7 @@ export class NTQQUserApi { ) } - static async getUinByUidV1(Uid: string) { + async getUinByUidV1(Uid: string) { const ret = await NTEventDispatch.CallNoListenerEvent <(Uin: string[]) => Promise<{ uinInfo: Map }>>( 'NodeIKernelUixConvertService/getUin', @@ -282,12 +281,12 @@ export class NTQQUserApi { ) let uin = ret.uinInfo.get(Uid) if (!uin) { - uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换 + uin = (await this.getUserDetailInfo(Uid)).uin //从QQ Native 转换 } return uin } - static async getUinByUidV2(uid: string) { + async getUinByUidV2(uid: string) { const session = getSession() if (session) { let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid) @@ -326,19 +325,19 @@ export class NTQQUserApi { })).uinInfo.get(uid) if (uin) return uin } - let uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(uid) + let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).getKey(uid) if (uin) return uin - uin = (await NTQQUserApi.getUserDetailInfo(uid)).uin //从QQ Native 转换 + uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 } - static async getUinByUid(Uid: string) { + async getUinByUid(Uid: string) { if (getBuildVersion() >= 26702) { - return (await NTQQUserApi.getUinByUidV2(Uid))! + return (await this.getUinByUidV2(Uid))! } - return await NTQQUserApi.getUinByUidV1(Uid) + return await this.getUinByUidV1(Uid) } - static async forceFetchClientKey() { + async forceFetchClientKey() { const session = getSession() if (session) { return await session.getTicketService().forceFetchClientKey('') @@ -351,4 +350,15 @@ export class NTQQUserApi { }) } } + + async getSelfNick(refresh = false) { + if ((refresh || !selfInfo.nick) && selfInfo.uid) { + const userInfo = await this.getUserDetailInfo(selfInfo.uid) + if (userInfo) { + Object.assign(selfInfo, { nick: userInfo.nick }) + return userInfo.nick + } + } + return selfInfo.nick + } } diff --git a/src/ntqqapi/api/webapi.ts b/src/ntqqapi/api/webapi.ts index 27ef955..e24fbe4 100644 --- a/src/ntqqapi/api/webapi.ts +++ b/src/ntqqapi/api/webapi.ts @@ -1,7 +1,11 @@ -import { getSelfUin } from '@/common/data' -import { log } from '@/common/utils/log' -import { NTQQUserApi } from './user' import { RequestUtil } from '@/common/utils/request' +import { Service, Context } from 'cordis' + +declare module 'cordis' { + interface Context { + ntWebApi: NTQQWebApi + } +} export enum WebHonorType { ALL = 'all', @@ -120,9 +124,13 @@ export interface GroupEssenceMsgRet { } } -export class WebApi { - static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise { - const { cookies: CookieValue, bkn: Bkn } = (await NTQQUserApi.getCookies('qun.qq.com')) +export class NTQQWebApi extends Service { + constructor(protected ctx: Context) { + super(ctx, 'ntWebApi', true) + } + + async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise { + const { cookies: CookieValue, bkn: Bkn } = await this.ctx.ntUserApi.getCookies('qun.qq.com') const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20' let ret: GroupEssenceMsgRet try { @@ -137,9 +145,9 @@ export class WebApi { return ret } - static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { + async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { const memberData: Array = new Array() - const cookieObject = await NTQQUserApi.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 retList: Promise[] = [] const params = new URLSearchParams({ @@ -147,7 +155,7 @@ export class WebApi { end: '40', sort: '1', gc: GroupCode, - bkn: WebApi.genBkn(cookieObject.skey) + bkn: this.genBkn(cookieObject.skey) }) const fastRet = await RequestUtil.HttpGetJson(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr }) if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { @@ -178,7 +186,7 @@ export class WebApi { return memberData } - static genBkn(sKey: string) { + genBkn(sKey: string) { sKey = sKey || ''; let hash = 5381; @@ -191,8 +199,8 @@ export class WebApi { } //实现未缓存 考虑2h缓存 - static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { - async function getDataInternal(Internal_groupCode: string, Internal_type: number) { + async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { + 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 res = ''; let resJson; @@ -208,13 +216,13 @@ export class WebApi { return resJson?.actorList; } } catch (e) { - log('获取当前群荣耀失败', url, e); + this.ctx.logger.error('获取当前群荣耀失败', url, e); } return undefined; } let HonorInfo: any = { group_id: groupCode }; - const cookieObject = await NTQQUserApi.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('; ') if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { @@ -241,7 +249,7 @@ export class WebApi { }); } } catch (e) { - log(e); + this.ctx.logger.error(e); } } if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { @@ -260,7 +268,7 @@ export class WebApi { }); } } catch (e) { - log(e); + this.ctx.logger.error(e); } } if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { @@ -279,7 +287,7 @@ export class WebApi { }); } } catch (e) { - log('获取群聊炽焰失败', e); + this.ctx.logger.error('获取群聊炽焰失败', e); } } if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { @@ -298,7 +306,7 @@ export class WebApi { }); } } catch (e) { - log('获取快乐源泉失败', e); + this.ctx.logger.error('获取快乐源泉失败', e); } } //冒尖小春笋好像已经被tx扬了 diff --git a/src/ntqqapi/api/window.ts b/src/ntqqapi/api/window.ts index e585e20..2c67385 100644 --- a/src/ntqqapi/api/window.ts +++ b/src/ntqqapi/api/window.ts @@ -2,30 +2,41 @@ import { invoke, NTClass, NTMethod } from '../ntcall' import { GeneralCallResult } from '../services' import { ReceiveCmd } from '../hook' import { BrowserWindow } from 'electron' +import { Service, Context } from 'cordis' + +declare module 'cordis' { + interface Context { + ntWindowApi: NTQQWindowApi + } +} export interface NTQQWindow { windowName: string windowUrlHash: string } -export class NTQQWindows { - static GroupHomeWorkWindow: NTQQWindow = { +export namespace NTQQWindows { + export const GroupHomeWorkWindow: NTQQWindow = { windowName: 'GroupHomeWorkWindow', windowUrlHash: '#/group-home-work', } - static GroupNotifyFilterWindow: NTQQWindow = { + export const GroupNotifyFilterWindow: NTQQWindow = { windowName: 'GroupNotifyFilterWindow', windowUrlHash: '#/group-notify-filter', } - static GroupEssenceWindow: NTQQWindow = { + export const GroupEssenceWindow: NTQQWindow = { windowName: 'GroupEssenceWindow', windowUrlHash: '#/group-essence', } } -export class NTQQWindowApi { +export class NTQQWindowApi extends Service { + constructor(protected ctx: Context) { + super(ctx, 'ntWindowApi', true) + } + // 打开窗口并获取对应的下发事件 - static async openWindow( + async openWindow( ntQQWindow: NTQQWindow, args: any[], cbCmd: ReceiveCmd | undefined, diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index bc039f4..a4bd057 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -15,22 +15,21 @@ import { } from './types' import { promises as fs } from 'node:fs' import ffmpeg from 'fluent-ffmpeg' -import { NTQQFileApi } from './api/file' import { calculateFileMD5, isGIF } from '../common/utils/file' -import { log } from '../common/utils/log' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { encodeSilk } from '../common/utils/audio' import { isNull } from '../common/utils' import faceConfig from './helper/face_config.json' +import { Context } from 'cordis' export const mFaceCache = new Map() // emojiId -> faceName -export class SendMsgElementConstructor { - static poke(groupCode: string, uin: string) { +export namespace SendMsgElementConstructor { + export function poke(groupCode: string, uin: string) { return null } - static text(content: string): SendTextElement { + export function text(content: string): SendTextElement { return { elementType: ElementType.TEXT, elementId: '', @@ -44,7 +43,7 @@ export class SendMsgElementConstructor { } } - static at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement { + export function at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement { return { elementType: ElementType.TEXT, elementId: '', @@ -58,7 +57,7 @@ export class SendMsgElementConstructor { } } - static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { + export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { return { elementType: ElementType.REPLY, elementId: '', @@ -71,8 +70,8 @@ export class SendMsgElementConstructor { } } - static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise { - const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType) + export async function pic(ctx: Context, picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise { + const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(picPath, ElementType.PIC, subType) if (fileSize === 0) { throw '文件异常,大小为0' } @@ -80,7 +79,7 @@ export class SendMsgElementConstructor { if (fileSize > 1024 * 1024 * 30) { throw `图片过大,最大支持${maxMB}MB,当前文件大小${fileSize}B` } - const imageSize = await NTQQFileApi.getImageSize(picPath) + const imageSize = await ctx.ntFileApi.getImageSize(picPath) const picElement = { md5HexStr: md5, fileSize: fileSize.toString(), @@ -96,7 +95,7 @@ export class SendMsgElementConstructor { thumbFileSize: 0, summary, } - log('图片信息', picElement) + ctx.logger.info('图片信息', picElement) return { elementType: ElementType.PIC, elementId: '', @@ -104,8 +103,8 @@ export class SendMsgElementConstructor { } } - static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise { - const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) + export async function file(ctx: Context, filePath: string, fileName: string = '', folderId: string = ''): Promise { + const { fileName: _fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(filePath, ElementType.FILE) if (fileSize === 0) { throw '文件异常,大小为 0' } @@ -122,16 +121,16 @@ export class SendMsgElementConstructor { return element } - static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise { + export async function video(ctx: Context, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise { try { await fs.stat(filePath) } catch (e) { throw `文件${filePath}异常,不存在` } - log('复制视频到QQ目录', filePath) - let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO) + ctx.logger.info('复制视频到QQ目录', filePath) + let { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.VIDEO) - log('复制视频到QQ目录完成', path) + ctx.logger.info('复制视频到QQ目录完成', path) if (fileSize === 0) { throw '文件异常,大小为0' } @@ -153,19 +152,19 @@ export class SendMsgElementConstructor { } try { videoInfo = await getVideoInfo(path) - log('视频信息', videoInfo) + ctx.logger.info('视频信息', videoInfo) } catch (e) { - log('获取视频信息失败', e) + ctx.logger.info('获取视频信息失败', e) } const createThumb = new Promise((resolve, reject) => { const thumbFileName = `${md5}_0.png` const thumbPath = pathLib.join(thumbDir, thumbFileName) - log('开始生成视频缩略图', filePath) + ctx.logger.info('开始生成视频缩略图', filePath) let completed = false function useDefaultThumb() { if (completed) return - log('获取视频封面失败,使用默认封面') + ctx.logger.info('获取视频封面失败,使用默认封面') fs.writeFile(thumbPath, defaultVideoThumb) .then(() => { resolve(thumbPath) @@ -194,14 +193,14 @@ export class SendMsgElementConstructor { size: videoInfo.width + 'x' + videoInfo.height, }) .on('end', () => { - log('生成视频缩略图', thumbPath) + ctx.logger.info('生成视频缩略图', thumbPath) completed = true resolve(thumbPath) }) }) let thumbPath = new Map() const _thumbPath = await createThumb - log('生成视频缩略图', _thumbPath) + ctx.logger.info('生成视频缩略图', _thumbPath) const thumbSize = (await fs.stat(_thumbPath)).size // log("生成缩略图", _thumbPath) thumbPath.set(0, _thumbPath) @@ -232,17 +231,17 @@ export class SendMsgElementConstructor { // sourceVideoCodecFormat: 2 }, } - log('videoElement', element) + ctx.logger.info('videoElement', element) return element } - static async ptt(pttPath: string): Promise { - const { converted, path: silkPath, duration } = await encodeSilk(pttPath) + export async function ptt(ctx: Context, pttPath: string): Promise { + const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath) if (!silkPath) { throw '语音转换失败, 请检查语音文件是否正常' } // log("生成语音", silkPath, duration); - const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT) + const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.PTT) if (fileSize === 0) { throw '文件异常,大小为0' } @@ -271,7 +270,7 @@ export class SendMsgElementConstructor { } } - static face(faceId: number): SendFaceElement { + export function face(faceId: number): SendFaceElement { // 从face_config.json中获取表情名称 const sysFaces = faceConfig.sysface const emojiFaces = faceConfig.emoji @@ -300,7 +299,7 @@ export class SendMsgElementConstructor { } } - static mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement { + export function mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement { return { elementType: ElementType.MFACE, marketFaceElement: { @@ -312,7 +311,7 @@ export class SendMsgElementConstructor { } } - static dice(resultId: number | null): SendFaceElement { + export function dice(resultId: number | null): SendFaceElement { // 实际测试并不能控制结果 // 随机1到6 @@ -336,7 +335,7 @@ export class SendMsgElementConstructor { } // 猜拳(石头剪刀布)表情 - static rps(resultId: number | null): SendFaceElement { + export function rps(resultId: number | null): SendFaceElement { // 实际测试并不能控制结果 if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1 return { @@ -357,7 +356,7 @@ export class SendMsgElementConstructor { } } - static ark(data: string): SendArkElement { + export function ark(data: string): SendArkElement { return { elementType: ElementType.ARK, elementId: '', diff --git a/src/ntqqapi/core.ts b/src/ntqqapi/core.ts new file mode 100644 index 0000000..bc0aec5 --- /dev/null +++ b/src/ntqqapi/core.ts @@ -0,0 +1,240 @@ +import fs from 'node:fs' +import { Service, Context } from 'cordis' +import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook' +import { MessageUnique } from '../common/utils/MessageUnique' +import { NTEventDispatch } from '../common/utils/EventTask' +import { wrapperConstructor, getSession } from './wrapper' +import { Config as LLOBConfig } from '../common/types' +import { llonebotError, TEMP_DIR } from '../common/globalVars' +import { isNumeric } from '../common/utils/helper' +import { NTMethod } from './ntcall' +import { + RawMessage, + GroupNotify, + FriendRequestNotify, + FriendRequest, + GroupMember, + CategoryFriend, + SimpleInfo, + User, + ChatType +} from './types' +import { selfInfo } from '../common/globalVars' +import { version } from '../version' + +declare module 'cordis' { + interface Context { + app: Core + } + interface Events { + 'nt/message-created': (input: RawMessage[]) => void + 'nt/message-deleted': (input: RawMessage[]) => void + 'nt/message-sent': (input: RawMessage[]) => void + 'nt/group-notify': (input: GroupNotify[]) => void + 'nt/friend-request': (input: FriendRequest[]) => void + 'nt/group-member-info-updated': (input: { groupCode: string; members: GroupMember[] }) => void + 'nt/friend-list-updated': (input: { groupCode: string; members: GroupMember[] }) => void + } +} + +class Core extends Service { + static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi'] + + constructor(protected ctx: Context, public config: Core.Config) { + super(ctx, 'app', true) + } + + public start() { + llonebotError.otherError = '' + const WrapperSession = getSession() + if (WrapperSession) { + NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession }) + } + MessageUnique.init(selfInfo.uin) + this.registerListener() + this.ctx.logger.info(`LLOneBot/${version}`) + } + + private registerListener() { + registerReceiveHook<{ + data: CategoryFriend[] + }>(ReceiveCmdS.FRIENDS, (payload) => { + type V2data = { userSimpleInfos: Map } + let friendList: User[] = []; + if ((payload as any).userSimpleInfos) { + friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => { + return { + ...v.coreInfo, + } + }) + } else { + for (const fData of payload.data) { + friendList.push(...fData.buddyList) + } + } + this.ctx.logger.info('好友列表变动', friendList.length) + for (const friend of friendList) { + this.ctx.ntMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }) + } + }) + + // 自动清理新消息文件 + registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { + if (!this.config.autoDeleteFile) { + return + } + for (const message of payload.msgList) { + for (const msgElement of message.elements) { + setTimeout(() => { + const picPath = msgElement.picElement?.sourcePath + const picThumbPath = [...msgElement.picElement?.thumbPath.values()] + const pttPath = msgElement.pttElement?.filePath + const filePath = msgElement.fileElement?.filePath + const videoPath = msgElement.videoElement?.filePath + const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!] + const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath] + if (msgElement.picElement) { + pathList.push(...Object.values(msgElement.picElement.thumbPath)) + } + for (const path of pathList) { + if (path) { + fs.unlink(picPath, () => { + this.ctx.logger.info('删除文件成功', path) + }) + } + } + }, this.config.autoDeleteFileSecond! * 1000) + } + } + }) + + registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { + Object.assign(selfInfo, { online: info.info.status !== 20 }) + }) + + const activatedPeerUids: string[] = [] + registerReceiveHook<{ + changedRecentContactLists: { + listType: number + sortedContactList: string[] + changedList: { + id: string // peerUid + chatType: ChatType + }[] + }[] + }>(ReceiveCmdS.RECENT_CONTACT, async (payload) => { + for (const recentContact of payload.changedRecentContactLists) { + for (const changedContact of recentContact.changedList) { + if (activatedPeerUids.includes(changedContact.id)) continue + activatedPeerUids.push(changedContact.id) + const peer = { peerUid: changedContact.id, chatType: changedContact.chatType } + if (changedContact.chatType === ChatType.temp) { + this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => { + this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => { + const lastTempMsg = msgList.at(-1) + if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) { + this.ctx.parallel('nt/message-created', [lastTempMsg!]) + } + }) + }) + } + else { + this.ctx.ntMsgApi.activateChat(peer) + } + } + } + }) + + registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => { + const peerUid = payload[0] as string + this.ctx.logger.info('激活的聊天窗口被删除,准备重新激活', peerUid) + let chatType = ChatType.friend + if (isNumeric(peerUid)) { + chatType = ChatType.group + } + else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) { + chatType = ChatType.temp + } + const peer = { peerUid, chatType } + await this.ctx.sleep(1000) + this.ctx.ntMsgApi.activateChat(peer).then((r) => { + this.ctx.logger.info('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) + }) + }) + + registerReceiveHook<{ + groupCode: string + dataSource: number + members: Set + }>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => { + const groupCode = payload.groupCode + const members = Array.from(payload.members.values()) + this.ctx.parallel('nt/group-member-info-updated', { groupCode, members }) + }) + + registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => { + this.ctx.parallel('nt/message-created', payload.msgList) + }) + + const recallMsgIds: string[] = [] // 避免重复上报 + registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => { + const list = payload.msgList.filter(v => { + if (recallMsgIds.includes(v.msgId)) { + return false + } + recallMsgIds.push(v.msgId) + return true + }) + this.ctx.parallel('nt/message-deleted', list) + }) + + registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => { + const { msgId, chatType, peerUid } = payload.msgRecord + const peer = { + chatType, + peerUid + } + MessageUnique.createMsg(peer, msgId) + if (!this.config.reportSelfMessage) { + return + } + this.ctx.parallel('nt/message-sent', [payload.msgRecord]) + }) + + const groupNotifyFlags: string[] = [] + registerReceiveHook<{ + doubt: boolean + oldestUnreadSeq: string + unreadCount: number + }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { + if (payload.unreadCount) { + let notifies: GroupNotify[] + try { + notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount) + } catch (e) { + return + } + const list = notifies.filter(v => { + const flag = v.group.groupCode + '|' + v.seq + '|' + v.type + if (groupNotifyFlags.includes(flag)) { + return false + } + groupNotifyFlags.push(flag) + return true + }) + this.ctx.parallel('nt/group-notify', list) + } + }) + + registerReceiveHook(ReceiveCmdS.FRIEND_REQUEST, payload => { + this.ctx.parallel('nt/friend-request', payload.data.buddyReqs) + }) + } +} + +namespace Core { + export interface Config extends LLOBConfig { + } +} + +export default Core \ No newline at end of file diff --git a/src/ntqqapi/helper/rkey.ts b/src/ntqqapi/helper/rkey.ts index 1fb1b7d..404fa5a 100644 --- a/src/ntqqapi/helper/rkey.ts +++ b/src/ntqqapi/helper/rkey.ts @@ -1,4 +1,4 @@ -import { log } from '@/common/utils' +import { Context } from "cordis" interface ServerRkeyData { group_rkey: string @@ -6,15 +6,15 @@ interface ServerRkeyData { expired_time: number } -class RkeyManager { - serverUrl: string = '' +export class RkeyManager { + private serverUrl: string = '' private rkeyData: ServerRkeyData = { group_rkey: '', private_rkey: '', expired_time: 0 } - constructor(serverUrl: string) { + constructor(protected ctx: Context, serverUrl: string) { this.serverUrl = serverUrl } @@ -23,7 +23,7 @@ class RkeyManager { try { await this.refreshRkey() } catch (e) { - log('获取rkey失败', e) + this.ctx.logger.error('获取rkey失败', e) } } return this.rkeyData @@ -58,5 +58,3 @@ class RkeyManager { }) } } - -export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey') diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 2a50649..f9de852 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,33 +1,11 @@ import type { BrowserWindow } from 'electron' import { NTClass, NTMethod } from './ntcall' -import { NTQQMsgApi, NTQQFriendApi } from './api' -import { - CategoryFriend, - ChatType, - GroupMember, - GroupMemberRole, - RawMessage, - SimpleInfo, - User -} from './types' -import { - getGroupMember, - setSelfInfo -} from '@/common/data' -import { postOb11Event } from '../onebot11/server/post-ob11-event' -import { getConfigUtil } from '@/common/config' -import fs from 'node:fs' import { log } from '@/common/utils' import { randomUUID } from 'node:crypto' -import { MessageUnique } from '../common/utils/MessageUnique' -import { isNumeric, sleep } from '@/common/utils' -import { OB11Constructor } from '../onebot11/constructor' -import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent' -import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent' -export let hookApiCallbacks: Record void> = {} +export const hookApiCallbacks: Record void> = {} -export let ReceiveCmdS = { +export const ReceiveCmdS = { RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2', UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate', UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate', @@ -48,7 +26,7 @@ export let ReceiveCmdS = { CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', SKEY_UPDATE: 'onSkeyUpdate', -} as const +} export type ReceiveCmd = string @@ -221,181 +199,4 @@ export function registerCallHook( export function removeReceiveHook(id: string) { const index = receiveHooks.findIndex((h) => h.id === id) receiveHooks.splice(index, 1) -} - -export async function startHook() { - registerReceiveHook<{ - groupCode: string - dataSource: number - members: Set - }>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => { - const groupCode = payload.groupCode - const members = Array.from(payload.members.values()) - // log("群成员信息变动", groupCode, members) - for (const member of members) { - const existMember = await getGroupMember(groupCode, member.uin) - if (existMember) { - if (member.cardName != existMember.cardName) { - log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName) - postOb11Event( - new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName), - ) - } else if (member.role != existMember.role) { - log('有管理员变动通知') - const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent( - member.role == GroupMemberRole.admin ? 'set' : 'unset', - parseInt(groupCode), - parseInt(member.uin) - ) - postOb11Event(groupAdminNoticeEvent, true) - } - Object.assign(existMember, member) - } - } - // const existGroup = groups.find(g => g.groupCode == groupCode); - // if (existGroup) { - // log("对比群成员", existGroup.members, members) - // for (const member of members) { - // const existMember = existGroup.members.find(m => m.uin == member.uin); - // if (existMember) { - // log("对比群名片", existMember.cardName, member.cardName) - // if (existMember.cardName != member.cardName) { - // postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName)); - // } - // Object.assign(existMember, member); - // } - // } - // } - }) - - // 好友列表变动 - registerReceiveHook<{ - data: CategoryFriend[] - }>(ReceiveCmdS.FRIENDS, (payload) => { - // log("onBuddyListChange", payload) - // let friendListV2: {userSimpleInfos: Map} = [] - type V2data = { userSimpleInfos: Map } - let friendList: User[] = []; - if ((payload as any).userSimpleInfos) { - // friendListV2 = payload as any - friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => { - return { - ...v.coreInfo, - } - }) - } - else { - for (const fData of payload.data) { - friendList.push(...fData.buddyList) - } - } - log('好友列表变动', friendList.length) - for (let friend of friendList) { - NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() - } - }) - - registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { - // 自动清理新消息文件 - const { autoDeleteFile } = getConfigUtil().getConfig() - if (!autoDeleteFile) { - return - } - for (const message of payload.msgList) { - // log("收到新消息,push到历史记录", message.msgId) - // dbUtil.addMsg(message).then() - // 清理文件 - - for (const msgElement of message.elements) { - setTimeout(() => { - const picPath = msgElement.picElement?.sourcePath - const picThumbPath = [...msgElement.picElement?.thumbPath.values()] - const pttPath = msgElement.pttElement?.filePath - const filePath = msgElement.fileElement?.filePath - const videoPath = msgElement.videoElement?.filePath - const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!] - const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath] - if (msgElement.picElement) { - pathList.push(...Object.values(msgElement.picElement.thumbPath)) - } - - // log("需要清理的文件", pathList); - for (const path of pathList) { - if (path) { - fs.unlink(picPath, () => { - log('删除文件成功', path) - }) - } - } - }, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000) - } - } - }) - - registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { - const { msgId, chatType, peerUid } = msgRecord - const peer = { - chatType, - peerUid - } - MessageUnique.createMsg(peer, msgId) - }) - - registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { - setSelfInfo({ - online: info.info.status !== 20 - }) - }) - - let activatedPeerUids: string[] = [] - registerReceiveHook<{ - changedRecentContactLists: { - listType: number - sortedContactList: string[] - changedList: { - id: string // peerUid - chatType: ChatType - }[] - }[] - }>(ReceiveCmdS.RECENT_CONTACT, async (payload) => { - for (const recentContact of payload.changedRecentContactLists) { - for (const changedContact of recentContact.changedList) { - if (activatedPeerUids.includes(changedContact.id)) continue - activatedPeerUids.push(changedContact.id) - const peer = { peerUid: changedContact.id, chatType: changedContact.chatType } - if (changedContact.chatType === ChatType.temp) { - log('收到临时会话消息', peer) - NTQQMsgApi.activateChatAndGetHistory(peer).then(() => { - NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => { - let lastTempMsg = msgList.pop() - log('激活窗口之前的第一条临时会话消息:', lastTempMsg) - if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) { - OB11Constructor.message(lastTempMsg!).then((r) => postOb11Event(r)) - } - }) - }) - } - else { - NTQQMsgApi.activateChat(peer).then() - } - } - } - }) - - registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => { - const peerUid = payload[0] as string - log('激活的聊天窗口被删除,准备重新激活', peerUid) - let chatType = ChatType.friend - if (isNumeric(peerUid)) { - chatType = ChatType.group - } - else if (!(await NTQQFriendApi.isBuddy(peerUid))) { - chatType = ChatType.temp - } - const peer = { peerUid, chatType } - await sleep(1000) - NTQQMsgApi.activateChat(peer).then((r) => { - log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) - }) - }) -} +} \ No newline at end of file diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index 086eb56..d813b4a 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -1,6 +1,6 @@ import { ipcMain } from 'electron' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook' -import { log } from '../common/utils/log' +import { log } from '../common/utils/LegacyLog' import { randomUUID } from 'node:crypto' import { GeneralCallResult } from './services' @@ -149,7 +149,7 @@ export function invoke(params: InvokeParams) { !afterFirstCmd && secondCallback() hookApiCallbacks[uuid] = (result: GeneralCallResult) => { if (result?.result === 0 || result === undefined) { - log(`${params.methodName} callback`, result) + //log(`${params.methodName} callback`, result) afterFirstCmd && secondCallback() } else { diff --git a/src/onebot11/action/BaseAction.ts b/src/onebot11/action/BaseAction.ts index 390a443..bee248e 100644 --- a/src/onebot11/action/BaseAction.ts +++ b/src/onebot11/action/BaseAction.ts @@ -1,11 +1,16 @@ import { ActionName, BaseCheckResult } from './types' import { OB11Response } from './OB11Response' import { OB11Return } from '../types' - -import { log } from '../../common/utils/log' +import { Context } from 'cordis' +import type Adapter from '../adapter' abstract class BaseAction { abstract actionName: ActionName + protected ctx: Context + + constructor(protected adapter: Adapter) { + this.ctx = adapter.ctx + } protected async check(payload: PayloadType): Promise { return { @@ -22,7 +27,7 @@ abstract class BaseAction { const resData = await this._handle(payload) return OB11Response.ok(resData) } catch (e: any) { - log('发生错误', e) + this.ctx.logger.error('发生错误', e) return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200) } } @@ -36,7 +41,7 @@ abstract class BaseAction { const resData = await this._handle(payload) return OB11Response.ok(resData, echo) } catch (e: any) { - log('发生错误', e) + this.ctx.logger.error('发生错误', e) return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo) } } diff --git a/src/onebot11/action/OB11Response.ts b/src/onebot11/action/OB11Response.ts index 0ce6b81..bc74400 100644 --- a/src/onebot11/action/OB11Response.ts +++ b/src/onebot11/action/OB11Response.ts @@ -1,6 +1,5 @@ import { OB11Return } from '../types' - -import { isNull } from '../../common/utils/helper' +import { isNullable } from 'cosmokit' export class OB11Response { static res(data: T, status: string, retcode: number, message: string = ''): OB11Return { @@ -16,7 +15,7 @@ export class OB11Response { static ok(data: T, echo: any = null) { let res = OB11Response.res(data, 'ok', 0) - if (!isNull(echo)) { + if (!isNullable(echo)) { res.echo = echo } return res @@ -24,7 +23,7 @@ export class OB11Response { static error(err: string, retcode: number, echo: any = null) { let res = OB11Response.res(null, 'failed', retcode, err) - if (!isNull(echo)) { + if (!isNullable(echo)) { res.echo = echo } return res diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index 35d24bc..2d83480 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -1,10 +1,8 @@ import BaseAction from '../BaseAction' import fsPromise from 'node:fs/promises' import { getConfigUtil } from '@/common/config' -import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api' import { ActionName } from '../types' -import { UUIDConverter } from '@/common/utils/helper' -import { Peer, ChatType, ElementType } from '@/ntqqapi/types' +import { Peer, ElementType } from '@/ntqqapi/types' import { MessageUnique } from '@/common/utils/MessageUnique' export interface GetFilePayload { @@ -30,7 +28,7 @@ export abstract class GetFileBase extends BaseAction { let res = await super._handle(payload) - res.file = await decodeSilk(res.file!, payload.out_format) + res.file = await decodeSilk(this.ctx, res.file!, payload.out_format) res.file_name = path.basename(res.file) res.file_size = fs.statSync(res.file).size.toString() - if (getConfigUtil().getConfig().enableLocalFile2Url){ + if (getConfigUtil().getConfig().enableLocalFile2Url) { res.base64 = fs.readFileSync(res.file, 'base64') } return res diff --git a/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts b/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts index 561c032..d139b90 100644 --- a/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts +++ b/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts @@ -1,7 +1,5 @@ - -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi } from '@/ntqqapi/api/group' +import BaseAction from '../BaseAction' +import { ActionName } from '../types' import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { @@ -19,7 +17,7 @@ export default class GoCQHTTPDelEssenceMsg extends BaseAction { if (!msg) { throw new Error('msg not found') } - return await NTQQGroupApi.removeGroupEssence( + return await this.ctx.ntGroupApi.removeGroupEssence( msg.Peer.peerUid, msg.MsgId, ) diff --git a/src/onebot11/action/go-cqhttp/DelGroupFile.ts b/src/onebot11/action/go-cqhttp/DelGroupFile.ts index eddca67..96f0c78 100644 --- a/src/onebot11/action/go-cqhttp/DelGroupFile.ts +++ b/src/onebot11/action/go-cqhttp/DelGroupFile.ts @@ -1,6 +1,5 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '@/ntqqapi/api' interface Payload { group_id: string | number @@ -12,6 +11,6 @@ export class GoCQHTTPDelGroupFile extends BaseAction { actionName = ActionName.GoCQHTTP_DelGroupFile async _handle(payload: Payload) { - await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id]) + await this.ctx.ntGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id]) } } \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/DownloadFile.ts b/src/onebot11/action/go-cqhttp/DownloadFile.ts index 611b60f..be9e430 100644 --- a/src/onebot11/action/go-cqhttp/DownloadFile.ts +++ b/src/onebot11/action/go-cqhttp/DownloadFile.ts @@ -3,7 +3,8 @@ import { ActionName } from '../types' import fs from 'fs' import fsPromise from 'fs/promises' import path from 'node:path' -import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils' +import { calculateFileMD5, httpDownload } from '@/common/utils' +import { TEMP_DIR } from '@/common/globalVars' import { randomUUID } from 'node:crypto' interface Payload { diff --git a/src/onebot11/action/go-cqhttp/GetForwardMsg.ts b/src/onebot11/action/go-cqhttp/GetForwardMsg.ts index 9d9aab1..ccbbe80 100644 --- a/src/onebot11/action/go-cqhttp/GetForwardMsg.ts +++ b/src/onebot11/action/go-cqhttp/GetForwardMsg.ts @@ -1,6 +1,5 @@ import BaseAction from '../BaseAction' import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types' -import { NTQQMsgApi } from '@/ntqqapi/api' import { OB11Constructor } from '../../constructor' import { ActionName } from '../types' import { MessageUnique } from '@/common/utils/MessageUnique' @@ -26,14 +25,14 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction { if (!rootMsg) { throw Error('msg not found') } - const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId) + const data = await this.ctx.ntMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId) if (data?.result !== 0) { throw Error('找不到相关的聊天记录' + data?.errMsg) } const msgList = data.msgList const messages = await Promise.all( msgList.map(async (msg) => { - const resMsg = await OB11Constructor.message(msg) + const resMsg = await OB11Constructor.message(this.ctx, msg) resMsg.message_id = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid, diff --git a/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts index 7260988..ec0f5b1 100644 --- a/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts +++ b/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts @@ -2,7 +2,6 @@ import BaseAction from '../BaseAction' import { OB11Message } from '../../types' import { ActionName } from '../types' import { ChatType } from '@/ntqqapi/types' -import { NTQQMsgApi } from '@/ntqqapi/api/msg' import { OB11Constructor } from '../../constructor' import { RawMessage } from '@/ntqqapi/types' import { MessageUnique } from '@/common/utils/MessageUnique' @@ -28,11 +27,11 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction OB11Constructor.message(msg))) + const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(this.ctx, msg))) return { messages: ob11MsgList } } } diff --git a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts index edb813d..a3db4ba 100644 --- a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts +++ b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts @@ -2,7 +2,6 @@ import BaseAction from '../BaseAction' import { OB11User } from '../../types' import { OB11Constructor } from '../../constructor' import { ActionName } from '../types' -import { NTQQUserApi } from '../../../ntqqapi/api/user' import { getBuildVersion } from '@/common/utils/QQBasicInfo' import { OB11UserSex } from '../../types' import { calcQQLevel } from '@/common/utils/qqlevel' @@ -17,8 +16,8 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction { if (!(getBuildVersion() >= 26702)) { const user_id = payload.user_id.toString() - const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id) - const uid = (await NTQQUserApi.getUidByUin(user_id))! + const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUin(user_id) + const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))! if (!uid || uid.indexOf('*') != -1) { const ret = { ...extendData, @@ -33,12 +32,12 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction{ +export class GoCQHTTHandleQuickOperation extends BaseAction { actionName = ActionName.GoCQHTTP_HandleQuickOperation protected async _handle(payload: Payload): Promise { - handleQuickOperation(payload.context, payload.operation).then().catch(log); + handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e)) return null } } \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts b/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts index 56eef28..9c23b7d 100644 --- a/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts +++ b/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts @@ -1,6 +1,5 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '@/ntqqapi/api/group' import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { @@ -18,7 +17,7 @@ export default class GoCQHTTPSetEssenceMsg extends BaseAction { if (!msg) { throw new Error('msg not found') } - return await NTQQGroupApi.addGroupEssence( + return await this.ctx.ntGroupApi.addGroupEssence( msg.Peer.peerUid, msg.MsgId ) diff --git a/src/onebot11/action/go-cqhttp/UploadFile.ts b/src/onebot11/action/go-cqhttp/UploadFile.ts index c7cdd99..cb726cd 100644 --- a/src/onebot11/action/go-cqhttp/UploadFile.ts +++ b/src/onebot11/action/go-cqhttp/UploadFile.ts @@ -6,7 +6,6 @@ import { ChatType, SendFileElement } from '@/ntqqapi/types' import { uri2local } from '@/common/utils' import { Peer } from '@/ntqqapi/types' import { sendMsg } from '../msg/SendMsg' -import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api' interface Payload { user_id: number | string @@ -29,8 +28,8 @@ export class GoCQHTTPUploadGroupFile extends BaseAction { if (!downloadResult.success) { throw new Error(downloadResult.errMsg) } - const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) - await sendMsg({ + const sendFileEle = await SendMsgElementConstructor.file(this.ctx, downloadResult.path, payload.name, payload.folder_id) + await sendMsg(this.ctx, { chatType: ChatType.group, peerUid: payload.group_id?.toString()!, }, [sendFileEle], [], true) @@ -43,11 +42,11 @@ export class GoCQHTTPUploadPrivateFile extends BaseAction { async getPeer(payload: Payload): Promise { if (payload.user_id) { - const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) + const peerUid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString()) if (!peerUid) { throw `私聊${payload.user_id}不存在` } - const isBuddy = await NTQQFriendApi.isBuddy(peerUid) + const isBuddy = await this.ctx.ntFriendApi.isBuddy(peerUid) return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid } } throw '缺少参数 user_id' @@ -63,8 +62,8 @@ export class GoCQHTTPUploadPrivateFile extends BaseAction { if (!downloadResult.success) { throw new Error(downloadResult.errMsg) } - const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) - await sendMsg(peer, [sendFileEle], [], true) + const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(this.ctx, downloadResult.path, payload.name) + await sendMsg(this.ctx, peer, [sendFileEle], [], true) return null } } diff --git a/src/onebot11/action/group/GetGroupEssence.ts b/src/onebot11/action/group/GetGroupEssence.ts index d87a066..b455c79 100644 --- a/src/onebot11/action/group/GetGroupEssence.ts +++ b/src/onebot11/action/group/GetGroupEssence.ts @@ -1,4 +1,4 @@ -import { GroupEssenceMsgRet, WebApi } from '@/ntqqapi/api' +import { GroupEssenceMsgRet } from '@/ntqqapi/api' import BaseAction from '../BaseAction' import { ActionName } from '../types' @@ -12,13 +12,5 @@ export class GetGroupEssence extends BaseAction { - // - // }) - return ret } } diff --git a/src/onebot11/action/group/GetGroupHonorInfo.ts b/src/onebot11/action/group/GetGroupHonorInfo.ts index a216e27..a57560a 100644 --- a/src/onebot11/action/group/GetGroupHonorInfo.ts +++ b/src/onebot11/action/group/GetGroupHonorInfo.ts @@ -1,4 +1,4 @@ -import { WebApi, WebHonorType } from '@/ntqqapi/api' +import { WebHonorType } from '@/ntqqapi/api' import { ActionName } from '../types' import BaseAction from '../BaseAction' @@ -18,6 +18,6 @@ export class GetGroupHonorInfo extends BaseAction> { if (!payload.type) { payload.type = WebHonorType.ALL } - return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type) + return await this.ctx.ntWebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type) } } diff --git a/src/onebot11/action/group/GetGroupInfo.ts b/src/onebot11/action/group/GetGroupInfo.ts index d251514..da5c527 100644 --- a/src/onebot11/action/group/GetGroupInfo.ts +++ b/src/onebot11/action/group/GetGroupInfo.ts @@ -2,7 +2,6 @@ import { OB11Group } from '../../types' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '@/ntqqapi/api' interface Payload { group_id: number | string @@ -12,7 +11,7 @@ class GetGroupInfo extends BaseAction { actionName = ActionName.GetGroupInfo protected async _handle(payload: Payload) { - const group = (await NTQQGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString()) + const group = (await this.ctx.ntGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString()) if (group) { return OB11Constructor.group(group) } else { diff --git a/src/onebot11/action/group/GetGroupList.ts b/src/onebot11/action/group/GetGroupList.ts index 08c5775..cd1bdc5 100644 --- a/src/onebot11/action/group/GetGroupList.ts +++ b/src/onebot11/action/group/GetGroupList.ts @@ -2,7 +2,6 @@ import { OB11Group } from '../../types' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api' interface Payload { no_cache: boolean | string @@ -12,7 +11,7 @@ class GetGroupList extends BaseAction { actionName = ActionName.GetGroupList protected async _handle(payload: Payload) { - const groupList = await NTQQGroupApi.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) } } diff --git a/src/onebot11/action/group/GetGroupMemberInfo.ts b/src/onebot11/action/group/GetGroupMemberInfo.ts index d7b2a10..4264368 100644 --- a/src/onebot11/action/group/GetGroupMemberInfo.ts +++ b/src/onebot11/action/group/GetGroupMemberInfo.ts @@ -1,10 +1,9 @@ import { OB11GroupMember } from '../../types' -import { getGroupMember, getSelfUid } from '@/common/data' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQUserApi, WebApi } from '@/ntqqapi/api' import { isNull } from '@/common/utils/helper' +import { selfInfo } from '@/common/globalVars' interface Payload { group_id: number | string @@ -15,18 +14,18 @@ class GetGroupMemberInfo extends BaseAction { actionName = ActionName.GetGroupMemberInfo protected async _handle(payload: Payload) { - const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) + const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), payload.user_id.toString()) if (member) { if (isNull(member.sex)) { //log('获取群成员详细信息') - const info = await NTQQUserApi.getUserDetailInfo(member.uid, true) + const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid, true) //log('群成员详细信息结果', info) Object.assign(member, info) } const ret = OB11Constructor.groupMember(payload.group_id.toString(), member) - const self = await getGroupMember(payload.group_id.toString(), getSelfUid()) + const self = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), selfInfo.uid) if (self?.role === 3 || self?.role === 4) { - const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()) + const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString()) const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id) if (target) { ret.join_time = target.join_time diff --git a/src/onebot11/action/group/GetGroupMemberList.ts b/src/onebot11/action/group/GetGroupMemberList.ts index 9edd6f4..8df0238 100644 --- a/src/onebot11/action/group/GetGroupMemberList.ts +++ b/src/onebot11/action/group/GetGroupMemberList.ts @@ -2,8 +2,7 @@ import { OB11GroupMember } from '../../types' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi, WebApi } from '@/ntqqapi/api' -import { getSelfUid } from '@/common/data' +import { selfInfo } from '@/common/globalVars' interface Payload { group_id: number | string @@ -14,7 +13,7 @@ class GetGroupMemberList extends BaseAction { actionName = ActionName.GetGroupMemberList protected async _handle(payload: Payload) { - const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) + const groupMembers = await this.ctx.ntGroupApi.getGroupMembers(payload.group_id.toString()) const groupMembersArr = Array.from(groupMembers.values()) let _groupMembers = groupMembersArr.map(item => { @@ -31,11 +30,11 @@ class GetGroupMemberList extends BaseAction { MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]) } - const selfRole = groupMembers.get(getSelfUid())?.role + const selfRole = groupMembers.get(selfInfo.uid)?.role const isPrivilege = selfRole === 3 || selfRole === 4 if (isPrivilege) { - const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()) + const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString()) for (let i = 0, len = webGroupMembers.length; i < len; i++) { if (!webGroupMembers[i]?.uin) { continue diff --git a/src/onebot11/action/group/SetGroupAddRequest.ts b/src/onebot11/action/group/SetGroupAddRequest.ts index 294fcb0..6834c11 100644 --- a/src/onebot11/action/group/SetGroupAddRequest.ts +++ b/src/onebot11/action/group/SetGroupAddRequest.ts @@ -1,7 +1,6 @@ import BaseAction from '../BaseAction' -import { GroupRequestOperateTypes } from '../../../ntqqapi/types' +import { GroupRequestOperateTypes } from '@/ntqqapi/types' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { flag: string @@ -15,7 +14,7 @@ export default class SetGroupAddRequest extends BaseAction { protected async _handle(payload: Payload): Promise { const flag = payload.flag.toString() const approve = payload.approve?.toString() !== 'false' - await NTQQGroupApi.handleGroupRequest(flag, + await this.ctx.ntGroupApi.handleGroupRequest(flag, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, payload.reason || '' ) diff --git a/src/onebot11/action/group/SetGroupAdmin.ts b/src/onebot11/action/group/SetGroupAdmin.ts index b56f88e..d110b80 100644 --- a/src/onebot11/action/group/SetGroupAdmin.ts +++ b/src/onebot11/action/group/SetGroupAdmin.ts @@ -1,8 +1,6 @@ import BaseAction from '../BaseAction' -import { getGroupMember } from '../../../common/data' -import { GroupMemberRole } from '../../../ntqqapi/types' +import { GroupMemberRole } from '@/ntqqapi/types' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { group_id: number @@ -14,12 +12,12 @@ export default class SetGroupAdmin extends BaseAction { actionName = ActionName.SetGroupAdmin protected async _handle(payload: Payload): Promise { - const member = await getGroupMember(payload.group_id, payload.user_id) + const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) const enable = payload.enable.toString() === 'true' if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQGroupApi.setMemberRole( + await this.ctx.ntGroupApi.setMemberRole( payload.group_id.toString(), member.uid, enable ? GroupMemberRole.admin : GroupMemberRole.normal, diff --git a/src/onebot11/action/group/SetGroupBan.ts b/src/onebot11/action/group/SetGroupBan.ts index e0a01cd..e393163 100644 --- a/src/onebot11/action/group/SetGroupBan.ts +++ b/src/onebot11/action/group/SetGroupBan.ts @@ -1,7 +1,5 @@ import BaseAction from '../BaseAction' -import { getGroupMember } from '../../../common/data' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { group_id: number @@ -13,11 +11,11 @@ export default class SetGroupBan extends BaseAction { actionName = ActionName.SetGroupBan protected async _handle(payload: Payload): Promise { - const member = await getGroupMember(payload.group_id, payload.user_id) + const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQGroupApi.banMember(payload.group_id.toString(), [ + await this.ctx.ntGroupApi.banMember(payload.group_id.toString(), [ { uid: member.uid, timeStamp: parseInt(payload.duration.toString()) }, ]) return null diff --git a/src/onebot11/action/group/SetGroupCard.ts b/src/onebot11/action/group/SetGroupCard.ts index abbbfa8..c8af245 100644 --- a/src/onebot11/action/group/SetGroupCard.ts +++ b/src/onebot11/action/group/SetGroupCard.ts @@ -1,7 +1,5 @@ import BaseAction from '../BaseAction' -import { getGroupMember } from '../../../common/data' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { group_id: number @@ -13,11 +11,11 @@ export default class SetGroupCard extends BaseAction { actionName = ActionName.SetGroupCard protected async _handle(payload: Payload): Promise { - const member = await getGroupMember(payload.group_id, payload.user_id) + const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || '') + await this.ctx.ntGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || '') return null } } diff --git a/src/onebot11/action/group/SetGroupKick.ts b/src/onebot11/action/group/SetGroupKick.ts index 93f82b6..f678da6 100644 --- a/src/onebot11/action/group/SetGroupKick.ts +++ b/src/onebot11/action/group/SetGroupKick.ts @@ -1,7 +1,5 @@ import BaseAction from '../BaseAction' -import { getGroupMember } from '../../../common/data' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { group_id: number @@ -13,11 +11,11 @@ export default class SetGroupKick extends BaseAction { actionName = ActionName.SetGroupKick protected async _handle(payload: Payload): Promise { - const member = await getGroupMember(payload.group_id, payload.user_id) + const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request) + await this.ctx.ntGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request) return null } } diff --git a/src/onebot11/action/group/SetGroupLeave.ts b/src/onebot11/action/group/SetGroupLeave.ts index c8337f2..9fbfc2b 100644 --- a/src/onebot11/action/group/SetGroupLeave.ts +++ b/src/onebot11/action/group/SetGroupLeave.ts @@ -1,7 +1,5 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' -import { log } from '../../../common/utils/log' interface Payload { group_id: number @@ -13,9 +11,9 @@ export default class SetGroupLeave extends BaseAction { protected async _handle(payload: Payload): Promise { try { - await NTQQGroupApi.quitGroup(payload.group_id.toString()) + await this.ctx.ntGroupApi.quitGroup(payload.group_id.toString()) } catch (e) { - log('退群失败', e) + this.ctx.logger.error('退群失败', e) throw e } } diff --git a/src/onebot11/action/group/SetGroupName.ts b/src/onebot11/action/group/SetGroupName.ts index a32bed6..827d9a8 100644 --- a/src/onebot11/action/group/SetGroupName.ts +++ b/src/onebot11/action/group/SetGroupName.ts @@ -1,6 +1,5 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { group_id: number @@ -11,7 +10,7 @@ export default class SetGroupName extends BaseAction { actionName = ActionName.SetGroupName protected async _handle(payload: Payload): Promise { - await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name) + await this.ctx.ntGroupApi.setGroupName(payload.group_id.toString(), payload.group_name) return null } } diff --git a/src/onebot11/action/group/SetGroupWholeBan.ts b/src/onebot11/action/group/SetGroupWholeBan.ts index fdf9a03..2da92e9 100644 --- a/src/onebot11/action/group/SetGroupWholeBan.ts +++ b/src/onebot11/action/group/SetGroupWholeBan.ts @@ -1,6 +1,5 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { group_id: number @@ -12,7 +11,7 @@ export default class SetGroupWholeBan extends BaseAction { protected async _handle(payload: Payload): Promise { const enable = payload.enable.toString() === 'true' - await NTQQGroupApi.banGroup(payload.group_id.toString(), enable) + await this.ctx.ntGroupApi.banGroup(payload.group_id.toString(), enable) return null } } diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index 6b6d8d9..71bf307 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -54,71 +54,70 @@ import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg' import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import GetEvent from './llonebot/GetEvent' import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile' +import type Adapter from '../adapter' - -export const actionHandlers = [ - new GetFile(), - new Debug(), - new GetConfigAction(), - new SetConfigAction(), - new GetGroupAddRequest(), - new SetQQAvatar(), - new GetFriendWithCategory(), - new GetEvent(), - // onebot11 - new SendLike(), - new GetMsg(), - new GetLoginInfo(), - new GetFriendList(), - new GetGroupList(), - new GetGroupInfo(), - new GetGroupMemberList(), - new GetGroupMemberInfo(), - new SendGroupMsg(), - new SendPrivateMsg(), - new SendMsg(), - new DeleteMsg(), - new SetGroupAddRequest(), - new SetFriendAddRequest(), - new SetGroupLeave(), - new GetVersionInfo(), - new CanSendRecord(), - new CanSendImage(), - new GetStatus(), - new SetGroupWholeBan(), - new SetGroupBan(), - new SetGroupKick(), - new SetGroupAdmin(), - new SetGroupName(), - new SetGroupCard(), - new GetImage(), - new GetRecord(), - new CleanCache(), - new GetCookies(), - new SetMsgEmojiLike(), - new ForwardFriendSingleMsg(), - new ForwardGroupSingleMsg(), - //以下为go-cqhttp api - new GetGroupEssence(), - new GetGroupHonorInfo(), - new GoCQHTTPSendForwardMsg(), - new GoCQHTTPSendGroupForwardMsg(), - new GoCQHTTPSendPrivateForwardMsg(), - new GoCQHTTPGetStrangerInfo(), - new GoCQHTTPDownloadFile(), - new GetGuildList(), - new GoCQHTTPMarkMsgAsRead(), - new GoCQHTTPUploadGroupFile(), - new GoCQHTTPUploadPrivateFile(), - new GoCQHTTPGetGroupMsgHistory(), - new GoCQHTTGetForwardMsgAction(), - new GoCQHTTHandleQuickOperation(), - new GoCQHTTPSetEssenceMsg(), - new GoCQHTTPDelEssenceMsg(), - new GoCQHTTPDelGroupFile() -] - -function initActionMap() { +export function initActionMap(adapter: Adapter) { + const actionHandlers = [ + new GetFile(adapter), + new Debug(adapter), + new GetConfigAction(adapter), + new SetConfigAction(adapter), + new GetGroupAddRequest(adapter), + new SetQQAvatar(adapter), + new GetFriendWithCategory(adapter), + new GetEvent(adapter), + // onebot11 + new SendLike(adapter), + new GetMsg(adapter), + new GetLoginInfo(adapter), + new GetFriendList(adapter), + new GetGroupList(adapter), + new GetGroupInfo(adapter), + new GetGroupMemberList(adapter), + new GetGroupMemberInfo(adapter), + new SendGroupMsg(adapter), + new SendPrivateMsg(adapter), + new SendMsg(adapter), + new DeleteMsg(adapter), + new SetGroupAddRequest(adapter), + new SetFriendAddRequest(adapter), + new SetGroupLeave(adapter), + new GetVersionInfo(adapter), + new CanSendRecord(adapter), + new CanSendImage(adapter), + new GetStatus(adapter), + new SetGroupWholeBan(adapter), + new SetGroupBan(adapter), + new SetGroupKick(adapter), + new SetGroupAdmin(adapter), + new SetGroupName(adapter), + new SetGroupCard(adapter), + new GetImage(adapter), + new GetRecord(adapter), + new CleanCache(adapter), + new GetCookies(adapter), + new SetMsgEmojiLike(adapter), + new ForwardFriendSingleMsg(adapter), + new ForwardGroupSingleMsg(adapter), + //以下为go-cqhttp api + new GetGroupEssence(adapter), + new GetGroupHonorInfo(adapter), + new GoCQHTTPSendForwardMsg(adapter), + new GoCQHTTPSendGroupForwardMsg(adapter), + new GoCQHTTPSendPrivateForwardMsg(adapter), + new GoCQHTTPGetStrangerInfo(adapter), + new GoCQHTTPDownloadFile(adapter), + new GetGuildList(adapter), + new GoCQHTTPMarkMsgAsRead(adapter), + new GoCQHTTPUploadGroupFile(adapter), + new GoCQHTTPUploadPrivateFile(adapter), + new GoCQHTTPGetGroupMsgHistory(adapter), + new GoCQHTTGetForwardMsgAction(adapter), + new GoCQHTTHandleQuickOperation(adapter), + new GoCQHTTPSetEssenceMsg(adapter), + new GoCQHTTPDelEssenceMsg(adapter), + new GoCQHTTPDelGroupFile(adapter) + ] const actionMap = new Map>() for (const action of actionHandlers) { actionMap.set(action.actionName, action) @@ -128,5 +127,3 @@ function initActionMap() { return actionMap } - -export const actionMap = initActionMap() diff --git a/src/onebot11/action/llonebot/Config.ts b/src/onebot11/action/llonebot/Config.ts index d50fb31..6400103 100644 --- a/src/onebot11/action/llonebot/Config.ts +++ b/src/onebot11/action/llonebot/Config.ts @@ -1,8 +1,7 @@ import BaseAction from '../BaseAction' -import { Config } from '../../../common/types' +import { Config } from '@/common/types' import { ActionName } from '../types' -import { setConfig } from '../../../main/setConfig' -import { getConfigUtil } from '../../../common/config' +import { getConfigUtil } from '@/common/config' export class GetConfigAction extends BaseAction { actionName = ActionName.GetConfig @@ -14,6 +13,6 @@ export class GetConfigAction extends BaseAction { export class SetConfigAction extends BaseAction { actionName = ActionName.SetConfig protected async _handle(payload: Config): Promise { - setConfig(payload).then() + getConfigUtil().setConfig(payload) } } diff --git a/src/onebot11/action/llonebot/Debug.ts b/src/onebot11/action/llonebot/Debug.ts index a184626..f89f213 100644 --- a/src/onebot11/action/llonebot/Debug.ts +++ b/src/onebot11/action/llonebot/Debug.ts @@ -1,16 +1,5 @@ import BaseAction from '../BaseAction' -// import * as ntqqApi from "../../../ntqqapi/api"; -import { - NTQQMsgApi, - NTQQFriendApi, - NTQQGroupApi, - NTQQUserApi, - NTQQFileApi, - NTQQFileCacheApi, - NTQQWindowApi, -} from '../../../ntqqapi/api' import { ActionName } from '../types' -import { log } from '../../../common/utils/log' interface Payload { method: string @@ -21,10 +10,10 @@ export default class Debug extends BaseAction { actionName = ActionName.Debug protected async _handle(payload: Payload): Promise { - log('debug call ntqq api', payload) - const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] + this.ctx.logger.info('debug call ntqq api', payload) + const { ntMsgApi, ntFileApi, ntFileCacheApi, ntFriendApi, ntGroupApi, ntUserApi, ntWindowApi } = this.ctx + const ntqqApi = [ntMsgApi, ntFriendApi, ntGroupApi, ntUserApi, ntFileApi, ntFileCacheApi, ntWindowApi] for (const ntqqApiClass of ntqqApi) { - //log('ntqqApiClass', ntqqApiClass) const method = ntqqApiClass[payload.method] if (method) { const result = method(...payload.args) @@ -35,8 +24,5 @@ export default class Debug extends BaseAction { } } throw `${payload.method}方法 不存在` - - // const info = await NTQQApi.getUserDetailInfo(friends[0].uid); - // return info } } diff --git a/src/onebot11/action/llonebot/GetEvent.ts b/src/onebot11/action/llonebot/GetEvent.ts index 09827b1..0d7c63c 100644 --- a/src/onebot11/action/llonebot/GetEvent.ts +++ b/src/onebot11/action/llonebot/GetEvent.ts @@ -1,8 +1,10 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { getHttpEvent } from '../../server/event-for-http' -import { PostEventType } from '../../server/post-ob11-event' -// import { log } from "../../../common/utils"; +import { getHttpEvent } from '../../helper/event-for-http' +import { OB11Message } from '../../types' +import { OB11BaseEvent } from '../../event/OB11BaseEvent' + +type PostEventType = OB11BaseEvent | OB11Message interface Payload { key: string @@ -14,10 +16,10 @@ export default class GetEvent extends BaseAction { protected async _handle(payload: Payload): Promise { let key = '' if (payload.key) { - key = payload.key; + key = payload.key } - let timeout = parseInt(payload.timeout?.toString()) || 0; - let evts = await getHttpEvent(key,timeout); - return evts; + let timeout = parseInt(payload.timeout?.toString()) || 0 + let evts = await getHttpEvent(key, timeout) + return evts } } diff --git a/src/onebot11/action/llonebot/GetGroupAddRequest.ts b/src/onebot11/action/llonebot/GetGroupAddRequest.ts index 562a9b9..e375600 100644 --- a/src/onebot11/action/llonebot/GetGroupAddRequest.ts +++ b/src/onebot11/action/llonebot/GetGroupAddRequest.ts @@ -1,8 +1,6 @@ -import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types' +import { GroupNotify, GroupNotifyStatus } from '@/ntqqapi/types' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQUserApi } from '../../../ntqqapi/api/user' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface OB11GroupRequestNotify { group_id: number @@ -14,11 +12,11 @@ export default class GetGroupAddRequest extends BaseAction { - const data = await NTQQGroupApi.getGroupIgnoreNotifies() + const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies() const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) const returnData: OB11GroupRequestNotify[] = [] for (const notify of notifies) { - const uin = await NTQQUserApi.getUinByUid(notify.user1.uid) + const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) returnData.push({ group_id: parseInt(notify.group.groupCode), user_id: parseInt(uin), diff --git a/src/onebot11/action/llonebot/SetQQAvatar.ts b/src/onebot11/action/llonebot/SetQQAvatar.ts index 3d6d041..ac10163 100644 --- a/src/onebot11/action/llonebot/SetQQAvatar.ts +++ b/src/onebot11/action/llonebot/SetQQAvatar.ts @@ -1,9 +1,7 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' import * as fs from 'node:fs' -import { NTQQUserApi } from '../../../ntqqapi/api/user' import { checkFileReceived, uri2local } from '../../../common/utils/file' -// import { log } from "../../../common/utils"; interface Payload { file: string @@ -19,22 +17,22 @@ export default class SetAvatar extends BaseAction { } if (path) { await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃,需要提前判断 - const ret = await NTQQUserApi.setQQAvatar(path) + const ret = await this.ctx.ntUserApi.setQQAvatar(path) if (!isLocal) { - fs.unlink(path, () => {}) + fs.unlink(path, () => { }) } if (!ret) { throw `头像${payload.file}设置失败,api无返回` } // log(`头像设置返回:${JSON.stringify(ret)}`) - if (ret['result'] == 1004022) { + if ((ret.result as number) === 1004022) { throw `头像${payload.file}设置失败,文件可能不是图片格式` - } else if (ret['result'] != 0) { + } else if (ret.result !== 0) { throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}` } } else { if (!isLocal) { - fs.unlink(path, () => {}) + fs.unlink(path, () => { }) } throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在` } diff --git a/src/onebot11/action/msg/DeleteMsg.ts b/src/onebot11/action/msg/DeleteMsg.ts index 831c5dc..1b94d16 100644 --- a/src/onebot11/action/msg/DeleteMsg.ts +++ b/src/onebot11/action/msg/DeleteMsg.ts @@ -1,6 +1,5 @@ import { ActionName } from '../types' import BaseAction from '../BaseAction' -import { NTQQMsgApi } from '@/ntqqapi/api/msg' import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { @@ -18,7 +17,7 @@ class DeleteMsg extends BaseAction { if (!msg) { throw `消息${payload.message_id}不存在` } - await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]) + await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId]) } } diff --git a/src/onebot11/action/msg/ForwardSingleMsg.ts b/src/onebot11/action/msg/ForwardSingleMsg.ts index 42a4f23..f0f901a 100644 --- a/src/onebot11/action/msg/ForwardSingleMsg.ts +++ b/src/onebot11/action/msg/ForwardSingleMsg.ts @@ -1,5 +1,4 @@ import BaseAction from '../BaseAction' -import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api' import { ChatType } from '@/ntqqapi/types' import { ActionName } from '../types' import { Peer } from '@/ntqqapi/types' @@ -14,7 +13,7 @@ interface Payload { abstract class ForwardSingleMsg extends BaseAction { protected async getTargetPeer(payload: Payload): Promise { if (payload.user_id) { - const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) + const peerUid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString()) if (!peerUid) { throw new Error(`无法找到私聊对象${payload.user_id}`) } @@ -32,7 +31,7 @@ abstract class ForwardSingleMsg extends BaseAction { throw new Error(`无法找到消息${payload.message_id}`) } const peer = await this.getTargetPeer(payload) - const ret = await NTQQMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId]) + const ret = await this.ctx.ntMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId]) if (ret.result !== 0) { throw new Error(`转发消息失败 ${ret.errMsg}`) } diff --git a/src/onebot11/action/msg/GetMsg.ts b/src/onebot11/action/msg/GetMsg.ts index e3a0e2b..e5f175b 100644 --- a/src/onebot11/action/msg/GetMsg.ts +++ b/src/onebot11/action/msg/GetMsg.ts @@ -2,9 +2,7 @@ import { OB11Message } from '../../types' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQMsgApi } from '@/ntqqapi/api' import { MessageUnique } from '@/common/utils/MessageUnique' -import { getMsgCache } from '@/common/data' export interface PayloadType { message_id: number | string @@ -30,8 +28,8 @@ class GetMsg extends BaseAction { peerUid: msgIdWithPeer.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType } - const msg = getMsgCache(msgIdWithPeer.MsgId) ?? (await NTQQMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0] - const retMsg = await OB11Constructor.message(msg) + 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) retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)! retMsg.message_seq = retMsg.message_id retMsg.real_id = retMsg.message_id diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index e62638c..220dc50 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -6,7 +6,6 @@ import { RawMessage, SendMessageElement, } from '@/ntqqapi/types' -import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data' import { OB11MessageCustomMusic, OB11MessageData, @@ -24,14 +23,14 @@ import fs from 'node:fs' import fsPromise from 'node:fs/promises' import { decodeCQCode } from '../../cqcode' import { getConfigUtil } from '@/common/config' -import { log } from '@/common/utils/log' import { sleep } from '@/common/utils/helper' import { uri2local } from '@/common/utils' -import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign' import { Peer } from '@/ntqqapi/types/msg' import { MessageUnique } from '@/common/utils/MessageUnique' import { OB11MessageFileBase } from '../../types' +import { Context } from 'cordis' +import { selfInfo } from '@/common/globalVars' export interface ReturnDataType { message_id: number @@ -72,6 +71,7 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26 async function handleOb11FileLikeMessage( + ctx: Context, { data: inputdata }: OB11MessageFileBase, { deleteAfterSentFiles }: Pick, ) { @@ -85,7 +85,7 @@ async function handleOb11FileLikeMessage( } = (await uri2local(inputdata?.url || inputdata.file)) if (!success) { - log('文件下载失败', errMsg) + ctx.logger.error('文件下载失败', errMsg) throw Error('文件下载失败' + errMsg) } @@ -97,6 +97,7 @@ async function handleOb11FileLikeMessage( } export async function createSendElements( + ctx: Context, messageData: OB11MessageData[], peer: Peer, ignoreTypes: OB11MessageDataType[] = [], @@ -128,10 +129,10 @@ export async function createSendElements( let isAdmin: boolean = true if (groupCode) { try { - remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo + remainAtAllCount = (await ctx.ntGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo .RemainAtAllCountForUin - log(`群${groupCode}剩余at全体次数`, remainAtAllCount) - const self = await getGroupMember(groupCode, getSelfUin()) + ctx.logger.info(`群${groupCode}剩余at全体次数`, remainAtAllCount) + const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin) isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner } catch (e) { } @@ -141,7 +142,7 @@ export async function createSendElements( } } else if (peer.chatType === ChatType.group) { - const atMember = await getGroupMember(peer.peerUid, atQQ) + const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ) if (atMember) { const display = `@${atMember.cardName || atMember.nick}` sendElements.push( @@ -149,7 +150,7 @@ export async function createSendElements( ) } else { const atNmae = sendMsg.data?.name - const uid = await NTQQUserApi.getUidByUin(atQQ) || '' + const uid = await ctx.ntUserApi.getUidByUin(atQQ) || '' const display = atNmae ? `@${atNmae}` : '' sendElements.push( SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display), @@ -163,10 +164,10 @@ export async function createSendElements( if (sendMsg.data?.id) { const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id) if (!replyMsgId) { - log('回复消息不存在', replyMsgId) + ctx.logger.warn('回复消息不存在', replyMsgId) continue } - const replyMsg = (await NTQQMsgApi.getMsgsByMsgId( + const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId( replyMsgId.Peer, [replyMsgId.MsgId!] )).msgList[0] @@ -203,7 +204,8 @@ export async function createSendElements( break case OB11MessageDataType.image: { const res = await SendMsgElementConstructor.pic( - (await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })).path, + ctx, + (await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path, sendMsg.data.summary || '', sendMsg.data.subType || 0 ) @@ -212,25 +214,25 @@ export async function createSendElements( } break case OB11MessageDataType.file: { - const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) - sendElements.push(await SendMsgElementConstructor.file(path, fileName)) + const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) + sendElements.push(await SendMsgElementConstructor.file(ctx, path, fileName)) } break case OB11MessageDataType.video: { - const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) + const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) let thumb = sendMsg.data.thumb if (thumb) { const uri2LocalRes = await uri2local(thumb) if (uri2LocalRes.success) thumb = uri2LocalRes.path } - const res = await SendMsgElementConstructor.video(path, fileName, thumb) + const res = await SendMsgElementConstructor.video(ctx, path, fileName, thumb) deleteAfterSentFiles.push(res.videoElement.filePath) sendElements.push(res) } break case OB11MessageDataType.voice: { - const { path } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) - sendElements.push(await SendMsgElementConstructor.ptt(path)) + const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) + sendElements.push(await SendMsgElementConstructor.ptt(ctx, path)) } break case OB11MessageDataType.json: { @@ -261,6 +263,7 @@ export async function createSendElements( } export async function sendMsg( + ctx: Context, peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], @@ -286,46 +289,46 @@ export async function sendMsg( totalSize += fs.statSync(fileElement.picElement.sourcePath).size } } catch (e) { - log('文件大小计算失败', e, fileElement) + ctx.logger.warn('文件大小计算失败', e, fileElement) } } //log('发送消息总大小', totalSize, 'bytes') const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s ) //log('设置消息超时时间', timeout) - const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) + const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) - log('消息发送', returnMsg.msgShortId) + ctx.logger.info('消息发送', returnMsg.msgShortId) deleteAfterSentFiles.map(path => fsPromise.unlink(path)) return returnMsg } -async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise { - // 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 NTQQUserApi.getUidByUin(payload.user_id.toString()) - const isBuddy = await NTQQFriendApi.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' -} - export class SendMsg extends BaseAction { actionName = ActionName.SendMsg + private async createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise { + // 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 { const messages = convertMessage2List(payload.message) const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node) @@ -343,12 +346,6 @@ export class SendMsg extends BaseAction { } } if (payload.user_id && payload.message_type !== 'group') { - const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) - const isBuddy = await NTQQFriendApi.isBuddy(uid!) - // 此处有问题 - if (!isBuddy) { - //return { valid: false, message: '异常消息' } - } } return { valid: true, @@ -362,7 +359,7 @@ export class SendMsg extends BaseAction { } else if (payload.message_type === 'private') { contextMode = ContextMode.Private } - const peer = await createContext(payload, contextMode) + const peer = await this.createContext(payload, contextMode) const messages = convertMessage2List( payload.message, payload.auto_escape === true || payload.auto_escape === 'true', @@ -412,7 +409,7 @@ export class SendMsg extends BaseAction { } let jsonContent: string try { - jsonContent = await new MusicSign(musicSignUrl).sign(postData) + jsonContent = await new MusicSign(this.ctx, musicSignUrl).sign(postData) if (!jsonContent) { throw '音乐消息生成失败,提交内容有误或者签名服务器签名失败' } @@ -426,13 +423,13 @@ export class SendMsg extends BaseAction { } } // log("send msg:", peer, sendElements) - const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, peer) + const { sendElements, deleteAfterSentFiles } = await createSendElements(this.ctx, messages, peer) if (sendElements.length === 1) { if (sendElements[0] === null) { return { message_id: 0 } } } - const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) + const returnMsg = await sendMsg(this.ctx, peer, sendElements, deleteAfterSentFiles) return { message_id: returnMsg.msgShortId! } } @@ -444,7 +441,7 @@ export class SendMsg extends BaseAction { } private async cloneMsg(msg: RawMessage): Promise { - log('克隆的目标消息', msg) + this.ctx.logger.info('克隆的目标消息', msg) let sendElements: SendMessageElement[] = [] for (const ele of msg.elements) { sendElements.push(ele as SendMessageElement) @@ -453,14 +450,14 @@ export class SendMsg extends BaseAction { // } } if (sendElements.length === 0) { - log('需要clone的消息无法解析,将会忽略掉', msg) + this.ctx.logger.warn('需要clone的消息无法解析,将会忽略掉', msg) } - log('克隆消息', sendElements) + this.ctx.logger.info('克隆消息', sendElements) try { - const nodeMsg = await NTQQMsgApi.sendMsg( + const nodeMsg = await this.ctx.ntMsgApi.sendMsg( { chatType: ChatType.friend, - peerUid: getSelfUid(), + peerUid: selfInfo.uid, }, sendElements, true, @@ -468,7 +465,7 @@ export class SendMsg extends BaseAction { await sleep(400) return nodeMsg } catch (e) { - log(e, '克隆转发消息失败,将忽略本条消息', msg) + this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg) } } @@ -476,7 +473,7 @@ export class SendMsg extends BaseAction { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) { const selfPeer = { chatType: ChatType.friend, - peerUid: getSelfUid(), + peerUid: selfInfo.uid, } let nodeMsgIds: string[] = [] // 先判断一遍是不是id和自定义混用 @@ -487,7 +484,7 @@ export class SendMsg extends BaseAction { if (nodeId) { const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId) if (!nodeMsg) { - log('转发消息失败,未找到消息', nodeId) + this.ctx.logger.warn('转发消息失败,未找到消息', nodeId) continue } nodeMsgIds.push(nodeMsg.MsgId) @@ -497,10 +494,11 @@ export class SendMsg extends BaseAction { // 提取消息段,发给自己生成消息id try { const { sendElements, deleteAfterSentFiles } = await createSendElements( + this.ctx, convertMessage2List(messageNode.data.content), destPeer ) - log('开始生成转发节点', sendElements) + this.ctx.logger.info('开始生成转发节点', sendElements) let sendElementsSplit: SendMessageElement[][] = [] let splitIndex = 0 for (const ele of sendElements) { @@ -518,19 +516,19 @@ export class SendMsg extends BaseAction { else { sendElementsSplit[splitIndex].push(ele) } - log(sendElementsSplit) + this.ctx.logger.info(sendElementsSplit) } // log("分割后的转发节点", sendElementsSplit) for (const eles of sendElementsSplit) { - const nodeMsg = await sendMsg(selfPeer, eles, [], true) + const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true) nodeMsgIds.push(nodeMsg.msgId) await sleep(400) - log('转发节点生成成功', nodeMsg.msgId) + this.ctx.logger.info('转发节点生成成功', nodeMsg.msgId) } deleteAfterSentFiles.map((f) => fs.unlink(f, () => { })) } catch (e) { - log('生成转发消息节点失败', e) + this.ctx.logger.error('生成转发消息节点失败', e) } } } @@ -542,7 +540,7 @@ export class SendMsg extends BaseAction { for (const msgId of nodeMsgIds) { const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId) if (nodeMsgPeer) { - const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0] + const nodeMsg = (await this.ctx.ntMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0] srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid } if (srcPeer.peerUid !== nodeMsg.peerUid) { needSendSelf = true @@ -570,7 +568,7 @@ export class SendMsg extends BaseAction { if (nodeMsgIds.length === 0) { throw Error('转发消息失败,节点为空') } - const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) + const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId) return returnMsg } diff --git a/src/onebot11/action/msg/SetMsgEmojiLike.ts b/src/onebot11/action/msg/SetMsgEmojiLike.ts index 0cc7436..1290f38 100644 --- a/src/onebot11/action/msg/SetMsgEmojiLike.ts +++ b/src/onebot11/action/msg/SetMsgEmojiLike.ts @@ -1,6 +1,5 @@ import { ActionName } from '../types' import BaseAction from '../BaseAction' -import { NTQQMsgApi } from '@/ntqqapi/api/msg' import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { @@ -22,11 +21,11 @@ export class SetMsgEmojiLike extends BaseAction { if (!payload.emoji_id) { throw new Error('emojiId not found') } - const msgData = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList + const msgData = (await this.ctx.ntMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) { throw new Error('find msg by msgid error') } - return await NTQQMsgApi.setEmojiLike( + return await this.ctx.ntMsgApi.setEmojiLike( msg.Peer, msgData[0].msgSeq, payload.emoji_id.toString(), diff --git a/src/onebot11/action/system/CleanCache.ts b/src/onebot11/action/system/CleanCache.ts index a7b6dc0..8614d24 100644 --- a/src/onebot11/action/system/CleanCache.ts +++ b/src/onebot11/action/system/CleanCache.ts @@ -2,8 +2,7 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' import fs from 'node:fs' import Path from 'node:path' -import { ChatType, ChatCacheListItemBasic, CacheFileType } from '../../../ntqqapi/types' -import { NTQQFileApi, NTQQFileCacheApi } from '../../../ntqqapi/api/file' +import { ChatCacheListItemBasic, CacheFileType } from '@/ntqqapi/types' export default class CleanCache extends BaseAction { actionName = ActionName.CleanCache @@ -14,23 +13,23 @@ export default class CleanCache extends BaseAction { // dbUtil.clearCache() const cacheFilePaths: string[] = [] - await NTQQFileCacheApi.setCacheSilentScan(false) + await this.ctx.ntFileCacheApi.setCacheSilentScan(false) - cacheFilePaths.push(await NTQQFileCacheApi.getHotUpdateCachePath()) - cacheFilePaths.push(await NTQQFileCacheApi.getDesktopTmpPath()) + cacheFilePaths.push(await this.ctx.ntFileCacheApi.getHotUpdateCachePath()) + cacheFilePaths.push(await this.ctx.ntFileCacheApi.getDesktopTmpPath()) - const list = await NTQQFileCacheApi.getCacheSessionPathList() + const list = await this.ctx.ntFileCacheApi.getCacheSessionPathList() list.forEach((e) => cacheFilePaths.push(e.value)) // await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知 - const cacheScanResult = await NTQQFileCacheApi.scanCache() + const cacheScanResult = await this.ctx.ntFileCacheApi.scanCache() const cacheSize = parseInt(cacheScanResult.size[6]) if (cacheScanResult.result !== 0) { throw 'Something went wrong while scanning cache. Code: ' + cacheScanResult.result } - await NTQQFileCacheApi.setCacheSilentScan(true) + await this.ctx.ntFileCacheApi.setCacheSilentScan(true) if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作 // await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了 @@ -53,11 +52,11 @@ export default class CleanCache extends BaseAction { const fileTypeAny: any = CacheFileType[name] const fileType: CacheFileType = fileTypeAny - cacheFileList.push(...(await NTQQFileCacheApi.getFileCacheInfo(fileType)).infos.map((file) => file.fileKey)) + cacheFileList.push(...(await this.ctx.ntFileCacheApi.getFileCacheInfo(fileType)).infos.map((file) => file.fileKey)) } // 一并清除 - await NTQQFileCacheApi.clearChatCache(chatCacheList, cacheFileList) + await this.ctx.ntFileCacheApi.clearChatCache(chatCacheList, cacheFileList) res() } catch (e) { console.error('清理缓存时发生了错误') @@ -83,22 +82,4 @@ function deleteCachePath(pathList: string[]) { for (const path of pathList) { emptyPath(path) } -} - -function getCacheList(type: ChatType) { - // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理 - return new Promise>((res, rej) => { - NTQQFileCacheApi.getChatCacheList(type, 1000, 0) - .then((data) => { - const list = data.infos.filter((e) => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0) - const result = list.map((e) => { - const result = { ...e.basicChatCacheInfo } - result.chatType = type - result.isChecked = true - return result - }) - res(result) - }) - .catch((e) => rej(e)) - }) -} +} \ No newline at end of file diff --git a/src/onebot11/action/system/GetLoginInfo.ts b/src/onebot11/action/system/GetLoginInfo.ts index c05fa33..b38204b 100644 --- a/src/onebot11/action/system/GetLoginInfo.ts +++ b/src/onebot11/action/system/GetLoginInfo.ts @@ -1,17 +1,16 @@ import { OB11User } from '../../types' -import { OB11Constructor } from '../../constructor' -import { getSelfInfo, getSelfNick } from '../../../common/data' import BaseAction from '../BaseAction' import { ActionName } from '../types' +import { selfInfo } from '@/common/globalVars' class GetLoginInfo extends BaseAction { actionName = ActionName.GetLoginInfo protected async _handle(payload: null) { - return OB11Constructor.selfInfo({ - ...getSelfInfo(), - nick: await getSelfNick(true) - }) + return { + user_id: parseInt(selfInfo.uin), + nickname: await this.ctx.ntUserApi.getSelfNick(true) + } } } diff --git a/src/onebot11/action/system/GetStatus.ts b/src/onebot11/action/system/GetStatus.ts index c3aa3da..e282ee7 100644 --- a/src/onebot11/action/system/GetStatus.ts +++ b/src/onebot11/action/system/GetStatus.ts @@ -1,14 +1,14 @@ import BaseAction from '../BaseAction' import { OB11Status } from '../../types' import { ActionName } from '../types' -import { getSelfInfo } from '../../../common/data' +import { selfInfo } from '@/common/globalVars' export default class GetStatus extends BaseAction { actionName = ActionName.GetStatus protected async _handle(payload: any): Promise { return { - online: getSelfInfo().online!, + online: selfInfo.online!, good: true, } } diff --git a/src/onebot11/action/user/GetCookie.ts b/src/onebot11/action/user/GetCookie.ts index ff32d08..e0bd672 100644 --- a/src/onebot11/action/user/GetCookie.ts +++ b/src/onebot11/action/user/GetCookie.ts @@ -1,5 +1,4 @@ import BaseAction from '../BaseAction' -import { NTQQUserApi, WebApi } from '@/ntqqapi/api' import { ActionName } from '../types' interface Response { @@ -18,10 +17,10 @@ export class GetCookies extends BaseAction { if (!payload.domain) { throw '缺少参数 domain' } - const cookiesObject = await NTQQUserApi.getCookies(payload.domain) + const cookiesObject = await this.ctx.ntUserApi.getCookies(payload.domain) //把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起 const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') - const bkn = cookiesObject.skey ? WebApi.genBkn(cookiesObject.skey) : '' + const bkn = cookiesObject.skey ? this.ctx.ntWebApi.genBkn(cookiesObject.skey) : '' return { cookies, bkn } } } diff --git a/src/onebot11/action/user/GetFriendList.ts b/src/onebot11/action/user/GetFriendList.ts index 963fe39..ee91b92 100644 --- a/src/onebot11/action/user/GetFriendList.ts +++ b/src/onebot11/action/user/GetFriendList.ts @@ -2,7 +2,6 @@ import BaseAction from '../BaseAction' import { OB11User } from '../../types' import { OB11Constructor } from '../../constructor' import { ActionName } from '../types' -import { NTQQFriendApi } from '@/ntqqapi/api' import { getBuildVersion } from '@/common/utils/QQBasicInfo' interface Payload { @@ -15,9 +14,9 @@ export class GetFriendList extends BaseAction { protected async _handle(payload: Payload) { const refresh = payload?.no_cache === true || payload?.no_cache === 'true' if (getBuildVersion() >= 26702) { - return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(refresh)) + return OB11Constructor.friendsV2(await this.ctx.ntFriendApi.getBuddyV2(refresh)) } - return OB11Constructor.friends(await NTQQFriendApi.getFriends(refresh)) + return OB11Constructor.friends(await this.ctx.ntFriendApi.getFriends(refresh)) } } @@ -28,7 +27,7 @@ export class GetFriendWithCategory extends BaseAction { protected async _handle(payload: void) { if (getBuildVersion() >= 26702) { //全新逻辑 - return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true)) + return OB11Constructor.friendsV2(await this.ctx.ntFriendApi.getBuddyV2ExWithCate(true)) } else { throw new Error('this ntqq version not support, must be 26702 or later') } diff --git a/src/onebot11/action/user/SendLike.ts b/src/onebot11/action/user/SendLike.ts index 101ea03..d63ff4f 100644 --- a/src/onebot11/action/user/SendLike.ts +++ b/src/onebot11/action/user/SendLike.ts @@ -1,6 +1,5 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQUserApi } from '@/ntqqapi/api' interface Payload { user_id: number | string @@ -13,8 +12,8 @@ export default class SendLike extends BaseAction { protected async _handle(payload: Payload): Promise { try { const qq = payload.user_id.toString() - const uid: string = await NTQQUserApi.getUidByUin(qq) || '' - const result = await NTQQUserApi.like(uid, +payload.times || 1) + const uid: string = await this.ctx.ntUserApi.getUidByUin(qq) || '' + const result = await this.ctx.ntUserApi.like(uid, +payload.times || 1) if (result?.result !== 0) { throw Error(result?.errMsg) } diff --git a/src/onebot11/action/user/SetFriendAddRequest.ts b/src/onebot11/action/user/SetFriendAddRequest.ts index 0813288..80116e8 100644 --- a/src/onebot11/action/user/SetFriendAddRequest.ts +++ b/src/onebot11/action/user/SetFriendAddRequest.ts @@ -13,7 +13,7 @@ export default class SetFriendAddRequest extends BaseAction { protected async _handle(payload: Payload): Promise { const approve = payload.approve?.toString() !== 'false' - await NTQQFriendApi.handleFriendRequest(payload.flag, approve) + await this.ctx.ntFriendApi.handleFriendRequest(payload.flag, approve) return null } } diff --git a/src/onebot11/adapter.ts b/src/onebot11/adapter.ts new file mode 100644 index 0000000..331e9b2 --- /dev/null +++ b/src/onebot11/adapter.ts @@ -0,0 +1,436 @@ +import { Service, Context } from 'cordis' +import { OB11Constructor } from './constructor' +import { + GroupNotify, + GroupNotifyTypes, + RawMessage, + BuddyReqType, + Peer, + FriendRequest, + GroupMember, + GroupMemberRole +} from '../ntqqapi/types' +import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest' +import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest' +import { MessageUnique } from '../common/utils/MessageUnique' +import { getConfigUtil } from '../common/config' +import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' +import { selfInfo } from '../common/globalVars' +import { OB11Config, Config as LLOBConfig } from '../common/types' +import { OB11WebSocket, OB11WebSocketReverseManager } from './connect/ws' +import { OB11Http, OB11HttpPost } from './connect/http' +import { OB11BaseEvent } from './event/OB11BaseEvent' +import { OB11Message } from './types' +import { OB11BaseMetaEvent } from './event/meta/OB11BaseMetaEvent' +import { postHttpEvent } from './helper/event-for-http' +import { initActionMap } from './action' +import { llonebotError } from '../common/globalVars' +import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' +import { OB11GroupAdminNoticeEvent } from './event/notice/OB11GroupAdminNoticeEvent' + +declare module 'cordis' { + interface Context { + onebot: OneBot11Adapter + } +} + +class OneBot11Adapter extends Service { + static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi'] + + public messages: Map = new Map() + public startTime = 0 + private ob11WebSocket: OB11WebSocket + private ob11WebSocketReverseManager: OB11WebSocketReverseManager + private ob11Http: OB11Http + private ob11HttpPost: OB11HttpPost + + constructor(public ctx: Context, public config: OneBot11Adapter.Config) { + super(ctx, 'onebot', true) + const actionMap = initActionMap(this) + this.ob11Http = new OB11Http(ctx, { + port: config.httpPort, + token: config.token, + actionMap + }) + this.ob11HttpPost = new OB11HttpPost(ctx, { + hosts: config.httpHosts, + heartInterval: config.heartInterval, + secret: config.httpSecret, + enableHttpHeart: config.enableHttpHeart + }) + this.ob11WebSocket = new OB11WebSocket(ctx, { + port: config.wsPort, + heartInterval: config.heartInterval, + token: config.token, + actionMap + }) + this.ob11WebSocketReverseManager = new OB11WebSocketReverseManager(ctx, { + hosts: config.wsHosts, + heartInterval: config.heartInterval, + token: config.token, + actionMap + }) + } + + /** 缓存近期消息内容 */ + public async addMsgCache(msg: RawMessage) { + const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000 + if (expire === 0) { + return + } + const id = msg.msgId + this.messages.set(id, msg) + setTimeout(() => { + this.messages.delete(id) + }, expire) + } + + /** 获取近期消息内容 */ + public getMsgCache(msgId: string) { + return this.messages.get(msgId) + } + + public dispatch(event: OB11BaseEvent | OB11Message) { + if (this.config.enableWs) { + this.ob11WebSocket.emitEvent(event) + } + if (this.config.enableWsReverse) { + this.ob11WebSocketReverseManager.emitEvent(event) + } + if (this.config.enableHttpPost) { + this.ob11HttpPost.emitEvent(event) + } + if ((event as OB11BaseMetaEvent).meta_event_type !== 'heartbeat') { + // 不上报心跳 + postHttpEvent(event) + } + } + + private async handleGroupNotify(notifies: GroupNotify[]) { + for (const notify of notifies) { + try { + notify.time = Date.now() + const notifyTime = parseInt(notify.seq) / 1000 + const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type + if (notifyTime < this.startTime) { + continue + } + if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { + this.ctx.logger.info('有成员退出通知', notify) + const member1Uin = (await this.ctx.ntUserApi.getUinByUid(notify.user1.uid))! + let operatorId = member1Uin + let subType: GroupDecreaseSubType = 'leave' + if (notify.user2.uid) { + // 是被踢的 + const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid) + if (member2Uin) { + operatorId = member2Uin + } + subType = 'kick' + } + const groupDecreaseEvent = new OB11GroupDecreaseEvent( + parseInt(notify.group.groupCode), + parseInt(member1Uin), + parseInt(operatorId), + subType, + ) + this.dispatch(groupDecreaseEvent) + } + else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { + this.ctx.logger.info('有加群请求') + let requestQQ = '' + try { + // 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(requestQQ) || 0, + flag, + notify.postscript, + invitorId! === undefined ? undefined : +invitorId, + 'add' + ) + this.dispatch(groupRequestEvent) + } + else if (notify.type == GroupNotifyTypes.INVITE_ME) { + this.ctx.logger.info('收到邀请我加群通知') + const userId = (await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) || '' + const groupInviteEvent = new OB11GroupRequestEvent( + parseInt(notify.group.groupCode), + parseInt(userId), + flag, + undefined, + undefined, + 'invite' + ) + this.dispatch(groupInviteEvent) + } + } catch (e: any) { + this.ctx.logger.error('解析群通知失败', e.stack.toString()) + } + } + } + + private handleMsg(msgList: RawMessage[]) { + for (let message of msgList) { + // 过滤启动之前的消息 + if (parseInt(message.msgTime) < this.startTime / 1000) { + continue + } + const peer: Peer = { + chatType: message.chatType, + peerUid: message.peerUid + } + message.msgShortId = MessageUnique.createMsg(peer, message.msgId) + this.addMsgCache(message) + + OB11Constructor.message(this.ctx, message) + .then((msg) => { + if (!this.config.debug && msg.message.length === 0) { + return + } + const isSelfMsg = msg.user_id.toString() === selfInfo.uin + if (isSelfMsg && !this.config.reportSelfMessage) { + return + } + if (isSelfMsg) { + msg.target_id = parseInt(message.peerUin) + } + this.dispatch(msg) + }) + .catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString())) + + OB11Constructor.GroupEvent(this.ctx, message).then((groupEvent) => { + if (groupEvent) { + this.dispatch(groupEvent) + } + }) + + OB11Constructor.PrivateEvent(this.ctx, message).then((privateEvent) => { + if (privateEvent) { + this.dispatch(privateEvent) + } + }) + } + } + + private handleRecallMsg(msgList: RawMessage[]) { + for (const message of msgList) { + if (message.recallTime != '0') { + const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId) + if (!oriMessageId) { + continue + } + OB11Constructor.RecallEvent(this.ctx, message, oriMessageId).then((recallEvent) => { + if (recallEvent) { + this.dispatch(recallEvent) + } + }) + } + } + } + + private async handleFriendRequest(buddyReqs: FriendRequest[]) { + for (const req of buddyReqs) { + if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) { + continue + } + if (+req.reqTime < this.startTime / 1000) { + continue + } + let userId = 0 + try { + const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid) + userId = parseInt(requesterUin) + } catch (e) { + this.ctx.logger.error('获取加好友者QQ号失败', e) + } + const flag = req.friendUid + '|' + req.reqTime + const comment = req.extWords + const friendRequestEvent = new OB11FriendRequestEvent( + userId, + comment, + flag + ) + this.dispatch(friendRequestEvent) + } + } + + private async handleConfigUpdated(config: LLOBConfig) { + const old = this.config + this.ob11Http.updateConfig({ + port: config.ob11.httpPort, + token: config.token, + }) + this.ob11HttpPost.updateConfig({ + hosts: config.ob11.httpHosts, + heartInterval: config.heartInterval, + secret: config.ob11.httpSecret, + enableHttpHeart: config.ob11.enableHttpHeart + }) + this.ob11WebSocket.updateConfig({ + port: config.ob11.wsPort, + heartInterval: config.heartInterval, + token: config.token, + }) + this.ob11WebSocketReverseManager.updateConfig({ + hosts: config.ob11.wsHosts, + heartInterval: config.heartInterval, + token: config.token, + }) + // 判断是否启用或关闭 HTTP 服务 + if (config.ob11.enableHttp !== old.enableHttp) { + if (!config.ob11.enableHttp) { + await this.ob11Http.stop() + } else { + this.ob11Http.start() + } + } + // HTTP 端口变化,重启服务 + if (config.ob11.httpPort !== old.httpPort) { + await this.ob11Http.stop() + this.ob11Http.start() + } + // 判断是否启用或关闭正向 WebSocket + if (config.ob11.enableWs !== old.enableWs) { + if (config.ob11.enableWs) { + this.ob11WebSocket.start() + } else { + await this.ob11WebSocket.stop() + } + } + // 正向 WebSocket 端口变化,重启服务 + if (config.ob11.wsPort !== old.wsPort) { + await this.ob11WebSocket.stop() + this.ob11WebSocket.start() + llonebotError.wsServerError = '' + } + // 判断是否启用或关闭反向ws + if (config.ob11.enableWsReverse !== old.enableWsReverse) { + if (config.ob11.enableWsReverse) { + this.ob11WebSocketReverseManager.start() + } else { + this.ob11WebSocketReverseManager.stop() + } + } + // 判断反向 WebSocket 地址有变化 + if (config.ob11.enableWsReverse) { + if (config.ob11.wsHosts.length !== old.wsHosts.length) { + this.ob11WebSocketReverseManager.stop() + this.ob11WebSocketReverseManager.start() + } else { + for (const newHost of config.ob11.wsHosts) { + if (!old.wsHosts.includes(newHost)) { + this.ob11WebSocketReverseManager.stop() + this.ob11WebSocketReverseManager.start() + break + } + } + } + } + if (config.ob11.enableHttpHeart !== old.enableHttpHeart) { + this.ob11HttpPost.stop() + this.ob11HttpPost.start() + } + Object.assign(this.config, { + ...config.ob11, + heartInterval: config.heartInterval, + token: config.token!, + debug: config.debug!, + reportSelfMessage: config.reportSelfMessage!, + msgCacheExpire: config.msgCacheExpire!, + }) + } + + private async handleGroupMemberInfoUpdated(groupCode: string, members: GroupMember[]) { + for (const member of members) { + const existMember = await this.ctx.ntGroupApi.getGroupMember(groupCode, member.uin) + if (existMember) { + if (member.cardName != existMember.cardName) { + this.ctx.logger.info('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName) + this.dispatch( + new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName), + ) + } else if (member.role != existMember.role) { + this.ctx.logger.info('有管理员变动通知') + const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent( + member.role == GroupMemberRole.admin ? 'set' : 'unset', + parseInt(groupCode), + parseInt(member.uin) + ) + this.dispatch(groupAdminNoticeEvent) + } + Object.assign(existMember, member) + } + } + } + + public start() { + this.startTime = Date.now() + if (this.config.enableWs) { + this.ob11WebSocket.start() + } + if (this.config.enableWsReverse) { + this.ob11WebSocketReverseManager.start() + } + if (this.config.enableHttp) { + this.ob11Http.start() + } + if (this.config.enableHttpPost) { + this.ob11HttpPost.start() + } + this.ctx.on('llonebot/config-updated', input => { + this.handleConfigUpdated(input) + }) + this.ctx.on('nt/message-created', input => { + this.handleMsg(input) + }) + this.ctx.on('nt/message-deleted', input => { + this.handleRecallMsg(input) + }) + this.ctx.on('nt/message-sent', input => { + this.handleRecallMsg(input) + }) + this.ctx.on('nt/group-notify', input => { + this.handleGroupNotify(input) + }) + this.ctx.on('nt/friend-request', input => { + this.handleFriendRequest(input) + }) + this.ctx.on('nt/group-member-info-updated', input => { + this.handleGroupMemberInfoUpdated(input.groupCode, input.members) + }) + } +} + +namespace OneBot11Adapter { + export interface Config extends OB11Config { + heartInterval: number + token: string + debug: boolean + reportSelfMessage: boolean + msgCacheExpire: number + } +} + +export default OneBot11Adapter \ No newline at end of file diff --git a/src/onebot11/connect/http.ts b/src/onebot11/connect/http.ts new file mode 100644 index 0000000..55bdb20 --- /dev/null +++ b/src/onebot11/connect/http.ts @@ -0,0 +1,211 @@ +import BaseAction from '../action/BaseAction' +import http from 'node:http' +import cors from 'cors' +import crypto from 'node:crypto' +import express, { Express, Request, Response } from 'express' +import { Context } from 'cordis' +import { llonebotError, selfInfo } from '@/common/globalVars' +import { OB11Response } from '../action/OB11Response' +import { OB11Message } from '../types' +import { OB11BaseEvent } from '../event/OB11BaseEvent' +import { handleQuickOperation, QuickOperationEvent } from '../helper/quick-operation' +import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' + +type RegisterHandler = (res: Response, payload: any) => Promise + +class OB11Http { + private readonly expressAPP: Express + private server?: http.Server + + constructor(protected ctx: Context, public config: OB11Http.Config) { + this.expressAPP = express() + // 添加 CORS 中间件 + this.expressAPP.use(cors()) + this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' })) + this.expressAPP.use((req, res, next) => { + // 兼容处理没有带content-type的请求 + // log("req.headers['content-type']", req.headers['content-type']) + req.headers['content-type'] = 'application/json' + const originalJson = express.json({ limit: '5000mb' }) + // 调用原始的express.json()处理器 + originalJson(req, res, (err) => { + if (err) { + ctx.logger.error('Error parsing JSON:', err) + return res.status(400).send('Invalid JSON') + } + next() + }) + }) + setTimeout(() => { + for (const [actionName, action] of config.actionMap) { + this.registerRouter('post', actionName, (res, payload) => action.handle(payload)) + this.registerRouter('get', actionName, (res, payload) => action.handle(payload)) + } + }, 0) + } + + public start() { + if (this.server) return + try { + this.expressAPP.get('/', (req: Request, res: Response) => { + res.send(`LLOneBot server 已启动`) + }) + this.server = this.expressAPP.listen(this.config.port, '0.0.0.0', () => { + this.ctx.logger.info(`HTTP server started 0.0.0.0:${this.config.port}`) + }) + llonebotError.httpServerError = '' + } catch (e: any) { + this.ctx.logger.error('HTTP服务启动失败', e.toString()) + llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString() + } + } + + public stop() { + return new Promise((resolve) => { + llonebotError.httpServerError = '' + if (this.server) { + this.server.close((err) => { + if (err) { + return resolve(false) + } + resolve(true) + }) + this.server = undefined + } else { + resolve(true) + } + }) + } + + public updateConfig(config: Partial) { + Object.assign(this.config, config) + } + + private authorize(req: Request, res: Response, next: () => void) { + let serverToken = this.config.token + let clientToken = '' + const authHeader = req.get('authorization') + if (authHeader) { + clientToken = authHeader.split('Bearer ').pop()! + this.ctx.logger.info('receive http header token', clientToken) + } else if (req.query.access_token) { + if (Array.isArray(req.query.access_token)) { + clientToken = req.query.access_token[0].toString() + } else { + clientToken = req.query.access_token.toString() + } + this.ctx.logger.info('receive http url token', clientToken) + } + + if (serverToken && clientToken != serverToken) { + return res.status(403).send(JSON.stringify({ message: 'token verify failed!' })) + } + next() + } + + private registerRouter(method: 'post' | 'get', url: string, handler: RegisterHandler) { + if (!url.startsWith('/')) { + url = '/' + url + } + + if (!this.expressAPP[method]) { + const err = `LLOneBot server register router failed,${method} not exist` + this.ctx.logger.error(err) + throw err + } + this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => { + let payload = req.body + if (method == 'get') { + payload = req.query + } else if (req.query) { + payload = { ...req.query, ...req.body } + } + this.ctx.logger.info('收到 HTTP 请求', url, payload) + try { + res.send(await handler(res, payload)) + } catch (e: any) { + res.send(OB11Response.error(e.stack.toString(), 200)) + } + }) + } +} + +namespace OB11Http { + export interface Config { + port: number + token?: string + actionMap: Map> + } +} + +class OB11HttpPost { + private disposeInterval?: () => void + + constructor(protected ctx: Context, public config: OB11HttpPost.Config) { + } + + public start() { + if (this.config.enableHttpHeart && !this.disposeInterval) { + this.disposeInterval = this.ctx.setInterval(() => { + // ws的心跳是ws自己维护的 + this.emitEvent(new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval)) + }, this.config.heartInterval) + } + } + + public stop() { + this.disposeInterval?.() + } + + public async emitEvent(event: OB11BaseEvent | OB11Message) { + const msgStr = JSON.stringify(event) + const headers = { + 'Content-Type': 'application/json', + 'x-self-id': selfInfo.uin, + } + if (this.config.secret) { + const hmac = crypto.createHmac('sha1', this.config.secret) + hmac.update(msgStr) + const sig = hmac.digest('hex') + headers['x-signature'] = 'sha1=' + sig + } + for (const host of this.config.hosts) { + fetch(host, { + method: 'POST', + headers, + body: msgStr, + }).then( + async (res) => { + if (event.post_type) { + this.ctx.logger.info(`HTTP 事件上报: ${host}`, event.post_type, res.status) + } + try { + const resJson = await res.json() + this.ctx.logger.info(`HTTP 事件上报后返回快速操作:`, JSON.stringify(resJson)) + handleQuickOperation(this.ctx, event as QuickOperationEvent, resJson).catch(e => this.ctx.logger.error(e)) + } catch (e) { + //log(`新消息事件HTTP上报没有返回快速操作,不需要处理`) + } + }, + (err: any) => { + this.ctx.logger.error(`HTTP 事件上报失败: ${host}`, err, event) + }, + ).catch(e => this.ctx.logger.error(e)) + } + } + + public updateConfig(config: Partial) { + Object.assign(this.config, config) + } +} + +namespace OB11HttpPost { + export interface Config { + hosts: string[] + secret?: string + enableHttpHeart?: boolean + heartInterval: number + } +} + +export { OB11Http, OB11HttpPost } \ No newline at end of file diff --git a/src/onebot11/connect/ws.ts b/src/onebot11/connect/ws.ts new file mode 100644 index 0000000..fa2adf4 --- /dev/null +++ b/src/onebot11/connect/ws.ts @@ -0,0 +1,329 @@ +import BaseAction from '../action/BaseAction' +import { Context } from 'cordis' +import { WebSocket, WebSocketServer } from 'ws' +import { llonebotError } from '@/common/globalVars' +import { IncomingMessage } from 'node:http' +import { OB11Return, OB11Message } from '../types' +import { OB11Response } from '../action/OB11Response' +import { ActionName } from '../action/types' +import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent' +import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' +import { selfInfo } from '@/common/globalVars' +import { OB11BaseEvent } from '../event/OB11BaseEvent' +import { version } from '../../version' + +class OB11WebSocket { + private wsServer?: WebSocketServer + private wsClients: WebSocket[] = [] + + constructor(protected ctx: Context, public config: OB11WebSocket.Config) { + } + + public start() { + if (this.wsServer) return + this.ctx.logger.info(`WebSocket server started 0.0.0.0:${this.config.port}`) + try { + this.wsServer = new WebSocketServer({ port: this.config.port, maxPayload: 1024 * 1024 * 1024 }) + llonebotError.wsServerError = '' + } catch (e: any) { + llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString() + return + } + this.wsServer?.on('connection', (socket, req) => { + this.authorize(socket, req) + this.connect(socket) + }) + } + + public stop() { + return new Promise((resolve) => { + llonebotError.wsServerError = '' + if (this.wsServer) { + this.wsServer.close((err) => { + if (err) { + return resolve(false) + } + resolve(true) + }) + this.wsServer = undefined + } else { + resolve(true) + } + }) + } + + public async emitEvent(event: OB11BaseEvent | OB11Message) { + this.wsClients.forEach(socket => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(event)) + this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', event.post_type) + } + }) + } + + public updateConfig(config: Partial) { + Object.assign(this.config, config) + } + + private reply(socket: WebSocket, data: OB11Return | OB11BaseEvent | OB11Message) { + if (socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(JSON.stringify(data)) + if (data['post_type']) { + this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data['post_type']) + } + } + + private authorize(socket: WebSocket, req: IncomingMessage) { + const url = req.url?.split('?').shift() + this.ctx.logger.info('ws connect', url) + let clientToken = '' + const authHeader = req.headers['authorization'] + if (authHeader) { + clientToken = authHeader.split('Bearer ').pop()! + this.ctx.logger.info('receive ws header token', clientToken) + } else { + const { searchParams } = new URL(`http://localhost${req.url}`) + const urlToken = searchParams.get('access_token') + if (urlToken) { + if (Array.isArray(urlToken)) { + clientToken = urlToken[0] + } else { + clientToken = urlToken + } + this.ctx.logger.info('receive ws url token', clientToken) + } + } + if (this.config.token && clientToken !== this.config.token) { + this.reply(socket, OB11Response.res(null, 'failed', 1403, 'token验证失败')) + return socket.close() + } + } + + private async handleAction(socket: WebSocket, msg: string) { + let receive: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } + try { + receive = JSON.parse(msg.toString()) + this.ctx.logger.info('收到正向 Websocket 消息', receive) + } catch (e) { + return this.reply(socket, OB11Response.error('json解析失败,请检查数据格式', 1400)) + } + const action: BaseAction = this.config.actionMap.get(receive.action!)! + if (!action) { + return this.reply(socket, OB11Response.error('不支持的api ' + receive.action, 1404, receive.echo)) + } + try { + const handleResult = await action.websocketHandle(receive.params, receive.echo) + handleResult.echo = receive.echo + this.reply(socket, handleResult) + } catch (e: any) { + this.reply(socket, OB11Response.error(`api处理出错:${e.stack}`, 1200, receive.echo)) + } + } + + private connect(socket: WebSocket) { + try { + this.reply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) + } catch (e) { + this.ctx.logger.error('发送生命周期失败', e) + } + + socket.on('error', err => this.ctx.logger.error(err.message)) + + socket.on('message', msg => { + this.handleAction(socket, msg.toString()) + }) + + socket.on('ping', () => { + socket.pong() + }) + + 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 连接断开') + }) + + this.wsClients.push(socket) + } +} + +namespace OB11WebSocket { + export interface Config { + port: number + heartInterval: number + token?: string + actionMap: Map> + } +} + +class OB11WebSocketReverse { + private running: boolean = false + private wsClient?: WebSocket + + constructor(protected ctx: Context, public config: OB11WebSocketReverse.Config) { + } + + public start() { + if (!this.running) { + this.running = true + this.tryConnect() + } + } + + public stop() { + this.running = false + this.wsClient?.close() + } + + public emitEvent(event: OB11BaseEvent | OB11Message) { + if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) { + this.wsClient.send(JSON.stringify(event)) + this.ctx.logger.info('WebSocket 事件上报', this.wsClient.url ?? '', event.post_type) + } + } + + private reply(socket: WebSocket, data: OB11Return | OB11BaseEvent | OB11Message) { + if (socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(JSON.stringify(data)) + if (data['post_type']) { + this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', data['post_type']) + } + } + + private async handleAction(msg: string) { + let receive: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } + try { + receive = JSON.parse(msg.toString()) + this.ctx.logger.info('收到反向Websocket消息', receive) + } catch (e) { + return this.reply(this.wsClient!, OB11Response.error('json解析失败,请检查数据格式', 1400, receive.echo)) + } + const action: BaseAction = this.config.actionMap.get(receive.action!)! + if (!action) { + return this.reply(this.wsClient!, OB11Response.error('不支持的api ' + receive.action, 1404, receive.echo)) + } + try { + let handleResult = await action.websocketHandle(receive.params, receive.echo) + this.reply(this.wsClient!, handleResult) + } catch (e) { + this.reply(this.wsClient!, OB11Response.error(`api处理出错:${e}`, 1200, receive.echo)) + } + } + + private tryConnect() { + if (this.wsClient && !this.running) { + return + } + this.wsClient = new WebSocket(this.config.url, { + maxPayload: 1024 * 1024 * 1024, + handshakeTimeout: 2000, + perMessageDeflate: false, + headers: { + 'X-Self-ID': selfInfo.uin, + 'Authorization': `Bearer ${this.config.token}`, + 'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段 + 'User-Agent': `LLOneBot/${version}`, + }, + }) + this.ctx.logger.info('Trying to connect to the websocket server: ' + this.config.url) + + this.wsClient.on('open', () => { + this.ctx.logger.info('Connected to the websocket server: ' + this.config.url) + try { + this.reply(this.wsClient!, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) + } catch (e) { + this.ctx.logger.error('发送生命周期失败', e) + } + }) + + this.wsClient.on('error', err => this.ctx.logger.error(err)) + + this.wsClient.on('message', data => { + this.handleAction(data.toString()) + }) + + this.wsClient.on('ping', () => { + this.wsClient?.pong() + }) + + const disposeHeartBeat = this.ctx.setInterval(() => { + if (this.wsClient) { + this.reply(this.wsClient, new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval)) + } + }, this.config.heartInterval) + + this.wsClient.on('close', () => { + disposeHeartBeat() + this.ctx.logger.info('The websocket connection: ' + this.config.url + ' closed, trying reconnecting...') + if (this.running) { + this.ctx.setTimeout(() => this.tryConnect(), 3000) + } + }) + } +} + +namespace OB11WebSocketReverse { + export interface Config { + url: string + heartInterval: number + token?: string + actionMap: Map> + } +} + +class OB11WebSocketReverseManager { + private list: OB11WebSocketReverse[] = [] + + constructor(protected ctx: Context, public config: OB11WebSocketReverseManager.Config) { + } + + public async start() { + for (const url of this.config.hosts) { + this.ctx.logger.info('开始连接反向 WebSocket', url) + try { + this.list.push(new OB11WebSocketReverse(this.ctx, { ...this.config, url })) + } catch (e: any) { + this.ctx.logger.error(e.stack) + } + } + } + + public stop() { + for (const ws of this.list) { + try { + ws.stop() + } catch (e: any) { + this.ctx.logger.error('反向 WebSocket 关闭:', e.stack) + } + } + this.list.length = 0 + } + + public async emitEvent(event: OB11BaseEvent | OB11Message) { + for (const ws of this.list) { + ws.emitEvent(event) + } + } + + public updateConfig(config: Partial) { + Object.assign(this.config, config) + } +} + +namespace OB11WebSocketReverseManager { + export interface Config { + hosts: string[] + heartInterval: number + token?: string + actionMap: Map> + } +} + +export { OB11WebSocket, OB11WebSocketReverseManager } \ No newline at end of file diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 4909588..c3e4d82 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -25,7 +25,6 @@ import { FriendV2, ChatType2 } from '../ntqqapi/types' -import { getGroupMember, getSelfUin } from '../common/data' import { EventType } from './event/OB11BaseEvent' import { encodeCQCode } from './cqcode' import { MessageUnique } from '../common/utils/MessageUnique' @@ -34,13 +33,11 @@ import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { calcQQLevel } from '../common/utils/qqlevel' -import { log } from '../common/utils/log' import { isNull, sleep } from '../common/utils/helper' import { getConfigUtil } from '../common/config' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' -import { NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQMsgApi } from '../ntqqapi/api' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { mFaceCache } from '../ntqqapi/constructor' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' @@ -50,16 +47,17 @@ import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11Poke import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { omit } from 'cosmokit' +import { Context } from 'cordis' +import { selfInfo } from '@/common/globalVars' -export class OB11Constructor { - static async message(msg: RawMessage): Promise { +export namespace OB11Constructor { + export async function message(ctx: Context, msg: RawMessage): Promise { let config = getConfigUtil().getConfig() const { - enableLocalFile2Url, debug, ob11: { messagePostFormat }, } = config - const selfUin = getSelfUin() + const selfUin = selfInfo.uin const resMsg: OB11Message = { self_id: parseInt(selfUin), user_id: parseInt(msg.senderUin!), @@ -86,19 +84,19 @@ export class OB11Constructor { if (msg.chatType == ChatType.group) { resMsg.sub_type = 'normal' resMsg.group_id = parseInt(msg.peerUin) - const member = await getGroupMember(msg.peerUin, msg.senderUin!) + const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin!) if (member) { - resMsg.sender.role = OB11Constructor.groupMemberRole(member.role) + resMsg.sender.role = groupMemberRole(member.role) resMsg.sender.nickname = member.nick } } else if (msg.chatType == ChatType.friend) { resMsg.sub_type = 'friend' - resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick + resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick } else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) { resMsg.sub_type = 'group' - const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid) + const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid) if (ret?.result === 0) { resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode) resMsg.sender.nickname = ret.tmpChatInfo!.fromNick @@ -123,7 +121,7 @@ export class OB11Constructor { const { atNtUid, content } = element.textElement let atQQ = element.textElement.atUid if (!atQQ || atQQ === '0') { - const atMember = await getGroupMember(msg.peerUin, atNtUid) + const atMember = await ctx.ntGroupApi.getGroupMember(msg.peerUin, atNtUid) if (atMember) { atQQ = atMember.uin } @@ -154,7 +152,7 @@ export class OB11Constructor { try { const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords) if (!records) throw new Error('找不到回复消息') - let replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({ + let replyMsg = (await ctx.ntMsgApi.getMsgsBySeqAndCount({ peerUid: msg.peerUid, guildId: '', chatType: msg.chatType, @@ -165,7 +163,7 @@ export class OB11Constructor { peerUid: msg.peerUid, guildId: '', } - replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0] + replyMsg = (await ctx.ntMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0] } // 284840486: 合并消息内侧 消息具体定位不到 if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { @@ -177,7 +175,7 @@ export class OB11Constructor { chatType: msg.chatType, }, replyMsg.msgId)?.toString() } catch (e: any) { - log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) + ctx.logger.error('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) continue } } @@ -192,7 +190,7 @@ export class OB11Constructor { message_data['data']['file'] = picElement.fileName message_data['data']['subType'] = picElement.picSubType //message_data['data']['file_id'] = picElement.fileUuid - message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement) + message_data['data']['url'] = await ctx.ntFileApi.getImageUrl(picElement) message_data['data']['file_size'] = picElement.fileSize MessageUnique.addFileCache({ peerUid: msg.peerUid, @@ -213,7 +211,7 @@ export class OB11Constructor { 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 NTQQFileApi.getVideoUrl({ + message_data['data']['url'] = await ctx.ntFileApi.getVideoUrl({ chatType: msg.chatType, peerUid: msg.peerUid, }, msg.msgId, element.elementId) @@ -323,7 +321,7 @@ export class OB11Constructor { return resMsg } - static async PrivateEvent(msg: RawMessage): Promise { + export async function PrivateEvent(ctx: Context, msg: RawMessage): Promise { if (msg.chatType !== ChatType.friend) { return } @@ -339,8 +337,8 @@ export class OB11Constructor { const poke_uid = pokedetail.filter(item => item.uid) if (poke_uid.length == 2) { return new OB11FriendPokeEvent( - parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)), - parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)), + parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid)), + parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid)), pokedetail ) } @@ -356,12 +354,12 @@ export class OB11Constructor { } } - static async GroupEvent(msg: RawMessage): Promise { + export async function GroupEvent(ctx: Context, msg: RawMessage): Promise { if (msg.chatType !== ChatType.group) { return } if (msg.senderUin) { - let member = await getGroupMember(msg.peerUid, msg.senderUin) + let member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin) if (member && member.cardName !== msg.sendMemberName) { const event = new OB11GroupCardEvent( parseInt(msg.peerUid), @@ -380,15 +378,15 @@ export class OB11Constructor { if (groupElement) { // log("收到群提示消息", groupElement) if (groupElement.type === TipGroupElementType.memberIncrease) { - log('收到群成员增加消息', groupElement) + ctx.logger.info('收到群成员增加消息', groupElement) await sleep(1000) - const member = await getGroupMember(msg.peerUid, groupElement.memberUid) + const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid) let memberUin = member?.uin if (!memberUin) { - memberUin = (await NTQQUserApi.getUserDetailInfo(groupElement.memberUid)).uin + memberUin = (await ctx.ntUserApi.getUserDetailInfo(groupElement.memberUid)).uin } // log("获取新群成员QQ", memberUin) - const adminMember = await getGroupMember(msg.peerUid, groupElement.adminUid) + const adminMember = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid) // log("获取同意新成员入群的管理员", adminMember) if (memberUin) { const operatorUin = adminMember?.uin || memberUin @@ -398,7 +396,7 @@ export class OB11Constructor { } } else if (groupElement.type === TipGroupElementType.ban) { - log('收到群群员禁言提示', groupElement) + ctx.logger.info('收到群群员禁言提示', groupElement) const memberUid = groupElement.shutUp?.member.uid const adminUid = groupElement.shutUp?.admin.uid let memberUin: string = '' @@ -406,8 +404,8 @@ export class OB11Constructor { let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban' if (memberUid) { memberUin = - (await getGroupMember(msg.peerUid, memberUid))?.uin || - (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin + (await ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUid))?.uin || + (await ctx.ntUserApi.getUserDetailInfo(memberUid))?.uin } else { memberUin = '0' // 0表示全员禁言 @@ -416,7 +414,7 @@ export class OB11Constructor { } } const adminUin = - (await getGroupMember(msg.peerUid, adminUid!))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid!))?.uin + (await ctx.ntGroupApi.getGroupMember(msg.peerUid, adminUid!))?.uin || (await ctx.ntUserApi.getUserDetailInfo(adminUid!))?.uin if (memberUin && adminUin) { return new OB11GroupBanEvent( parseInt(msg.peerUid), @@ -428,14 +426,14 @@ export class OB11Constructor { } } else if (groupElement.type === TipGroupElementType.kicked) { - log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) - NTQQGroupApi.quitGroup(msg.peerUid).then() + ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) + ctx.ntGroupApi.quitGroup(msg.peerUid) try { - const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUidByUin(groupElement.adminUid)) + const adminUin = (await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await ctx.ntUserApi.getUidByUin(groupElement.adminUid)) if (adminUin) { return new OB11GroupDecreaseEvent( parseInt(msg.peerUid), - parseInt(getSelfUin()), + parseInt(selfInfo.uin), parseInt(adminUin), 'kick_me' ) @@ -443,7 +441,7 @@ export class OB11Constructor { } catch (e) { return new OB11GroupDecreaseEvent( parseInt(msg.peerUid), - parseInt(getSelfUin()), + parseInt(selfInfo.uin), 0, 'leave' ) @@ -475,12 +473,12 @@ export class OB11Constructor { ignoreAttributes: false, attributeNamePrefix: '', }).parse(xmlElement.content) - log('收到表情回应我的消息', emojiLikeData) + ctx.logger.info('收到表情回应我的消息', emojiLikeData) try { const senderUin = emojiLikeData.gtip.qq.jp const msgSeq = emojiLikeData.gtip.url.msgseq const emojiId = emojiLikeData.gtip.face.id - const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({ + const replyMsgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount({ chatType: ChatType.group, guildId: '', peerUid: msg.peerUid, @@ -502,7 +500,7 @@ export class OB11Constructor { likes ) } catch (e: any) { - log('解析表情回应消息失败', e.stack) + ctx.logger.error('解析表情回应消息失败', e.stack) } } @@ -510,7 +508,7 @@ export class OB11Constructor { grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER && xmlElement?.templId == '10179' ) { - log('收到新人被邀请进群消息', grayTipElement) + ctx.logger.info('收到新人被邀请进群消息', grayTipElement) if (xmlElement?.content) { const regex = /jp="(\d+)"/g @@ -551,7 +549,7 @@ export class OB11Constructor { { txt: '头衔', type: 'nor' } ] } - + * */ if (grayTipElement.jsonGrayTipElement.busiId == 1061) { //判断业务类型 @@ -562,14 +560,14 @@ export class OB11Constructor { if (poke_uid.length == 2) { return new OB11GroupPokeEvent( parseInt(msg.peerUid), - parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)), - parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)), + parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid)), + parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid)), pokedetail ) } } if (grayTipElement.jsonGrayTipElement.busiId == 2401) { - log('收到群精华消息', json) + ctx.logger.info('收到群精华消息', json) const searchParams = new URL(json.items[0].jp).searchParams const msgSeq = searchParams.get('msgSeq')! const Group = searchParams.get('groupCode') @@ -578,7 +576,7 @@ export class OB11Constructor { chatType: ChatType.group, peerUid: Group! } - const msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList + const msgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList if (!msgList?.length) { return } @@ -598,8 +596,8 @@ export class OB11Constructor { if (grayTipElement.jsonGrayTipElement.busiId == 2407) { const memberUin = json.items[1].param[0] const title = json.items[3].txt - log('收到群成员新头衔消息', json) - getGroupMember(msg.peerUid, memberUin).then(member => { + ctx.logger.info('收到群成员新头衔消息', json) + ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUin).then(member => { if (!isNull(member)) { member.memberSpecialTitle = title } @@ -611,7 +609,8 @@ export class OB11Constructor { } } - static async RecallEvent( + export async function RecallEvent( + ctx: Context, msg: RawMessage, shortId: number ): Promise { @@ -623,7 +622,7 @@ export class OB11Constructor { } const revokeElement = msgElement.grayTipElement.revokeElement if (msg.chatType === ChatType.group) { - const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid) + const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement.operatorUid) return new OB11GroupRecallNoticeEvent( parseInt(msg.peerUid), parseInt(msg.senderUin!), @@ -636,7 +635,7 @@ export class OB11Constructor { } } - static friend(friend: User): OB11User { + export function friend(friend: User): OB11User { return { user_id: parseInt(friend.uin), nickname: friend.nick, @@ -646,21 +645,14 @@ export class OB11Constructor { } } - static selfInfo(selfInfo: SelfInfo): OB11User { - return { - user_id: parseInt(selfInfo.uin), - nickname: selfInfo.nick, - } - } - - static friends(friends: User[]): OB11User[] { + export function friends(friends: User[]): OB11User[] { return friends.map(OB11Constructor.friend) } - static friendsV2(friends: FriendV2[]): OB11User[] { + export function friendsV2(friends: FriendV2[]): OB11User[] { const data: OB11User[] = [] for (const friend of friends) { - const sexValue = this.sex(friend.baseInfo.sex!) + const sexValue = sex(friend.baseInfo.sex!) data.push({ ...omit(friend.baseInfo, ['richBuffer']), ...friend.coreInfo, @@ -676,7 +668,7 @@ export class OB11Constructor { return data } - static groupMemberRole(role: number): OB11GroupMemberRole | undefined { + export function groupMemberRole(role: number): OB11GroupMemberRole | undefined { return { 4: OB11GroupMemberRole.owner, 3: OB11GroupMemberRole.admin, @@ -684,7 +676,7 @@ export class OB11Constructor { }[role] } - static sex(sex: Sex): OB11UserSex { + export function sex(sex: Sex): OB11UserSex { const sexMap = { [Sex.male]: OB11UserSex.male, [Sex.female]: OB11UserSex.female, @@ -693,7 +685,7 @@ export class OB11Constructor { return sexMap[sex] || OB11UserSex.unknown } - static groupMember(group_id: string, member: GroupMember): OB11GroupMember { + export function groupMember(group_id: string, member: GroupMember): OB11GroupMember { return { group_id: parseInt(group_id), user_id: parseInt(member.uin), @@ -716,7 +708,7 @@ export class OB11Constructor { } } - static stranger(user: User): OB11User { + export function stranger(user: User): OB11User { return { ...user, user_id: parseInt(user.uin), @@ -729,12 +721,11 @@ export class OB11Constructor { } } - static groupMembers(group: Group): OB11GroupMember[] { - log('construct ob11 group members', group) + export function groupMembers(group: Group): OB11GroupMember[] { return group.members.map((m) => OB11Constructor.groupMember(group.groupCode, m)) } - static group(group: Group): OB11Group { + export function group(group: Group): OB11Group { return { group_id: parseInt(group.groupCode), group_name: group.groupName, @@ -743,7 +734,7 @@ export class OB11Constructor { } } - static groups(groups: Group[]): OB11Group[] { + export function groups(groups: Group[]): OB11Group[] { return groups.map(OB11Constructor.group) } } diff --git a/src/onebot11/event/OB11BaseEvent.ts b/src/onebot11/event/OB11BaseEvent.ts index 88d3338..f9a5388 100644 --- a/src/onebot11/event/OB11BaseEvent.ts +++ b/src/onebot11/event/OB11BaseEvent.ts @@ -1,4 +1,4 @@ -import { getSelfUin } from '../../common/data' +import { selfInfo } from "@/common/globalVars" export enum EventType { META = 'meta_event', @@ -10,6 +10,6 @@ export enum EventType { export abstract class OB11BaseEvent { time = Math.floor(Date.now() / 1000) - self_id = parseInt(getSelfUin()) + self_id = parseInt(selfInfo.uin) abstract post_type: EventType } diff --git a/src/onebot11/server/event-for-http.ts b/src/onebot11/helper/event-for-http.ts similarity index 89% rename from src/onebot11/server/event-for-http.ts rename to src/onebot11/helper/event-for-http.ts index d310a9a..dc00f2e 100644 --- a/src/onebot11/server/event-for-http.ts +++ b/src/onebot11/helper/event-for-http.ts @@ -1,4 +1,7 @@ -import { PostEventType } from './post-ob11-event' +import { OB11Message } from '../types' +import { OB11BaseEvent } from '../event/OB11BaseEvent' + +type PostEventType = OB11Message | OB11BaseEvent interface HttpEventType { seq: number @@ -19,7 +22,7 @@ export function postHttpEvent(event: PostEventType) { eventList.push({ seq: curentSeq, event: event - }); + }) while (eventList.length > 100) { eventList.shift() } @@ -29,7 +32,7 @@ export async function getHttpEvent(userKey: string, timeout = 0) { const toRetEvent: PostEventType[] = [] // 清除过时的user,5分钟没访问过的user将被删除 - const now = Date.now(); + const now = Date.now() for (let key in httpUser) { let user = httpUser[key] if (now - user.lastAccessTime > 1000 * 60 * 5) { diff --git a/src/onebot11/action/quick-operation.ts b/src/onebot11/helper/quick-operation.ts similarity index 60% rename from src/onebot11/action/quick-operation.ts rename to src/onebot11/helper/quick-operation.ts index 0000441..23dda5c 100644 --- a/src/onebot11/action/quick-operation.ts +++ b/src/onebot11/helper/quick-operation.ts @@ -1,15 +1,12 @@ -// handle quick action, create at 2024-5-18 10:54:39 by linyuchen - - import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' -import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api' import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' -import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' -import { isNull, log } from '@/common/utils' +import { convertMessage2List, createSendElements, sendMsg } from '../action/msg/SendMsg' import { getConfigUtil } from '@/common/config' import { MessageUnique } from '@/common/utils/MessageUnique' +import { isNullable } from 'cosmokit' +import { Context } from 'cordis' interface QuickOperationPrivateMessage { @@ -44,23 +41,23 @@ export type QuickOperation = QuickOperationPrivateMessage & export type QuickOperationEvent = OB11Message | OB11FriendRequestEvent | OB11GroupRequestEvent; -export async function handleQuickOperation(context: QuickOperationEvent, quickAction: QuickOperation) { - if (context.post_type === 'message') { - handleMsg(context as OB11Message, quickAction).then().catch(log) +export async function handleQuickOperation(ctx: Context, event: QuickOperationEvent, quickAction: QuickOperation) { + if (event.post_type === 'message') { + handleMsg(ctx, event as OB11Message, quickAction).then().catch(e => ctx.logger.error(e)) } - if (context.post_type === 'request') { - const friendRequest = context as OB11FriendRequestEvent - const groupRequest = context as OB11GroupRequestEvent + if (event.post_type === 'request') { + const friendRequest = event as OB11FriendRequestEvent + const groupRequest = event as OB11GroupRequestEvent if ((friendRequest).request_type === 'friend') { - handleFriendRequest(friendRequest, quickAction).then().catch(log) + handleFriendRequest(ctx, friendRequest, quickAction).then().catch(e => ctx.logger.error(e)) } else if (groupRequest.request_type === 'group') { - handleGroupRequest(groupRequest, quickAction).then().catch(log) + handleGroupRequest(ctx, groupRequest, quickAction).then().catch(e => ctx.logger.error(e)) } } } -async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { +async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { const reply = quickAction.reply const ob11Config = getConfigUtil().getConfig().ob11 const peer: Peer = { @@ -68,7 +65,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes peerUid: msg.user_id.toString(), } if (msg.message_type == 'private') { - peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))! + peer.peerUid = (await ctx.ntUserApi.getUidByUin(msg.user_id.toString()))! if (msg.sub_type === 'group') { peer.chatType = ChatType.temp } @@ -99,8 +96,8 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes } } replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) - const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer) - sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) + const { sendElements, deleteAfterSentFiles } = await createSendElements(ctx, replyMessage, peer) + sendMsg(ctx, peer, sendElements, deleteAfterSentFiles, false).catch(e => ctx.logger.error(e)) } if (msg.message_type === 'group') { const groupMsgQuickAction = quickAction as QuickOperationGroupMessage @@ -108,40 +105,38 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes if (!rawMessage) return // handle group msg if (groupMsgQuickAction.delete) { - NTQQMsgApi.recallMsg(peer, [rawMessage.MsgId]).then().catch(log) + ctx.ntMsgApi.recallMsg(peer, [rawMessage.MsgId]).catch(e => ctx.logger.error(e)) } if (groupMsgQuickAction.kick) { - const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) - NTQQGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).then().catch(log) + const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) + ctx.ntGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).catch(e => ctx.logger.error(e)) } if (groupMsgQuickAction.ban) { - const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) - NTQQGroupApi.banMember(peer.peerUid, [ + const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) + ctx.ntGroupApi.banMember(peer.peerUid, [ { uid: msgList[0].senderUid, timeStamp: groupMsgQuickAction.ban_duration || 60 * 30, }, - ]).then().catch(log) + ]).catch(e => ctx.logger.error(e)) } } } -async function handleFriendRequest(request: OB11FriendRequestEvent, - quickAction: QuickOperationFriendRequest) { - if (!isNull(quickAction.approve)) { +async function handleFriendRequest(ctx: Context, request: OB11FriendRequestEvent, quickAction: QuickOperationFriendRequest) { + if (!isNullable(quickAction.approve)) { // todo: set remark - NTQQFriendApi.handleFriendRequest(request.flag, quickAction.approve).then().catch(log) + ctx.ntFriendApi.handleFriendRequest(request.flag, quickAction.approve).catch(e => ctx.logger.error(e)) } } -async function handleGroupRequest(request: OB11GroupRequestEvent, - quickAction: QuickOperationGroupRequest) { - if (!isNull(quickAction.approve)) { - NTQQGroupApi.handleGroupRequest( +async function handleGroupRequest(ctx: Context, request: OB11GroupRequestEvent, quickAction: QuickOperationGroupRequest) { + if (!isNullable(quickAction.approve)) { + ctx.ntGroupApi.handleGroupRequest( request.flag, quickAction.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, quickAction.reason, - ).then().catch(log) + ).catch(e => ctx.logger.error(e)) } } \ No newline at end of file diff --git a/src/onebot11/server/http.ts b/src/onebot11/server/http.ts deleted file mode 100644 index 5c1db8d..0000000 --- a/src/onebot11/server/http.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Response } from 'express' -import { OB11Response } from '../action/OB11Response' -import { HttpServerBase } from '@/common/server/http' -import { actionMap } from '../action' -import { getConfigUtil } from '@/common/config' -import { postOb11Event } from './post-ob11-event' -import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' -import { getSelfInfo } from '@/common/data' - -class OB11HTTPServer extends HttpServerBase { - name = 'LLOneBot server' - - handleFailed(res: Response, payload: any, e: any) { - res.send(OB11Response.error(e.stack.toString(), 200)) - } - - protected listen(port: number) { - if (getConfigUtil().getConfig().ob11.enableHttp) { - super.listen(port) - } - } -} - -export const ob11HTTPServer = new OB11HTTPServer() - -setTimeout(() => { - for (const [actionName, action] of actionMap) { - for (const method of ['post', 'get']) { - ob11HTTPServer.registerRouter(method, actionName, (res, payload) => action.handle(payload)) - } - } -}, 0) - -class HTTPHeart { - intervalId: NodeJS.Timeout | null = null - start() { - const { heartInterval } = getConfigUtil().getConfig() - if (this.intervalId) { - clearInterval(this.intervalId) - } - this.intervalId = setInterval(() => { - // ws的心跳是ws自己维护的 - postOb11Event(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!), false, false) - }, heartInterval) - } - - stop() { - if (this.intervalId) { - clearInterval(this.intervalId) - } - } -} - -export const httpHeart = new HTTPHeart() diff --git a/src/onebot11/server/post-ob11-event.ts b/src/onebot11/server/post-ob11-event.ts deleted file mode 100644 index c7f8257..0000000 --- a/src/onebot11/server/post-ob11-event.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { OB11Message } from '../types' -import { getSelfUin } from '@/common/data' -import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent' -import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent' -import { WebSocket as WebSocketClass } from 'ws' -import { wsReply } from './ws/reply' -import { log } from '@/common/utils' -import { getConfigUtil } from '@/common/config' -import crypto from 'crypto' -import { handleQuickOperation, QuickOperationEvent } from '../action/quick-operation' -import { postHttpEvent } from './event-for-http' - -export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent - -const eventWSList: WebSocketClass[] = [] - -export function registerWsEventSender(ws: WebSocketClass) { - eventWSList.push(ws) -} - -export function unregisterWsEventSender(ws: WebSocketClass) { - let index = eventWSList.indexOf(ws) - if (index !== -1) { - eventWSList.splice(index, 1) - } -} - -export function postWsEvent(event: PostEventType) { - for (const ws of eventWSList) { - new Promise((resolve) => { - wsReply(ws, event) - resolve(undefined) - }).then() - } -} - -export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = true) { - const config = getConfigUtil().getConfig() - const selfUin = getSelfUin() - // 判断msg是否是event - if (!config.reportSelfMessage && !reportSelf) { - if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfUin) { - return - } - } - if (config.ob11.enableHttpPost) { - const msgStr = JSON.stringify(msg) - const hmac = crypto.createHmac('sha1', config.ob11.httpSecret!) - hmac.update(msgStr) - const sig = hmac.digest('hex') - let headers = { - 'Content-Type': 'application/json', - 'x-self-id': selfUin, - } - if (config.ob11.httpSecret) { - headers['x-signature'] = 'sha1=' + sig - } - for (const host of config.ob11.httpHosts) { - fetch(host, { - method: 'POST', - headers, - body: msgStr, - }).then( - async (res) => { - if (msg.post_type) { - log(`HTTP 事件上报: ${host} `, msg.post_type) - } - try { - const resJson = await res.json() - log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson)) - handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log); - } catch (e) { - //log(`新消息事件HTTP上报没有返回快速操作,不需要处理`) - return - } - }, - (err: any) => { - log(`新消息事件HTTP上报失败: ${host} `, err, msg) - }, - ).catch(log) - } - } - if (postWs) { - postWsEvent(msg) - } - if (!(msg.post_type == 'meta_event' && (msg as OB11BaseMetaEvent).meta_event_type == 'heartbeat')) { - // 不上报心跳 - postHttpEvent(msg) - } -} diff --git a/src/onebot11/server/ws/ReverseWebsocket.ts b/src/onebot11/server/ws/ReverseWebsocket.ts deleted file mode 100644 index 8272bf9..0000000 --- a/src/onebot11/server/ws/ReverseWebsocket.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { getSelfInfo } from '../../../common/data' -import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' -import { ActionName } from '../../action/types' -import { OB11Response } from '../../action/OB11Response' -import BaseAction from '../../action/BaseAction' -import { actionMap } from '../../action' -import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event' -import { wsReply } from './reply' -import { WebSocket as WebSocketClass } from 'ws' -import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' -import { log } from '../../../common/utils/log' -import { getConfigUtil } from '../../../common/config' -import { version } from '../../../version' - -export let rwsList: ReverseWebsocket[] = [] - -export class ReverseWebsocket { - public websocket?: WebSocketClass - public url: string - private running: boolean = false - - public constructor(url: string) { - this.url = url - this.running = true - this.connect() - } - - public stop() { - this.running = false - this.websocket?.close() - } - - public onopen() { - wsReply(this.websocket!, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) - } - - public async onmessage(msg: string) { - let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } - let echo = null - try { - receiveData = JSON.parse(msg.toString()) - echo = receiveData.echo - log('收到反向Websocket消息', receiveData) - } catch (e) { - return wsReply(this.websocket!, OB11Response.error('json解析失败,请检查数据格式', 1400, echo)) - } - const action: BaseAction = actionMap.get(receiveData.action!)! - if (!action) { - return wsReply(this.websocket!, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo)) - } - try { - let handleResult = await action.websocketHandle(receiveData.params, echo) - wsReply(this.websocket!, handleResult) - } catch (e) { - wsReply(this.websocket!, OB11Response.error(`api处理出错:${e}`, 1200, echo)) - } - } - - public onclose = () => { - log('反向ws断开', this.url) - unregisterWsEventSender(this.websocket!) - if (this.running) { - this.reconnect() - } - } - - public send(msg: string) { - if (this.websocket && this.websocket.readyState == WebSocket.OPEN) { - this.websocket.send(msg) - } - } - - private reconnect() { - setTimeout(() => { - this.connect() - }, 3000) // TODO: 重连间隔在配置文件中实现 - } - - private connect() { - const { token, heartInterval } = getConfigUtil().getConfig() - const selfInfo = getSelfInfo() - this.websocket = new WebSocketClass(this.url, { - maxPayload: 1024 * 1024 * 1024, - handshakeTimeout: 2000, - perMessageDeflate: false, - headers: { - 'X-Self-ID': selfInfo.uin, - Authorization: `Bearer ${token}`, - 'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段 - 'User-Agent': `LLOneBot/${version}`, - }, - }) - registerWsEventSender(this.websocket) - log('Trying to connect to the websocket server: ' + this.url) - - this.websocket.on('open', () => { - log('Connected to the websocket server: ' + this.url) - this.onopen() - }) - - this.websocket.on('message', async (data) => { - await this.onmessage(data.toString()) - }) - - this.websocket.on('error', log) - - this.websocket.on('ping',()=>{ - this.websocket?.pong() - }) - - const wsClientInterval = setInterval(() => { - postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) - }, heartInterval) // 心跳包 - this.websocket.on('close', () => { - clearInterval(wsClientInterval) - log('The websocket connection: ' + this.url + ' closed, trying reconnecting...') - this.onclose() - }) - } -} - -class OB11ReverseWebsockets { - start() { - for (const url of getConfigUtil().getConfig().ob11.wsHosts) { - log('开始连接反向ws', url) - new Promise(() => { - try { - rwsList.push(new ReverseWebsocket(url)) - } catch (e: any) { - log(e.stack) - } - }).then() - } - } - - stop() { - for (let rws of rwsList) { - try { - rws.stop() - } catch (e: any) { - log('反向ws关闭:', e.stack) - } - } - rwsList.length = 0 - } - - restart() { - this.stop() - this.start() - } -} - -export const ob11ReverseWebsockets = new OB11ReverseWebsockets() diff --git a/src/onebot11/server/ws/WebsocketServer.ts b/src/onebot11/server/ws/WebsocketServer.ts deleted file mode 100644 index ee6560f..0000000 --- a/src/onebot11/server/ws/WebsocketServer.ts +++ /dev/null @@ -1,134 +0,0 @@ -import BaseAction from '../../action/BaseAction' -import { WebSocket, WebSocketServer } from 'ws' -import { actionMap } from '../../action' -import { OB11Response } from '../../action/OB11Response' -import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event' -import { ActionName } from '../../action/types' -import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' -import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' -import { IncomingMessage } from 'node:http' -import { wsReply } from './reply' -import { getSelfInfo } from '@/common/data' -import { log } from '@/common/utils/log' -import { getConfigUtil } from '@/common/config' -import { llonebotError } from '@/common/data' - -export class OB11WebsocketServer { - private ws?: WebSocketServer - - constructor() { - log(`llonebot websocket service started`) - } - - start(port: number) { - try { - this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 }) - llonebotError.wsServerError = '' - } catch (e: any) { - llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString() - return - } - this.ws?.on('connection', (socket, req) => { - const url = req.url?.split('?').shift() - this.authorize(socket, req) - this.onConnect(socket, url!) - }) - } - - stop() { - llonebotError.wsServerError = '' - this.ws?.close(err => { - log('ws server close failed!', err) - }) - this.ws = undefined - } - - restart(port: number) { - this.stop() - this.start(port) - } - - private authorize(socket: WebSocket, req: IncomingMessage) { - const { token } = getConfigUtil().getConfig() - const url = req.url?.split('?').shift() - log('ws connect', url) - let clientToken = '' - const authHeader = req.headers['authorization'] - if (authHeader) { - clientToken = authHeader.split('Bearer ').pop()! - log('receive ws header token', clientToken) - } else { - const { searchParams } = new URL(`http://localhost${req.url}`) - const urlToken = searchParams.get('access_token') - if (urlToken) { - if (Array.isArray(urlToken)) { - clientToken = urlToken[0] - } else { - clientToken = urlToken - } - log('receive ws url token', clientToken) - } - } - if (token && clientToken !== token) { - this.authorizeFailed(socket) - return socket.close() - } - } - - private authorizeFailed(socket: WebSocket) { - socket.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败'))) - } - - private async handleAction(socket: WebSocket, actionName: string, params: any, echo?: any) { - const action: BaseAction = actionMap.get(actionName)! - if (!action) { - return wsReply(socket, OB11Response.error('不支持的api ' + actionName, 1404, echo)) - } - try { - const handleResult = await action.websocketHandle(params, echo) - handleResult.echo = echo - wsReply(socket, handleResult) - } catch (e: any) { - wsReply(socket, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) - } - } - - private onConnect(socket: WebSocket, url: string) { - if (['/api', '/api/', '/'].includes(url)) { - socket.on('message', async (msg) => { - let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } - let echo: any - try { - receiveData = JSON.parse(msg.toString()) - echo = receiveData.echo - log('收到正向Websocket消息', receiveData) - } catch (e) { - return wsReply(socket, OB11Response.error('json解析失败,请检查数据格式', 1400, echo)) - } - this.handleAction(socket, receiveData.action!, receiveData.params, receiveData.echo) - }) - } - if (['/event', '/event/', '/'].includes(url)) { - registerWsEventSender(socket) - - log('event上报ws客户端已连接') - - try { - wsReply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) - } catch (e) { - log('发送生命周期失败', e) - } - const { heartInterval } = getConfigUtil().getConfig() - const intervalId = setInterval(() => { - postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!)) - }, heartInterval) // 心跳包 - socket.on('close', () => { - log('event上报ws客户端已断开') - clearInterval(intervalId) - unregisterWsEventSender(socket) - }) - } - } -} - -export const ob11WebsocketServer = new OB11WebsocketServer() diff --git a/src/onebot11/server/ws/reply.ts b/src/onebot11/server/ws/reply.ts deleted file mode 100644 index 2be847b..0000000 --- a/src/onebot11/server/ws/reply.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { WebSocket as WebSocketClass } from 'ws' -import { PostEventType } from '../post-ob11-event' -import { log } from '@/common/utils/log' -import { OB11Return } from '../../types' - -export function wsReply(wsClient: WebSocketClass, data: OB11Return | PostEventType) { - try { - wsClient.send(JSON.stringify(data)) - if (data['post_type']) { - log('WebSocket 事件上报', wsClient.url ?? '', data['post_type']) - } - } catch (e: any) { - log('WebSocket 上报失败', e.stack, data) - } -} diff --git a/src/preload.ts b/src/preload.ts index 86235d0..9fda898 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,6 +1,4 @@ -// Electron 主进程 与 渲染进程 交互的桥梁 - -import { CheckVersion, Config, LLOneBotError } from './common/types' +import { CheckVersion, Config } from './common/types' import { CHANNEL_ERROR, CHANNEL_GET_CONFIG, @@ -9,6 +7,7 @@ import { CHANNEL_SELECT_FILE, CHANNEL_SET_CONFIG, CHANNEL_UPDATE, + CHANNEL_SET_CONFIG_CONFIRMED, } from './common/channels' const { contextBridge } = require('electron') @@ -24,8 +23,9 @@ const llonebot = { updateLLOneBot: async (): Promise => { return ipcRenderer.invoke(CHANNEL_UPDATE) }, - setConfig: (ask: boolean, config: Config) => { - ipcRenderer.send(CHANNEL_SET_CONFIG, ask, config) + setConfig: async (ask: boolean, config: Config) => { + const isSuccess = await ipcRenderer.invoke(CHANNEL_SET_CONFIG, ask, config) + if (isSuccess) ipcRenderer.send(CHANNEL_SET_CONFIG_CONFIRMED, config) }, getConfig: async (): Promise => { return ipcRenderer.invoke(CHANNEL_GET_CONFIG) diff --git a/src/renderer/index.ts b/src/renderer/index.ts index fa88e6a..530ddb8 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -2,36 +2,14 @@ import { CheckVersion } from '../common/types' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components' // @ts-ignore import StyleRaw from './style.css?raw' -import { iconSvg } from './icon' import { version } from '../version' -// 打开设置界面时触发 - -function aprilFoolsEgg(node: Element) { - let today = new Date() - if (today.getDate() === 1) { - console.log('超时空猫猫!!!') - node.querySelector('.name')!.innerHTML = 'ChronoCat' - } -} - -function initSideBar() { - document.querySelectorAll('.nav-item.liteloader').forEach((node) => { - if (node.textContent?.startsWith('LLOneBot')) { - aprilFoolsEgg(node) - let iconEle = node.querySelector('.q-icon') - iconEle!.innerHTML = iconSvg - } - }) -} - function isEmpty(value: unknown) { return value === undefined || value === null || value === '' } async function onSettingWindowCreated(view: Element) { - window.llonebot.log('setting window created') - initSideBar() + //window.llonebot.log('setting window created') let config = await window.llonebot.getConfig() let ob11Config = { ...config.ob11 }