This commit is contained in:
idranme
2024-08-28 04:48:07 +08:00
parent bc3c8b1259
commit 5501980ab3
95 changed files with 2115 additions and 2069 deletions

View File

@@ -3,7 +3,7 @@
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息
TG 群:<https://t.me/+nLZEnpne-pQ1OWFl> TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>

View File

@@ -32,7 +32,7 @@
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.25", "@types/fluent-ffmpeg": "^2.1.26",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^31.4.0", "electron": "^31.4.0",

View File

@@ -1,5 +1,6 @@
export const CHANNEL_GET_CONFIG = 'llonebot_get_config' export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
export const CHANNEL_SET_CONFIG = 'llonebot_set_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_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error' export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update' export const CHANNEL_UPDATE = 'llonebot_update'

View File

@@ -2,8 +2,7 @@ import fs from 'node:fs'
import { Config, OB11Config } from './types' import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper' import { mergeNewProperties } from './utils/helper'
import path from 'node:path' import path from 'node:path'
import { getSelfUin } from './data' import { selfInfo, DATA_DIR } from './globalVars'
import { DATA_DIR } from './utils'
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string
@@ -94,6 +93,6 @@ export class ConfigUtil {
} }
export function getConfigUtil() { 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) return new ConfigUtil(configFilePath)
} }

View File

@@ -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<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString()
const memberUinOrUidStr = memberUinOrUid.toString()
let members = groupMembers.get(groupCodeStr)
if (!members) {
try {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表
groupMembers.set(groupCodeStr, members)
}
catch (e) {
return null
}
}
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else {
member = members!.get(memberUinOrUidStr)
}
return member
}
let member = getMember()
if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember()
}
return member
}
const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export async function getSelfNick(force = false): Promise<string> {
if ((!selfInfo.nick || force) && selfInfo.uid) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
selfInfo.nick = userInfo.nick
return userInfo.nick
}
}
return selfInfo.nick
}
export function getSelfInfo() {
return selfInfo
}
export function setSelfInfo(data: Partial<SelfInfo>) {
Object.assign(selfInfo, data)
}
export function getSelfUid() {
return selfInfo['uid']
}
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
}
const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
export function getMsgCache(msgId: string) {
return messages.get(msgId)
}

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

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

View File

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

View File

@@ -22,7 +22,7 @@ export interface Config {
enableLLOB: boolean enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64 enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean

View File

@@ -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, () => { })
}

View File

@@ -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 fsPromise from 'node:fs/promises'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' 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' import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables { interface SQLiteTables extends Tables {

View File

@@ -1,12 +1,11 @@
import path from 'node:path' import path from 'node:path'
import os from 'node:os' import os from 'node:os'
import { systemPlatform } from './system'
export const exePath = process.execPath export const exePath = process.execPath
function getPKGPath() { function getPKGPath() {
let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json') 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') p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json')
} }
return p return p

View File

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

View File

@@ -1,7 +1,7 @@
import fs from 'node:fs' import fs from 'node:fs'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { TEMP_DIR } from './index' import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'

View File

@@ -1,14 +1,9 @@
import path from 'node:path'
export * from './file' export * from './file'
export * from './helper' export * from './helper'
export * from './log' export * from './LegacyLog'
export * from './qqlevel' export * from './qqlevel'
export * from './QQBasicInfo' export * from './QQBasicInfo'
export * from './upgrade' 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 { getVideoInfo } from './video'
export { checkFfmpeg } from './video' export { checkFfmpeg } from './video'
export { encodeSilk } from './audio' export { encodeSilk } from './audio'

View File

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

View File

@@ -1,6 +1,5 @@
import https from 'node:https'; import https from 'node:https'
import http from 'node:http'; import http from 'node:http'
import { log } from '@/common/utils/log'
export class RequestUtil { export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET // 适用于获取服务器下发cookies时获取仅GET

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { version } from '../../version' import { version } from '../../version'
import * as path from 'node:path' import * as path from 'node:path'
import * as fs from 'node:fs' import * as fs from 'node:fs'
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.' import { copyFolder, httpDownload, log } from '.'
import compressing from 'compressing' import compressing from 'compressing'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/'] const downloadMirrorHosts = ['https://mirror.ghproxy.com/']
const checkVersionMirrorHosts = ['https://kkgithub.com'] const checkVersionMirrorHosts = ['https://kkgithub.com']
@@ -10,9 +11,9 @@ const checkVersionMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion() const latestVersionText = await getRemoteVersion()
const latestVersion = latestVersionText.split('.') const latestVersion = latestVersionText.split('.')
log('llonebot last version', latestVersion) //log('llonebot last version', latestVersion)
const currentVersion: string[] = version.split('.') const currentVersion: string[] = version.split('.')
log('llonebot current version', currentVersion) //log('llonebot current version', currentVersion)
for (let k of [0, 1, 2]) { for (let k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) { if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) {
log('') log('')
@@ -92,6 +93,6 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) {
// log("releasePage", releasePage); // log("releasePage", releasePage);
if (releasePage === 'error') return '' if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0] return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch {} } catch { }
return '' return ''
} }

View File

@@ -1,6 +1,6 @@
import { log } from './log'
import ffmpeg from 'fluent-ffmpeg' import ffmpeg from 'fluent-ffmpeg'
import fs from 'fs' import fs from 'node:fs'
import { log } from './LegacyLog'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
const defaultVideoThumbB64 = const defaultVideoThumbB64 =
@@ -43,43 +43,19 @@ export async function getVideoInfo(filePath: string) {
}) })
} }
export async function encodeMp4(filePath: string) { export function checkFfmpeg(newPath?: string): Promise<boolean> {
let videoInfo = await getVideoInfo(filePath)
log('视频信息', videoInfo)
if (videoInfo.format.indexOf('mp4') === -1) {
log('视频需要转换为MP4格式', filePath)
// 转成mp4
const newPath: string = await new Promise<string>((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<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
log('开始检查ffmpeg', newPath) log(`开始检查 FFmpeg ${newPath ?? ''}`)
if (newPath) { if (newPath) {
ffmpeg.setFfmpegPath(newPath) ffmpeg.setFfmpegPath(newPath)
} }
try { try {
ffmpeg.getAvailableFormats((err, formats) => { ffmpeg.getAvailableFormats((err, formats) => {
if (err) { 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) resolve(false)
} else { } else {
log('ffmpeg is installed.') log('FFmpeg is installed.')
resolve(true) resolve(true)
} }
}) })

View File

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

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

@@ -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)
}
}

View File

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

View File

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

View File

@@ -16,8 +16,7 @@ import {
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { ReceiveCmdS } from '../hook' 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 { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
@@ -26,10 +25,26 @@ import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit' 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 */ /** 27187 TODO */
static async getVideoUrl(peer: Peer, msgId: string, elementId: string) { async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession() const session = getSession()
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId, msgId,
@@ -38,14 +53,14 @@ export class NTQQFileApi {
{ downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url { downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url
} }
static async getFileType(filePath: string) { async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return fileTypeFromFile(filePath)
} }
// 上传文件到QQ的文件夹 // 上传文件到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) const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext || '' let ext = (await this.getFileType(filePath))?.ext || ''
if (ext) { if (ext) {
ext = '.' + ext ext = '.' + ext
} }
@@ -96,7 +111,7 @@ export class NTQQFileApi {
} }
} }
static async downloadMedia( async downloadMedia(
msgId: string, msgId: string,
chatType: ChatType, chatType: ChatType,
peerUid: string, peerUid: string,
@@ -192,7 +207,7 @@ export class NTQQFileApi {
return filePath return filePath
} }
static async getImageSize(filePath: string) { async getImageSize(filePath: string) {
return await invoke<{ width: number; height: number }>({ return await invoke<{ width: number; height: number }>({
className: NTClass.FS_API, className: NTClass.FS_API,
methodName: NTMethod.IMAGE_SIZE, methodName: NTMethod.IMAGE_SIZE,
@@ -200,7 +215,7 @@ export class NTQQFileApi {
}) })
} }
static async getImageUrl(element: PicElement) { async getImageUrl(element: PicElement) {
if (!element) { if (!element) {
return '' return ''
} }
@@ -217,7 +232,7 @@ export class NTQQFileApi {
if (UrlRkey) { if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url 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 UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}` return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
} else { } else {
@@ -228,13 +243,17 @@ export class NTQQFileApi {
// 没有url需要自己拼接 // 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', element) this.ctx.logger.error('图片url获取失败', element)
return '' return ''
} }
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi extends Service {
static async setCacheSilentScan(isSilent: boolean = true) { constructor(protected ctx: Context) {
super(ctx, 'ntFileCacheApi', true)
}
async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_SET_SILENCE, methodName: NTMethod.CACHE_SET_SILENCE,
args: [ args: [
@@ -246,7 +265,7 @@ export class NTQQFileCacheApi {
}) })
} }
static getCacheSessionPathList() { getCacheSessionPathList() {
return invoke< return invoke<
{ {
key: string key: string
@@ -258,7 +277,7 @@ export class NTQQFileCacheApi {
}) })
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return invoke<any>({ return invoke<any>({
// TODO: 目前还不知道真正的返回值是什么 // TODO: 目前还不知道真正的返回值是什么
methodName: NTMethod.CACHE_CLEAR, methodName: NTMethod.CACHE_CLEAR,
@@ -271,7 +290,7 @@ export class NTQQFileCacheApi {
}) })
} }
static addCacheScannedPaths(pathMap: object = {}) { addCacheScannedPaths(pathMap: object = {}) {
return invoke<GeneralCallResult>({ return invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_ADD_SCANNED_PATH, methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [ args: [
@@ -283,7 +302,7 @@ export class NTQQFileCacheApi {
}) })
} }
static scanCache() { scanCache() {
invoke<GeneralCallResult>({ invoke<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true, classNameIsRegister: true,
@@ -295,21 +314,21 @@ export class NTQQFileCacheApi {
}) })
} }
static getHotUpdateCachePath() { getHotUpdateCachePath() {
return invoke<string>({ return invoke<string>({
className: NTClass.HOTUPDATE_API, className: NTClass.HOTUPDATE_API,
methodName: NTMethod.CACHE_PATH_HOT_UPDATE, methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
}) })
} }
static getDesktopTmpPath() { getDesktopTmpPath() {
return invoke<string>({ return invoke<string>({
className: NTClass.BUSINESS_API, className: NTClass.BUSINESS_API,
methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP, 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<ChatCacheList>((res, rej) => { return new Promise<ChatCacheList>((res, rej) => {
invoke<ChatCacheList>({ invoke<ChatCacheList>({
methodName: NTMethod.CACHE_CHAT_GET, 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 } const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return invoke<CacheFileList>({ return invoke<CacheFileList>({
@@ -346,7 +365,7 @@ export class NTQQFileCacheApi {
}) })
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_CHAT_CLEAR, methodName: NTMethod.CACHE_CHAT_CLEAR,
args: [ args: [

View File

@@ -6,10 +6,21 @@ import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
import { pick } from 'cosmokit' 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 */ /** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { async getFriends(forced = false) {
const data = await invoke<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
@@ -30,7 +41,7 @@ export class NTQQFriendApi {
return _friends return _friends
} }
static async handleFriendRequest(flag: string, accept: boolean) { async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|') const data = flag.split('|')
if (data.length < 2) { if (data.length < 2) {
return return
@@ -60,7 +71,7 @@ export class NTQQFriendApi {
} }
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> { async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const session = getSession() const session = getSession()
if (session) { if (session) {
const uids: string[] = [] const uids: string[] = []
@@ -90,7 +101,7 @@ export class NTQQFriendApi {
} }
} }
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> { async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000) const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
const session = getSession() const session = getSession()
if (session) { if (session) {
@@ -122,7 +133,7 @@ export class NTQQFriendApi {
return retMap return retMap
} }
static async getBuddyV2ExWithCate(refresh = false) { async getBuddyV2ExWithCate(refresh = false) {
const session = getSession() const session = getSession()
if (session) { if (session) {
const uids: string[] = [] const uids: string[] = []
@@ -170,7 +181,7 @@ export class NTQQFriendApi {
} }
} }
static async isBuddy(uid: string): Promise<boolean> { async isBuddy(uid: string): Promise<boolean> {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getBuddyService().isBuddy(uid) return session.getBuddyService().isBuddy(uid)

View File

@@ -2,14 +2,28 @@ import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners' import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services' import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/helper'
export class NTQQGroupApi { declare module 'cordis' {
static async getGroups(forced = false): Promise<Group[]> { interface Context {
ntGroupApi: NTQQGroupApi
}
}
export class NTQQGroupApi extends Service {
private groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true)
}
async getGroups(forced = false): Promise<Group[]> {
if (NTEventDispatch.initialised) { if (NTEventDispatch.initialised) {
type ListenerType = NodeIKernelGroupListener['onGroupListUpdate'] type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
const [, , groupList] = await NTEventDispatch.CallNormalEvent const [, , groupList] = await NTEventDispatch.CallNormalEvent
@@ -37,7 +51,7 @@ export class NTQQGroupApi {
} }
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession() const session = getSession()
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>> let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
if (session) { if (session) {
@@ -73,16 +87,48 @@ export class NTQQGroupApi {
return result.result.infos return result.result.infos
} }
static async getGroupIgnoreNotifies() { async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
await NTQQGroupApi.getSingleScreenNotifies(14) const groupCodeStr = groupCode.toString()
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>( 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<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow, NTQQWindows.GroupNotifyFilterWindow,
[], [],
ReceiveCmdS.GROUP_NOTIFY, ReceiveCmdS.GROUP_NOTIFY,
) )
} }
static async getSingleScreenNotifies(num: number) { async getSingleScreenNotifies(num: number) {
if (NTEventDispatch.initialised) { if (NTEventDispatch.initialised) {
const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void> <(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
@@ -112,12 +158,12 @@ export class NTQQGroupApi {
} }
/** 27187 TODO */ /** 27187 TODO */
static async delGroupFile(groupCode: string, files: string[]) { async delGroupFile(groupCode: string, files: string[]) {
const session = getSession() const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files) 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 flagitem = flag.split('|')
const groupCode = flagitem[0] const groupCode = flagitem[0]
const seq = flagitem[1] const seq = flagitem[1]
@@ -157,7 +203,7 @@ export class NTQQGroupApi {
} }
} }
static async quitGroup(groupQQ: string) { async quitGroup(groupQQ: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().quitGroup(groupQQ) return session.getGroupService().quitGroup(groupQQ)
@@ -169,7 +215,7 @@ export class NTQQGroupApi {
} }
} }
static async kickMember( async kickMember(
groupQQ: string, groupQQ: string,
kickUids: string[], kickUids: string[],
refuseForever = false, 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为解除禁言 // timeStamp为秒数, 0为解除禁言
const session = getSession() const session = getSession()
if (session) { 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() const session = getSession()
if (session) { if (session) {
return session.getGroupService().setGroupShutUp(groupQQ, shutUp) 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() const session = getSession()
if (session) { if (session) {
return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName) 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() const session = getSession()
if (session) { if (session) {
return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role) 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() const session = getSession()
if (session) { if (session) {
return session.getGroupService().modifyGroupName(groupQQ, groupName, false) 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< return await invoke<
GeneralCallResult & { GeneralCallResult & {
atInfo: { atInfo: {
@@ -308,7 +354,7 @@ export class NTQQGroupApi {
} }
/** 27187 TODO */ /** 27187 TODO */
static async removeGroupEssence(GroupCode: string, msgId: string) { async removeGroupEssence(GroupCode: string, msgId: string) {
const session = getSession() const session = getSession()
// 代码没测过 // 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
@@ -323,7 +369,7 @@ export class NTQQGroupApi {
} }
/** 27187 TODO */ /** 27187 TODO */
static async addGroupEssence(GroupCode: string, msgId: string) { async addGroupEssence(GroupCode: string, msgId: string) {
const session = getSession() const session = getSession()
// 代码没测过 // 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom

View File

@@ -1,9 +1,16 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult, TmpChatInfoApi } from '../services' import { GeneralCallResult, TmpChatInfoApi } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { getSelfNick, getSelfUid } from '../../common/data'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' 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() { function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000) const timestamp = Math.floor(Date.now() / 1000)
@@ -15,8 +22,12 @@ function generateMsgId() {
return msgId return msgId
} }
export class NTQQMsgApi { export class NTQQMsgApi extends Service {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) { constructor(protected ctx: Context) {
super(ctx, 'ntMsgApi', true)
}
async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getMsgService().getTempChatInfo(chatType, peerUid) 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//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
@@ -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() const session = getSession()
if (session) { if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId) 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<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTMethod.ACTIVE_CHAT_PREVIEW, methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
} }
static async activateChatAndGetHistory(peer: Peer) { async activateChatAndGetHistory(peer: Peer) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTMethod.ACTIVE_CHAT_HISTORY, 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 (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed') if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession() 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() const session = getSession()
// 消息时间从旧到新 // 消息时间从旧到新
if (session) { 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() const session = getSession()
if (session) { if (session) {
return session.getMsgService().recallMsg({ 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() const msgId = generateMsgId()
peer.guildId = msgId peer.guildId = msgId
let msgList: RawMessage[] let msgList: RawMessage[]
@@ -218,7 +229,7 @@ export class NTQQMsgApi {
return retMsg! return retMsg!
} }
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], []) return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
@@ -239,12 +250,12 @@ export class NTQQMsgApi {
} }
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await getSelfNick() const senderShowName = await this.ctx.ntUserApi.getSelfNick(true)
const msgInfos = msgIds.map(id => { const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName } return { msgId: id, senderShowName }
}) })
const selfUid = getSelfUid() const selfUid = selfInfo.uid
let msgList: RawMessage[] let msgList: RawMessage[]
if (NTEventDispatch.initialised) { if (NTEventDispatch.initialised) {
const data = await NTEventDispatch.CallNormalEvent< const data = await NTEventDispatch.CallNormalEvent<
@@ -312,7 +323,7 @@ export class NTQQMsgApi {
throw new Error('转发消息超时') 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() const session = getSession()
if (session) { if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z) return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)
@@ -335,7 +346,7 @@ export class NTQQMsgApi {
} }
/** 27187 TODO */ /** 27187 TODO */
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) { async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession() const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer, chatInfo: peer,
@@ -350,7 +361,7 @@ export class NTQQMsgApi {
return ret return ret
} }
static async getSingleMsg(peer: Peer, seq: string) { async getSingleMsg(peer: Peer, seq: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return await session.getMsgService().getSingleMsg(peer, seq) return await session.getMsgService().getSingleMsg(peer, seq)

View File

@@ -1,18 +1,28 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { groupMembers, getSelfUin } from '@/common/data' import { getBuildVersion } from '@/common/utils'
import { CacheClassFuncAsync, getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services'
import { NodeIKernelProfileListener } from '../listeners' import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { NTQQFriendApi } from './friend'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
export class NTQQUserApi { declare module 'cordis' {
static async setQQAvatar(filePath: string) { interface Context {
ntUserApi: NTQQUserApi
}
}
export class NTQQUserApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntUserApi', true)
}
async setQQAvatar(filePath: string) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTMethod.SET_QQ_AVATAR, methodName: NTMethod.SET_QQ_AVATAR,
args: [ args: [
@@ -25,7 +35,7 @@ export class NTQQUserApi {
}) })
} }
static async fetchUserDetailInfo(uid: string) { async fetchUserDetailInfo(uid: string) {
let info: UserDetailInfoListenerArg let info: UserDetailInfoListenerArg
if (NTEventDispatch.initialised) { if (NTEventDispatch.initialised) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo'] type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
@@ -74,9 +84,9 @@ export class NTQQUserApi {
return ret return ret
} }
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return NTQQUserApi.fetchUserDetailInfo(uid) return this.fetchUserDetailInfo(uid)
} }
if (NTEventDispatch.initialised) { if (NTEventDispatch.initialised) {
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
@@ -111,30 +121,29 @@ export class NTQQUserApi {
} }
} }
static async getSkey(): Promise<string> { async getSkey(): Promise<string> {
const clientKeyData = await NTQQUserApi.forceFetchClientKey() const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) { if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败') 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 + '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey return (await RequestUtil.HttpsGetCookies(url))?.skey
} }
@CacheClassFuncAsync(1800 * 1000) async getCookies(domain: string) {
static async getCookies(domain: string) { const clientKeyData = await this.forceFetchClientKey()
const clientKeyData = await NTQQUserApi.forceFetchClientKey()
if (clientKeyData?.result !== 0) { if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败') 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 requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl) const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
return cookies return cookies
} }
static genBkn(sKey: string) { genBkn(sKey: string) {
sKey = sKey || '' sKey = sKey || ''
let hash = 5381 let hash = 5381
@@ -146,7 +155,7 @@ export class NTQQUserApi {
return (hash & 0x7fffffff).toString() return (hash & 0x7fffffff).toString()
} }
static async like(uid: string, count = 1) { async like(uid: string, count = 1) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getProfileLikeService().setBuddyProfileLike({ return session.getProfileLikeService().setBuddyProfileLike({
@@ -173,22 +182,12 @@ export class NTQQUserApi {
} }
} }
static async getUidByUinV1(Uin: string) { async getUidByUinV1(Uin: string) {
const session = getSession() const session = getSession()
// 通用转换开始尝试 // 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin) let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
//Uid 群友列表转
if (!uid) { if (!uid) {
for (let groupMembersList of groupMembers.values()) { let unveifyUid = (await this.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
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 特殊转换 方法三
if (unveifyUid.indexOf('*') == -1) { if (unveifyUid.indexOf('*') == -1) {
uid = unveifyUid uid = unveifyUid
} }
@@ -196,7 +195,7 @@ export class NTQQUserApi {
return uid return uid
} }
static async getUidByUinV2(uin: string) { async getUidByUinV2(uin: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin) let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin)
@@ -234,18 +233,18 @@ export class NTQQUserApi {
})).uidInfo.get(uin) })).uidInfo.get(uin)
if (uid) return uid 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 if (unveifyUid.indexOf('*') == -1) return unveifyUid
} }
static async getUidByUin(Uin: string) { async getUidByUin(Uin: string) {
if (getBuildVersion() >= 26702) { 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) { if (NTEventDispatch.initialised) {
return await NTEventDispatch.CallNoListenerEvent return await NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUinV2>>( <(Uin: string) => Promise<UserDetailInfoByUinV2>>(
@@ -264,7 +263,7 @@ export class NTQQUserApi {
} }
} }
static async getUserDetailInfoByUin(Uin: string) { async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>( <(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin', 'NodeIKernelProfileService/getUserDetailInfoByUin',
@@ -273,7 +272,7 @@ export class NTQQUserApi {
) )
} }
static async getUinByUidV1(Uid: string) { async getUinByUidV1(Uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>( <(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin', 'NodeIKernelUixConvertService/getUin',
@@ -282,12 +281,12 @@ export class NTQQUserApi {
) )
let uin = ret.uinInfo.get(Uid) let uin = ret.uinInfo.get(Uid)
if (!uin) { if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换 uin = (await this.getUserDetailInfo(Uid)).uin //从QQ Native 转换
} }
return uin return uin
} }
static async getUinByUidV2(uid: string) { async getUinByUidV2(uid: string) {
const session = getSession() const session = getSession()
if (session) { if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid) let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
@@ -326,19 +325,19 @@ export class NTQQUserApi {
})).uinInfo.get(uid) })).uinInfo.get(uid)
if (uin) return uin 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 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) { 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() const session = getSession()
if (session) { if (session) {
return await session.getTicketService().forceFetchClientKey('') 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
}
} }

View File

@@ -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 { RequestUtil } from '@/common/utils/request'
import { Service, Context } from 'cordis'
declare module 'cordis' {
interface Context {
ntWebApi: NTQQWebApi
}
}
export enum WebHonorType { export enum WebHonorType {
ALL = 'all', ALL = 'all',
@@ -120,9 +124,13 @@ export interface GroupEssenceMsgRet {
} }
} }
export class WebApi { export class NTQQWebApi extends Service {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> { constructor(protected ctx: Context) {
const { cookies: CookieValue, bkn: Bkn } = (await NTQQUserApi.getCookies('qun.qq.com')) super(ctx, 'ntWebApi', true)
}
async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> {
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' 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 let ret: GroupEssenceMsgRet
try { try {
@@ -137,9 +145,9 @@ export class WebApi {
return ret return ret
} }
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
const cookieObject = await 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 cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const retList: Promise<WebApiGroupMemberRet>[] = [] const retList: Promise<WebApiGroupMemberRet>[] = []
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -147,7 +155,7 @@ export class WebApi {
end: '40', end: '40',
sort: '1', sort: '1',
gc: GroupCode, gc: GroupCode,
bkn: WebApi.genBkn(cookieObject.skey) bkn: this.genBkn(cookieObject.skey)
}) })
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr }) const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
@@ -178,7 +186,7 @@ export class WebApi {
return memberData return memberData
} }
static genBkn(sKey: string) { genBkn(sKey: string) {
sKey = sKey || ''; sKey = sKey || '';
let hash = 5381; let hash = 5381;
@@ -191,8 +199,8 @@ export class WebApi {
} }
//实现未缓存 考虑2h缓存 //实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) { const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString(); let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = ''; let res = '';
let resJson; let resJson;
@@ -208,13 +216,13 @@ export class WebApi {
return resJson?.actorList; return resJson?.actorList;
} }
} catch (e) { } catch (e) {
log('获取当前群荣耀失败', url, e); this.ctx.logger.error('获取当前群荣耀失败', url, e);
} }
return undefined; return undefined;
} }
let HonorInfo: any = { group_id: groupCode }; let HonorInfo: any = { group_id: groupCode };
const cookieObject = await 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 cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
@@ -241,7 +249,7 @@ export class WebApi {
}); });
} }
} catch (e) { } catch (e) {
log(e); this.ctx.logger.error(e);
} }
} }
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
@@ -260,7 +268,7 @@ export class WebApi {
}); });
} }
} catch (e) { } catch (e) {
log(e); this.ctx.logger.error(e);
} }
} }
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
@@ -279,7 +287,7 @@ export class WebApi {
}); });
} }
} catch (e) { } catch (e) {
log('获取群聊炽焰失败', e); this.ctx.logger.error('获取群聊炽焰失败', e);
} }
} }
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
@@ -298,7 +306,7 @@ export class WebApi {
}); });
} }
} catch (e) { } catch (e) {
log('获取快乐源泉失败', e); this.ctx.logger.error('获取快乐源泉失败', e);
} }
} }
//冒尖小春笋好像已经被tx扬了 //冒尖小春笋好像已经被tx扬了

View File

@@ -2,30 +2,41 @@ import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { ReceiveCmd } from '../hook' import { ReceiveCmd } from '../hook'
import { BrowserWindow } from 'electron' import { BrowserWindow } from 'electron'
import { Service, Context } from 'cordis'
declare module 'cordis' {
interface Context {
ntWindowApi: NTQQWindowApi
}
}
export interface NTQQWindow { export interface NTQQWindow {
windowName: string windowName: string
windowUrlHash: string windowUrlHash: string
} }
export class NTQQWindows { export namespace NTQQWindows {
static GroupHomeWorkWindow: NTQQWindow = { export const GroupHomeWorkWindow: NTQQWindow = {
windowName: 'GroupHomeWorkWindow', windowName: 'GroupHomeWorkWindow',
windowUrlHash: '#/group-home-work', windowUrlHash: '#/group-home-work',
} }
static GroupNotifyFilterWindow: NTQQWindow = { export const GroupNotifyFilterWindow: NTQQWindow = {
windowName: 'GroupNotifyFilterWindow', windowName: 'GroupNotifyFilterWindow',
windowUrlHash: '#/group-notify-filter', windowUrlHash: '#/group-notify-filter',
} }
static GroupEssenceWindow: NTQQWindow = { export const GroupEssenceWindow: NTQQWindow = {
windowName: 'GroupEssenceWindow', windowName: 'GroupEssenceWindow',
windowUrlHash: '#/group-essence', windowUrlHash: '#/group-essence',
} }
} }
export class NTQQWindowApi { export class NTQQWindowApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntWindowApi', true)
}
// 打开窗口并获取对应的下发事件 // 打开窗口并获取对应的下发事件
static async openWindow<R = GeneralCallResult>( async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: any[], args: any[],
cbCmd: ReceiveCmd | undefined, cbCmd: ReceiveCmd | undefined,

View File

@@ -15,22 +15,21 @@ import {
} from './types' } from './types'
import { promises as fs } from 'node:fs' import { promises as fs } from 'node:fs'
import ffmpeg from 'fluent-ffmpeg' import ffmpeg from 'fluent-ffmpeg'
import { NTQQFileApi } from './api/file'
import { calculateFileMD5, isGIF } from '../common/utils/file' import { calculateFileMD5, isGIF } from '../common/utils/file'
import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils' import { isNull } from '../common/utils'
import faceConfig from './helper/face_config.json' import faceConfig from './helper/face_config.json'
import { Context } from 'cordis'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export class SendMsgElementConstructor { export namespace SendMsgElementConstructor {
static poke(groupCode: string, uin: string) { export function poke(groupCode: string, uin: string) {
return null return null
} }
static text(content: string): SendTextElement { export function text(content: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', 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 { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', 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 { return {
elementType: ElementType.REPLY, elementType: ElementType.REPLY,
elementId: '', elementId: '',
@@ -71,8 +70,8 @@ export class SendMsgElementConstructor {
} }
} }
static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> { export async function pic(ctx: Context, picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(picPath, ElementType.PIC, subType)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
@@ -80,7 +79,7 @@ export class SendMsgElementConstructor {
if (fileSize > 1024 * 1024 * 30) { if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const imageSize = await NTQQFileApi.getImageSize(picPath) const imageSize = await ctx.ntFileApi.getImageSize(picPath)
const picElement = { const picElement = {
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize.toString(), fileSize: fileSize.toString(),
@@ -96,7 +95,7 @@ export class SendMsgElementConstructor {
thumbFileSize: 0, thumbFileSize: 0,
summary, summary,
} }
log('图片信息', picElement) ctx.logger.info('图片信息', picElement)
return { return {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: '', elementId: '',
@@ -104,8 +103,8 @@ export class SendMsgElementConstructor {
} }
} }
static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> { export async function file(ctx: Context, filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) const { fileName: _fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常,大小为 0' throw '文件异常,大小为 0'
} }
@@ -122,16 +121,16 @@ export class SendMsgElementConstructor {
return element return element
} }
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
try { try {
await fs.stat(filePath) await fs.stat(filePath)
} catch (e) { } catch (e) {
throw `文件${filePath}异常,不存在` throw `文件${filePath}异常,不存在`
} }
log('复制视频到QQ目录', filePath) ctx.logger.info('复制视频到QQ目录', filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO) let { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.VIDEO)
log('复制视频到QQ目录完成', path) ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
@@ -153,19 +152,19 @@ export class SendMsgElementConstructor {
} }
try { try {
videoInfo = await getVideoInfo(path) videoInfo = await getVideoInfo(path)
log('视频信息', videoInfo) ctx.logger.info('视频信息', videoInfo)
} catch (e) { } catch (e) {
log('获取视频信息失败', e) ctx.logger.info('获取视频信息失败', e)
} }
const createThumb = new Promise<string>((resolve, reject) => { const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png` const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumbDir, thumbFileName) const thumbPath = pathLib.join(thumbDir, thumbFileName)
log('开始生成视频缩略图', filePath) ctx.logger.info('开始生成视频缩略图', filePath)
let completed = false let completed = false
function useDefaultThumb() { function useDefaultThumb() {
if (completed) return if (completed) return
log('获取视频封面失败,使用默认封面') ctx.logger.info('获取视频封面失败,使用默认封面')
fs.writeFile(thumbPath, defaultVideoThumb) fs.writeFile(thumbPath, defaultVideoThumb)
.then(() => { .then(() => {
resolve(thumbPath) resolve(thumbPath)
@@ -194,14 +193,14 @@ export class SendMsgElementConstructor {
size: videoInfo.width + 'x' + videoInfo.height, size: videoInfo.width + 'x' + videoInfo.height,
}) })
.on('end', () => { .on('end', () => {
log('生成视频缩略图', thumbPath) ctx.logger.info('生成视频缩略图', thumbPath)
completed = true completed = true
resolve(thumbPath) resolve(thumbPath)
}) })
}) })
let thumbPath = new Map() let thumbPath = new Map()
const _thumbPath = await createThumb const _thumbPath = await createThumb
log('生成视频缩略图', _thumbPath) ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await fs.stat(_thumbPath)).size const thumbSize = (await fs.stat(_thumbPath)).size
// log("生成缩略图", _thumbPath) // log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
@@ -232,17 +231,17 @@ export class SendMsgElementConstructor {
// sourceVideoCodecFormat: 2 // sourceVideoCodecFormat: 2
}, },
} }
log('videoElement', element) ctx.logger.info('videoElement', element)
return element return element
} }
static async ptt(pttPath: string): Promise<SendPttElement> { export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath) const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
if (!silkPath) { if (!silkPath) {
throw '语音转换失败, 请检查语音文件是否正常' throw '语音转换失败, 请检查语音文件是否正常'
} }
// log("生成语音", silkPath, duration); // 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) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
@@ -271,7 +270,7 @@ export class SendMsgElementConstructor {
} }
} }
static face(faceId: number): SendFaceElement { export function face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji 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 { return {
elementType: ElementType.MFACE, elementType: ElementType.MFACE,
marketFaceElement: { marketFaceElement: {
@@ -312,7 +311,7 @@ export class SendMsgElementConstructor {
} }
} }
static dice(resultId: number | null): SendFaceElement { export function dice(resultId: number | null): SendFaceElement {
// 实际测试并不能控制结果 // 实际测试并不能控制结果
// 随机1到6 // 随机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 if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return { return {
@@ -357,7 +356,7 @@ export class SendMsgElementConstructor {
} }
} }
static ark(data: string): SendArkElement { export function ark(data: string): SendArkElement {
return { return {
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: '', elementId: '',

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

@@ -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<string, SimpleInfo> }
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<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
if (!this.config.autoDeleteFile) {
return
}
for (const message of payload.msgList) {
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: 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<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
this.ctx.parallel('nt/group-member-info-updated', { groupCode, members })
})
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
this.ctx.parallel('nt/message-created', payload.msgList)
})
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
const list = payload.msgList.filter(v => {
if (recallMsgIds.includes(v.msgId)) {
return false
}
recallMsgIds.push(v.msgId)
return true
})
this.ctx.parallel('nt/message-deleted', list)
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
const { msgId, chatType, peerUid } = payload.msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
if (!this.config.reportSelfMessage) {
return
}
this.ctx.parallel('nt/message-sent', [payload.msgRecord])
})
const groupNotifyFlags: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
let notifies: GroupNotify[]
try {
notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
return
}
const list = notifies.filter(v => {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type
if (groupNotifyFlags.includes(flag)) {
return false
}
groupNotifyFlags.push(flag)
return true
})
this.ctx.parallel('nt/group-notify', list)
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
})
}
}
namespace Core {
export interface Config extends LLOBConfig {
}
}
export default Core

View File

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

View File

@@ -1,33 +1,11 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTClass, NTMethod } from './ntcall' 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 { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' 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<string, (apiReturn: any) => void> = {} export const hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export let ReceiveCmdS = { export const ReceiveCmdS = {
RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2', RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2',
UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate', UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate', UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate',
@@ -48,7 +26,7 @@ export let ReceiveCmdS = {
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate', SKEY_UPDATE: 'onSkeyUpdate',
} as const }
export type ReceiveCmd = string export type ReceiveCmd = string
@@ -222,180 +200,3 @@ export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex((h) => h.id === id) const index = receiveHooks.findIndex((h) => h.id === id)
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1)
} }
export async function startHook() {
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
} else if (member.role != existMember.role) {
log('有管理员变动通知')
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent(
member.role == GroupMemberRole.admin ? 'set' : 'unset',
parseInt(groupCode),
parseInt(member.uin)
)
postOb11Event(groupAdminNoticeEvent, true)
}
Object.assign(existMember, member)
}
}
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
// 好友列表变动
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
// log("onBuddyListChange", payload)
// let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = [];
if ((payload as any).userSimpleInfos) {
// friendListV2 = payload as any
friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
return {
...v.coreInfo,
}
})
}
else {
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
log('好友列表变动', friendList.length)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const { msgId, chatType, peerUid } = msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
setSelfInfo({
online: info.info.status !== 20
})
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) {
OB11Constructor.message(lastTempMsg!).then((r) => postOb11Event(r))
}
})
})
}
else {
NTQQMsgApi.activateChat(peer).then()
}
}
}
})
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else if (!(await 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 })
})
})
}

View File

@@ -1,6 +1,6 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log' import { log } from '../common/utils/LegacyLog'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { GeneralCallResult } from './services' import { GeneralCallResult } from './services'
@@ -149,7 +149,7 @@ export function invoke<ReturnType>(params: InvokeParams<ReturnType>) {
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
if (result?.result === 0 || result === undefined) { if (result?.result === 0 || result === undefined) {
log(`${params.methodName} callback`, result) //log(`${params.methodName} callback`, result)
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {

View File

@@ -1,11 +1,16 @@
import { ActionName, BaseCheckResult } from './types' import { ActionName, BaseCheckResult } from './types'
import { OB11Response } from './OB11Response' import { OB11Response } from './OB11Response'
import { OB11Return } from '../types' import { OB11Return } from '../types'
import { Context } from 'cordis'
import { log } from '../../common/utils/log' import type Adapter from '../adapter'
abstract class BaseAction<PayloadType, ReturnDataType> { abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName abstract actionName: ActionName
protected ctx: Context
constructor(protected adapter: Adapter) {
this.ctx = adapter.ctx
}
protected async check(payload: PayloadType): Promise<BaseCheckResult> { protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return { return {
@@ -22,7 +27,7 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData) return OB11Response.ok(resData)
} catch (e: any) { } catch (e: any) {
log('发生错误', e) this.ctx.logger.error('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200) return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
} }
} }
@@ -36,7 +41,7 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData, echo) return OB11Response.ok(resData, echo)
} catch (e: any) { } catch (e: any) {
log('发生错误', e) this.ctx.logger.error('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo) return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
} }
} }

View File

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

View File

@@ -1,10 +1,8 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { UUIDConverter } from '@/common/utils/helper' import { Peer, ElementType } from '@/ntqqapi/types'
import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
export interface GetFilePayload { export interface GetFilePayload {
@@ -30,7 +28,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
} }
if (fileCache?.length) { if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia( const downloadPath = await this.ctx.ntFileApi.downloadMedia(
fileCache[0].msgId, fileCache[0].msgId,
fileCache[0].chatType, fileCache[0].chatType,
fileCache[0].peerUid, fileCache[0].peerUid,
@@ -50,7 +48,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
guildId: '' guildId: ''
} }
if (fileCache[0].elementType === ElementType.PIC) { if (fileCache[0].elementType === ElementType.PIC) {
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId]) const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) { if (msgList.msgList.length === 0) {
throw new Error('msg not found') throw new Error('msg not found')
} }
@@ -59,9 +57,9 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
if (!findEle) { if (!findEle) {
throw new Error('element not found') throw new Error('element not found')
} }
res.url = await NTQQFileApi.getImageUrl(findEle.picElement) res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement)
} else if (fileCache[0].elementType === ElementType.VIDEO) { } else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId) res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
} }
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) { if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try { try {

View File

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

View File

@@ -1,7 +1,5 @@
import BaseAction from '../BaseAction'
import BaseAction from '../BaseAction'; import { ActionName } from '../types'
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
@@ -19,7 +17,7 @@ export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
if (!msg) { if (!msg) {
throw new Error('msg not found') throw new Error('msg not found')
} }
return await NTQQGroupApi.removeGroupEssence( return await this.ctx.ntGroupApi.removeGroupEssence(
msg.Peer.peerUid, msg.Peer.peerUid,
msg.MsgId, msg.MsgId,
) )

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
group_id: string | number group_id: string | number
@@ -12,6 +11,6 @@ export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> {
actionName = ActionName.GoCQHTTP_DelGroupFile actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) { 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])
} }
} }

View File

@@ -3,7 +3,8 @@ import { ActionName } from '../types'
import fs from 'fs' import fs from 'fs'
import fsPromise from 'fs/promises' import fsPromise from 'fs/promises'
import path from 'node:path' 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' import { randomUUID } from 'node:crypto'
interface Payload { interface Payload {

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types' import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
@@ -26,14 +25,14 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
if (!rootMsg) { if (!rootMsg) {
throw Error('msg not found') 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) { if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg) throw Error('找不到相关的聊天记录' + data?.errMsg)
} }
const msgList = data.msgList const msgList = data.msgList
const messages = await Promise.all( const messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
const resMsg = await OB11Constructor.message(msg) const resMsg = await OB11Constructor.message(this.ctx, msg)
resMsg.message_id = MessageUnique.createMsg({ resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,

View File

@@ -2,7 +2,6 @@ import BaseAction from '../BaseAction'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { RawMessage } from '@/ntqqapi/types' import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
@@ -28,11 +27,11 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
let msgList: RawMessage[] | undefined let msgList: RawMessage[] | undefined
// 包含 message_seq 0 // 包含 message_seq 0
if (!payload.message_seq) { if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count))?.msgList msgList = (await this.ctx.ntMsgApi.getLastestMsgByUids(peer, count))?.msgList
} else { } else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在` if (!startMsgId) throw `消息${payload.message_seq}不存在`
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
} }
if (!msgList?.length) throw '未找到消息' if (!msgList?.length) throw '未找到消息'
if (isReverseOrder) msgList.reverse() if (isReverseOrder) msgList.reverse()
@@ -41,7 +40,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId) msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
}) })
) )
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg))) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(this.ctx, msg)))
return { messages: ob11MsgList } return { messages: ob11MsgList }
} }
} }

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
@@ -18,7 +17,7 @@ export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
if (!msg) { if (!msg) {
throw new Error('msg not found') throw new Error('msg not found')
} }
return await NTQQGroupApi.addGroupEssence( return await this.ctx.ntGroupApi.addGroupEssence(
msg.Peer.peerUid, msg.Peer.peerUid,
msg.MsgId msg.MsgId
) )

View File

@@ -6,7 +6,6 @@ import { ChatType, SendFileElement } from '@/ntqqapi/types'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg' import { sendMsg } from '../msg/SendMsg'
import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
user_id: number | string user_id: number | string
@@ -29,8 +28,8 @@ export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) const sendFileEle = await SendMsgElementConstructor.file(this.ctx, downloadResult.path, payload.name, payload.folder_id)
await sendMsg({ await sendMsg(this.ctx, {
chatType: ChatType.group, chatType: ChatType.group,
peerUid: payload.group_id?.toString()!, peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true) }, [sendFileEle], [], true)
@@ -43,11 +42,11 @@ export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
async getPeer(payload: Payload): Promise<Peer> { async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { 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) { if (!peerUid) {
throw `私聊${payload.user_id}不存在` 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 } return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
} }
throw '缺少参数 user_id' throw '缺少参数 user_id'
@@ -63,8 +62,8 @@ export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(this.ctx, downloadResult.path, payload.name)
await sendMsg(peer, [sendFileEle], [], true) await sendMsg(this.ctx, peer, [sendFileEle], [], true)
return null return null
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
import { WebApi, WebHonorType } from '@/ntqqapi/api' import { WebHonorType } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
@@ -18,6 +18,6 @@ export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
if (!payload.type) { if (!payload.type) {
payload.type = WebHonorType.ALL payload.type = WebHonorType.ALL
} }
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type) return await this.ctx.ntWebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type)
} }
} }

View File

@@ -2,7 +2,6 @@ import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -12,7 +11,7 @@ class GetGroupInfo extends BaseAction<Payload, OB11Group> {
actionName = ActionName.GetGroupInfo actionName = ActionName.GetGroupInfo
protected async _handle(payload: Payload) { 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) { if (group) {
return OB11Constructor.group(group) return OB11Constructor.group(group)
} else { } else {

View File

@@ -2,7 +2,6 @@ import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -12,7 +11,7 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { 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) return OB11Constructor.groups(groupList)
} }
} }

View File

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

View File

@@ -2,8 +2,7 @@ import { OB11GroupMember } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi, WebApi } from '@/ntqqapi/api' import { selfInfo } from '@/common/globalVars'
import { getSelfUid } from '@/common/data'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -14,7 +13,7 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList actionName = ActionName.GetGroupMemberList
protected async _handle(payload: Payload) { 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()) const groupMembersArr = Array.from(groupMembers.values())
let _groupMembers = groupMembersArr.map(item => { let _groupMembers = groupMembersArr.map(item => {
@@ -31,11 +30,11 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]) 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 const isPrivilege = selfRole === 3 || selfRole === 4
if (isPrivilege) { 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++) { for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) { if (!webGroupMembers[i]?.uin) {
continue continue

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,71 +54,70 @@ import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent' import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile' import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile'
import type Adapter from '../adapter'
export function initActionMap(adapter: Adapter) {
export const actionHandlers = [ const actionHandlers = [
new GetFile(), new GetFile(adapter),
new Debug(), new Debug(adapter),
new GetConfigAction(), new GetConfigAction(adapter),
new SetConfigAction(), new SetConfigAction(adapter),
new GetGroupAddRequest(), new GetGroupAddRequest(adapter),
new SetQQAvatar(), new SetQQAvatar(adapter),
new GetFriendWithCategory(), new GetFriendWithCategory(adapter),
new GetEvent(), new GetEvent(adapter),
// onebot11 // onebot11
new SendLike(), new SendLike(adapter),
new GetMsg(), new GetMsg(adapter),
new GetLoginInfo(), new GetLoginInfo(adapter),
new GetFriendList(), new GetFriendList(adapter),
new GetGroupList(), new GetGroupList(adapter),
new GetGroupInfo(), new GetGroupInfo(adapter),
new GetGroupMemberList(), new GetGroupMemberList(adapter),
new GetGroupMemberInfo(), new GetGroupMemberInfo(adapter),
new SendGroupMsg(), new SendGroupMsg(adapter),
new SendPrivateMsg(), new SendPrivateMsg(adapter),
new SendMsg(), new SendMsg(adapter),
new DeleteMsg(), new DeleteMsg(adapter),
new SetGroupAddRequest(), new SetGroupAddRequest(adapter),
new SetFriendAddRequest(), new SetFriendAddRequest(adapter),
new SetGroupLeave(), new SetGroupLeave(adapter),
new GetVersionInfo(), new GetVersionInfo(adapter),
new CanSendRecord(), new CanSendRecord(adapter),
new CanSendImage(), new CanSendImage(adapter),
new GetStatus(), new GetStatus(adapter),
new SetGroupWholeBan(), new SetGroupWholeBan(adapter),
new SetGroupBan(), new SetGroupBan(adapter),
new SetGroupKick(), new SetGroupKick(adapter),
new SetGroupAdmin(), new SetGroupAdmin(adapter),
new SetGroupName(), new SetGroupName(adapter),
new SetGroupCard(), new SetGroupCard(adapter),
new GetImage(), new GetImage(adapter),
new GetRecord(), new GetRecord(adapter),
new CleanCache(), new CleanCache(adapter),
new GetCookies(), new GetCookies(adapter),
new SetMsgEmojiLike(), new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(), new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(), new ForwardGroupSingleMsg(adapter),
//以下为go-cqhttp api //以下为go-cqhttp api
new GetGroupEssence(), new GetGroupEssence(adapter),
new GetGroupHonorInfo(), new GetGroupHonorInfo(adapter),
new GoCQHTTPSendForwardMsg(), new GoCQHTTPSendForwardMsg(adapter),
new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendGroupForwardMsg(adapter),
new GoCQHTTPSendPrivateForwardMsg(), new GoCQHTTPSendPrivateForwardMsg(adapter),
new GoCQHTTPGetStrangerInfo(), new GoCQHTTPGetStrangerInfo(adapter),
new GoCQHTTPDownloadFile(), new GoCQHTTPDownloadFile(adapter),
new GetGuildList(), new GetGuildList(adapter),
new GoCQHTTPMarkMsgAsRead(), new GoCQHTTPMarkMsgAsRead(adapter),
new GoCQHTTPUploadGroupFile(), new GoCQHTTPUploadGroupFile(adapter),
new GoCQHTTPUploadPrivateFile(), new GoCQHTTPUploadPrivateFile(adapter),
new GoCQHTTPGetGroupMsgHistory(), new GoCQHTTPGetGroupMsgHistory(adapter),
new GoCQHTTGetForwardMsgAction(), new GoCQHTTGetForwardMsgAction(adapter),
new GoCQHTTHandleQuickOperation(), new GoCQHTTHandleQuickOperation(adapter),
new GoCQHTTPSetEssenceMsg(), new GoCQHTTPSetEssenceMsg(adapter),
new GoCQHTTPDelEssenceMsg(), new GoCQHTTPDelEssenceMsg(adapter),
new GoCQHTTPDelGroupFile() new GoCQHTTPDelGroupFile(adapter)
] ]
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>() const actionMap = new Map<string, BaseAction<any, any>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {
actionMap.set(action.actionName, action) actionMap.set(action.actionName, action)
@@ -128,5 +127,3 @@ function initActionMap() {
return actionMap return actionMap
} }
export const actionMap = initActionMap()

View File

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

View File

@@ -1,16 +1,5 @@
import BaseAction from '../BaseAction' 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 { ActionName } from '../types'
import { log } from '../../../common/utils/log'
interface Payload { interface Payload {
method: string method: string
@@ -21,10 +10,10 @@ export default class Debug extends BaseAction<Payload, any> {
actionName = ActionName.Debug actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
log('debug call ntqq api', payload) this.ctx.logger.info('debug call ntqq api', payload)
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] const { ntMsgApi, ntFileApi, ntFileCacheApi, ntFriendApi, ntGroupApi, ntUserApi, ntWindowApi } = this.ctx
const ntqqApi = [ntMsgApi, ntFriendApi, ntGroupApi, ntUserApi, ntFileApi, ntFileCacheApi, ntWindowApi]
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
//log('ntqqApiClass', ntqqApiClass)
const method = ntqqApiClass[payload.method] const method = ntqqApiClass[payload.method]
if (method) { if (method) {
const result = method(...payload.args) const result = method(...payload.args)
@@ -35,8 +24,5 @@ export default class Debug extends BaseAction<Payload, any> {
} }
} }
throw `${payload.method}方法 不存在` throw `${payload.method}方法 不存在`
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
} }
} }

View File

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

View File

@@ -1,8 +1,6 @@
import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types' import { GroupNotify, GroupNotifyStatus } from '@/ntqqapi/types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
group_id: number group_id: number
@@ -14,11 +12,11 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
actionName = ActionName.GetGroupIgnoreAddRequest actionName = ActionName.GetGroupIgnoreAddRequest
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> { protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQGroupApi.getGroupIgnoreNotifies() const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies()
const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE)
const returnData: OB11GroupRequestNotify[] = [] const returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { 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({ returnData.push({
group_id: parseInt(notify.group.groupCode), group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin), user_id: parseInt(uin),

View File

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

View File

@@ -1,6 +1,5 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
@@ -18,7 +17,7 @@ class DeleteMsg extends BaseAction<Payload, void> {
if (!msg) { if (!msg) {
throw `消息${payload.message_id}不存在` throw `消息${payload.message_id}不存在`
} }
await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]) await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId])
} }
} }

View File

@@ -1,5 +1,4 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
@@ -14,7 +13,7 @@ interface Payload {
abstract class ForwardSingleMsg extends BaseAction<Payload, null> { abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
protected async getTargetPeer(payload: Payload): Promise<Peer> { protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { 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) { if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`) throw new Error(`无法找到私聊对象${payload.user_id}`)
} }
@@ -32,7 +31,7 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
throw new Error(`无法找到消息${payload.message_id}`) throw new Error(`无法找到消息${payload.message_id}`)
} }
const peer = await this.getTargetPeer(payload) 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) { if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`) throw new Error(`转发消息失败 ${ret.errMsg}`)
} }

View File

@@ -2,9 +2,7 @@ import { OB11Message } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
import { getMsgCache } from '@/common/data'
export interface PayloadType { export interface PayloadType {
message_id: number | string message_id: number | string
@@ -30,8 +28,8 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
peerUid: msgIdWithPeer.Peer.peerUid, peerUid: msgIdWithPeer.Peer.peerUid,
chatType: msgIdWithPeer.Peer.chatType chatType: msgIdWithPeer.Peer.chatType
} }
const msg = getMsgCache(msgIdWithPeer.MsgId) ?? (await NTQQMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0] const msg = this.adapter.getMsgCache(msgIdWithPeer.MsgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const retMsg = await OB11Constructor.message(msg) const retMsg = await OB11Constructor.message(this.ctx, msg)
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)! retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_seq = retMsg.message_id retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id retMsg.real_id = retMsg.message_id

View File

@@ -6,7 +6,6 @@ import {
RawMessage, RawMessage,
SendMessageElement, SendMessageElement,
} from '@/ntqqapi/types' } from '@/ntqqapi/types'
import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
@@ -24,14 +23,14 @@ import fs from 'node:fs'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import { decodeCQCode } from '../../cqcode' import { decodeCQCode } from '../../cqcode'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { log } from '@/common/utils/log'
import { sleep } from '@/common/utils/helper' import { sleep } from '@/common/utils/helper'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
import { OB11MessageFileBase } from '../../types' import { OB11MessageFileBase } from '../../types'
import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
export interface ReturnDataType { export interface ReturnDataType {
message_id: number 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 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
async function handleOb11FileLikeMessage( async function handleOb11FileLikeMessage(
ctx: Context,
{ data: inputdata }: OB11MessageFileBase, { data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: Pick<MessageContext, 'deleteAfterSentFiles'>, { deleteAfterSentFiles }: Pick<MessageContext, 'deleteAfterSentFiles'>,
) { ) {
@@ -85,7 +85,7 @@ async function handleOb11FileLikeMessage(
} = (await uri2local(inputdata?.url || inputdata.file)) } = (await uri2local(inputdata?.url || inputdata.file))
if (!success) { if (!success) {
log('文件下载失败', errMsg) ctx.logger.error('文件下载失败', errMsg)
throw Error('文件下载失败' + errMsg) throw Error('文件下载失败' + errMsg)
} }
@@ -97,6 +97,7 @@ async function handleOb11FileLikeMessage(
} }
export async function createSendElements( export async function createSendElements(
ctx: Context,
messageData: OB11MessageData[], messageData: OB11MessageData[],
peer: Peer, peer: Peer,
ignoreTypes: OB11MessageDataType[] = [], ignoreTypes: OB11MessageDataType[] = [],
@@ -128,10 +129,10 @@ export async function createSendElements(
let isAdmin: boolean = true let isAdmin: boolean = true
if (groupCode) { if (groupCode) {
try { try {
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo remainAtAllCount = (await ctx.ntGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin .RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount) ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember(groupCode, getSelfUin()) const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin)
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) { } catch (e) {
} }
@@ -141,7 +142,7 @@ export async function createSendElements(
} }
} }
else if (peer.chatType === ChatType.group) { else if (peer.chatType === ChatType.group) {
const atMember = await getGroupMember(peer.peerUid, atQQ) const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ)
if (atMember) { if (atMember) {
const display = `@${atMember.cardName || atMember.nick}` const display = `@${atMember.cardName || atMember.nick}`
sendElements.push( sendElements.push(
@@ -149,7 +150,7 @@ export async function createSendElements(
) )
} else { } else {
const atNmae = sendMsg.data?.name const atNmae = sendMsg.data?.name
const uid = await NTQQUserApi.getUidByUin(atQQ) || '' const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : '' const display = atNmae ? `@${atNmae}` : ''
sendElements.push( sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display), SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display),
@@ -163,10 +164,10 @@ export async function createSendElements(
if (sendMsg.data?.id) { if (sendMsg.data?.id) {
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id) const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
if (!replyMsgId) { if (!replyMsgId) {
log('回复消息不存在', replyMsgId) ctx.logger.warn('回复消息不存在', replyMsgId)
continue continue
} }
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId( const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId(
replyMsgId.Peer, replyMsgId.Peer,
[replyMsgId.MsgId!] [replyMsgId.MsgId!]
)).msgList[0] )).msgList[0]
@@ -203,7 +204,8 @@ export async function createSendElements(
break break
case OB11MessageDataType.image: { case OB11MessageDataType.image: {
const res = await SendMsgElementConstructor.pic( const res = await SendMsgElementConstructor.pic(
(await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })).path, ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '', sendMsg.data.summary || '',
sendMsg.data.subType || 0 sendMsg.data.subType || 0
) )
@@ -212,25 +214,25 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.file: { case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.file(path, fileName)) sendElements.push(await SendMsgElementConstructor.file(ctx, path, fileName))
} }
break break
case OB11MessageDataType.video: { case OB11MessageDataType.video: {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
let thumb = sendMsg.data.thumb let thumb = sendMsg.data.thumb
if (thumb) { if (thumb) {
const uri2LocalRes = await uri2local(thumb) const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path if (uri2LocalRes.success) thumb = uri2LocalRes.path
} }
const res = await SendMsgElementConstructor.video(path, fileName, thumb) const res = await SendMsgElementConstructor.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath) deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.ptt(path)) sendElements.push(await SendMsgElementConstructor.ptt(ctx, path))
} }
break break
case OB11MessageDataType.json: { case OB11MessageDataType.json: {
@@ -261,6 +263,7 @@ export async function createSendElements(
} }
export async function sendMsg( export async function sendMsg(
ctx: Context,
peer: Peer, peer: Peer,
sendElements: SendMessageElement[], sendElements: SendMessageElement[],
deleteAfterSentFiles: string[], deleteAfterSentFiles: string[],
@@ -286,46 +289,46 @@ export async function sendMsg(
totalSize += fs.statSync(fileElement.picElement.sourcePath).size totalSize += fs.statSync(fileElement.picElement.sourcePath).size
} }
} catch (e) { } catch (e) {
log('文件大小计算失败', e, fileElement) ctx.logger.warn('文件大小计算失败', e, fileElement)
} }
} }
//log('发送消息总大小', totalSize, 'bytes') //log('发送消息总大小', totalSize, 'bytes')
const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s ) const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
//log('设置消息超时时间', timeout) //log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
log('消息发送', returnMsg.msgShortId) ctx.logger.info('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path)) deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg return returnMsg
} }
async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await 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<OB11PostSendMsg, ReturnDataType> { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
private async createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
const isBuddy = await this.ctx.ntFriendApi.isBuddy(Uid!)
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!,
guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
}
}
throw '请指定 group_id 或 user_id'
}
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message) const messages = convertMessage2List(payload.message)
const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node) const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node)
@@ -343,12 +346,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
if (payload.user_id && payload.message_type !== 'group') { 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 { return {
valid: true, valid: true,
@@ -362,7 +359,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} else if (payload.message_type === 'private') { } else if (payload.message_type === 'private') {
contextMode = ContextMode.Private contextMode = ContextMode.Private
} }
const peer = await createContext(payload, contextMode) const peer = await this.createContext(payload, contextMode)
const messages = convertMessage2List( const messages = convertMessage2List(
payload.message, payload.message,
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',
@@ -412,7 +409,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
let jsonContent: string let jsonContent: string
try { try {
jsonContent = await new MusicSign(musicSignUrl).sign(postData) jsonContent = await new MusicSign(this.ctx, musicSignUrl).sign(postData)
if (!jsonContent) { if (!jsonContent) {
throw '音乐消息生成失败,提交内容有误或者签名服务器签名失败' throw '音乐消息生成失败,提交内容有误或者签名服务器签名失败'
} }
@@ -426,13 +423,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
// log("send msg:", peer, sendElements) // 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.length === 1) {
if (sendElements[0] === null) { if (sendElements[0] === null) {
return { message_id: 0 } 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! } return { message_id: returnMsg.msgShortId! }
} }
@@ -444,7 +441,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> { private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
log('克隆的目标消息', msg) this.ctx.logger.info('克隆的目标消息', msg)
let sendElements: SendMessageElement[] = [] let sendElements: SendMessageElement[] = []
for (const ele of msg.elements) { for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement) sendElements.push(ele as SendMessageElement)
@@ -453,14 +450,14 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// } // }
} }
if (sendElements.length === 0) { if (sendElements.length === 0) {
log('需要clone的消息无法解析将会忽略掉', msg) this.ctx.logger.warn('需要clone的消息无法解析将会忽略掉', msg)
} }
log('克隆消息', sendElements) this.ctx.logger.info('克隆消息', sendElements)
try { try {
const nodeMsg = await NTQQMsgApi.sendMsg( const nodeMsg = await this.ctx.ntMsgApi.sendMsg(
{ {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: getSelfUid(), peerUid: selfInfo.uid,
}, },
sendElements, sendElements,
true, true,
@@ -468,7 +465,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
await sleep(400) await sleep(400)
return nodeMsg return nodeMsg
} catch (e) { } catch (e) {
log(e, '克隆转发消息失败,将忽略本条消息', msg) this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
} }
} }
@@ -476,7 +473,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = { const selfPeer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: getSelfUid(), peerUid: selfInfo.uid,
} }
let nodeMsgIds: string[] = [] let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用 // 先判断一遍是不是id和自定义混用
@@ -487,7 +484,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeId) { if (nodeId) {
const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId) const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!nodeMsg) { if (!nodeMsg) {
log('转发消息失败,未找到消息', nodeId) this.ctx.logger.warn('转发消息失败,未找到消息', nodeId)
continue continue
} }
nodeMsgIds.push(nodeMsg.MsgId) nodeMsgIds.push(nodeMsg.MsgId)
@@ -497,10 +494,11 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// 提取消息段发给自己生成消息id // 提取消息段发给自己生成消息id
try { try {
const { sendElements, deleteAfterSentFiles } = await createSendElements( const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx,
convertMessage2List(messageNode.data.content), convertMessage2List(messageNode.data.content),
destPeer destPeer
) )
log('开始生成转发节点', sendElements) this.ctx.logger.info('开始生成转发节点', sendElements)
let sendElementsSplit: SendMessageElement[][] = [] let sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0 let splitIndex = 0
for (const ele of sendElements) { for (const ele of sendElements) {
@@ -518,19 +516,19 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
else { else {
sendElementsSplit[splitIndex].push(ele) sendElementsSplit[splitIndex].push(ele)
} }
log(sendElementsSplit) this.ctx.logger.info(sendElementsSplit)
} }
// log("分割后的转发节点", sendElementsSplit) // log("分割后的转发节点", sendElementsSplit)
for (const eles of 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) nodeMsgIds.push(nodeMsg.msgId)
await sleep(400) await sleep(400)
log('转发节点生成成功', nodeMsg.msgId) this.ctx.logger.info('转发节点生成成功', nodeMsg.msgId)
} }
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
})) }))
} catch (e) { } catch (e) {
log('生成转发消息节点失败', e) this.ctx.logger.error('生成转发消息节点失败', e)
} }
} }
} }
@@ -542,7 +540,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
for (const msgId of nodeMsgIds) { for (const msgId of nodeMsgIds) {
const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId) const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsgPeer) { 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 } srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) { if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true needSendSelf = true
@@ -570,7 +568,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeMsgIds.length === 0) { if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空') 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) returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return returnMsg return returnMsg
} }

View File

@@ -1,6 +1,5 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
@@ -22,11 +21,11 @@ export class SetMsgEmojiLike extends BaseAction<Payload, any> {
if (!payload.emoji_id) { if (!payload.emoji_id) {
throw new Error('emojiId not found') 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) { if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) {
throw new Error('find msg by msgid error') throw new Error('find msg by msgid error')
} }
return await NTQQMsgApi.setEmojiLike( return await this.ctx.ntMsgApi.setEmojiLike(
msg.Peer, msg.Peer,
msgData[0].msgSeq, msgData[0].msgSeq,
payload.emoji_id.toString(), payload.emoji_id.toString(),

View File

@@ -2,8 +2,7 @@ import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import fs from 'node:fs' import fs from 'node:fs'
import Path from 'node:path' import Path from 'node:path'
import { ChatType, ChatCacheListItemBasic, CacheFileType } from '../../../ntqqapi/types' import { ChatCacheListItemBasic, CacheFileType } from '@/ntqqapi/types'
import { NTQQFileApi, NTQQFileCacheApi } from '../../../ntqqapi/api/file'
export default class CleanCache extends BaseAction<void, void> { export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache actionName = ActionName.CleanCache
@@ -14,23 +13,23 @@ export default class CleanCache extends BaseAction<void, void> {
// dbUtil.clearCache() // dbUtil.clearCache()
const cacheFilePaths: string[] = [] const cacheFilePaths: string[] = []
await NTQQFileCacheApi.setCacheSilentScan(false) await this.ctx.ntFileCacheApi.setCacheSilentScan(false)
cacheFilePaths.push(await NTQQFileCacheApi.getHotUpdateCachePath()) cacheFilePaths.push(await this.ctx.ntFileCacheApi.getHotUpdateCachePath())
cacheFilePaths.push(await NTQQFileCacheApi.getDesktopTmpPath()) 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)) list.forEach((e) => cacheFilePaths.push(e.value))
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知 // await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQFileCacheApi.scanCache() const cacheScanResult = await this.ctx.ntFileCacheApi.scanCache()
const cacheSize = parseInt(cacheScanResult.size[6]) const cacheSize = parseInt(cacheScanResult.size[6])
if (cacheScanResult.result !== 0) { if (cacheScanResult.result !== 0) {
throw 'Something went wrong while scanning cache. Code: ' + cacheScanResult.result throw 'Something went wrong while scanning cache. Code: ' + cacheScanResult.result
} }
await NTQQFileCacheApi.setCacheSilentScan(true) await this.ctx.ntFileCacheApi.setCacheSilentScan(true)
if (cacheSize > 0 && cacheFilePaths.length > 2) { if (cacheSize > 0 && cacheFilePaths.length > 2) {
// 存在缓存文件且大小不为 0 时执行清理动作 // 存在缓存文件且大小不为 0 时执行清理动作
// await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了 // await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了
@@ -53,11 +52,11 @@ export default class CleanCache extends BaseAction<void, void> {
const fileTypeAny: any = CacheFileType[name] const fileTypeAny: any = CacheFileType[name]
const fileType: CacheFileType = fileTypeAny 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() res()
} catch (e) { } catch (e) {
console.error('清理缓存时发生了错误') console.error('清理缓存时发生了错误')
@@ -84,21 +83,3 @@ function deleteCachePath(pathList: string[]) {
emptyPath(path) emptyPath(path)
} }
} }
function getCacheList(type: ChatType) {
// NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理
return new Promise<Array<ChatCacheListItemBasic>>((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))
})
}

View File

@@ -1,17 +1,16 @@
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor'
import { getSelfInfo, getSelfNick } from '../../../common/data'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars'
class GetLoginInfo extends BaseAction<null, OB11User> { class GetLoginInfo extends BaseAction<null, OB11User> {
actionName = ActionName.GetLoginInfo actionName = ActionName.GetLoginInfo
protected async _handle(payload: null) { protected async _handle(payload: null) {
return OB11Constructor.selfInfo({ return {
...getSelfInfo(), user_id: parseInt(selfInfo.uin),
nick: await getSelfNick(true) nickname: await this.ctx.ntUserApi.getSelfNick(true)
}) }
} }
} }

View File

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

View File

@@ -1,5 +1,4 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Response { interface Response {
@@ -18,10 +17,10 @@ export class GetCookies extends BaseAction<Payload, Response> {
if (!payload.domain) { if (!payload.domain) {
throw '缺少参数 domain' throw '缺少参数 domain'
} }
const cookiesObject = await NTQQUserApi.getCookies(payload.domain) const cookiesObject = await this.ctx.ntUserApi.getCookies(payload.domain)
//把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起 //把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') 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 } return { cookies, bkn }
} }
} }

View File

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

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
user_id: number | string user_id: number | string
@@ -13,8 +12,8 @@ export default class SendLike extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
try { try {
const qq = payload.user_id.toString() const qq = payload.user_id.toString()
const uid: string = await NTQQUserApi.getUidByUin(qq) || '' const uid: string = await this.ctx.ntUserApi.getUidByUin(qq) || ''
const result = await NTQQUserApi.like(uid, +payload.times || 1) const result = await this.ctx.ntUserApi.like(uid, +payload.times || 1)
if (result?.result !== 0) { if (result?.result !== 0) {
throw Error(result?.errMsg) throw Error(result?.errMsg)
} }

View File

@@ -13,7 +13,7 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve?.toString() !== 'false' const approve = payload.approve?.toString() !== 'false'
await NTQQFriendApi.handleFriendRequest(payload.flag, approve) await this.ctx.ntFriendApi.handleFriendRequest(payload.flag, approve)
return null return null
} }
} }

436
src/onebot11/adapter.ts Normal file
View File

@@ -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<string, RawMessage> = 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

View File

@@ -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<any>
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<boolean>((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<OB11Http.Config>) {
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<string, BaseAction<any, any>>
}
}
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<OB11HttpPost.Config>) {
Object.assign(this.config, config)
}
}
namespace OB11HttpPost {
export interface Config {
hosts: string[]
secret?: string
enableHttpHeart?: boolean
heartInterval: number
}
}
export { OB11Http, OB11HttpPost }

329
src/onebot11/connect/ws.ts Normal file
View File

@@ -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<boolean>((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<OB11WebSocket.Config>) {
Object.assign(this.config, config)
}
private reply(socket: WebSocket, data: OB11Return<any> | 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<any, any> = 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<string, BaseAction<any, any>>
}
}
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<any> | 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<any, any> = 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<string, BaseAction<any, any>>
}
}
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<OB11WebSocketReverseManager.Config>) {
Object.assign(this.config, config)
}
}
namespace OB11WebSocketReverseManager {
export interface Config {
hosts: string[]
heartInterval: number
token?: string
actionMap: Map<string, BaseAction<any, any>>
}
}
export { OB11WebSocket, OB11WebSocketReverseManager }

View File

@@ -25,7 +25,6 @@ import {
FriendV2, FriendV2,
ChatType2 ChatType2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { getGroupMember, getSelfUin } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { MessageUnique } from '../common/utils/MessageUnique' import { MessageUnique } from '../common/utils/MessageUnique'
@@ -34,13 +33,11 @@ import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { calcQQLevel } from '../common/utils/qqlevel' import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log'
import { isNull, sleep } from '../common/utils/helper' import { isNull, sleep } from '../common/utils/helper'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQMsgApi } from '../ntqqapi/api'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/constructor' import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
@@ -50,16 +47,17 @@ import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11Poke
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit } from 'cosmokit' import { omit } from 'cosmokit'
import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
export class OB11Constructor { export namespace OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { export async function message(ctx: Context, msg: RawMessage): Promise<OB11Message> {
let config = getConfigUtil().getConfig() let config = getConfigUtil().getConfig()
const { const {
enableLocalFile2Url,
debug, debug,
ob11: { messagePostFormat }, ob11: { messagePostFormat },
} = config } = config
const selfUin = getSelfUin() const selfUin = selfInfo.uin
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfUin), self_id: parseInt(selfUin),
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
@@ -86,19 +84,19 @@ export class OB11Constructor {
if (msg.chatType == ChatType.group) { if (msg.chatType == ChatType.group) {
resMsg.sub_type = 'normal' resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin) resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin!) const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin!)
if (member) { if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role) resMsg.sender.role = groupMemberRole(member.role)
resMsg.sender.nickname = member.nick resMsg.sender.nickname = member.nick
} }
} }
else if (msg.chatType == ChatType.friend) { else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = '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) { else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group' 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) { if (ret?.result === 0) {
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode) resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
@@ -123,7 +121,7 @@ export class OB11Constructor {
const { atNtUid, content } = element.textElement const { atNtUid, content } = element.textElement
let atQQ = element.textElement.atUid let atQQ = element.textElement.atUid
if (!atQQ || atQQ === '0') { if (!atQQ || atQQ === '0') {
const atMember = await getGroupMember(msg.peerUin, atNtUid) const atMember = await ctx.ntGroupApi.getGroupMember(msg.peerUin, atNtUid)
if (atMember) { if (atMember) {
atQQ = atMember.uin atQQ = atMember.uin
} }
@@ -154,7 +152,7 @@ export class OB11Constructor {
try { try {
const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords) const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords)
if (!records) throw new Error('找不到回复消息') if (!records) throw new Error('找不到回复消息')
let replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({ let replyMsg = (await ctx.ntMsgApi.getMsgsBySeqAndCount({
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
chatType: msg.chatType, chatType: msg.chatType,
@@ -165,7 +163,7 @@ export class OB11Constructor {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
} }
replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0] replyMsg = (await ctx.ntMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0]
} }
// 284840486: 合并消息内侧 消息具体定位不到 // 284840486: 合并消息内侧 消息具体定位不到
if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') {
@@ -177,7 +175,7 @@ export class OB11Constructor {
chatType: msg.chatType, chatType: msg.chatType,
}, replyMsg.msgId)?.toString() }, replyMsg.msgId)?.toString()
} catch (e: any) { } catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) ctx.logger.error('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
continue continue
} }
} }
@@ -192,7 +190,7 @@ export class OB11Constructor {
message_data['data']['file'] = picElement.fileName message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = picElement.picSubType message_data['data']['subType'] = picElement.picSubType
//message_data['data']['file_id'] = picElement.fileUuid //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 message_data['data']['file_size'] = picElement.fileSize
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
@@ -213,7 +211,7 @@ export class OB11Constructor {
message_data['data']['path'] = videoElement.filePath message_data['data']['path'] = videoElement.filePath
//message_data['data']['file_id'] = videoElement.fileUuid //message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_size'] = videoElement.fileSize 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, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,
}, msg.msgId, element.elementId) }, msg.msgId, element.elementId)
@@ -323,7 +321,7 @@ export class OB11Constructor {
return resMsg return resMsg
} }
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent | void> { export async function PrivateEvent(ctx: Context, msg: RawMessage): Promise<OB11BaseNoticeEvent | void> {
if (msg.chatType !== ChatType.friend) { if (msg.chatType !== ChatType.friend) {
return return
} }
@@ -339,8 +337,8 @@ export class OB11Constructor {
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11FriendPokeEvent( return new OB11FriendPokeEvent(
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail pokedetail
) )
} }
@@ -356,12 +354,12 @@ export class OB11Constructor {
} }
} }
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | void> { export async function GroupEvent(ctx: Context, msg: RawMessage): Promise<OB11GroupNoticeEvent | void> {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.group) {
return return
} }
if (msg.senderUin) { 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) { if (member && member.cardName !== msg.sendMemberName) {
const event = new OB11GroupCardEvent( const event = new OB11GroupCardEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
@@ -380,15 +378,15 @@ export class OB11Constructor {
if (groupElement) { if (groupElement) {
// log("收到群提示消息", groupElement) // log("收到群提示消息", groupElement)
if (groupElement.type === TipGroupElementType.memberIncrease) { if (groupElement.type === TipGroupElementType.memberIncrease) {
log('收到群成员增加消息', groupElement) ctx.logger.info('收到群成员增加消息', groupElement)
await sleep(1000) 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 let memberUin = member?.uin
if (!memberUin) { if (!memberUin) {
memberUin = (await NTQQUserApi.getUserDetailInfo(groupElement.memberUid)).uin memberUin = (await ctx.ntUserApi.getUserDetailInfo(groupElement.memberUid)).uin
} }
// log("获取新群成员QQ", memberUin) // log("获取新群成员QQ", memberUin)
const adminMember = await getGroupMember(msg.peerUid, groupElement.adminUid) const adminMember = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid)
// log("获取同意新成员入群的管理员", adminMember) // log("获取同意新成员入群的管理员", adminMember)
if (memberUin) { if (memberUin) {
const operatorUin = adminMember?.uin || memberUin const operatorUin = adminMember?.uin || memberUin
@@ -398,7 +396,7 @@ export class OB11Constructor {
} }
} }
else if (groupElement.type === TipGroupElementType.ban) { else if (groupElement.type === TipGroupElementType.ban) {
log('收到群群员禁言提示', groupElement) ctx.logger.info('收到群群员禁言提示', groupElement)
const memberUid = groupElement.shutUp?.member.uid const memberUid = groupElement.shutUp?.member.uid
const adminUid = groupElement.shutUp?.admin.uid const adminUid = groupElement.shutUp?.admin.uid
let memberUin: string = '' let memberUin: string = ''
@@ -406,8 +404,8 @@ export class OB11Constructor {
let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban' let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban'
if (memberUid) { if (memberUid) {
memberUin = memberUin =
(await getGroupMember(msg.peerUid, memberUid))?.uin || (await ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(memberUid))?.uin (await ctx.ntUserApi.getUserDetailInfo(memberUid))?.uin
} }
else { else {
memberUin = '0' // 0表示全员禁言 memberUin = '0' // 0表示全员禁言
@@ -416,7 +414,7 @@ export class OB11Constructor {
} }
} }
const adminUin = 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) { if (memberUin && adminUin) {
return new OB11GroupBanEvent( return new OB11GroupBanEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
@@ -428,14 +426,14 @@ export class OB11Constructor {
} }
} }
else if (groupElement.type === TipGroupElementType.kicked) { else if (groupElement.type === TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
NTQQGroupApi.quitGroup(msg.peerUid).then() ctx.ntGroupApi.quitGroup(msg.peerUid)
try { 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) { if (adminUin) {
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(getSelfUin()), parseInt(selfInfo.uin),
parseInt(adminUin), parseInt(adminUin),
'kick_me' 'kick_me'
) )
@@ -443,7 +441,7 @@ export class OB11Constructor {
} catch (e) { } catch (e) {
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(getSelfUin()), parseInt(selfInfo.uin),
0, 0,
'leave' 'leave'
) )
@@ -475,12 +473,12 @@ export class OB11Constructor {
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: '', attributeNamePrefix: '',
}).parse(xmlElement.content) }).parse(xmlElement.content)
log('收到表情回应我的消息', emojiLikeData) ctx.logger.info('收到表情回应我的消息', emojiLikeData)
try { try {
const senderUin = emojiLikeData.gtip.qq.jp const senderUin = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq const msgSeq = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id const emojiId = emojiLikeData.gtip.face.id
const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({ const replyMsgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount({
chatType: ChatType.group, chatType: ChatType.group,
guildId: '', guildId: '',
peerUid: msg.peerUid, peerUid: msg.peerUid,
@@ -502,7 +500,7 @@ export class OB11Constructor {
likes likes
) )
} catch (e: any) { } catch (e: any) {
log('解析表情回应消息失败', e.stack) ctx.logger.error('解析表情回应消息失败', e.stack)
} }
} }
@@ -510,7 +508,7 @@ export class OB11Constructor {
grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER && grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER &&
xmlElement?.templId == '10179' xmlElement?.templId == '10179'
) { ) {
log('收到新人被邀请进群消息', grayTipElement) ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
if (xmlElement?.content) { if (xmlElement?.content) {
const regex = /jp="(\d+)"/g const regex = /jp="(\d+)"/g
@@ -562,14 +560,14 @@ export class OB11Constructor {
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11GroupPokeEvent( return new OB11GroupPokeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail pokedetail
) )
} }
} }
if (grayTipElement.jsonGrayTipElement.busiId == 2401) { if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
log('收到群精华消息', json) ctx.logger.info('收到群精华消息', json)
const searchParams = new URL(json.items[0].jp).searchParams const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')! const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode') const Group = searchParams.get('groupCode')
@@ -578,7 +576,7 @@ export class OB11Constructor {
chatType: ChatType.group, chatType: ChatType.group,
peerUid: 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) { if (!msgList?.length) {
return return
} }
@@ -598,8 +596,8 @@ export class OB11Constructor {
if (grayTipElement.jsonGrayTipElement.busiId == 2407) { if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
const memberUin = json.items[1].param[0] const memberUin = json.items[1].param[0]
const title = json.items[3].txt const title = json.items[3].txt
log('收到群成员新头衔消息', json) ctx.logger.info('收到群成员新头衔消息', json)
getGroupMember(msg.peerUid, memberUin).then(member => { ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNull(member)) { if (!isNull(member)) {
member.memberSpecialTitle = title member.memberSpecialTitle = title
} }
@@ -611,7 +609,8 @@ export class OB11Constructor {
} }
} }
static async RecallEvent( export async function RecallEvent(
ctx: Context,
msg: RawMessage, msg: RawMessage,
shortId: number shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
@@ -623,7 +622,7 @@ export class OB11Constructor {
} }
const revokeElement = msgElement.grayTipElement.revokeElement const revokeElement = msgElement.grayTipElement.revokeElement
if (msg.chatType === ChatType.group) { 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( return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(msg.senderUin!), parseInt(msg.senderUin!),
@@ -636,7 +635,7 @@ export class OB11Constructor {
} }
} }
static friend(friend: User): OB11User { export function friend(friend: User): OB11User {
return { return {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),
nickname: friend.nick, nickname: friend.nick,
@@ -646,21 +645,14 @@ export class OB11Constructor {
} }
} }
static selfInfo(selfInfo: SelfInfo): OB11User { export function friends(friends: User[]): OB11User[] {
return {
user_id: parseInt(selfInfo.uin),
nickname: selfInfo.nick,
}
}
static friends(friends: User[]): OB11User[] {
return friends.map(OB11Constructor.friend) return friends.map(OB11Constructor.friend)
} }
static friendsV2(friends: FriendV2[]): OB11User[] { export function friendsV2(friends: FriendV2[]): OB11User[] {
const data: OB11User[] = [] const data: OB11User[] = []
for (const friend of friends) { for (const friend of friends) {
const sexValue = this.sex(friend.baseInfo.sex!) const sexValue = sex(friend.baseInfo.sex!)
data.push({ data.push({
...omit(friend.baseInfo, ['richBuffer']), ...omit(friend.baseInfo, ['richBuffer']),
...friend.coreInfo, ...friend.coreInfo,
@@ -676,7 +668,7 @@ export class OB11Constructor {
return data return data
} }
static groupMemberRole(role: number): OB11GroupMemberRole | undefined { export function groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return { return {
4: OB11GroupMemberRole.owner, 4: OB11GroupMemberRole.owner,
3: OB11GroupMemberRole.admin, 3: OB11GroupMemberRole.admin,
@@ -684,7 +676,7 @@ export class OB11Constructor {
}[role] }[role]
} }
static sex(sex: Sex): OB11UserSex { export function sex(sex: Sex): OB11UserSex {
const sexMap = { const sexMap = {
[Sex.male]: OB11UserSex.male, [Sex.male]: OB11UserSex.male,
[Sex.female]: OB11UserSex.female, [Sex.female]: OB11UserSex.female,
@@ -693,7 +685,7 @@ export class OB11Constructor {
return sexMap[sex] || OB11UserSex.unknown return sexMap[sex] || OB11UserSex.unknown
} }
static groupMember(group_id: string, member: GroupMember): OB11GroupMember { export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
return { return {
group_id: parseInt(group_id), group_id: parseInt(group_id),
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
@@ -716,7 +708,7 @@ export class OB11Constructor {
} }
} }
static stranger(user: User): OB11User { export function stranger(user: User): OB11User {
return { return {
...user, ...user,
user_id: parseInt(user.uin), user_id: parseInt(user.uin),
@@ -729,12 +721,11 @@ export class OB11Constructor {
} }
} }
static groupMembers(group: Group): OB11GroupMember[] { export function groupMembers(group: Group): OB11GroupMember[] {
log('construct ob11 group members', group)
return group.members.map((m) => OB11Constructor.groupMember(group.groupCode, m)) return group.members.map((m) => OB11Constructor.groupMember(group.groupCode, m))
} }
static group(group: Group): OB11Group { export function group(group: Group): OB11Group {
return { return {
group_id: parseInt(group.groupCode), group_id: parseInt(group.groupCode),
group_name: group.groupName, group_name: group.groupName,
@@ -743,7 +734,7 @@ export class OB11Constructor {
} }
} }
static groups(groups: Group[]): OB11Group[] { export function groups(groups: Group[]): OB11Group[] {
return groups.map(OB11Constructor.group) return groups.map(OB11Constructor.group)
} }
} }

View File

@@ -1,4 +1,4 @@
import { getSelfUin } from '../../common/data' import { selfInfo } from "@/common/globalVars"
export enum EventType { export enum EventType {
META = 'meta_event', META = 'meta_event',
@@ -10,6 +10,6 @@ export enum EventType {
export abstract class OB11BaseEvent { export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000) time = Math.floor(Date.now() / 1000)
self_id = parseInt(getSelfUin()) self_id = parseInt(selfInfo.uin)
abstract post_type: EventType abstract post_type: EventType
} }

View File

@@ -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 { interface HttpEventType {
seq: number seq: number
@@ -19,7 +22,7 @@ export function postHttpEvent(event: PostEventType) {
eventList.push({ eventList.push({
seq: curentSeq, seq: curentSeq,
event: event event: event
}); })
while (eventList.length > 100) { while (eventList.length > 100) {
eventList.shift() eventList.shift()
} }
@@ -29,7 +32,7 @@ export async function getHttpEvent(userKey: string, timeout = 0) {
const toRetEvent: PostEventType[] = [] const toRetEvent: PostEventType[] = []
// 清除过时的user5分钟没访问过的user将被删除 // 清除过时的user5分钟没访问过的user将被删除
const now = Date.now(); const now = Date.now()
for (let key in httpUser) { for (let key in httpUser) {
let user = httpUser[key] let user = httpUser[key]
if (now - user.lastAccessTime > 1000 * 60 * 5) { if (now - user.lastAccessTime > 1000 * 60 * 5) {

View File

@@ -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 { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' import { convertMessage2List, createSendElements, sendMsg } from '../action/msg/SendMsg'
import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { MessageUnique } from '@/common/utils/MessageUnique' import { MessageUnique } from '@/common/utils/MessageUnique'
import { isNullable } from 'cosmokit'
import { Context } from 'cordis'
interface QuickOperationPrivateMessage { interface QuickOperationPrivateMessage {
@@ -44,23 +41,23 @@ export type QuickOperation = QuickOperationPrivateMessage &
export type QuickOperationEvent = OB11Message | OB11FriendRequestEvent | OB11GroupRequestEvent; export type QuickOperationEvent = OB11Message | OB11FriendRequestEvent | OB11GroupRequestEvent;
export async function handleQuickOperation(context: QuickOperationEvent, quickAction: QuickOperation) { export async function handleQuickOperation(ctx: Context, event: QuickOperationEvent, quickAction: QuickOperation) {
if (context.post_type === 'message') { if (event.post_type === 'message') {
handleMsg(context as OB11Message, quickAction).then().catch(log) handleMsg(ctx, event as OB11Message, quickAction).then().catch(e => ctx.logger.error(e))
} }
if (context.post_type === 'request') { if (event.post_type === 'request') {
const friendRequest = context as OB11FriendRequestEvent const friendRequest = event as OB11FriendRequestEvent
const groupRequest = context as OB11GroupRequestEvent const groupRequest = event as OB11GroupRequestEvent
if ((friendRequest).request_type === 'friend') { 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') { 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 reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11 const ob11Config = getConfigUtil().getConfig().ob11
const peer: Peer = { const peer: Peer = {
@@ -68,7 +65,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peerUid: msg.user_id.toString(), peerUid: msg.user_id.toString(),
} }
if (msg.message_type == 'private') { if (msg.message_type == 'private') {
peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))! peer.peerUid = (await ctx.ntUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') { if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
} }
@@ -99,8 +96,8 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
} }
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer) const { sendElements, deleteAfterSentFiles } = await createSendElements(ctx, replyMessage, peer)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) sendMsg(ctx, peer, sendElements, deleteAfterSentFiles, false).catch(e => ctx.logger.error(e))
} }
if (msg.message_type === 'group') { if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
@@ -108,40 +105,38 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
if (!rawMessage) return if (!rawMessage) return
// handle group msg // handle group msg
if (groupMsgQuickAction.delete) { 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) { if (groupMsgQuickAction.kick) {
const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).then().catch(log) ctx.ntGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).catch(e => ctx.logger.error(e))
} }
if (groupMsgQuickAction.ban) { if (groupMsgQuickAction.ban) {
const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.banMember(peer.peerUid, [ ctx.ntGroupApi.banMember(peer.peerUid, [
{ {
uid: msgList[0].senderUid, uid: msgList[0].senderUid,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30, timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
}, },
]).then().catch(log) ]).catch(e => ctx.logger.error(e))
} }
} }
} }
async function handleFriendRequest(request: OB11FriendRequestEvent, async function handleFriendRequest(ctx: Context, request: OB11FriendRequestEvent, quickAction: QuickOperationFriendRequest) {
quickAction: QuickOperationFriendRequest) { if (!isNullable(quickAction.approve)) {
if (!isNull(quickAction.approve)) {
// todo: set remark // 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, async function handleGroupRequest(ctx: Context, request: OB11GroupRequestEvent, quickAction: QuickOperationGroupRequest) {
quickAction: QuickOperationGroupRequest) { if (!isNullable(quickAction.approve)) {
if (!isNull(quickAction.approve)) { ctx.ntGroupApi.handleGroupRequest(
NTQQGroupApi.handleGroupRequest(
request.flag, request.flag,
quickAction.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, quickAction.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
quickAction.reason, quickAction.reason,
).then().catch(log) ).catch(e => ctx.logger.error(e))
} }
} }

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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<any, any> = 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()

View File

@@ -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<any, any> = 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()

View File

@@ -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<any> | 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)
}
}

View File

@@ -1,6 +1,4 @@
// Electron 主进程 与 渲染进程 交互的桥梁 import { CheckVersion, Config } from './common/types'
import { CheckVersion, Config, LLOneBotError } from './common/types'
import { import {
CHANNEL_ERROR, CHANNEL_ERROR,
CHANNEL_GET_CONFIG, CHANNEL_GET_CONFIG,
@@ -9,6 +7,7 @@ import {
CHANNEL_SELECT_FILE, CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
CHANNEL_UPDATE, CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED,
} from './common/channels' } from './common/channels'
const { contextBridge } = require('electron') const { contextBridge } = require('electron')
@@ -24,8 +23,9 @@ const llonebot = {
updateLLOneBot: async (): Promise<boolean> => { updateLLOneBot: async (): Promise<boolean> => {
return ipcRenderer.invoke(CHANNEL_UPDATE) return ipcRenderer.invoke(CHANNEL_UPDATE)
}, },
setConfig: (ask: boolean, config: Config) => { setConfig: async (ask: boolean, config: Config) => {
ipcRenderer.send(CHANNEL_SET_CONFIG, ask, config) const isSuccess = await ipcRenderer.invoke(CHANNEL_SET_CONFIG, ask, config)
if (isSuccess) ipcRenderer.send(CHANNEL_SET_CONFIG_CONFIRMED, config)
}, },
getConfig: async (): Promise<Config> => { getConfig: async (): Promise<Config> => {
return ipcRenderer.invoke(CHANNEL_GET_CONFIG) return ipcRenderer.invoke(CHANNEL_GET_CONFIG)

View File

@@ -2,36 +2,14 @@ import { CheckVersion } from '../common/types'
import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components'
// @ts-ignore // @ts-ignore
import StyleRaw from './style.css?raw' import StyleRaw from './style.css?raw'
import { iconSvg } from './icon'
import { version } from '../version' 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) { function isEmpty(value: unknown) {
return value === undefined || value === null || value === '' return value === undefined || value === null || value === ''
} }
async function onSettingWindowCreated(view: Element) { async function onSettingWindowCreated(view: Element) {
window.llonebot.log('setting window created') //window.llonebot.log('setting window created')
initSideBar()
let config = await window.llonebot.getConfig() let config = await window.llonebot.getConfig()
let ob11Config = { ...config.ob11 } let ob11Config = { ...config.ob11 }