Compare commits

..

16 Commits

Author SHA1 Message Date
idranme
d5875c9e5b Merge pull request #451 from LLOneBot/dev
release: 3.33.9
2024-09-27 16:53:44 +08:00
idranme
a58fb31f8e Merge pull request #448 from LLOneBot/dev
release: 3.33.8
2024-09-26 12:57:16 +08:00
idranme
f9270c38cf Merge pull request #444 from LLOneBot/dev
release: 3.33.7
2024-09-25 14:59:34 +08:00
idranme
e5ab6134cd Merge pull request #441 from LLOneBot/dev
release: 3.33.6
2024-09-23 23:43:50 +08:00
idranme
24f09d485e Merge pull request #438 from LLOneBot/dev
release: 3.33.5
2024-09-22 21:31:55 +08:00
idranme
f400d43b8a Merge pull request #436 from LLOneBot/dev
release: 3.33.4
2024-09-21 23:29:47 +08:00
idranme
1c6364d98f Merge pull request #435 from LLOneBot/dev
release: 3.33.3
2024-09-21 21:52:54 +08:00
idranme
ac07c98ae1 Merge pull request #434 from LLOneBot/dev
release: 3.33.2
2024-09-20 23:00:09 +08:00
idranme
6c66dab3dc Merge pull request #433 from LLOneBot/dev
release: 3.33.1
2024-09-19 18:31:01 +08:00
idranme
936b1d911c Merge pull request #428 from LLOneBot/dev
release: 3.33.0
2024-09-18 20:59:57 +08:00
idranme
1876dd29ac Merge pull request #423 from LLOneBot/dev
release: 3.32.8
2024-09-17 11:59:57 +08:00
idranme
64c5eb6c04 Merge pull request #422 from LLOneBot/dev
release: 3.32.7
2024-09-16 20:48:15 +08:00
idranme
476d498e44 Merge pull request #417 from LLOneBot/dev
release: 3.32.6
2024-09-15 17:48:35 +08:00
idranme
b03bcf9a7c Merge pull request #415 from LLOneBot/dev
release: 3.32.5
2024-09-13 18:59:37 +08:00
idranme
9284fc7e8a Merge pull request #413 from LLOneBot/dev
3.32.4
2024-09-12 18:14:23 +08:00
idranme
1d63473a04 Merge pull request #411 from LLOneBot/dev
3.32.3
2024-09-11 20:52:52 +08:00
133 changed files with 2766 additions and 11447 deletions

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
* text eol=lf
*.png -text
*.jpg -text
*.ico -text
*.gif -text
*.webp -text

View File

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

View File

@@ -3,17 +3,13 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "4.1.3", "version": "3.33.9",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",
"link": "https://github.com/linyuchen" "link": "https://github.com/linyuchen"
},
{
"name": "idranme",
"link": "https://github.com/idranme"
} }
], ],
"repository": { "repository": {

View File

@@ -12,38 +12,36 @@
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"", "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .", "format": "prettier -cw .",
"check": "tsc", "check": "tsc",
"compile:proto": "pbjs --no-create --no-convert --no-delimited --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js profileLikeTip.proto groupNotify.proto message.proto richMedia.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js" "compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.6.0", "@minatojs/driver-sqlite": "^4.6.0",
"@satorijs/element": "^3.1.7",
"@satorijs/protocol": "^1.4.2",
"compare-versions": "^6.1.1",
"cordis": "^3.18.1", "cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.3", "cosmokit": "^1.6.2",
"express": "^5.0.1", "express": "^5.0.0",
"fast-xml-parser": "^4.5.0",
"file-type": "^19.5.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.6.0", "minato": "^3.6.0",
"protobufjs": "^7.4.0", "protobufjs": "^7.4.0",
"silk-wasm": "^3.6.3", "silk-wasm": "^3.6.1",
"ts-case-convert": "^2.1.0",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.0", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.26", "@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",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3", "protobufjs-cli": "^1.1.3",
"typescript": "^5.6.3", "typescript": "^5.6.2",
"vite": "^5.4.10 ", "vite": "^5.4.7",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.5.1" "packageManager": "yarn@4.5.0"
} }

View File

@@ -6,7 +6,7 @@ const manifest = {
type: 'extension', type: 'extension',
name: 'LLOneBot', name: 'LLOneBot',
slug: 'LLOneBot', slug: 'LLOneBot',
description: '实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发', description: '实现 OneBot 11 协议,用于 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [

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

@@ -1,6 +1,6 @@
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { Config, OB11Config, SatoriConfig } from './types' import { Config, OB11Config } from './types'
import { selfInfo, DATA_DIR } from './globalVars' import { selfInfo, DATA_DIR } from './globalVars'
import { mergeNewProperties } from './utils/misc' import { mergeNewProperties } from './utils/misc'
@@ -22,7 +22,6 @@ export class ConfigUtil {
reloadConfig(): Config { reloadConfig(): Config {
const ob11Default: OB11Config = { const ob11Default: OB11Config = {
enable: true,
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: '', httpSecret: '',
@@ -34,24 +33,17 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
listenLocalhost: false, listenLocalhost: false
reportSelfMessage: false
}
const satoriDefault: SatoriConfig = {
enable: true,
port: 5600,
listen: '0.0.0.0',
token: ''
} }
const defaultConfig: Config = { const defaultConfig: Config = {
enableLLOB: true, enableLLOB: true,
satori: satoriDefault,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
token: '', token: '',
enableLocalFile2Url: false, enableLocalFile2Url: false,
debug: false, debug: false,
log: true, log: false,
reportSelfMessage: false,
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
@@ -74,7 +66,6 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http') this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts') this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort') this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
this.checkOldConfig(jsonData.ob11, jsonData, 'reportSelfMessage', 'reportSelfMessage')
this.config = jsonData this.config = jsonData
return this.config return this.config
} }
@@ -88,8 +79,8 @@ export class ConfigUtil {
private checkOldConfig( private checkOldConfig(
currentConfig: OB11Config, currentConfig: OB11Config,
oldConfig: Config, oldConfig: Config,
currentKey: 'httpPort' | 'httpHosts' | 'wsPort' | 'reportSelfMessage', currentKey: 'httpPort' | 'httpHosts' | 'wsPort',
oldKey: 'http' | 'hosts' | 'wsPort' | 'reportSelfMessage', oldKey: 'http' | 'hosts' | 'wsPort',
) { ) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]

View File

@@ -1,5 +1,4 @@
export interface OB11Config { export interface OB11Config {
enable: boolean
httpPort: number httpPort: number
httpHosts: string[] httpHosts: string[]
httpSecret?: string httpSecret?: string
@@ -17,24 +16,21 @@ export interface OB11Config {
*/ */
enableQOAutoQuote?: boolean enableQOAutoQuote?: boolean
listenLocalhost: boolean listenLocalhost: boolean
reportSelfMessage: boolean
} }
export interface SatoriConfig { export interface CheckVersion {
enable: boolean result: boolean
listen: string version: string
port: number
token: string
} }
export interface Config { export interface Config {
enableLLOB: boolean enableLLOB: boolean
satori: SatoriConfig
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
log?: boolean log?: boolean
autoDeleteFile?: boolean autoDeleteFile?: boolean
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
@@ -49,13 +45,6 @@ export interface Config {
hosts?: string[] hosts?: string[]
/** @deprecated */ /** @deprecated */
wsPort?: string wsPort?: string
/** @deprecated */
reportSelfMessage?: boolean
}
export interface CheckVersion {
result: boolean
version: string
} }
export interface LLOneBotError { export interface LLOneBotError {

View File

@@ -53,6 +53,7 @@ function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?
} }
export async function encodeSilk(ctx: Context, filePath: string) { export async function encodeSilk(ctx: Context, filePath: string) {
try {
const file = await fsPromise.readFile(filePath) const file = await fsPromise.readFile(filePath)
if (!isSilk(file)) { if (!isSilk(file)) {
ctx.logger.info(`语音文件${filePath}需要转换成silk`) ctx.logger.info(`语音文件${filePath}需要转换成silk`)
@@ -92,6 +93,10 @@ export async function encodeSilk(ctx: Context, filePath: string) {
duration, duration,
} }
} }
} catch (err) {
ctx.logger.error('convert silk failed', (err as Error).stack)
return {}
}
} }
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'

View File

@@ -4,7 +4,7 @@ import path from 'node:path'
import { TEMP_DIR } from '../globalVars' 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'
import { Context } from 'cordis' import { fileTypeFromFile } from 'file-type'
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
@@ -118,15 +118,12 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(ctx: Context, uri: string, needExt?: boolean): Promise<Uri2LocalRes> { export async function uri2local(uri: string, filename?: string, needExt?: boolean): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri) const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) { if (type === FileUriType.FileURL) {
const fileUri = uri.replace('%', '%25').replace('#', '%23') const filePath = fileURLToPath(uri)
const filePath = fileURLToPath(fileUri)
const fileName = path.basename(filePath) const fileName = path.basename(filePath)
// console.log('fileURLToPath', filePath)
// console.log('fileName', fileName)
return { success: true, errMsg: '', fileName, path: filePath, isLocal: true } return { success: true, errMsg: '', fileName, path: filePath, isLocal: true }
} }
@@ -139,16 +136,15 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
try { try {
const res = await fetchFile(uri) const res = await fetchFile(uri)
const match = res.url.match(/.+\/([^/?]*)(?=\?)?/) const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
let filename: string
if (match?.[1]) { if (match?.[1]) {
filename = match[1].replace(/[/\\:*?"<>|]/g, '_') filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_')
} else { } else {
filename = randomUUID() filename ??= randomUUID()
} }
let filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data) await fsPromise.writeFile(filePath, res.data)
if (needExt && !path.extname(filePath)) { if (needExt && !path.extname(filePath)) {
const ext = (await ctx.ntFileApi.getFileType(filePath)).ext const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`
@@ -161,12 +157,12 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
} }
if (type === FileUriType.OneBotBase64) { if (type === FileUriType.OneBotBase64) {
let filename = randomUUID() filename ??= randomUUID()
let filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '') const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) { if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(filePath)).ext const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`
@@ -178,12 +174,12 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri) const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
if (capture) { if (capture) {
let filename = randomUUID() filename ??= randomUUID()
const [, _type, base64] = capture const [, _type, base64] = capture
let filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) { if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(filePath)).ext const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`

View File

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

View File

@@ -3,19 +3,25 @@ import { writeFile } from 'node:fs/promises'
import { version } from '../../version' import { version } from '../../version'
import { log, fetchFile } from '.' import { log, fetchFile } from '.'
import { TEMP_DIR } from '../globalVars' import { TEMP_DIR } from '../globalVars'
import { compare } from 'compare-versions'
const downloadMirrorHosts = ['https://ghp.ci/'] const downloadMirrorHosts = ['https://ghp.ci/']
const releasesMirrorHosts = ['https://kkgithub.com'] const releasesMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersion = await getRemoteVersion() const latestVersionText = await getRemoteVersion()
const latestVersion = latestVersionText.split('.')
log('LLOneBot latest version', latestVersion) log('LLOneBot latest version', latestVersion)
if (latestVersion === '') { const currentVersion = version.split('.')
return { result: false, version: latestVersion } //log('llonebot current version', currentVersion)
for (const k of [0, 1, 2]) {
const latest = parseInt(latestVersion[k])
const current = parseInt(currentVersion[k])
if (latest > current) {
log('')
return { result: true, version: latestVersionText }
} else if (latest < current) {
break
} }
if (compare(latestVersion, version, '>')) {
return { result: true, version: latestVersion }
} }
return { result: false, version: version } return { result: false, version: version }
} }

View File

@@ -28,7 +28,7 @@ export default class Log {
}, },
} }
Logger.targets.push(target) Logger.targets.push(target)
ctx.on('llob/config-updated', input => { ctx.on('llonebot/config-updated', input => {
enable = input.log! enable = input.log!
}) })
} }

View File

@@ -2,10 +2,6 @@ import path from 'node:path'
import Log from './log' import Log from './log'
import Core from '../ntqqapi/core' import Core from '../ntqqapi/core'
import OneBot11Adapter from '../onebot11/adapter' import OneBot11Adapter from '../onebot11/adapter'
import SatoriAdapter from '../satori/adapter'
import Database from 'minato'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { import {
@@ -15,12 +11,15 @@ import {
CHANNEL_LOG, CHANNEL_LOG,
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 { startHook } from '../ntqqapi/hook' import { getBuildVersion } from '../common/utils'
import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { getSession } from '../ntqqapi/wrapper'
import { Context } from 'cordis' import { Context } from 'cordis'
import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars' import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars'
import { log, logFileName } from '../common/utils/legacyLog' import { log, logFileName } from '../common/utils/legacyLog'
@@ -34,11 +33,15 @@ import {
NTQQWebApi, NTQQWebApi,
NTQQWindowApi NTQQWindowApi
} from '../ntqqapi/api' } from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises'
import { existsSync, mkdirSync } from 'node:fs' import { existsSync, mkdirSync } from 'node:fs'
import Database from 'minato'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
declare module 'cordis' { declare module 'cordis' {
interface Events { interface Events {
'llob/config-updated': (input: LLOBConfig) => void 'llonebot/config-updated': (input: LLOBConfig) => void
} }
} }
@@ -54,29 +57,6 @@ function onLoad() {
mkdirSync(LOG_DIR) mkdirSync(LOG_DIR)
} }
if (!existsSync(TEMP_DIR)) {
mkdirSync(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
mkdirSync(dbDir)
}
const ctx = new Context()
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Database)
let started = false
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => { ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
return checkNewVersion() return checkNewVersion()
}) })
@@ -135,9 +115,7 @@ function onLoad() {
if (!ask) { if (!ask) {
getConfigUtil().setConfig(config) getConfigUtil().setConfig(config)
log('配置已更新', config) log('配置已更新', config)
if (started) { checkFfmpeg(config.ffmpeg).then()
ctx.parallel('llob/config-updated', config)
}
resolve(true) resolve(true)
return return
} }
@@ -154,9 +132,7 @@ function onLoad() {
if (result.response === 0) { if (result.response === 0) {
getConfigUtil().setConfig(config) getConfigUtil().setConfig(config)
log('配置已更新', config) log('配置已更新', config)
if (started) { checkFfmpeg(config.ffmpeg).then()
ctx.parallel('llob/config-updated', config)
}
resolve(true) resolve(true)
} }
}) })
@@ -171,65 +147,87 @@ function onLoad() {
log(arg) log(arg)
}) })
const intervalId = setInterval(async () => { async function start() {
const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
online: true
})
if (self.uin) {
clearInterval(intervalId)
log('process pid', process.pid) log('process pid', process.pid)
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
if (config.enableLLOB && (config.satori.enable || config.ob11.enable)) {
startHook()
await ctx.sleep(600)
} else {
llonebotError.otherError = 'LLOneBot 未启动' llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭,不启动 LLOneBot') log('LLOneBot 开关设置为关闭不启动LLOneBot')
return return
} }
if (!existsSync(TEMP_DIR)) {
await mkdir(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
await mkdir(dbDir)
}
const ctx = new Context()
ctx.plugin(Log, { ctx.plugin(Log, {
enable: config.log!, enable: config.log!,
filename: logFileName filename: logFileName
}) })
ctx.plugin(SQLiteDriver, { ctx.plugin(NTQQFileApi)
path: path.join(dbDir, `${selfInfo.uin}.db`) ctx.plugin(NTQQFileCacheApi)
}) ctx.plugin(NTQQFriendApi)
ctx.plugin(Store, { ctx.plugin(NTQQGroupApi)
msgCacheExpire: config.msgCacheExpire! * 1000 ctx.plugin(NTQQMsgApi)
}) ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config) ctx.plugin(Core, config)
if (config.ob11.enable) {
ctx.plugin(OneBot11Adapter, { ctx.plugin(OneBot11Adapter, {
...config.ob11, ...config.ob11,
heartInterval: config.heartInterval, heartInterval: config.heartInterval,
token: config.token!, token: config.token!,
debug: config.debug!, debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
musicSignUrl: config.musicSignUrl, musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!, enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg, ffmpeg: config.ffmpeg,
}) })
} ctx.plugin(Database)
if (config.satori.enable) { ctx.plugin(SQLiteDriver, {
ctx.plugin(SatoriAdapter, { path: path.join(dbDir, `${selfInfo.uin}.db`)
...config.satori, })
ffmpeg: config.ffmpeg, ctx.plugin(Store)
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config)
}) })
} }
ctx.start() const buildVersion = getBuildVersion()
started = true
llonebotError.otherError = '' const intervalId = setInterval(() => {
const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
online: true
})
if (self.uin && (buildVersion >= 27187 || getSession())) {
clearInterval(intervalId)
start()
} }
}, 500) }, 600)
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (![2, 4, 6].includes(window.id)) {
return
}
if (window.id === 2) {
mainWindow = window
}
//log('window create', window.webContents.getURL().toString())
try {
hookNTQQApiCall(window, window.id !== 2)
hookNTQQApiReceive(window, window.id !== 2)
} catch (e) {
log('LLOneBot hook error: ', String(e))
}
} }
try { try {

View File

@@ -1,4 +1,4 @@
import { Peer, RawMessage } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
import { FileCacheV2 } from '@/common/types' import { FileCacheV2 } from '@/common/types'
@@ -24,15 +24,13 @@ interface MsgInfo {
peer: Peer peer: Peer
} }
class Store extends Service { export default class Store extends Service {
static inject = ['database', 'model'] static inject = ['database', 'model']
private cache: LimitedHashTable<string, number> private cache: LimitedHashTable<string, number>
private messages: Map<string, RawMessage>
constructor(protected ctx: Context, public config: Store.Config) { constructor(protected ctx: Context) {
super(ctx, 'store', true) super(ctx, 'store', true)
this.cache = new LimitedHashTable(1000) this.cache = new LimitedHashTable(1000)
this.messages = new Map()
this.initDatabase() this.initDatabase()
} }
@@ -62,10 +60,9 @@ class Store extends Service {
} }
createMsgShortId(peer: Peer, msgId: string): number { createMsgShortId(peer: Peer, msgId: string): number {
// OneBot 11 要求 message_id 为 int32
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}` const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(cacheKey).digest() const hash = createHash('md5').update(cacheKey).digest()
hash[0] &= 0x7f //保证shortId为正数 hash[0] &= 0x7f //设置第一个bit为0 保证shortId为正数
const shortId = hash.readInt32BE() const shortId = hash.readInt32BE()
this.cache.set(cacheKey, shortId) this.cache.set(cacheKey, shortId)
this.ctx.database.upsert('message', [{ this.ctx.database.upsert('message', [{
@@ -126,29 +123,4 @@ class Store extends Service {
getFileCacheById(fileUuid: string) { getFileCacheById(fileUuid: string) {
return this.ctx.database.get('file_v2', { fileUuid }) return this.ctx.database.get('file_v2', { fileUuid })
} }
async addMsgCache(msg: RawMessage) {
const expire = this.config.msgCacheExpire
if (expire === 0) {
return
}
const id = msg.msgId
this.messages.set(id, msg)
setTimeout(() => {
this.messages.delete(id)
}, expire)
}
getMsgCache(msgId: string) {
return this.messages.get(msgId)
}
} }
namespace Store {
export interface Config {
/** 单位为毫秒 */
msgCacheExpire: number
}
}
export default Store

View File

@@ -16,8 +16,10 @@ import path from 'node:path'
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { RkeyManager } from '@/ntqqapi/helper/rkey' import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { RichMediaDownloadCompleteNotify, RichMediaUploadCompleteNotify, RMBizType, Peer } from '@/ntqqapi/types/msg' import { getSession } from '@/ntqqapi/wrapper'
import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import { copyFile, stat, unlink } from 'node:fs/promises' import { copyFile, stat, unlink } from 'node:fs/promises'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@@ -37,7 +39,17 @@ export class NTQQFileApi extends Service {
this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey') this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey')
} }
async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string | undefined> { async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession()
if (session) {
return (await session.getRichMediaService().getVideoPlayUrlV2(
peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 }
)).urlResult.domainUrl[0]?.url
} else {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{ const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer, peer,
msgId, msgId,
@@ -46,24 +58,20 @@ export class NTQQFileApi extends Service {
exParams: { exParams: {
downSourceType: 1, downSourceType: 1,
triggerType: 1 triggerType: 1
} },
}]) }, null])
if (data.result !== 0) { if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data) this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url return data.urlResult.domainUrl[0]?.url
} }
async getFileType(filePath: string) {
return await invoke<{
ext: string
mime: string
}>(NTMethod.FILE_TYPE, [filePath], {
className: NTClass.FS_API
})
} }
/** 上传文件到 QQ 的文件夹 */ async getFileType(filePath: string) {
return fileTypeFromFile(filePath)
}
// 上传文件到QQ的文件夹
async uploadFile(filePath: string, elementType = ElementType.Pic, elementSubType = 0) { async uploadFile(filePath: string, elementType = ElementType.Pic, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let fileName = path.basename(filePath) let fileName = path.basename(filePath)
@@ -113,9 +121,10 @@ export class NTQQFileApi extends Service {
return sourcePath return sourcePath
} }
} }
const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>( const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
'nodeIKernelMsgService/downloadRichMedia', 'nodeIKernelMsgService/downloadRichMedia',
[{ [
{
getReq: { getReq: {
fileModelId: '0', fileModelId: '0',
downloadSourceType: 0, downloadSourceType: 0,
@@ -128,7 +137,9 @@ export class NTQQFileApi extends Service {
downloadType: 1, downloadType: 1,
filePath: thumbPath, filePath: thumbPath,
}, },
}], },
null,
],
{ {
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId, cmdCB: payload => payload.notifyInfo.msgId === msgId,
@@ -163,8 +174,8 @@ export class NTQQFileApi extends Service {
if (url) { if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = parsedUrl.searchParams.get('appid') const imageAppid = parsedUrl.searchParams.get('appid')
const isNTPic = imageAppid && ['1406', '1407'].includes(imageAppid) const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNTPic) { if (isNewPic) {
let rkey = parsedUrl.searchParams.get('rkey') let rkey = parsedUrl.searchParams.get('rkey')
if (rkey) { if (rkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
@@ -185,13 +196,16 @@ export class NTQQFileApi extends Service {
} }
async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) { async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) {
const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>( const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
'nodeIKernelRichMediaService/downloadFileForModelId', 'nodeIKernelRichMediaService/downloadFileForModelId',
[{ [
{
peer, peer,
fileModelIdList: [fileModelId], fileModelIdList: [fileModelId],
save_path: '' save_path: ''
}], },
null,
],
{ {
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.fileModelId === fileModelId, cmdCB: payload => payload.notifyInfo.fileModelId === fileModelId,
@@ -201,38 +215,6 @@ export class NTQQFileApi extends Service {
) )
return data.notifyInfo.filePath return data.notifyInfo.filePath
} }
async ocrImage(path: string) {
return await invoke(
'nodeIKernelNodeMiscService/wantWinScreenOCR',
[
{ url: path },
{ timeout: 5000 }
]
)
}
async uploadRMFileWithoutMsg(filePath: string, bizType: RMBizType, peerUid: string) {
const data = await invoke<{
notifyInfo: RichMediaUploadCompleteNotify
}>(
'nodeIKernelRichMediaService/uploadRMFileWithoutMsg',
[{
params: {
filePath,
bizType,
peerUid,
useNTV2: true
}
}],
{
cbCmd: ReceiveCmdS.MEDIA_UPLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.filePath === filePath,
timeout: 10 * Time.second,
}
)
return data.notifyInfo
}
} }
export class NTQQFileCacheApi extends Service { export class NTQQFileCacheApi extends Service {
@@ -241,19 +223,21 @@ export class NTQQFileCacheApi extends Service {
} }
async setCacheSilentScan(isSilent: boolean = true) { async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }]) return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }, null])
} }
getCacheSessionPathList() { getCacheSessionPathList() {
return invoke<Array<{ return invoke<
{
key: string key: string
value: string value: string
}>>(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API }) }[]
>(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
} }
scanCache() { scanCache() {
invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { registerEvent: true }) invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { registerEvent: true })
return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [], { timeout: 300 * Time.second }) return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second })
} }
getHotUpdateCachePath() { getHotUpdateCachePath() {
@@ -273,13 +257,13 @@ export class NTQQFileCacheApi extends Service {
pageSize: pageSize, pageSize: pageSize,
order: 1, order: 1,
lastRecord: _lastRecord, lastRecord: _lastRecord,
}]) }, null])
} }
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{ return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{
chats, chats,
fileKeys, fileKeys,
}]) }, null])
} }
} }

View File

@@ -1,6 +1,8 @@
import { Friend, SimpleInfo, CategoryFriend } from '../types' import { Friend, FriendV2, SimpleInfo, CategoryFriend, BuddyListReqType } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper'
import { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
declare module 'cordis' { declare module 'cordis' {
@@ -16,22 +18,38 @@ export class NTQQFriendApi extends Service {
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() { async getFriends() {
const res = await invoke<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
categroyName: string categroyName: string
categroyMbCount: number categroyMbCount: number
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>('getBuddyList', [], { }>(
'getBuddyList',
[],
{
className: NTClass.NODE_STORE_API, className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false afterFirstCmd: false,
}) }
return res.data.flatMap(e => e.buddyList) )
const _friends: Friend[] = []
for (const item of data.data) {
_friends.push(...item.buddyList)
}
return _friends
} }
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) { async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
const session = getSession()
if (session) {
return session.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
} else {
return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{ return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
approvalInfo: { approvalInfo: {
friendUid, friendUid,
@@ -40,8 +58,9 @@ export class NTQQFriendApi extends Service {
}, },
}]) }])
} }
}
async getBuddyV2(refresh = false): Promise<SimpleInfo[]> { async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
@@ -58,7 +77,7 @@ export class NTQQFriendApi extends Service {
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!)) return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
} }
/** uid -> uin */ /** uid => uin */
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> { async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map() const retMap: Map<string, string> = new Map()
const data = await invoke<{ const data = await invoke<{
@@ -99,27 +118,22 @@ export class NTQQFriendApi extends Service {
} }
async isBuddy(uid: string): Promise<boolean> { async isBuddy(uid: string): Promise<boolean> {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }]) const session = getSession()
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
}
} }
async getBuddyRecommendContact(uin: string) { async getBuddyRecommendContact(uin: string) {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }]) const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }, null])
return ret.arkMsg return ret.arkMsg
} }
async setBuddyRemark(uid: string, remark: string) { async setBuddyRemark(uid: string, remark: string) {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{ return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark } remarkParams: { uid, remark }
}]) }, null])
}
async delBuddy(friendUid: string) {
return await invoke('nodeIKernelBuddyService/delBuddy', [{
delInfo: {
friendUid,
tempBlock: false,
tempBothDel: true
}
}])
} }
} }

View File

@@ -8,13 +8,15 @@ import {
GetFileListParam, GetFileListParam,
PublishGroupBulletinReq, PublishGroupBulletinReq,
GroupAllInfo, GroupAllInfo,
GroupFileInfo, GroupFileInfo
GroupBulletinListResult
} from '../types' } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window' import { NTQQWindows } from './window'
import { getSession } from '../wrapper'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@@ -25,6 +27,8 @@ declare module 'cordis' {
export class NTQQGroupApi extends Service { export class NTQQGroupApi extends Service {
static inject = ['ntWindowApi'] static inject = ['ntWindowApi']
public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true) super(ctx, 'ntGroupApi', true)
} }
@@ -45,45 +49,54 @@ export class NTQQGroupApi extends Service {
return result.groupList return result.groupList
} }
async getGroupMembers(groupCode: string, num = 3000) { async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ const session = getSession()
groupCode, let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
scene: 'groupMemberList_MainWindow' if (session) {
}]) const groupService = session.getGroupService()
const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }]) const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow')
if (data.errCode !== 0) { result = await groupService.getNextMemberList(sceneId, undefined, num)
throw new Error('获取群成员列表出错,' + data.errMsg) } else {
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
} }
return data.result.infos if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg)
}
return result.result.infos
} }
async getGroupMember(groupCode: string, uid: string, forceUpdate = false) { async getGroupMember(groupCode: string, memberUinOrUid: string) {
await invoke('nodeIKernelGroupListener/onMemberInfoChange', [], { if (!this.groupMembers.has(groupCode)) {
registerEvent: true try {
}) // 更新群成员列表
this.groupMembers.set(groupCode, await this.getGroupMembers(groupCode))
const data = await invoke<{
groupCode: string
members: Map<string, GroupMember>
}>(
'nodeIKernelGroupService/getMemberInfo',
[{
groupCode,
uids: [uid],
forceUpdate
}],
{
cbCmd: 'nodeIKernelGroupListener/onMemberInfoChange',
afterFirstCmd: false,
cmdCB: payload => payload.members.has(uid),
timeout: 2000
} }
) catch (e) {
return data.members.get(uid)! return
}
}
let members = this.groupMembers.get(groupCode)!
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) {
member = Array.from(members.values()).find(member => member.uin === memberUinOrUid)
} else {
member = members.get(memberUinOrUid)
}
return member
}
let member = getMember()
if (!member) {
this.groupMembers.set(groupCode, await this.getGroupMembers(groupCode))
members = this.groupMembers.get(groupCode)!
member = getMember()
}
return member
} }
async getGroupIgnoreNotifies() { async getGroupIgnoreNotifies() {
await this.getSingleScreenNotifies(false, 14) await this.getSingleScreenNotifies(14)
return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>( return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow, NTQQWindows.GroupNotifyFilterWindow,
[], [],
@@ -91,18 +104,16 @@ export class NTQQGroupApi extends Service {
) )
} }
async getSingleScreenNotifies(doubt: boolean, number: number, startSeq = '') { async getSingleScreenNotifies(num: number) {
await invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true }) invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true })
return (await invoke<GroupNotifies>(
const data = await invoke<GroupNotifies>(
'nodeIKernelGroupService/getSingleScreenNotifies', 'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt, startSeq, number }], [{ doubt: false, startSeq: '', number: num }, null],
{ {
cbCmd: ReceiveCmdS.GROUP_NOTIFY, cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false, afterFirstCmd: false,
} }
) )).notifies
return data.notifies
} }
async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
@@ -110,9 +121,20 @@ export class NTQQGroupApi extends Service {
const groupCode = flagitem[0] const groupCode = flagitem[0]
const seq = flagitem[1] const seq = flagitem[1]
const type = parseInt(flagitem[2]) const type = parseInt(flagitem[2])
const doubt = flagitem[3] === '1' const session = getSession()
if (session) {
return session.getGroupService().operateSysNotify(false, {
operateType, // 2 拒绝
targetMsg: {
seq, // 通知序列号
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
} else {
return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{ return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
doubt, doubt: false,
operateMsg: { operateMsg: {
operateType, operateType,
targetMsg: { targetMsg: {
@@ -122,43 +144,88 @@ export class NTQQGroupApi extends Service {
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}, },
}, },
}]) }, null])
}
} }
async quitGroup(groupCode: string) { async quitGroup(groupCode: string) {
return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }]) const session = getSession()
if (session) {
return session.getGroupService().quitGroup(groupCode)
} else {
return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }, null])
}
} }
async kickMember(groupCode: string, kickUids: string[], refuseForever = false, kickReason = '') { async kickMember(groupCode: string, kickUids: string[], refuseForever = false, kickReason = '') {
const session = getSession()
if (session) {
return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
} else {
return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }]) return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
} }
}
/** timeStamp为秒数, 0为解除禁言 */
async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) { async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
const session = getSession()
if (session) {
return session.getGroupService().setMemberShutUp(groupCode, memList)
} else {
return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }]) return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
} }
}
async banGroup(groupCode: string, shutUp: boolean) { async banGroup(groupCode: string, shutUp: boolean) {
return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }]) const session = getSession()
if (session) {
return session.getGroupService().setGroupShutUp(groupCode, shutUp)
} else {
return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }, null])
}
} }
async setMemberCard(groupCode: string, memberUid: string, cardName: string) { async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }]) const session = getSession()
if (session) {
return session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName)
} else {
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }, null])
}
} }
async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) { async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }]) const session = getSession()
if (session) {
return session.getGroupService().modifyMemberRole(groupCode, memberUid, role)
} else {
return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }, null])
}
} }
async setGroupName(groupCode: string, groupName: string) { async setGroupName(groupCode: string, groupName: string) {
return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }]) const session = getSession()
if (session) {
return session.getGroupService().modifyGroupName(groupCode, groupName, false)
} else {
return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }, null])
}
} }
async getGroupRemainAtTimes(groupCode: string) { async getGroupRemainAtTimes(groupCode: string) {
return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }]) return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null])
} }
async removeGroupEssence(groupCode: string, msgId: string) { async removeGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().removeGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')! const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false) const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/removeGroupEssence', [{ return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
@@ -167,10 +234,20 @@ export class NTQQGroupApi extends Service {
msgRandom: Number(data?.msgList[0].msgRandom), msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq) msgSeq: Number(data?.msgList[0].msgSeq)
} }
}]) }, null])
}
} }
async addGroupEssence(groupCode: string, msgId: string) { async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().addGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')! const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false) const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/addGroupEssence', [{ return await invoke('nodeIKernelGroupService/addGroupEssence', [{
@@ -179,29 +256,33 @@ export class NTQQGroupApi extends Service {
msgRandom: Number(data?.msgList[0].msgRandom), msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq) msgSeq: Number(data?.msgList[0].msgSeq)
} }
}]) }, null])
}
} }
async createGroupFileFolder(groupId: string, folderName: string) { async createGroupFileFolder(groupId: string, folderName: string) {
return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }]) return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }, null])
} }
async deleteGroupFileFolder(groupId: string, folderId: string) { async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }]) return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null])
} }
async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) { async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }]) return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }, null])
} }
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) { async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { registerEvent: true }) invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { registerEvent: true })
const data = await invoke<{ fileInfo: GroupFileInfo }>( const data = await invoke<{ fileInfo: GroupFileInfo }>(
'nodeIKernelRichMediaService/getGroupFileList', 'nodeIKernelRichMediaService/getGroupFileList',
[{ [
{
groupId, groupId,
fileListForm fileListForm
}], },
null,
],
{ {
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate', cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false, afterFirstCmd: false,
@@ -214,17 +295,17 @@ export class NTQQGroupApi extends Service {
async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) { async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
const ntUserApi = this.ctx.get('ntUserApi')! const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')! const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }]) return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }, null])
} }
async uploadGroupBulletinPic(groupCode: string, path: string) { async uploadGroupBulletinPic(groupCode: string, path: string) {
const ntUserApi = this.ctx.get('ntUserApi')! const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')! const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }]) return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }, null])
} }
async getGroupRecommendContact(groupCode: string) { async getGroupRecommendContact(groupCode: string) {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }]) const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null])
return ret.arkJson return ret.arkJson
} }
@@ -235,7 +316,7 @@ export class NTQQGroupApi extends Service {
msgSeq: +msgSeq, msgSeq: +msgSeq,
msgRandom: +msgRandom msgRandom: +msgRandom
} }
}]) }, null])
} }
async getGroupHonorList(groupCode: string) { async getGroupHonorList(groupCode: string) {
@@ -244,101 +325,26 @@ export class NTQQGroupApi extends Service {
req: { req: {
groupCode: [+groupCode] groupCode: [+groupCode]
} }
}]) }, null])
} }
async getGroupAllInfo(groupCode: string) { async getGroupAllInfo(groupCode: string, timeout = 1000) {
invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], { invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], { registerEvent: true })
registerEvent: true
})
return await invoke<{ groupAll: GroupAllInfo }>( return await invoke<{ groupAll: GroupAllInfo }>(
'nodeIKernelGroupService/getGroupAllInfo', 'nodeIKernelGroupService/getGroupAllInfo',
[{ [
{
groupCode, groupCode,
source: 4 source: 4
}], },
null
],
{ {
cbCmd: 'nodeIKernelGroupListener/onGroupAllInfoChange', cbCmd: 'nodeIKernelGroupListener/onGroupAllInfoChange',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => payload.groupAll.groupCode === groupCode cmdCB: payload => payload.groupAll.groupCode === groupCode,
timeout
} }
) )
} }
async getGroupBulletinList(groupCode: string) {
invoke('nodeIKernelGroupListener/onGetGroupBulletinListResult', [], {
registerEvent: true
})
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke<{
groupCode: string
context: string
result: GroupBulletinListResult
}>(
'nodeIKernelGroupService/getGroupBulletinList',
[{
groupCode,
psKey,
context: '',
req: {
startIndex: -1,
num: 20,
needInstructionsForJoinGroup: 1,
needPublisherInfo: 1
}
}],
{
cbCmd: 'nodeIKernelGroupListener/onGetGroupBulletinListResult',
cmdCB: payload => payload.groupCode === groupCode,
afterFirstCmd: false
}
)
}
async setGroupAvatar(groupCode: string, path: string) {
return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }])
}
async searchMember(groupCode: string, keyword: string) {
await invoke('nodeIKernelGroupListener/onSearchMemberChange', [], {
registerEvent: true
})
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
groupCode,
scene: 'groupMemberList_MainWindow'
}])
const data = await invoke<{
sceneId: string
keyword: string
infos: Map<string, GroupMember>
}>(
'nodeIKernelGroupService/searchMember',
[{ sceneId, keyword }],
{
cbCmd: 'nodeIKernelGroupListener/onSearchMemberChange',
cmdCB: payload => {
return payload.sceneId === sceneId && payload.keyword === keyword
},
afterFirstCmd: false
}
)
return data.infos
}
async getGroupFileCount(groupId: string) {
return await invoke(
'nodeIKernelRichMediaService/batchGetGroupFileCount',
[{ groupIds: [groupId] }]
)
}
async getGroupFileSpace(groupId: string) {
return await invoke(
'nodeIKernelRichMediaService/getGroupSpace',
[{ groupId }]
)
}
} }

View File

@@ -1,5 +1,7 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType } from '../types'
import { getSession } from '@/ntqqapi/wrapper'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -17,80 +19,98 @@ export class NTQQMsgApi extends Service {
} }
async getTempChatInfo(chatType: ChatType, peerUid: string) { async getTempChatInfo(chatType: ChatType, peerUid: string) {
return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }]) const session = getSession()
if (session) {
return session.getMsgService().getTempChatInfo(chatType, peerUid)
} else {
return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
}
} }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean) { async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) {
// nt_qq/global/nt_data/Emoji/emoji-resource/sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq/global/nt_data/Emoji/emoji-resource/face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
const session = getSession()
const emojiType = emojiId.length > 3 ? '2' : '1' const emojiType = emojiId.length > 3 ? '2' : '1'
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }]) if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiType, setEmoji)
} else {
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
}
} }
async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }]) const session = getSession()
if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }, null])
}
} }
async activateChat(peer: Peer) { async activateChat(peer: Peer) {
return await invoke(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }]) return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }, null])
} }
async activateChatAndGetHistory(peer: Peer, cnt: number) { async activateChatAndGetHistory(peer: Peer) {
// 消息从旧到新 return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null])
return await invoke(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt, msgId: '0', queryOrder: true }])
} }
async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) { async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }]) return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null])
} }
async getMsgsByMsgId(peer: Peer, msgIds: string[]) { 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')
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }]) const session = getSession()
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
}
} }
async getMsgHistory(peer: Peer, msgId: string, cnt: number, queryOrder = false) { async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) {
// 默认情况下消息时间从新到旧 const session = getSession()
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder }]) // 消息时间从旧到新
if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else {
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
}
} }
async recallMsg(peer: Peer, msgIds: string[]) { async recallMsg(peer: Peer, msgIds: string[]) {
return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }]) const session = getSession()
if (session) {
return session.getMsgService().recallMsg(peer, msgIds)
} else {
return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
}
} }
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) { async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
const uniqueId = await this.generateMsgUniqueId(peer.chatType) const msgId = await this.generateMsgUniqueId(peer.chatType)
const msgAttributeInfos = new Map() peer.guildId = msgId
msgAttributeInfos.set(0, {
attrType: 0,
attrId: uniqueId,
vasMsgInfo: {
msgNamePlateInfo: {},
bubbleInfo: {},
avatarPendantInfo: {},
vasFont: {},
iceBreakInfo: {}
}
})
let sentMsgId: string
const data = await invoke<{ msgList: RawMessage[] }>( const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/sendMsg', 'nodeIKernelMsgService/sendMsg',
[{ [
{
msgId: '0', msgId: '0',
peer, peer,
msgElements, msgElements,
msgAttributeInfos msgAttributeInfos: new Map()
}], },
null
],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if (msgRecord.msgAttrs.get(0)?.attrId === uniqueId && msgRecord.sendStatus === 2) { if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
sentMsgId = msgRecord.msgId
return true return true
} }
} }
@@ -99,39 +119,23 @@ export class NTQQMsgApi extends Service {
timeout timeout
} }
) )
return data.msgList.find(msgRecord => msgRecord.guildId === msgId)
return data.msgList.find(msgRecord => msgRecord.msgId === sentMsgId)
} }
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const uniqueId = await this.generateMsgUniqueId(destPeer.chatType) const session = getSession()
destPeer.guildId = uniqueId if (session) {
const data = await invoke<{ msgList: RawMessage[] }>( return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
'nodeIKernelMsgService/forwardMsgWithComment', } else {
[{ return await invoke(NTMethod.FORWARD_MSG, [{
msgIds, msgIds,
srcContact: srcPeer, srcContact: srcPeer,
dstContacts: [destPeer], dstContacts: [destPeer],
commentElements: [], commentElements: [],
msgAttributeInfos: new Map(), msgAttributeInfos: new Map(),
}], }, null])
{
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true
} }
} }
return false
},
timeout: 3000
}
)
delete destPeer.guildId
return data.msgList.filter(msgRecord => msgRecord.guildId === uniqueId)
}
async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await this.ctx.ntUserApi.getSelfNick(true) const senderShowName = await this.ctx.ntUserApi.getSelfNick(true)
@@ -141,46 +145,66 @@ export class NTQQMsgApi extends Service {
const selfUid = selfInfo.uid const selfUid = selfInfo.uid
const data = await invoke<{ msgList: RawMessage[] }>( const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/multiForwardMsgWithComment', 'nodeIKernelMsgService/multiForwardMsgWithComment',
[{ [
{
msgInfos, msgInfos,
srcContact: srcPeer, srcContact: srcPeer,
dstContact: destPeer, dstContact: destPeer,
commentElements: [], commentElements: [],
msgAttributeInfos: new Map(), msgAttributeInfos: new Map(),
}], },
null,
],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if ( if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
msgRecord.msgType === 11 &&
msgRecord.subMsgType === 7 &&
msgRecord.peerUid === destPeer.peerUid &&
msgRecord.senderUid === selfUid
) {
const element = msgRecord.elements[0]
const data = JSON.parse(element.arkElement!.bytesData)
if (data.app !== 'com.tencent.multimsg' || !data.meta.detail.resid) {
continue
}
return true return true
} }
} }
return false return false
} },
} }
) )
return data.msgList.find(msgRecord => { for (const msg of data.msgList) {
const { arkElement } = msgRecord.elements[0] const arkElement = msg.elements.find(ele => ele.arkElement)
if (arkElement?.bytesData.includes('com.tencent.multimsg')) { if (!arkElement) {
return true continue
}
const forwardData = JSON.parse(arkElement.arkElement!.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
continue
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) {
return msg
}
}
throw new Error('转发消息超时')
}
async getMsgsBySeqAndCount(peer: Peer, msgSeq: string, count: number, desc: boolean, z: boolean) {
const session = getSession()
if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, msgSeq, count, desc, z)
} else {
return await invoke('nodeIKernelMsgService/getMsgsBySeqAndCount', [{
peer,
cnt: count,
msgSeq,
queryOrder: desc
}, null])
} }
})!
} }
async getSingleMsg(peer: Peer, msgSeq: string) { async getSingleMsg(peer: Peer, msgSeq: string) {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }]) const session = getSession()
if (session) {
return await session.getMsgService().getSingleMsg(peer, msgSeq)
} else {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
}
} }
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) { async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
@@ -198,7 +222,7 @@ export class NTQQMsgApi extends Service {
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: 1, pageLimit: 1,
} }
}]) }, null])
} }
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) { async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) {
@@ -216,11 +240,11 @@ export class NTQQMsgApi extends Service {
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: 1, pageLimit: 1,
} }
}]) }, null])
} }
async setMsgRead(peer: Peer) { async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }]) return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null])
} }
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) { async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
@@ -230,7 +254,7 @@ export class NTQQMsgApi extends Service {
emojiId, emojiId,
emojiType, emojiType,
cnt: count cnt: count
}]) }, null])
} }
async fetchFavEmojiList(count: number) { async fetchFavEmojiList(count: number) {
@@ -239,12 +263,11 @@ export class NTQQMsgApi extends Service {
count, count,
backwardFetch: true, backwardFetch: true,
forceRefresh: true forceRefresh: true
}]) }, null])
} }
async generateMsgUniqueId(chatType: number) { async generateMsgUniqueId(chatType: number) {
const time = await this.getServerTime() const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }])
const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType, time }])
if (typeof uniqueId === 'string') { if (typeof uniqueId === 'string') {
return uniqueId return uniqueId
} else { } else {
@@ -252,36 +275,4 @@ export class NTQQMsgApi extends Service {
return `${Date.now()}${random}` return `${Date.now()}${random}`
} }
} }
async queryMsgsById(chatType: ChatType, msgId: string) {
const msgTime = this.getMsgTimeFromId(msgId)
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId,
msgTime: '0',
msgSeq: '0',
params: {
chatInfo: {
peerUid: '',
chatType
},
filterMsgToTime: msgTime,
filterMsgFromTime: msgTime,
isIncludeCurrent: true,
pageLimit: 1,
}
}])
}
getMsgTimeFromId(msgId: string) {
// 小概率相差1毫秒
return String(BigInt(msgId) >> 32n)
}
async getServerTime() {
return await invoke('nodeIKernelMSFService/getServerTime', [])
}
async fetchUnitedCommendConfig(groups: string[]) {
return await invoke('nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', [{ groups }])
}
} }

View File

@@ -1,8 +1,9 @@
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfo, UserDetailSource, ProfileBizType, SimpleInfo } from '../types' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg, UserDetailSource, ProfileBizType } from '../types'
import { invoke, NTClass } from '../ntcall' import { invoke } from '../ntcall'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { isNullable, pick, Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -19,25 +20,31 @@ export class NTQQUserApi extends Service {
super(ctx, 'ntUserApi', true) super(ctx, 'ntUserApi', true)
} }
async setSelfAvatar(path: string) { async setQQAvatar(path: string) {
return await invoke( return await invoke(
'nodeIKernelProfileService/setHeader', 'nodeIKernelProfileService/setHeader',
[{ path }], [
{ path },
null,
],
{ {
timeout: 10 * Time.second // 10秒不一定够 timeout: 10 * Time.second, // 10秒不一定够
} }
) )
} }
async fetchUserDetailInfo(uid: string) { async fetchUserDetailInfo(uid: string) {
const result = await invoke<{ info: UserDetailInfo }>( const result = await invoke<{ info: UserDetailInfoListenerArg }>(
'nodeIKernelProfileService/fetchUserDetailInfo', 'nodeIKernelProfileService/fetchUserDetailInfo',
[{ [
{
callFrom: 'BuddyProfileStore', callFrom: 'BuddyProfileStore',
uid: [uid], uid: [uid],
source: UserDetailSource.KSERVER, source: UserDetailSource.KSERVER,
bizList: [ProfileBizType.KALL] bizList: [ProfileBizType.KALL]
}], },
null
],
{ {
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
@@ -63,10 +70,13 @@ export class NTQQUserApi extends Service {
} }
const result = await invoke<{ info: User }>( const result = await invoke<{ info: User }>(
'nodeIKernelProfileService/getUserDetailInfoWithBizInfo', 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
[{ [
{
uid, uid,
bizList: [0] bizList: [0]
}], },
null,
],
{ {
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
@@ -88,77 +98,103 @@ export class NTQQUserApi extends Service {
} }
async getPSkey(domains: string[]) { async getPSkey(domains: string[]) {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }]) return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null])
} }
async like(uid: string, count = 1) { async like(uid: string, count = 1) {
const session = getSession()
if (session) {
return session.getProfileLikeService().setBuddyProfileLike({
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})
} else {
return await invoke( return await invoke(
'nodeIKernelProfileLikeService/setBuddyProfileLike', 'nodeIKernelProfileLikeService/setBuddyProfileLike',
[{ [
{
doLikeUserInfo: { doLikeUserInfo: {
friendUid: uid, friendUid: uid,
sourceId: 71, sourceId: 71,
doLikeCount: count, doLikeCount: count,
doLikeTollCount: 0 doLikeTollCount: 0
} }
}] },
null,
],
) )
} }
}
async getUidByUinV1(uin: string, groupCode?: string) { async getUidByUinV1(uin: string) {
let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin) const session = getSession()
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) { if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
uid = friends.find(item => item.uin === uin)?.uid for (const member of membersList.values()) {
if (member.uin === uin) {
uid = member.uid
break
} }
if (!uid && groupCode) {
let member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
if (member.size === 0) {
await this.ctx.ntGroupApi.getGroupMembers(groupCode, 1)
await this.ctx.sleep(30)
member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
} }
uid = Array.from(member.values()).find(e => e.uin === uin)?.uid if (uid) break
}
} }
if (!uid) { if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (!unveifyUid.includes('*')) { if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid uid = unveifyUid
} }
} }
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid return uid
} }
async getUidByUinV2(uin: string) { async getUidByUinV2(uin: string, groupCode?: string) {
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uinList: [uin] }])).uids.get(uin) let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
if (uid) return uid if (uid) return uid
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin) uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid if (uid) return uid
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin) uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid if (uid) return uid
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid
//if (!unveifyUid.includes('*')) return unveifyUid if (!unveifyUid.includes('*')) return unveifyUid
return unveifyUid if (groupCode) {
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uin)
return member?.uid
}
} }
async getUidByUin(uin: string, groupCode?: string) { async getUidByUin(uin: string, groupCode?: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.getUidByUinV2(uin) return this.getUidByUinV2(uin, groupCode)
} }
return this.getUidByUinV1(uin, groupCode) return this.getUidByUinV1(uin)
} }
async getUserDetailInfoByUinV2(uin: string) { async getUserDetailInfoByUinV2(uin: string) {
return await invoke<UserDetailInfoByUinV2>( return await invoke<UserDetailInfoByUinV2>(
'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
[{ uin }] [
{ uin },
null,
],
) )
} }
async getUserDetailInfoByUin(uin: string) { async getUserDetailInfoByUin(uin: string) {
return await invoke<UserDetailInfoByUin>( return await invoke<UserDetailInfoByUin>(
'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
[{ uin }] [
{ uin },
null,
],
) )
} }
@@ -166,21 +202,31 @@ export class NTQQUserApi extends Service {
const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }]) const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])
let uin = ret.uinInfo.get(uid) let uin = ret.uinInfo.get(uid)
if (!uin) { if (!uin) {
uin = (await this.getUserDetailInfo(uid)).uin uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
} }
return uin return uin
} }
async getUinByUidV2(uid: string) { async getUinByUidV2(uid: string) {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uidList: [uid] }])).uins.get(uid) const session = getSession()
if (uin && uin !== '0') return uin if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid)
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
} else {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid) uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid) uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.ctx.ntFriendApi.getBuddyIdMap()).get(uid) }
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
return uin return uin
} }
@@ -192,13 +238,21 @@ export class NTQQUserApi extends Service {
} }
async forceFetchClientKey() { async forceFetchClientKey() {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }]) const session = getSession()
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }, null])
}
} }
async getSelfNick(refresh = true) { async getSelfNick(refresh = false) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) { if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const data = await this.getUserSimpleInfo(selfInfo.uid) const userInfo = await this.getUserDetailInfo(selfInfo.uid)
selfInfo.nick = data.nick if (userInfo) {
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
} }
return selfInfo.nick return selfInfo.nick
} }
@@ -210,7 +264,7 @@ export class NTQQUserApi extends Service {
extStatus, extStatus,
batteryStatus, batteryStatus,
} }
}]) }, null])
} }
async getProfileLike(uid: string) { async getProfileLike(uid: string) {
@@ -225,77 +279,6 @@ export class NTQQUserApi extends Service {
start: 0, start: 0,
limit: 20, limit: 20,
} }
}]) }, null])
}
async getUserSimpleInfoV2(uid: string, force = true) {
const data = await invoke<{ profiles: Record<string, SimpleInfo> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => !isNullable(payload.profiles[uid]),
}
)
return data.profiles[uid].coreInfo
}
async getUserSimpleInfo(uid: string, force = true) {
if (getBuildVersion() >= 26702) {
return this.getUserSimpleInfoV2(uid, force)
}
const data = await invoke<{ profiles: Map<string, User> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'nodeIKernelProfileListener/onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => payload.profiles.has(uid),
}
)
const profile = data.profiles.get(uid)!
return pick(profile, ['nick', 'remark', 'uid', 'uin'])
}
async getCoreAndBaseInfo(uids: string[]) {
return await invoke(
'nodeIKernelProfileService/getCoreAndBaseInfo',
[{
uids,
callFrom: 'nodeStore'
}]
)
}
async getRobotUinRange() {
const data = await invoke(
'nodeIKernelRobotService/getRobotUinRange',
[{
req: {
justFetchMsgConfig: '1',
type: 1,
version: 0,
aioKeywordVersion: 0
}
}]
)
return data.response.robotUinRanges
}
async quitAccount() {
return await invoke(
'quitAccount',
[],
{
className: NTClass.BUSINESS_API,
}
)
} }
} }

View File

@@ -35,7 +35,7 @@ export class NTQQWindowApi extends Service {
super(ctx, 'ntWindowApi', true) super(ctx, 'ntWindowApi', true)
} }
/** 打开窗口并获取对应的下发事件 */ // 打开窗口并获取对应的下发事件
async openWindow<R = GeneralCallResult>( async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: unknown[], args: unknown[],
@@ -53,6 +53,7 @@ export class NTQQWindowApi extends Service {
) )
setTimeout(() => { setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) { for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) { if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) {
w.close() w.close()
} }

View File

@@ -2,6 +2,7 @@ import { unlink } from 'node:fs/promises'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook' import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc' import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall' import { NTMethod } from './ntcall'
import { import {
@@ -12,45 +13,39 @@ import {
GroupMember, GroupMember,
CategoryFriend, CategoryFriend,
SimpleInfo, SimpleInfo,
ChatType, ChatType
BuddyReqType,
GrayTipElementSubType
} from './types' } from './types'
import { selfInfo } from '../common/globalVars' import { selfInfo } from '../common/globalVars'
import { version } from '../version' import { version } from '../version'
import { invoke } from './ntcall' import { invoke } from './ntcall'
import { Native } from './native/index'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
app: Core app: Core
} }
interface Events { interface Events {
'nt/message-created': (input: RawMessage) => void 'nt/message-created': (input: RawMessage[]) => void
'nt/message-deleted': (input: RawMessage) => void 'nt/message-deleted': (input: RawMessage) => void
'nt/message-sent': (input: RawMessage) => void 'nt/message-sent': (input: RawMessage) => void
'nt/group-notify': (input: { notify: GroupNotify, doubt: boolean }) => void 'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest) => void 'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/system-message-created': (input: Uint8Array) => void 'nt/system-message-created': (input: Uint8Array) => void
} }
} }
class Core extends Service { class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store'] static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi']
public startTime = 0
public native
constructor(protected ctx: Context, public config: Core.Config) { constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true) super(ctx, 'app', true)
this.native = new Native(ctx)
} }
public start() { public start() {
this.startTime = Date.now() llonebotError.otherError = ''
this.registerListener() this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`) this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llob/config-updated', input => { this.ctx.on('llonebot/config-updated', input => {
Object.assign(this.config, input) Object.assign(this.config, input)
}) })
} }
@@ -122,18 +117,16 @@ class Core extends Service {
activatedPeerUids.push(contact.id) activatedPeerUids.push(contact.id)
const peer = { peerUid: contact.id, chatType: contact.chatType } const peer = { peerUid: contact.id, chatType: contact.chatType }
if (contact.chatType === ChatType.TempC2CFromGroup) { if (contact.chatType === ChatType.TempC2CFromGroup) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer, 2).then(res => { this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => {
for (const msg of res.msgList) { this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
if (Date.now() / 1000 - Number(msg.msgTime) > 3) { const lastTempMsg = msgList.at(-1)
continue if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) {
} this.ctx.parallel('nt/message-created', [lastTempMsg!])
if (msg.senderUin && msg.senderUin !== '0') {
this.ctx.store.addMsgCache(msg)
}
this.ctx.parallel('nt/message-created', msg)
} }
}) })
} else { })
}
else {
this.ctx.ntMsgApi.activateChat(peer) this.ctx.ntMsgApi.activateChat(peer)
} }
} }
@@ -168,17 +161,7 @@ class Core extends Service {
}) })
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => { registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
const startTime = this.startTime / 1000 this.ctx.parallel('nt/message-created', payload.msgList)
for (const message of payload.msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < startTime) {
continue
}
if (message.senderUin && message.senderUin !== '0') {
this.ctx.store.addMsgCache(message)
}
this.ctx.parallel('nt/message-created', message)
}
}) })
const sentMsgIds = new Map<string, boolean>() const sentMsgIds = new Map<string, boolean>()
@@ -186,30 +169,24 @@ class Core extends Service {
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => { registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
for (const msg of payload.msgList) { for (const msg of payload.msgList) {
if ( if (msg.recallTime !== '0' && !recallMsgIds.includes(msg.msgId)) {
msg.recallTime !== '0' &&
msg.msgType === 5 &&
msg.subMsgType === 4 &&
msg.elements[0]?.grayTipElement?.subElementType === GrayTipElementSubType.Revoke &&
!recallMsgIds.includes(msg.msgId)
) {
recallMsgIds.shift()
recallMsgIds.push(msg.msgId) recallMsgIds.push(msg.msgId)
this.ctx.parallel('nt/message-deleted', msg) this.ctx.parallel('nt/message-deleted', msg)
} else if (sentMsgIds.get(msg.msgId)) { } else if (sentMsgIds.get(msg.msgId)) {
if (msg.sendStatus === 2) {
sentMsgIds.delete(msg.msgId) sentMsgIds.delete(msg.msgId)
this.ctx.parallel('nt/message-sent', msg) this.ctx.parallel('nt/message-sent', msg)
} }
} }
}
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
if (!this.config.reportSelfMessage) {
return
}
sentMsgIds.set(payload.msgRecord.msgId, true) sentMsgIds.set(payload.msgRecord.msgId, true)
}) })
const groupNotifyIgnore: string[] = [] const groupNotifyFlags: string[] = []
registerReceiveHook<{ registerReceiveHook<{
doubt: boolean doubt: boolean
oldestUnreadSeq: string oldestUnreadSeq: string
@@ -218,31 +195,24 @@ class Core extends Service {
if (payload.unreadCount) { if (payload.unreadCount) {
let notifies: GroupNotify[] let notifies: GroupNotify[]
try { try {
notifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(payload.doubt, payload.unreadCount) notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) { } catch (e) {
return return
} }
for (const notify of notifies) { const list = notifies.filter(v => {
const notifyTime = Math.trunc(+notify.seq / 1000) const flag = v.group.groupCode + '|' + v.seq + '|' + v.type
if (groupNotifyIgnore.includes(notify.seq) || notifyTime < this.startTime) { if (groupNotifyFlags.includes(flag)) {
continue return false
}
groupNotifyIgnore.push(notify.seq)
this.ctx.parallel('nt/group-notify', { notify, doubt: payload.doubt })
} }
groupNotifyFlags.push(flag)
return true
})
this.ctx.parallel('nt/group-notify', list)
} }
}) })
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
for (const req of payload.data.buddyReqs) { this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.MeInitiatorWaitPeerConfirm)) {
continue
}
if (+req.reqTime < this.startTime / 1000) {
continue
}
this.ctx.parallel('nt/friend-request', req)
}
}) })
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { registerEvent: true }) invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { registerEvent: true })

View File

@@ -16,14 +16,16 @@ import {
SendTextElement, SendTextElement,
SendVideoElement, SendVideoElement,
} from './types' } from './types'
import { stat, writeFile, copyFile, unlink, access } from 'node:fs/promises' import { stat, writeFile, copyFile, unlink } from 'node:fs/promises'
import { calculateFileMD5 } from '../common/utils/file' import { calculateFileMD5 } from '../common/utils/file'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import { Context } from 'cordis' import { Context } from 'cordis'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
export namespace SendElement { //export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export namespace SendElementEntities {
export function text(content: string): SendTextElement { export function text(content: string): SendTextElement {
return { return {
elementType: ElementType.Text, elementType: ElementType.Text,
@@ -52,15 +54,15 @@ export namespace SendElement {
} }
} }
export function reply(msgSeq: string, msgId: string, senderUin: string): SendReplyElement { export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return { return {
elementType: ElementType.Reply, elementType: ElementType.Reply,
elementId: '', elementId: '',
replyElement: { replyElement: {
replayMsgSeq: msgSeq, replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, replayMsgId: msgId, // raw.msgId
senderUin: senderUin, senderUin: senderUin,
senderUinStr: senderUin, senderUinStr: senderUinStr,
}, },
} }
} }
@@ -115,17 +117,25 @@ export namespace SendElement {
} }
export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
await access(filePath) try {
await stat(filePath)
} catch (e) {
throw `文件${filePath}异常,不存在`
}
ctx.logger.info('复制视频到QQ目录', filePath)
const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video) const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video)
ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
throw new Error('文件异常,大小为 0') throw '文件异常大小为0'
} }
const maxMB = 100 const maxMB = 100;
if (fileSize > 1024 * 1024 * maxMB) { if (fileSize > 1024 * 1024 * maxMB) {
throw new Error(`视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`) throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const thumbDir = pathLib.dirname(path.replaceAll('\\', '/').replace(`/Ori/`, `/Thumb/`)) let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumbDir = pathLib.dirname(thumbDir)
// log("thumb 目录", thumb)
let videoInfo = { let videoInfo = {
width: 1920, width: 1920,
height: 1080, height: 1080,
@@ -186,6 +196,7 @@ export namespace SendElement {
const _thumbPath = await createThumb const _thumbPath = await createThumb
ctx.logger.info('生成视频缩略图', _thumbPath) ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await stat(_thumbPath)).size const thumbSize = (await stat(_thumbPath)).size
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
const element: SendVideoElement = { const element: SendVideoElement = {
@@ -210,9 +221,13 @@ export namespace SendElement {
export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> { export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath) const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
if (!silkPath) {
throw '语音转换失败, 请检查语音文件是否正常'
}
// log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt)
if (fileSize === 0) { if (fileSize === 0) {
throw new Error('文件异常,大小为 0') throw '文件异常大小为0'
} }
if (converted) { if (converted) {
unlink(silkPath) unlink(silkPath)
@@ -238,19 +253,19 @@ export namespace SendElement {
} }
} }
export function face(faceId: number, faceType?: number): SendFaceElement { export function face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface const sysFaces = faceConfig.sysface
const face = sysFaces.find(face => face.QSid === String(faceId)) const emojiFaces = faceConfig.emoji
if (!faceType) { const face = sysFaces.find((face) => face.QSid === faceId.toString())
if (faceId < 222) { faceId = parseInt(faceId.toString())
faceType = 1 // let faceType = parseInt(faceId.toString().substring(0, 1));
} else { let faceType = 1
if (faceId >= 222) {
faceType = 2 faceType = 2
} }
if (face?.AniStickerType) { if (face?.AniStickerType) {
faceType = 3 faceType = 3;
}
} }
return { return {
elementType: ElementType.Face, elementType: ElementType.Face,

View File

@@ -1,15 +1,5 @@
{ {
"sysface": [ "sysface": [
{
"QSid": "419",
"QDes": "/火车",
"IQLid": "419",
"AQLid": "419",
"EMCode": "10419",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "47"
},
{ {
"QSid": "392", "QSid": "392",
"QDes": "/龙年快乐", "QDes": "/龙年快乐",

View File

@@ -31,18 +31,30 @@ export class RkeyManager {
isExpired(): boolean { isExpired(): boolean {
const now = new Date().getTime() / 1000 const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time return now > this.rkeyData.expired_time
} }
async refreshRkey() { async refreshRkey() {
//刷新rkey
this.rkeyData = await this.fetchServerRkey() this.rkeyData = await this.fetchServerRkey()
} }
async fetchServerRkey(): Promise<ServerRkeyData> { async fetchServerRkey() {
const response = await fetch(this.serverUrl) return new Promise<ServerRkeyData>((resolve, reject) => {
fetch(this.serverUrl)
.then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText) return reject(response.statusText) // 请求失败,返回错误信息
} }
return response.json() return response.json() // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data)
})
.catch(error => {
reject(error)
})
})
} }
} }

View File

@@ -1,7 +1,8 @@
import { NTMethod } from './ntcall' import type { BrowserWindow } from 'electron'
import { NTClass, NTMethod } from './ntcall'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { ipcMain } from 'electron' import { Dict } from 'cosmokit'
export const hookApiCallbacks: Record<string, (res: any) => void> = {} export const hookApiCallbacks: Record<string, (res: any) => void> = {}
@@ -25,78 +26,119 @@ export enum ReceiveCmdS {
SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE = 'onSkeyUpdate',
} }
type NTReturnData = [
{
type: 'request'
eventName: NTClass
callbackId?: string
},
{
cmdName: ReceiveCmdS
cmdType: 'event'
payload: unknown
}[]
]
const logHook = false const logHook = false
const receiveHooks: Map<string, { const receiveHooks: Array<{
method: ReceiveCmdS[] method: ReceiveCmdS[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
}> = new Map() id: string
}> = []
const callHooks: Array<{ const callHooks: Array<{
method: NTMethod[] method: NTMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function startHook() { export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
const senderExclude = Symbol() window.webContents.send = new Proxy(window.webContents.send, {
apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
ipcMain.emit = new Proxy(ipcMain.emit, { try {
apply(target, thisArg, args: [eventName: string, ...args: any]) { if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) {
if (args[2]?.eventName.startsWith('ns-LoggerApi')) { log('received ntqq api message', args)
return target.apply(thisArg, args)
} }
if (logHook) { } catch { }
log('request', args) if (!onlyLog) {
} if (args[2] instanceof Array) {
const event = args[1]
if (event.sender && !event.sender[senderExclude]) {
event.sender[senderExclude] = true
event.sender.send = new Proxy(event.sender.send, {
apply(target, thisArg, args: any[]) {
if (args[1].eventName?.startsWith('ns-LoggerApi')) {
return target.apply(thisArg, args)
}
if (logHook) {
log('received', args)
}
const callbackId = args[1].callbackId
if (callbackId) {
if (hookApiCallbacks[callbackId]) {
Promise.resolve(hookApiCallbacks[callbackId](args[2]))
delete hookApiCallbacks[callbackId]
}
} else if (args[2]) {
if (['IPC_DOWN_2', 'IPC_DOWN_3'].includes(args[0])) {
for (const receiveData of args[2]) { for (const receiveData of args[2]) {
for (const hook of receiveHooks.values()) { const ntMethodName = receiveData.cmdName
if (hook.method.includes(receiveData.cmdName)) { for (const hook of receiveHooks) {
if (hook.method.includes(ntMethodName)) {
Promise.resolve(hook.hookFunc(receiveData.payload)) Promise.resolve(hook.hookFunc(receiveData.payload))
} }
} }
} }
} }
if (args[1]?.callbackId) {
const callbackId = args[1].callbackId
if (hookApiCallbacks[callbackId]) {
Promise.resolve(hookApiCallbacks[callbackId](args[2]))
delete hookApiCallbacks[callbackId]
}
}
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
} },
}) })
} }
if (args[3]?.length) { export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
const method = args[3][0] const webContents = window.webContents as Dict
const callParams = args[3].slice(1) const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
for (const hook of callHooks) {
if (hook.method.includes(method)) { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
if (!isLogger) {
try {
logHook && log('call NTQQ api', thisArg, args)
} catch (e) { }
if (!onlyLog) {
try {
const _args: unknown[] = args[3][1]
const cmdName = _args[0] as NTMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
Promise.resolve(hook.hookFunc(callParams)) Promise.resolve(hook.hookFunc(callParams))
} }
})
} catch { }
} }
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
} },
}) })
if (webContents._events['-ipc-message']?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg
} else {
webContents._events['-ipc-message'] = proxyIpcMsg
}
/*const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs)
},
})
const ret = target.apply(thisArg, args)
//HOOK_LOG && log('call NTQQ invoke api return', ret)
return ret
},
})
if (webContents._events['-ipc-invoke']?.[0]) {
webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
} else {
webContents._events['-ipc-invoke'] = proxyIpcInvoke
}*/
} }
export function registerReceiveHook<PayloadType>( export function registerReceiveHook<PayloadType>(
@@ -107,9 +149,10 @@ export function registerReceiveHook<PayloadType>(
if (!Array.isArray(method)) { if (!Array.isArray(method)) {
method = [method] method = [method]
} }
receiveHooks.set(id, { receiveHooks.push({
method: method as ReceiveCmdS[], method: method as ReceiveCmdS[],
hookFunc, hookFunc,
id,
}) })
return id return id
} }
@@ -128,5 +171,6 @@ export function registerCallHook(
} }
export function removeReceiveHook(id: string) { export function removeReceiveHook(id: string) {
receiveHooks.delete(id) const index = receiveHooks.findIndex((h) => h.id === id)
receiveHooks.splice(index, 1)
} }

View File

@@ -1,99 +0,0 @@
import { Context } from 'cordis'
import { Dict } from 'cosmokit'
import { getBuildVersion } from '@/common/utils/misc'
import { TEMP_DIR } from '@/common/globalVars'
import { copyFile } from 'fs/promises'
import { ChatType, Peer } from '../types'
import path from 'node:path'
import addon from './external/crychic-win32-x64.node?asset'
export class Native {
public activated = false
private crychic?: Dict
private seq = 0
private cb: Map<number, Function> = new Map()
constructor(private ctx: Context) {
ctx.on('ready', () => {
this.start()
})
}
checkPlatform() {
return process.platform === 'win32' && process.arch === 'x64'
}
checkVersion() {
const version = getBuildVersion()
// 27333—27597
return version >= 27333 && version < 28060
}
async start() {
if (this.crychic) {
return
}
if (!this.checkPlatform()) {
return
}
if (!this.checkVersion()) {
return
}
const handler = async (name: string, ...e: unknown[]) => {
if (name === 'cb') {
this.cb.get(e[0] as number)?.(e[1])
}
}
try {
const fileName = path.basename(addon)
const dest = path.join(TEMP_DIR, fileName)
try {
await copyFile(addon, dest)
} catch (e) {
// resource busy or locked?
this.ctx.logger.warn(e)
}
this.crychic = require(dest)
this.crychic!.setCryHandler(handler)
this.crychic!.init()
this.activated = true
} catch (e) {
this.ctx.logger.warn('crychic 加载失败', e)
}
}
async sendFriendPoke(uin: number) {
if (!this.crychic) return
this.crychic.sendFriendPoke(uin)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
}
async sendGroupPoke(groupCode: number, memberUin: number) {
if (!this.crychic) return
this.crychic.sendGroupPoke(memberUin, groupCode)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
}
uploadForward(peer: Peer, transmit: Uint8Array) {
return new Promise<string>(async (resolve, reject) => {
if (!this.crychic) return
let groupCode = 0
const uid = peer.peerUid
const isGroup = peer.chatType === ChatType.Group
if (isGroup) {
groupCode = +uid
}
const seq = ++this.seq
this.cb.set(seq, (resid: string) => {
this.cb.delete(seq)
resolve(resid)
})
setTimeout(() => {
this.cb.delete(seq)
reject(new Error('fake forward timeout'))
}, 5000)
this.crychic.uploadForward(seq, isGroup, uid, groupCode, transmit)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
})
}
}

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 { getBuildVersion, log } from '../common/utils' import { log } from '../common/utils/legacyLog'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { import {
GeneralCallResult, GeneralCallResult,
@@ -13,9 +13,7 @@ import {
NodeIKernelUixConvertService, NodeIKernelUixConvertService,
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService, NodeIKernelTipOffService
NodeIKernelRobotService,
NodeIKernelNodeMiscService
} from './services' } from './services'
export enum NTClass { export enum NTClass {
@@ -41,6 +39,7 @@ export enum NTMethod {
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes', EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
SELF_INFO = 'fetchAuthData', SELF_INFO = 'fetchAuthData',
FILE_TYPE = 'getFileType', FILE_TYPE = 'getFileType',
@@ -94,8 +93,6 @@ interface NTService {
nodeIKernelRichMediaService: NodeIKernelRichMediaService nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
nodeIKernelNodeMiscService: NodeIKernelNodeMiscService
} }
interface InvokeOptions<ReturnType> { interface InvokeOptions<ReturnType> {
@@ -114,7 +111,7 @@ export function invoke<
M extends keyof NTService[S] & string = any M extends keyof NTService[S] & string = any
>(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) { >(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const className = options.className ?? NTClass.NT_API const className = options.className ?? NTClass.NT_API
const channel = options.channel ?? getBuildVersion() >= 28788 ? NTChannel.IPC_UP_3 : NTChannel.IPC_UP_2 const channel = options.channel ?? NTChannel.IPC_UP_2
const timeout = options.timeout ?? 5000 const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true const afterFirstCmd = options.afterFirstCmd ?? true
let eventName = className + '-' + channel[channel.length - 1] let eventName = className + '-' + channel[channel.length - 1]
@@ -124,14 +121,9 @@ export function invoke<
return new Promise<R>((resolve, reject) => { return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args] const apiArgs = [method, ...args]
const callbackId = randomUUID() const callbackId = randomUUID()
let eventId: string
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (eventId) { log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
removeReceiveHook(eventId) reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, args)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${JSON.stringify(args)}`)
}, timeout) }, timeout)
if (!options.cbCmd) { if (!options.cbCmd) {
@@ -145,15 +137,19 @@ export function invoke<
let result: unknown let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
eventId = registerReceiveHook<R>(options.cbCmd!, (payload) => { const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
if (options.cmdCB) { if (options.cmdCB) {
if (!options.cmdCB(payload, result)) { if (options.cmdCB(payload, result)) {
return removeReceiveHook(hookId)
}
}
removeReceiveHook(eventId)
clearTimeout(timeoutId) clearTimeout(timeoutId)
resolve(payload) resolve(payload)
}
}
else {
removeReceiveHook(hookId)
clearTimeout(timeoutId)
resolve(payload)
}
}) })
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback()
@@ -163,12 +159,9 @@ export function invoke<
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
clearTimeout(timeoutId)
if (eventId) {
removeReceiveHook(eventId)
}
log('ntqq api call failed,', method, args, res) log('ntqq api call failed,', method, args, res)
reject(`ntqq api call failed, ${method}, ${JSON.stringify(res)}`) clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res?.errMsg}`)
} }
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
syntax = "proto3";
package SysMsg;
message GroupMemberChange {
uint32 groupCode = 1;
string memberUid = 3;
uint32 type = 4; // 130:主动 131:被动
string adminUid = 5;
}
message GroupInvite {
uint32 groupCode = 1;
string operatorUid = 5;
}

View File

@@ -1,134 +0,0 @@
syntax = "proto3";
package Msg;
message RoutingHead {
optional uint64 fromUin = 1;
optional string fromUid = 2;
optional uint32 fromAppid = 3;
optional uint32 fromInstid = 4;
optional uint64 toUin = 5;
optional string toUid = 6;
optional C2c c2c = 7;
optional Group group = 8;
}
message C2c {
optional string friendName = 6;
}
message Group {
optional uint64 groupCode = 1;
optional uint32 groupType = 2;
optional uint64 groupInfoSeq = 3;
optional string groupCard = 4;
optional uint32 groupCardType = 5;
optional uint32 groupLevel = 6;
optional string groupName = 7;
optional string extGroupKeyInfo = 8;
optional uint32 msgFlag = 9;
}
message ContentHead {
optional uint64 msgType = 1;
optional uint64 subType = 2;
optional uint32 c2cCmd = 3;
optional uint64 random = 4;
optional uint64 msgSeq = 5;
optional uint64 msgTime = 6;
optional uint32 pkgNum = 7;
optional uint32 pkgIndex = 8;
optional uint32 divSeq = 9;
optional uint32 autoReply = 10;
optional uint64 ntMsgSeq = 11;
optional uint64 msgUid = 12;
optional ContentHeadField15 field15 = 15;
}
message ContentHeadField15 {
optional uint32 field1 = 1;
optional uint32 field2 = 2;
optional uint32 field3 = 3;
optional string field4 = 4;
optional string field5 = 5;
}
message Message {
optional RoutingHead routingHead = 1;
optional ContentHead contentHead = 2;
optional MessageBody body = 3;
}
message MessageBody {
optional RichText richText = 1;
optional bytes msgContent = 2;
optional bytes msgEncryptContent = 3;
}
message RichText {
optional Attr attr = 1;
repeated Elem elems = 2;
}
message Elem {
optional Text text = 1;
optional Face face = 2;
optional LightAppElem lightApp = 51;
optional CommonElem commonElem = 53;
}
message Text {
optional string str = 1;
optional string link = 2;
optional bytes attr6Buf = 3;
optional bytes attr7Buf = 4;
optional bytes buf = 11;
optional bytes pbReserve = 12;
}
message Face {
optional uint32 index = 1;
optional bytes old = 2;
optional bytes buf = 11;
}
message LightAppElem {
optional bytes data = 1;
optional bytes msgResid = 2;
}
message CommonElem {
required uint32 serviceType = 1;
optional bytes pbElem = 2;
optional uint32 businessType = 3;
}
message Attr {
optional int32 codePage = 1;
optional int32 time = 2;
optional int32 random = 3;
optional int32 color = 4;
optional int32 size = 5;
optional int32 effect = 6;
optional int32 charSet = 7;
optional int32 pitchAndFamily = 8;
optional string fontName = 9;
optional bytes reserveData = 10;
}
message MarkdownElem {
string content = 1;
}
message PbMultiMsgItem {
string fileName = 1;
PbMultiMsgNew buffer = 2;
}
message PbMultiMsgNew {
repeated Message msg = 1;
}
message PbMultiMsgTransmit {
repeated Message msg = 1;
repeated PbMultiMsgItem pbItemList = 2;
}

View File

@@ -1,76 +0,0 @@
syntax = "proto3";
package RichMedia;
message MsgInfo {
repeated MsgInfoBody msgInfoBody = 1;
ExtBizInfo extBizInfo = 2;
}
message MsgInfoBody {
IndexNode index = 1;
PicInfo pic = 2;
bool fileExist = 5;
}
message IndexNode {
FileInfo info = 1;
string fileUuid = 2;
uint32 storeID = 3;
uint32 uploadTime = 4;
uint32 expire = 5;
uint32 type = 6; //0
}
message FileInfo {
uint32 fileSize = 1;
string md5HexStr = 2;
string sha1HexStr = 3;
string fileName = 4;
FileType fileType = 5;
uint32 width = 6;
uint32 height = 7;
uint32 time = 8;
uint32 original = 9;
}
message FileType {
uint32 type = 1;
uint32 picFormat = 2;
uint32 videoFormat = 3;
uint32 pttFormat = 4;
}
message PicInfo {
string urlPath = 1;
PicUrlExtParams ext = 2;
string domain = 3;
}
message PicUrlExtParams {
string originalParam = 1;
string bigParam = 2;
string thumbParam = 3;
}
message ExtBizInfo {
PicExtBizInfo pic = 1;
VideoExtBizInfo video = 2;
uint32 busiType = 10;
}
message PicExtBizInfo {
uint32 bizType = 1;
string summary = 2;
}
message VideoExtBizInfo {
bytes pbReserve = 3;
}
message PicFileIdInfo {
bytes sha1 = 2;
uint32 size = 3;
uint32 appid = 4;
uint32 time = 5;
uint32 expire = 10;
}

View File

@@ -0,0 +1,30 @@
syntax = "proto3";
package SysMsg;
message SystemMessage {
repeated SystemMessageHeader header = 1;
repeated SystemMessageMsgSpec msgSpec = 2;
SystemMessageBodyWrapper bodyWrapper = 3;
}
message SystemMessageHeader {
uint32 peerNumber = 1;
string peerString = 2;
uint32 uin = 5;
optional string uid = 6;
}
message SystemMessageMsgSpec {
uint32 msgType = 1;
uint32 subType = 2;
uint32 subSubType = 3;
uint32 msgSeq = 5;
uint32 time = 6;
//uint64 msgId = 12;
uint32 other = 13;
}
message SystemMessageBodyWrapper {
bytes body = 2;
// Find the first [08], or ignore the first 7 bytes?
}

View File

@@ -13,7 +13,7 @@ export interface NodeIKernelBuddyService {
}[] }[]
}> }>
setBuddyRemark(arg: unknown): void setBuddyRemark(uid: number, remark: string): void
isBuddy(uid: string): boolean isBuddy(uid: string): boolean

View File

@@ -123,8 +123,4 @@ export interface NodeIKernelGroupService {
addGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown> addGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown> removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
setHeader(args: unknown[]): Promise<GeneralCallResult>
searchMember(sceneId: string, keyword: string): Promise<void>
} }

View File

@@ -10,9 +10,7 @@ export interface NodeIKernelMsgService {
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult> setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult & { forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
detailErr: Map<unknown, unknown>
}>
forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult> forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
@@ -20,10 +18,6 @@ export interface NodeIKernelMsgService {
getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & { msgList: RawMessage[] }> getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getAioFirstViewLatestMsgsAndAddActiveChat(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsIncludeSelfAndAddActiveChat(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
@@ -77,7 +71,7 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown downloadRichMedia(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): Promise<GeneralCallResult> setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{ getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number result: number
@@ -95,6 +89,4 @@ export interface NodeIKernelMsgService {
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi> getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi>
sendSsoCmdReqByContend(ssoCmd: string, content: string): Promise<GeneralCallResult & { rsp: string }>
} }

View File

@@ -1,15 +0,0 @@
export interface NodeIKernelNodeMiscService {
wantWinScreenOCR(...args: unknown[]): Promise<{
code: number
errMsg: string
result: {
text: string
[key: `pt${number}`]: {
x: string
y: string
}
charBox: unknown[]
score: ''
}[]
}>
}

View File

@@ -3,7 +3,7 @@ import { GeneralCallResult } from './common'
import { Dict } from 'cosmokit' import { Dict } from 'cosmokit'
export interface NodeIKernelProfileLikeService { export interface NodeIKernelProfileLikeService {
setBuddyProfileLike(...args: unknown[]): GeneralCallResult & { succCounts: number } setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & { getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
info: { info: {

View File

@@ -39,21 +39,5 @@ export interface NodeIKernelRichMediaService {
failFileIdList: Array<unknown> failFileIdList: Array<unknown>
} }
}> }>
batchGetGroupFileCount(groupIds: string[]): Promise<GeneralCallResult & {
groupCodes: string[]
groupFileCounts: number[]
}>
getGroupSpace(groupId: string): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: string
usedSpace: string
allUpload: boolean
}
}>
} }

View File

@@ -1,13 +0,0 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelRobotService {
getRobotUinRange(req: unknown): Promise<GeneralCallResult & {
response: {
version: number
robotUinRanges: {
minUin: string
maxUin: string
}[]
}
}>
}

View File

@@ -9,5 +9,3 @@ export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelRobotService'
export * from './NodeIKernelNodeMiscService'

View File

@@ -1,3 +1,5 @@
import { QQLevel, Sex } from './user'
export enum GroupListUpdateType { export enum GroupListUpdateType {
REFRESHALL, REFRESHALL,
GETALL, GETALL,
@@ -20,9 +22,9 @@ export interface Group {
hasModifyConfGroupName: boolean hasModifyConfGroupName: boolean
remarkName: string remarkName: string
hasMemo: boolean hasMemo: boolean
groupShutupExpireTime: string groupShutupExpireTime: string //"0",
personShutupExpireTime: string personShutupExpireTime: string //"0",
discussToGroupUin: string discussToGroupUin: string //"0",
discussToGroupMaxMsgSeq: number discussToGroupMaxMsgSeq: number
discussToGroupTime: number discussToGroupTime: number
groupFlagExt: number //1073938496, groupFlagExt: number //1073938496,
@@ -30,57 +32,39 @@ export interface Group {
groupCreditLevel: number //0, groupCreditLevel: number //0,
groupFlagExt3: number //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
memberUin: string memberUin: string //"0",
memberUid: string memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
} }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string createTime: string
} }
export enum GroupMemberRole { export enum GroupMemberRole {
Normal = 2, normal = 2,
Admin = 3, admin = 3,
Owner = 4, owner = 4,
} }
export interface GroupMember { export interface GroupMember {
uid: string memberSpecialTitle?: string
qid: string
uin: string
nick: string
remark: string
cardType: number
cardName: string
role: GroupMemberRole
avatarPath: string avatarPath: string
shutUpTime: number cardName: string
cardType: number
isDelete: boolean isDelete: boolean
isSpecialConcerned: boolean nick: string
isSpecialShield: boolean qid: string
remark: string
role: GroupMemberRole // 群主:4, 管理员:3群员:2
shutUpTime: number // 禁言时间,单位是什么暂时不清楚
uid: string // 加密的字符串
uin: string // QQ号
isRobot: boolean isRobot: boolean
groupHonor: Uint8Array sex?: Sex
memberRealLevel: number qqLevel?: QQLevel
memberLevel: number isChangeRole: boolean
globalGroupLevel: number
globalGroupPoint: number
memberTitleId: number
memberSpecialTitle: string
specialTitleExpireTime: string
userShowFlag: number
userShowFlagNew: number
richFlag: number
mssVipType: number
bigClubLevel: number
bigClubFlag: number
autoRemark: string
creditLevel: number
joinTime: number joinTime: number
lastSpeakTime: number lastSpeakTime: number
memberFlag: number memberLevel: number
memberFlagExt: number
memberMobileFlag: number
memberFlagExt2: number
isSpecialShielded: boolean
cardNameId: number
} }
export interface PublishGroupBulletinReq { export interface PublishGroupBulletinReq {
@@ -136,55 +120,3 @@ export interface GroupAllInfo {
joinGroupAuth: string joinGroupAuth: string
isAllowModifyConfGroupName: number isAllowModifyConfGroupName: number
} }
export interface GroupBulletinListResult {
groupCode: string
srvCode: number
readOnly: number
role: number
inst: unknown[]
feeds: {
uin: string
feedId: string
publishTime: string
msg: {
text: string
textFace: string
pics: {
id: string
width: number
height: number
}[]
title: string
}
type: number
fn: number
cn: number
vn: number
settings: {
isShowEditCard: number
remindTs: number
tipWindowType: number
confirmRequired: number
}
pinned: number
readNum: number
is_read: number
is_all_confirm: number
}[]
groupInfo: {
groupCode: string
classId: number
}
gln: number
tst: number
publisherInfos: {
uin: string
nick: string
avatar: string
}[]
server_time: string
svrt: string
nextIndex: number
jointime: string
}

View File

@@ -80,7 +80,7 @@ export interface SendVideoElement {
export interface SendArkElement { export interface SendArkElement {
elementType: ElementType.Ark elementType: ElementType.Ark
elementId: '' elementId: ''
arkElement: Partial<ArkElement> arkElement: ArkElement
} }
export type SendMessageElement = export type SendMessageElement =
@@ -197,60 +197,6 @@ export interface PicElement {
md5HexStr?: string md5HexStr?: string
} }
export interface TipAioOpGrayTipElement {
operateType: number
peerUid: string
fromGrpCodeOfTmpChat: string
}
export enum TipGroupElementType {
MemberIncrease = 1,
Kicked = 3, // 被移出群
Ban = 8,
}
export interface TipGroupElement {
type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
role: number
groupName: string // 暂时获取不到
memberUid: string
memberNick: string
memberRemark: string
adminUid: string
adminNick: string
adminRemark: string
createGroup: null
memberAdd?: {
showType: number
otherAdd?: {
uid: string
name: string
}
otherAddByOtherQRCode?: unknown
otherAddByYourQRCode?: unknown
youAddByOtherQRCode?: unknown
otherInviteOther?: unknown
otherInviteYou?: unknown
youInviteOther?: unknown
}
shutUp?: {
curTime: string
duration: string // 禁言时间,秒
admin: {
uid: string
card: string
name: string
role: GroupMemberRole
}
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
}
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
Revoke = 1, Revoke = 1,
Proclamation = 2, Proclamation = 2,
@@ -288,18 +234,14 @@ export interface GrayTipElement {
xmlElement?: { xmlElement?: {
templId: string templId: string
content: string content: string
templParam: Map<string, string>
members: Map<string, string> // uid -> remark
} }
jsonGrayTipElement?: { jsonGrayTipElement?: {
busiId: string busiId: string
jsonStr: string jsonStr: string
xmlToJsonParam?: {
templParam: Map<string, string>
}
} }
} }
export enum FaceIndex { export enum FaceIndex {
Dice = 358, Dice = 358,
RPS = 359, // 石头剪刀布 RPS = 359, // 石头剪刀布
@@ -326,10 +268,6 @@ export interface MarketFaceElement {
key: string key: string
imageWidth?: number imageWidth?: number
imageHeight?: number imageHeight?: number
supportSize?: {
width: number
height: number
}[]
} }
export interface VideoElement { export interface VideoElement {
@@ -388,6 +326,58 @@ export interface InlineKeyboardElement {
] ]
} }
export interface TipAioOpGrayTipElement {
// 这是什么提示来着?
operateType: number
peerUid: string
fromGrpCodeOfTmpChat: string
}
export enum TipGroupElementType {
MemberIncrease = 1,
Kicked = 3, // 被移出群
Ban = 8,
}
export interface TipGroupElement {
type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
role: 0 // 暂时不知
groupName: string // 暂时获取不到
memberUid: string
memberNick: string
memberRemark: string
adminUid: string
adminNick: string
adminRemark: string
createGroup: null
memberAdd?: {
showType: 1
otherAdd: null
otherAddByOtherQRCode: null
otherAddByYourQRCode: null
youAddByOtherQRCode: null
otherInviteOther: null
otherInviteYou: null
youInviteOther: null
}
shutUp?: {
curTime: string
duration: string // 禁言时间,秒
admin: {
uid: string
card: string
name: string
role: GroupMemberRole
}
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
}
export interface StructLongMsgElement { export interface StructLongMsgElement {
xmlContent: string xmlContent: string
resId: string resId: string
@@ -419,29 +409,11 @@ export interface RawMessage {
guildId: string guildId: string
sendNickName: string sendNickName: string
sendMemberName?: string // 发送者群名片 sendMemberName?: string // 发送者群名片
sendRemarkName?: string // 发送者好友备注
chatType: ChatType chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[] records: RawMessage[]
elements: MessageElement[] elements: MessageElement[]
peerName: string
multiTransInfo?: {
status: number
msgId: number
friendFlag: number
fromFaceUrl: string
}
emojiLikesList: {
emojiId: string
emojiType: string
likesCnt: string
isClicked: boolean
}[]
msgAttrs: Map<number, {
attrType: number
attrId: string
}>
} }
export interface Peer { export interface Peer {
@@ -483,7 +455,7 @@ export interface MessageElement {
actionBarElement?: unknown actionBarElement?: unknown
} }
export interface RichMediaDownloadCompleteNotify { export interface OnRichMediaDownloadCompleteParams {
fileModelId: string fileModelId: string
msgElementId: string msgElementId: string
msgId: string msgId: string
@@ -587,31 +559,3 @@ export interface GetFileListParam {
showOnlinedocFolder: number showOnlinedocFolder: number
folderId?: string folderId?: string
} }
export interface RichMediaUploadCompleteNotify {
fileId: string
fileDownType: number
filePath: string
totalSize: string
trasferStatus: number
commonFileInfo: {
uuid: string
fileName: string
fileSize: string
md5: string
sha: string
}
}
export enum RMBizType {
Unknown,
C2CFile,
GroupFile,
C2CPic,
GroupPic,
DiscPic,
C2CVideo,
GroupVideo,
C2CPtt,
GroupPtt,
}

View File

@@ -1,19 +1,19 @@
export enum GroupNotifyType { export enum GroupNotifyType {
InvitedByMember = 1, INVITED_BY_MEMBER = 1,
RefuseInvited, REFUSE_INVITED,
RefusedByAdminiStrator, REFUSED_BY_ADMINI_STRATOR,
AgreedTojoinDirect, // 有人接受了邀请入群 AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群
InvitedNeedAdminiStratorPass, // 有人邀请了别人入群 INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群
AgreedToJoinByAdminiStrator, AGREED_TO_JOIN_BY_ADMINI_STRATOR,
RequestJoinNeedAdminiStratorPass, REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS,
SetAdmin, SET_ADMIN,
KickMemberNotifyAdmin, KICK_MEMBER_NOTIFY_ADMIN,
KickMemberNotifyKicked, KICK_MEMBER_NOTIFY_KICKED,
MemberLeaveNotifyAdmin, // 主动退出 MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出
CancelAdminNotifyCanceled, // 我被取消管理员 CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员
CancelAdminNotifyAdmin, // 其他人取消管理员 CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员
TransferGroupNotifyOldowner, TRANSFER_GROUP_NOTIFY_OLDOWNER,
TransferGroupNotifyAdmin TRANSFER_GROUP_NOTIFY_ADMIN
} }
export interface GroupNotifies { export interface GroupNotifies {
@@ -23,11 +23,11 @@ export interface GroupNotifies {
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
Init, // 初始化 KINIT, // 初始化
Unhandle, // 未处理 KUNHANDLE, // 未处理
Agreed, // 同意 KAGREED, // 同意
Refused, // 拒绝 KREFUSED, // 拒绝
Ignored // 忽略 KIGNORED // 忽略
} }
export interface GroupNotify { export interface GroupNotify {
@@ -56,8 +56,20 @@ export enum GroupRequestOperateTypes {
} }
export enum BuddyReqType { export enum BuddyReqType {
MsgInfo = 12, KMEINITIATOR,
MeInitiatorWaitPeerConfirm = 13, KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
@@ -79,3 +91,41 @@ export interface FriendRequestNotify {
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
}

View File

@@ -1,8 +1,7 @@
export enum Sex { export enum Sex {
Unknown = 0, male = 0,
Male = 1, female = 2,
Female = 2, unknown = 255,
Hidden = 255
} }
export interface QQLevel { export interface QQLevel {
@@ -68,7 +67,6 @@ export interface User {
recommendImgFlag?: number recommendImgFlag?: number
disableEmojiShortCuts?: number disableEmojiShortCuts?: number
pendantId?: string pendantId?: string
age?: number
} }
export interface SelfInfo extends User { export interface SelfInfo extends User {
@@ -102,12 +100,12 @@ export interface BaseInfo {
birthday_month: number birthday_month: number
birthday_day: number birthday_day: number
age: number age: number
sex: Sex sex: number
eMail: string eMail: string
phoneNum: string phoneNum: string
categoryId: number categoryId: number
richTime: number richTime: number
richBuffer: Uint8Array richBuffer: string
} }
interface MusicInfo { interface MusicInfo {
@@ -223,6 +221,11 @@ interface RelationFlags {
isHidePrivilegeIcon: number isHidePrivilegeIcon: number
} }
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt { interface CommonExt {
constellation: number constellation: number
shengXiao: number shengXiao: number
@@ -252,7 +255,7 @@ interface PhotoWall {
picList: Pic[] picList: Pic[]
} }
export interface UserDetailInfo { export interface UserDetailInfoListenerArg {
uid: string uid: string
uin: string uin: string
simpleInfo: SimpleInfo simpleInfo: SimpleInfo
@@ -297,7 +300,7 @@ export interface UserDetailInfoByUin {
birthday_year: number birthday_year: number
birthday_month: number birthday_month: number
birthday_day: number birthday_day: number
sex: number sex: number //0
topTime: string topTime: string
constellation: number constellation: number
shengXiao: number shengXiao: number

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

@@ -0,0 +1,54 @@
import {
NodeIKernelBuddyService,
NodeIKernelGroupService,
NodeIKernelProfileService,
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService
} from './services'
import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
getProfileLikeService(): NodeIKernelProfileLikeService
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
const wrapperApi: WrapperApi = {}
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (const export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
}
return dlopenRet
}
export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession']
}

View File

@@ -1,21 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
user_id: number | string
}
export class DeleteFriend extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DeleteFriend
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const uin = payload.user_id.toString()
const uid = await this.ctx.ntUserApi.getUidByUin(uin)
if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntFriendApi.delBuddy(uid)
return null
}
}

View File

@@ -23,8 +23,8 @@ interface FileResponse {
export class DownloadFile extends BaseAction<Payload, FileResponse> { export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile actionName = ActionName.GoCQHTTP_DownloadFile
payloadSchema = Schema.object({ payloadSchema = Schema.object({
url: Schema.string(), url: String,
base64: Schema.string(), base64: String,
headers: Schema.union([String, Schema.array(String)]) headers: Schema.union([String, Schema.array(String)])
}) })

View File

@@ -1,32 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Response {
file_count: number
limit_count: number
used_space: number
total_space: number
}
export class GetGroupFileSystemInfo extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
const groupId = payload.group_id.toString()
const { groupFileCounts } = await this.ctx.ntGroupApi.getGroupFileCount(groupId)
const { groupSpaceResult } = await this.ctx.ntGroupApi.getGroupFileSpace(groupId)
return {
file_count: groupFileCounts[0],
limit_count: 10000,
used_space: +groupSpaceResult.usedSpace,
total_space: +groupSpaceResult.totalSpace,
}
}
}

View File

@@ -50,7 +50,7 @@ export class GetGroupFileUrl extends BaseAction<Payload, Response> {
private async search(groupId: string, fileId: string, folderId?: string) { private async search(groupId: string, fileId: string, folderId?: string) {
let modelId: string | undefined let modelId: string | undefined
let nextIndex: number | undefined let nextIndex: number | undefined
const folders: GroupFileInfo['item'] = [] let folders: GroupFileInfo['item'] = []
while (nextIndex !== 0) { while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, { const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1, sortType: 1,

View File

@@ -30,12 +30,12 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
const { count, reverseOrder } = payload const { count, reverseOrder } = payload
const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() } const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] let msgList: RawMessage[]
if (!payload.message_seq || +payload.message_seq === 0) { if (!payload.message_seq || payload.message_seq === '0') {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else { } else {
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`) if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count, true)).msgList msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
} }
if (!msgList?.length) throw new Error('未找到消息') if (!msgList?.length) throw new Error('未找到消息')
if (reverseOrder) msgList.reverse() if (reverseOrder) msgList.reverse()

View File

@@ -1,48 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Notice {
sender_id: number
publish_time: number
message: {
text: string
images: {
height: string
width: string
id: string
}[]
}
}
export class GetGroupNotice extends BaseAction<Payload, Notice[]> {
actionName = ActionName.GoCQHTTP_GetGroupNotice
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupBulletinList(payload.group_id.toString())
const result: Notice[] = []
for (const feed of data.result.feeds) {
result.push({
sender_id: +feed.uin,
publish_time: +feed.publishTime,
message: {
text: feed.msg.text,
images: feed.msg.pics.map(image => {
return {
height: String(image.height),
width: String(image.width),
id: image.id
}
})
}
})
}
return result
}
}

View File

@@ -28,7 +28,7 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
actionName = ActionName.GoCQHTTP_GetGroupSystemMsg actionName = ActionName.GoCQHTTP_GetGroupSystemMsg
async _handle() { async _handle() {
const singleScreenNotifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(false, 10) const singleScreenNotifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(10)
const data: Response = { invited_requests: [], join_requests: [] } const data: Response = { invited_requests: [], join_requests: [] }
for (const notify of singleScreenNotifies) { for (const notify of singleScreenNotifies) {
if (notify.type == 1) { if (notify.type == 1) {
@@ -38,7 +38,7 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
invitor_nick: notify.user1.nickName, invitor_nick: notify.user1.nickName,
group_id: +notify.group.groupCode, group_id: +notify.group.groupCode,
group_name: notify.group.groupName, group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.Unhandle, checked: notify.status !== GroupNotifyStatus.KUNHANDLE,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0 actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
}) })
} else if (notify.type == 7) { } else if (notify.type == 7) {
@@ -49,7 +49,7 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
message: notify.postscript, message: notify.postscript,
group_id: +notify.group.groupCode, group_id: +notify.group.groupCode,
group_name: notify.group.groupName, group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.Unhandle, checked: notify.status !== GroupNotifyStatus.KUNHANDLE,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0 actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
}) })
} }

View File

@@ -3,51 +3,59 @@ import { OB11User } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/misc' import { calcQQLevel } from '@/common/utils/misc'
interface Payload { interface Payload {
user_id: number | string user_id: number | string
} }
interface Response extends OB11User { export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
reg_time: number
long_nick: string
}
export class GetStrangerInfo extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
payloadSchema = Schema.object({ payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required() user_id: Schema.union([Number, String]).required()
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload): Promise<OB11User> {
const uin = payload.user_id.toString() if (!(getBuildVersion() >= 26702)) {
if (getBuildVersion() >= 26702) { const user_id = payload.user_id.toString()
const data = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(uin) const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUin(user_id)
return { const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
user_id: parseInt(data.detail.uin) || 0, if (!uid || uid.indexOf('*') != -1) {
nickname: data.detail.simpleInfo.coreInfo.nick, const ret = {
sex: OB11Entities.sex(data.detail.simpleInfo.baseInfo.sex), ...extendData,
age: data.detail.simpleInfo.baseInfo.age, user_id: parseInt(extendData.info.uin) || 0,
qid: data.detail.simpleInfo.baseInfo.qid, nickname: extendData.info.nick,
level: data.detail.commonExt.qqLevel && calcQQLevel(data.detail.commonExt.qqLevel) || 0, sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0, login_days: 0,
reg_time: data.detail.commonExt.regTime, uid: ''
long_nick: data.detail.simpleInfo.baseInfo.longNick
} }
return ret
}
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Entities.stranger(data)
} else { } else {
const data = await this.ctx.ntUserApi.getUserDetailInfoByUin(uin) const user_id = payload.user_id.toString()
return { const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id)
user_id: parseInt(data.info.uin) || 0, const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
nickname: data.info.nick, if (!uid || uid.indexOf('*') != -1) {
sex: OB11Entities.sex(data.info.sex), const ret = {
age: data.info.birthday_year === 0 ? 0 : new Date().getFullYear() - data.info.birthday_year, ...extendData,
qid: data.info.qid, user_id: parseInt(extendData.detail.uin) || 0,
level: data.info.qqLevel && calcQQLevel(data.info.qqLevel) || 0, nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0, login_days: 0,
reg_time: data.info.regTime, uid: ''
long_nick: data.info.longNick
} }
return ret
}
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Entities.stranger(data)
} }
} }
} }

View File

@@ -1,63 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { uri2local } from '@/common/utils/file'
import { access, unlink } from 'node:fs/promises'
interface Payload {
image: string
}
interface TextDetection {
text: string
confidence: number
coordinates: {
x: number //int32
y: number
}[]
}
interface Response {
texts: TextDetection[]
language: string
}
export class OCRImage extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_OCRImage
payloadSchema = Schema.object({
image: Schema.string().required()
})
protected async _handle(payload: Payload) {
const { errMsg, isLocal, path, success } = await uri2local(this.ctx, payload.image, true)
if (!success) {
throw new Error(errMsg)
}
await access(path)
const data = await this.ctx.ntFileApi.ocrImage(path)
if (!isLocal) {
unlink(path)
}
const texts = data.result.map(item => {
const ret: TextDetection = {
text: item.text,
confidence: 1,
coordinates: []
}
for (let i = 0; i < 4; i++) {
const pt = item[`pt${i + 1}`]
ret.coordinates.push({
x: parseInt(pt.x),
y: parseInt(pt.y)
})
}
return ret
})
return {
texts,
language: ''
}
}
}

View File

@@ -1,42 +1,34 @@
import { unlink } from 'node:fs/promises' import { unlink } from 'node:fs/promises'
import { OB11MessageData, OB11MessageNode } from '../../types' import { OB11MessageNode } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types' import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { message2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
import { MessageEncoder } from '@/onebot11/helper/createMultiMessage'
import { Msg } from '@/ntqqapi/proto/compiled'
interface Payload { interface Payload {
user_id?: string | number user_id?: string | number
group_id?: string | number group_id?: string | number
messages?: OB11MessageNode[] messages: OB11MessageNode[]
message?: OB11MessageNode[]
message_type?: 'group' | 'private' message_type?: 'group' | 'private'
} }
interface Response { interface Response {
message_id: number message_id: number
forward_id: string forward_id?: string
} }
export class SendForwardMsg extends BaseAction<Payload, Response> { export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.SendForwardMsg actionName = ActionName.GoCQHTTP_SendForwardMsg
payloadSchema = Schema.object({ payloadSchema = Schema.object({
user_id: Schema.union([Number, String]), user_id: Schema.union([Number, String]),
group_id: Schema.union([Number, String]), group_id: Schema.union([Number, String]),
messages: Schema.array(Schema.any()), messages: Schema.array(Schema.any()).required(),
message: Schema.array(Schema.any()),
message_type: Schema.union(['group', 'private']) message_type: Schema.union(['group', 'private'])
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const messages = payload.messages ?? payload.message
if (!messages) {
throw new Error('未指定消息内容')
}
let contextMode = CreatePeerMode.Normal let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') { if (payload.message_type === 'group') {
contextMode = CreatePeerMode.Group contextMode = CreatePeerMode.Group
@@ -44,90 +36,9 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
contextMode = CreatePeerMode.Private contextMode = CreatePeerMode.Private
} }
const peer = await createPeer(this.ctx, payload, contextMode) const peer = await createPeer(this.ctx, payload, contextMode)
const msg = await this.handleForwardNode(peer, payload.messages)
const nodes = this.parseNodeContent(messages) const msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
let fake = true return { message_id: msgShortId }
for (const node of nodes) {
if (node.data.id) {
fake = false
break
}
if (node.data.content?.some(e => {
return !MessageEncoder.support.includes(e.type)
})) {
fake = false
break
}
}
if (fake && this.ctx.app.native.activated) {
return await this.handleFakeForwardNode(peer, nodes)
} else {
return await this.handleForwardNode(peer, nodes)
}
}
private parseNodeContent(nodes: OB11MessageNode[]) {
return nodes.map(e => {
return {
type: e.type,
data: {
...e.data,
content: e.data.content ? message2List(e.data.content) : undefined
}
}
})
}
private async handleFakeForwardNode(peer: Peer, nodes: OB11MessageNode[]): Promise<Response> {
const encoder = new MessageEncoder(this.ctx, peer)
const raw = await encoder.generate(nodes)
const transmit = Msg.PbMultiMsgTransmit.encode({ pbItemList: raw.multiMsgItems }).finish()
const resid = await this.ctx.app.native.uploadForward(peer, transmit.subarray(1))
const uuid = crypto.randomUUID()
try {
const msg = await this.ctx.ntMsgApi.sendMsg(peer, [{
elementType: 10,
elementId: '',
arkElement: {
bytesData: JSON.stringify({
app: 'com.tencent.multimsg',
config: {
autosize: 1,
forward: 1,
round: 1,
type: 'normal',
width: 300
},
desc: '[聊天记录]',
extra: JSON.stringify({
filename: uuid,
tsum: raw.tsum,
}),
meta: {
detail: {
news: raw.news,
resid,
source: raw.source,
summary: raw.summary,
uniseq: uuid,
}
},
prompt: '[聊天记录]',
ver: '0.0.0.5',
view: 'contact'
})
}
}], 1800)
const msgShortId = this.ctx.store.createMsgShortId({
chatType: msg!.chatType,
peerUid: msg!.peerUid
}, msg!.msgId)
return { message_id: msgShortId, forward_id: resid }
} catch (e) {
this.ctx.logger.error('合并转发失败', e)
throw new Error(`发送伪造合并转发消息失败 (res_id: ${resid} `)
}
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> { private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
@@ -154,7 +65,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
} }
// 返回一个合并转发的消息id // 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<Response> { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = { const selfPeer = {
chatType: ChatType.C2C, chatType: ChatType.C2C,
peerUid: selfInfo.uid, peerUid: selfInfo.uid,
@@ -179,7 +90,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
try { try {
const { sendElements, deleteAfterSentFiles } = await createSendElements( const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx, this.ctx,
messageNode.data.content as OB11MessageData[], convertMessage2List(messageNode.data.content),
destPeer destPeer
) )
this.ctx.logger.info('开始生成转发节点', sendElements) this.ctx.logger.info('开始生成转发节点', sendElements)
@@ -246,13 +157,8 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
if (retMsgIds.length === 0) { if (retMsgIds.length === 0) {
throw Error('转发消息失败,节点为空') throw Error('转发消息失败,节点为空')
} }
const msg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds) const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
const resid = JSON.parse(msg.elements[0].arkElement!.bytesData).meta.detail.resid return returnMsg
const msgShortId = this.ctx.store.createMsgShortId({
chatType: msg.chatType,
peerUid: msg.peerUid
}, msg.msgId)
return { message_id: msgShortId, forward_id: resid }
} }
} }

View File

@@ -28,7 +28,7 @@ export class SendGroupNotice extends BaseAction<Payload, null> {
let picInfo: { id: string, width: number, height: number } | undefined let picInfo: { id: string, width: number, height: number } | undefined
if (payload.image) { if (payload.image) {
const { path, isLocal, success, errMsg } = await uri2local(this.ctx, payload.image, true) const { path, isLocal, success, errMsg } = await uri2local(payload.image, undefined, true)
if (!success) { if (!success) {
throw new Error(`设置群公告失败, 错误信息: uri2local: ${errMsg}`) throw new Error(`设置群公告失败, 错误信息: uri2local: ${errMsg}`)
} }

View File

@@ -1,6 +1,6 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElement } from '@/ntqqapi/entities' import { SendElementEntities } from '@/ntqqapi/entities'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -23,15 +23,11 @@ export class UploadGroupFile extends BaseAction<Payload, null> {
}) })
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const { success, errMsg, path, fileName } = await uri2local(this.ctx, payload.file) const { success, errMsg, path, fileName } = await uri2local(payload.file)
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const name = payload.name || fileName const file = await SendElementEntities.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id)
if (name.includes('/') || name.includes('\\')) {
throw new Error(`文件名 ${name} 不合法`)
}
const file = await SendElement.file(this.ctx, path, name, payload.folder ?? payload.folder_id)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [file], []) await sendMsg(this.ctx, peer, [file], [])
return null return null

View File

@@ -1,6 +1,6 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElement } from '@/ntqqapi/entities' import { SendElementEntities } from '@/ntqqapi/entities'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -19,15 +19,11 @@ export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null
}) })
protected async _handle(payload: UploadPrivateFilePayload): Promise<null> { protected async _handle(payload: UploadPrivateFilePayload): Promise<null> {
const { success, errMsg, path, fileName } = await uri2local(this.ctx, payload.file) const { success, errMsg, path, fileName } = await uri2local(payload.file)
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const name = payload.name || fileName const sendFileEle = await SendElementEntities.file(this.ctx, path, payload.name || fileName)
if (name.includes('/') || name.includes('\\')) {
throw new Error(`文件名 ${name} 不合法`)
}
const sendFileEle = await SendElement.file(this.ctx, path, name)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
await sendMsg(this.ctx, peer, [sendFileEle], []) await sendMsg(this.ctx, peer, [sendFileEle], [])
return null return null

View File

@@ -2,36 +2,32 @@ import { BaseAction, Schema } from '../BaseAction'
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { calcQQLevel, parseBool } from '@/common/utils/misc' import { isNullable } from 'cosmokit'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
user_id: number | string user_id: number | string
no_cache: boolean
} }
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> { class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo actionName = ActionName.GetGroupMemberInfo
payloadSchema = Schema.object({ payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(), user_id: Schema.union([Number, String]).required()
no_cache: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString() const groupCode = payload.group_id.toString()
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString(), groupCode) const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, payload.user_id.toString())
if (!uid) throw new Error('无法获取用户信息')
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid, payload.no_cache)
if (member) { if (member) {
const ret = OB11Entities.groupMember(+groupCode, member) if (isNullable(member.sex)) {
const date = Math.trunc(Date.now() / 1000) const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
Object.assign(member, info)
}
const ret = OB11Entities.groupMember(groupCode, member)
const date = Math.round(Date.now() / 1000)
ret.last_sent_time ??= date ret.last_sent_time ??= date
ret.join_time ??= date ret.join_time ??= date
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
ret.sex = OB11Entities.sex(info.sex!)
ret.qq_level = info.qqLevel && calcQQLevel(info.qqLevel) || 0
ret.age = info.age ?? 0
return ret return ret
} }
throw new Error(`群成员${payload.user_id}不存在`) throw new Error(`群成员${payload.user_id}不存在`)

View File

@@ -17,26 +17,19 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString() const groupCode = payload.group_id.toString()
let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode) let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
for (let i = 0; i < 5; i++) { if (groupMembers.size === 0) {
if (groupMembers.size > 0) { await this.ctx.sleep(100)
break
}
await this.ctx.sleep(60)
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode) groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
} }
const groupMembersArr = Array.from(groupMembers.values())
const date = Math.round(Date.now() / 1000)
const date = Math.trunc(Date.now() / 1000) return groupMembersArr.map(item => {
const groupId = Number(payload.group_id) const member = OB11Entities.groupMember(groupCode, item)
const ret: OB11GroupMember[] = []
for (const item of groupMembers.values()) {
const member = OB11Entities.groupMember(groupId, item)
member.join_time ??= date member.join_time ??= date
member.last_sent_time ??= date member.last_sent_time ??= date
ret.push(member) return member
} })
return ret
} }
} }

View File

@@ -25,7 +25,7 @@ export default class SetGroupAdmin extends BaseAction<Payload, null> {
await this.ctx.ntGroupApi.setMemberRole( await this.ctx.ntGroupApi.setMemberRole(
groupCode, groupCode,
uid, uid,
payload.enable ? GroupMemberRole.Admin : GroupMemberRole.Normal payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal
) )
return null return null
} }

View File

@@ -4,7 +4,7 @@ import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
user_id: number | string user_id: number | string
duration: number | string duration: number
} }
export default class SetGroupBan extends BaseAction<Payload, null> { export default class SetGroupBan extends BaseAction<Payload, null> {
@@ -12,7 +12,7 @@ export default class SetGroupBan extends BaseAction<Payload, null> {
payloadSchema = Schema.object({ payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(), user_id: Schema.union([Number, String]).required(),
duration: Schema.union([Number, String]).default(30 * 60) duration: Schema.number().default(30 * 60)
}) })
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
@@ -21,7 +21,7 @@ export default class SetGroupBan extends BaseAction<Payload, null> {
const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode) const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode)
if (!uid) throw new Error('无法获取用户信息') if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntGroupApi.banMember(groupCode, [ await this.ctx.ntGroupApi.banMember(groupCode, [
{ uid, timeStamp: +payload.duration }, { uid, timeStamp: payload.duration },
]) ])
return null return null
} }

View File

@@ -45,7 +45,7 @@ import { GetGroupMsgHistory } from './go-cqhttp/GetGroupMsgHistory'
import GetFile from './file/GetFile' import GetFile from './file/GetFile'
import { GetForwardMsg } from './go-cqhttp/GetForwardMsg' import { GetForwardMsg } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie' import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './llonebot/SetMsgEmojiLike' import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg' import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetEssenceMsgList } from './go-cqhttp/GetGroupEssence' import { GetEssenceMsgList } from './go-cqhttp/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo' import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
@@ -70,14 +70,6 @@ import { GetFriendWithCategory } from './llonebot/GetFriendWithCategory'
import { UploadGroupFile } from './go-cqhttp/UploadGroupFile' import { UploadGroupFile } from './go-cqhttp/UploadGroupFile'
import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile' import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile'
import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl' import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl'
import { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
import { DeleteFriend } from './go-cqhttp/DeleteFriend'
import { OCRImage } from './go-cqhttp/OCRImage'
import { GroupPoke } from './llonebot/GroupPoke'
import { FriendPoke } from './llonebot/FriendPoke'
import { GetGroupFileSystemInfo } from './go-cqhttp/GetGroupFileSystemInfo'
import { GetCredentials } from './system/GetCredentials'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -94,10 +86,6 @@ export function initActionMap(adapter: Adapter) {
new GetFriendMsgHistory(adapter), new GetFriendMsgHistory(adapter),
new FetchEmojiLike(adapter), new FetchEmojiLike(adapter),
new FetchCustomFace(adapter), new FetchCustomFace(adapter),
new SetMsgEmojiLike(adapter),
new GetRobotUinRange(adapter),
new GroupPoke(adapter),
new FriendPoke(adapter),
// onebot11 // onebot11
new SendLike(adapter), new SendLike(adapter),
new GetMsg(adapter), new GetMsg(adapter),
@@ -128,9 +116,9 @@ export function initActionMap(adapter: Adapter) {
new GetRecord(adapter), new GetRecord(adapter),
new CleanCache(adapter), new CleanCache(adapter),
new GetCookies(adapter), new GetCookies(adapter),
new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(adapter), new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(adapter), new ForwardGroupSingleMsg(adapter),
new GetCredentials(adapter),
// go-cqhttp // go-cqhttp
new GetEssenceMsgList(adapter), new GetEssenceMsgList(adapter),
new GetGroupHonorInfo(adapter), new GetGroupHonorInfo(adapter),
@@ -156,13 +144,9 @@ export function initActionMap(adapter: Adapter) {
new GetGroupRootFiles(adapter), new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter), new SendGroupNotice(adapter),
new GetGroupFilesByFolder(adapter), new GetGroupFilesByFolder(adapter),
new GetGroupFileUrl(adapter), new GetGroupFileUrl(adapter)
new GetGroupNotice(adapter),
new DeleteFriend(adapter),
new OCRImage(adapter),
new GetGroupFileSystemInfo(adapter),
] ]
const actionMap = new Map() const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {
actionMap.set(action.actionName, action) actionMap.set(action.actionName, action)
actionMap.set(action.actionName + '_async', action) actionMap.set(action.actionName + '_async', action)

View File

@@ -16,8 +16,10 @@ export default class Debug extends BaseAction<Payload, unknown> {
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass] const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass]
if (method && method instanceof Function) { if (method && method instanceof Function) {
const result = await method.apply(ntqqApiClass, payload.args) const result = method.apply(ntqqApiClass, payload.args)
this.ctx.logger.info('debug', result) if (method.constructor.name === 'AsyncFunction') {
return await result
}
return result return result
} }
} }

View File

@@ -1,25 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc'
interface Payload {
user_id: number | string
}
export class FriendPoke extends BaseAction<Payload, null> {
actionName = ActionName.FriendPoke
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform()) {
throw new Error('当前系统平台或架构不支持')
}
if (!this.ctx.app.native.checkVersion()) {
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
}
await this.ctx.app.native.sendFriendPoke(+payload.user_id)
return null
}
}

View File

@@ -13,7 +13,7 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
protected async _handle(): Promise<OB11GroupRequestNotify[]> { protected async _handle(): Promise<OB11GroupRequestNotify[]> {
const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies() const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies()
const notifies = data.notifies.filter(notify => notify.status === GroupNotifyStatus.Unhandle) const notifies = data.notifies.filter(notify => notify.status === GroupNotifyStatus.KUNHANDLE)
const returnData: OB11GroupRequestNotify[] = [] const returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)

View File

@@ -1,11 +0,0 @@
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
import { Dict } from 'cosmokit'
export class GetRobotUinRange extends BaseAction<void, Dict[]> {
actionName = ActionName.GetRobotUinRange
async _handle() {
return await this.ctx.ntUserApi.getRobotUinRange()
}
}

View File

@@ -1,27 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc'
interface Payload {
group_id: number | string
user_id: number | string
}
export class GroupPoke extends BaseAction<Payload, null> {
actionName = ActionName.GroupPoke
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform()) {
throw new Error('当前系统平台或架构不支持')
}
if (!this.ctx.app.native.checkVersion()) {
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
}
await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id)
return null
}
}

View File

@@ -11,13 +11,13 @@ export default class SetAvatar extends BaseAction<Payload, null> {
actionName = ActionName.SetQQAvatar actionName = ActionName.SetQQAvatar
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const { path, isLocal, errMsg } = await uri2local(this.ctx, payload.file) const { path, isLocal, errMsg } = await uri2local(payload.file)
if (errMsg) { if (errMsg) {
throw new Error(errMsg) throw new Error(errMsg)
} }
if (path) { if (path) {
await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃需要提前判断 await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃需要提前判断
const ret = await this.ctx.ntUserApi.setSelfAvatar(path) const ret = await this.ctx.ntUserApi.setQQAvatar(path)
if (!isLocal) { if (!isLocal) {
unlink(path) unlink(path)
} }

View File

@@ -19,8 +19,8 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
} }
const peer = await createPeer(this.ctx, payload) const peer = await createPeer(this.ctx, payload)
const ret = await this.ctx.ntMsgApi.forwardMsg(msg.peer, peer, [msg.msgId]) const ret = await this.ctx.ntMsgApi.forwardMsg(msg.peer, peer, [msg.msgId])
if (ret.length === 0) { if (ret.result !== 0) {
throw new Error(`转发消息失败`) throw new Error(`转发消息失败 ${ret.errMsg}`)
} }
return null return null
} }

View File

@@ -25,11 +25,14 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
peerUid: msgInfo.peer.peerUid, peerUid: msgInfo.peer.peerUid,
chatType: msgInfo.peer.chatType chatType: msgInfo.peer.chatType
} }
const msg = this.ctx.store.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0] const msg = this.adapter.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0]
const retMsg = await OB11Entities.message(this.ctx, msg) const retMsg = await OB11Entities.message(this.ctx, msg)
if (!retMsg) { if (!retMsg) {
throw new Error('消息为空') throw new Error('消息为空')
} }
retMsg.message_id = this.ctx.store.createMsgShortId(peer, msg.msgId)
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg return retMsg
} }
} }

View File

@@ -9,7 +9,7 @@ import {
import { BaseAction } from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { message2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
interface ReturnData { interface ReturnData {
message_id: number message_id: number
@@ -26,14 +26,14 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
contextMode = CreatePeerMode.Private contextMode = CreatePeerMode.Private
} }
const peer = await createPeer(this.ctx, payload, contextMode) const peer = await createPeer(this.ctx, payload, contextMode)
const messages = message2List( const messages = convertMessage2List(
payload.message, payload.message,
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',
) )
if (this.getSpecialMsgNum(messages, OB11MessageDataType.Node)) { if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
throw new Error('请使用 /send_group_forward_msg 或 /send_private_forward_msg 进行合并转发') throw new Error('请使用 /send_group_forward_msg 或 /send_private_forward_msg 进行合并转发')
} }
else if (this.getSpecialMsgNum(messages, OB11MessageDataType.Music)) { else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) {
const music = messages[0] as OB11MessageMusic const music = messages[0] as OB11MessageMusic
if (music) { if (music) {
const { musicSignUrl } = this.adapter.config const { musicSignUrl } = this.adapter.config
@@ -78,7 +78,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
throw `签名音乐消息失败:${e}` throw `签名音乐消息失败:${e}`
} }
messages[0] = { messages[0] = {
type: OB11MessageDataType.Json, type: OB11MessageDataType.json,
data: { data: jsonContent }, data: { data: jsonContent },
} as OB11MessageJson } as OB11MessageJson
} }

View File

@@ -1,26 +0,0 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
domain: string
}
interface Response {
cookies: string
csrf_token: number
}
export class GetCredentials extends BaseAction<Payload, Response> {
actionName = ActionName.GetCredentials
payloadSchema = Schema.object({
domain: Schema.string().required()
})
protected async _handle(payload: Payload) {
const cookiesObject = await this.ctx.ntUserApi.getCookies(payload.domain)
//把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const bkn = cookiesObject.skey ? this.ctx.ntWebApi.genBkn(cookiesObject.skey) : ''
return { cookies, csrf_token: +bkn }
}
}

View File

@@ -24,11 +24,6 @@ export enum ActionName {
FetchEmojiLike = 'fetch_emoji_like', FetchEmojiLike = 'fetch_emoji_like',
FetchCustomFace = 'fetch_custom_face', FetchCustomFace = 'fetch_custom_face',
GetFriendMsgHistory = 'get_friend_msg_history', GetFriendMsgHistory = 'get_friend_msg_history',
SendForwardMsg = 'send_forward_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
GetRobotUinRange = 'get_robot_uin_range',
GroupPoke = 'group_poke',
FriendPoke = 'friend_poke',
// onebot 11 // onebot 11
SendLike = 'send_like', SendLike = 'send_like',
GetLoginInfo = 'get_login_info', GetLoginInfo = 'get_login_info',
@@ -42,6 +37,7 @@ export enum ActionName {
SendGroupMsg = 'send_group_msg', SendGroupMsg = 'send_group_msg',
SendPrivateMsg = 'send_private_msg', SendPrivateMsg = 'send_private_msg',
DeleteMsg = 'delete_msg', DeleteMsg = 'delete_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
SetGroupAddRequest = 'set_group_add_request', SetGroupAddRequest = 'set_group_add_request',
SetFriendAddRequest = 'set_friend_add_request', SetFriendAddRequest = 'set_friend_add_request',
SetGroupLeave = 'set_group_leave', SetGroupLeave = 'set_group_leave',
@@ -61,8 +57,8 @@ export enum ActionName {
GetCookies = 'get_cookies', GetCookies = 'get_cookies',
ForwardFriendSingleMsg = 'forward_friend_single_msg', ForwardFriendSingleMsg = 'forward_friend_single_msg',
ForwardGroupSingleMsg = 'forward_group_single_msg', ForwardGroupSingleMsg = 'forward_group_single_msg',
GetCredentials = 'get_credentials', // 以下为go-cqhttp api
// go-cqhttp GoCQHTTP_SendForwardMsg = 'send_forward_msg',
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg', GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg', GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_GetStrangerInfo = 'get_stranger_info', GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
@@ -86,9 +82,5 @@ export enum ActionName {
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files', GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_SendGroupNotice = '_send_group_notice', GoCQHTTP_SendGroupNotice = '_send_group_notice',
GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder', GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder',
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url', GoCQHTTP_GetGroupFileUrl = 'get_group_file_url'
GoCQHTTP_GetGroupNotice = '_get_group_notice',
GoCQHTTP_DeleteFriend = 'delete_friend',
GoCQHTTP_OCRImage = 'ocr_image',
GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info',
} }

View File

@@ -1,22 +1,22 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload {
domain: string
}
interface Response { interface Response {
cookies: string cookies: string
bkn: string bkn: string
} }
interface Payload {
domain: string
}
export class GetCookies extends BaseAction<Payload, Response> { export class GetCookies extends BaseAction<Payload, Response> {
actionName = ActionName.GetCookies actionName = ActionName.GetCookies
payloadSchema = Schema.object({
domain: Schema.string().required()
})
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (!payload.domain) {
throw '缺少参数 domain'
}
const cookiesObject = await this.ctx.ntUserApi.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('; ')

View File

@@ -23,11 +23,11 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
if (payload.remark) { if (payload.remark) {
await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark) await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark)
} }
/*await this.ctx.ntMsgApi.activateChat({ await this.ctx.ntMsgApi.activateChat({
peerUid: uid, peerUid: uid,
chatType: ChatType.C2C, chatType: ChatType.C2C,
guildId: '' guildId: ''
})*/ })
return null return null
} }
} }

View File

@@ -4,25 +4,30 @@ import {
GroupNotify, GroupNotify,
GroupNotifyType, GroupNotifyType,
RawMessage, RawMessage,
BuddyReqType,
Peer,
FriendRequest, FriendRequest,
GroupMember,
GroupMemberRole,
GroupNotifyStatus GroupNotifyStatus
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { selfInfo } from '../common/globalVars' import { selfInfo } from '../common/globalVars'
import { OB11Config, Config as LLOBConfig } from '../common/types' import { OB11Config, Config as LLOBConfig } from '../common/types'
import { OB11WebSocket, OB11WebSocketReverseManager } from './connect/ws' import { OB11WebSocket, OB11WebSocketReverseManager } from './connect/ws'
import { OB11Http, OB11HttpPost } from './connect/http' import { OB11Http, OB11HttpPost } from './connect/http'
import { OB11BaseEvent } from './event/OB11BaseEvent' import { OB11BaseEvent } from './event/OB11BaseEvent'
import { OB11Message } from './types'
import { OB11BaseMetaEvent } from './event/meta/OB11BaseMetaEvent' import { OB11BaseMetaEvent } from './event/meta/OB11BaseMetaEvent'
import { postHttpEvent } from './helper/eventForHttp' import { postHttpEvent } from './helper/eventForHttp'
import { initActionMap } from './action' import { initActionMap } from './action'
import { llonebotError } from '../common/globalVars' import { llonebotError } from '../common/globalVars'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from './event/notice/OB11GroupAdminNoticeEvent' import { OB11GroupAdminNoticeEvent } from './event/notice/OB11GroupAdminNoticeEvent'
import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent' import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent'
import { Msg, SysMsg } from '@/ntqqapi/proto/compiled' import { SysMsg } from '@/ntqqapi/proto/compiled'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@@ -31,15 +36,14 @@ declare module 'cordis' {
} }
class OneBot11Adapter extends Service { class OneBot11Adapter extends Service {
static inject = [ static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store']
'ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi',
'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', public messages: Map<string, RawMessage> = new Map()
'store', 'app' public startTime = 0
] private ob11WebSocket: OB11WebSocket
private ob11WebSocket private ob11WebSocketReverseManager: OB11WebSocketReverseManager
private ob11WebSocketReverseManager private ob11Http: OB11Http
private ob11Http private ob11HttpPost: OB11HttpPost
private ob11HttpPost
constructor(public ctx: Context, public config: OneBot11Adapter.Config) { constructor(public ctx: Context, public config: OneBot11Adapter.Config) {
super(ctx, 'onebot', true) super(ctx, 'onebot', true)
@@ -71,7 +75,25 @@ class OneBot11Adapter extends Service {
}) })
} }
public dispatch(event: OB11BaseEvent) { /** 缓存近期消息内容 */
public async addMsgCache(msg: RawMessage) {
const expire = this.config.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) { if (this.config.enableWs) {
this.ob11WebSocket.emitEvent(event) this.ob11WebSocket.emitEvent(event)
} }
@@ -87,24 +109,36 @@ class OneBot11Adapter extends Service {
} }
} }
private async handleGroupNotify(notify: GroupNotify, doubt: boolean) { private async handleGroupNotify(notifies: GroupNotify[]) {
for (const notify of notifies) {
try { try {
const flag = `${notify.group.groupCode}|${notify.seq}|${notify.type}|${doubt === true ? '1' : '0'}` const notifyTime = parseInt(notify.seq) / 1000
if ([GroupNotifyType.MemberLeaveNotifyAdmin, GroupNotifyType.KickMemberNotifyAdmin].includes(notify.type)) { if (notifyTime < this.startTime) {
continue
}
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) {
this.ctx.logger.info('有成员退出通知', notify)
const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
let operatorId = member1Uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) { if (notify.user2.uid) {
this.ctx.logger.info('有群成员被踢', notify.group.groupCode, notify.user1.uid, notify.user2.uid) // 是被踢的
const memberUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const adminUin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid) if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick'
}
const event = new OB11GroupDecreaseEvent( const event = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(memberUin), parseInt(member1Uin),
parseInt(adminUin), parseInt(operatorId),
'kick', subType,
) )
this.dispatch(event) this.dispatch(event)
} }
} else if (notify.type === GroupNotifyType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
else if (notify.type === GroupNotifyType.RequestJoinNeedAdminiStratorPass && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('有加群请求') this.ctx.logger.info('有加群请求')
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent( const event = new OB11GroupRequestEvent(
@@ -115,7 +149,7 @@ class OneBot11Adapter extends Service {
) )
this.dispatch(event) this.dispatch(event)
} }
else if (notify.type === GroupNotifyType.InvitedByMember && notify.status === GroupNotifyStatus.Unhandle) { else if (notify.type === GroupNotifyType.INVITED_BY_MEMBER && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到邀请我加群通知') this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid) const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent( const event = new OB11GroupRequestEvent(
@@ -128,7 +162,7 @@ class OneBot11Adapter extends Service {
) )
this.dispatch(event) this.dispatch(event)
} }
else if (notify.type === GroupNotifyType.InvitedNeedAdminiStratorPass && notify.status === GroupNotifyStatus.Unhandle) { else if (notify.type === GroupNotifyType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到群员邀请加群通知') this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent( const event = new OB11GroupRequestEvent(
@@ -139,27 +173,22 @@ class OneBot11Adapter extends Service {
) )
this.dispatch(event) this.dispatch(event)
} }
else if ([
GroupNotifyType.SetAdmin,
GroupNotifyType.CancelAdminNotifyCanceled,
GroupNotifyType.CancelAdminNotifyAdmin
].includes(notify.type)) {
this.ctx.logger.info('收到管理员变动通知')
const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupAdminNoticeEvent(
notify.type === GroupNotifyType.SetAdmin ? 'set' : 'unset',
parseInt(notify.group.groupCode),
parseInt(uin),
)
this.dispatch(event)
}
} catch (e) { } catch (e) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack) this.ctx.logger.error('解析群通知失败', (e as Error).stack)
} }
} }
}
private handleMsg(message: RawMessage) { private handleMsg(msgList: RawMessage[]) {
OB11Entities.message(this.ctx, message).then(msg => { for (const message of msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < this.startTime / 1000) {
continue
}
this.addMsgCache(message)
OB11Entities.message(this.ctx, message)
.then((msg) => {
if (!msg) { if (!msg) {
return return
} }
@@ -167,23 +196,28 @@ class OneBot11Adapter extends Service {
return return
} }
const isSelfMsg = msg.user_id.toString() === selfInfo.uin const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) { if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin) msg.target_id = parseInt(message.peerUin)
} }
this.dispatch(msg) this.dispatch(msg)
}).catch(e => this.ctx.logger.error('handling incoming messages', e)) })
.catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString()))
OB11Entities.groupEvent(this.ctx, message).then(groupEvent => { OB11Entities.groupEvent(this.ctx, message).then((groupEvent) => {
if (groupEvent) { if (groupEvent) {
this.dispatch(groupEvent) this.dispatch(groupEvent)
} }
}).catch(e => this.ctx.logger.error('handling incoming group events', e)) })
OB11Entities.privateEvent(this.ctx, message).then(privateEvent => { OB11Entities.privateEvent(this.ctx, message).then((privateEvent) => {
if (privateEvent) { if (privateEvent) {
this.dispatch(privateEvent) this.dispatch(privateEvent)
} }
}).catch(e => this.ctx.logger.error('handling incoming buddy events', e)) })
}
} }
private handleRecallMsg(message: RawMessage) { private handleRecallMsg(message: RawMessage) {
@@ -202,7 +236,14 @@ class OneBot11Adapter extends Service {
}) })
} }
private async handleFriendRequest(req: FriendRequest) { 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 let userId = 0
try { try {
const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid) const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
@@ -219,6 +260,7 @@ class OneBot11Adapter extends Service {
) )
this.dispatch(friendRequestEvent) this.dispatch(friendRequestEvent)
} }
}
private async handleConfigUpdated(config: LLOBConfig) { private async handleConfigUpdated(config: LLOBConfig) {
const old = this.config const old = this.config
@@ -301,6 +343,7 @@ class OneBot11Adapter extends Service {
heartInterval: config.heartInterval, heartInterval: config.heartInterval,
token: config.token, token: config.token,
debug: config.debug, debug: config.debug,
reportSelfMessage: config.reportSelfMessage,
msgCacheExpire: config.msgCacheExpire, msgCacheExpire: config.msgCacheExpire,
musicSignUrl: config.musicSignUrl, musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url, enableLocalFile2Url: config.enableLocalFile2Url,
@@ -308,7 +351,31 @@ class OneBot11Adapter extends Service {
}) })
} }
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() { public start() {
this.startTime = Date.now()
if (this.config.enableWs) { if (this.config.enableWs) {
this.ob11WebSocket.start() this.ob11WebSocket.start()
} }
@@ -321,7 +388,7 @@ class OneBot11Adapter extends Service {
if (this.config.enableHttpPost) { if (this.config.enableHttpPost) {
this.ob11HttpPost.start() this.ob11HttpPost.start()
} }
this.ctx.on('llob/config-updated', input => { this.ctx.on('llonebot/config-updated', input => {
this.handleConfigUpdated(input) this.handleConfigUpdated(input)
}) })
this.ctx.on('nt/message-created', input => { this.ctx.on('nt/message-created', input => {
@@ -331,45 +398,28 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input) this.handleRecallMsg(input)
}) })
this.ctx.on('nt/message-sent', input => { this.ctx.on('nt/message-sent', input => {
if (!this.config.reportSelfMessage) { this.handleMsg([input])
return
}
this.handleMsg(input)
}) })
this.ctx.on('nt/group-notify', input => { this.ctx.on('nt/group-notify', input => {
const { doubt, notify } = input this.handleGroupNotify(input)
this.handleGroupNotify(notify, doubt)
}) })
this.ctx.on('nt/friend-request', input => { this.ctx.on('nt/friend-request', input => {
this.handleFriendRequest(input) this.handleFriendRequest(input)
}) })
this.ctx.on('nt/system-message-created', async input => { this.ctx.on('nt/group-member-info-updated', input => {
const sysMsg = Msg.Message.decode(input) this.handleGroupMemberInfoUpdated(input.groupCode, input.members)
const { msgType, subType } = sysMsg.contentHead ?? {} })
if (msgType === 528 && subType === 39) { this.ctx.on('nt/system-message-created', input => {
const tip = SysMsg.ProfileLikeTip.decode(sysMsg.body!.msgContent!) const sysMsg = SysMsg.SystemMessage.decode(input)
const { msgType, subType, subSubType } = sysMsg.msgSpec[0] ?? {}
if (msgType === 528 && subType === 39 && subSubType === 39) {
const tip = SysMsg.ProfileLikeTip.decode(sysMsg.bodyWrapper!.body!)
if (tip.msgType !== 0 || tip.subType !== 203) return if (tip.msgType !== 0 || tip.subType !== 203) return
const detail = tip.content?.msg?.detail const detail = tip.content?.msg?.detail
if (!detail) return if (!detail) return
const [times] = detail.txt?.match(/\d+/) ?? ['0'] const [times] = detail.txt?.match(/\d+/) ?? ['0']
const event = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times) const profileLikeEvent = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
this.dispatch(event) this.dispatch(profileLikeEvent)
} else if (msgType === 33) {
const tip = SysMsg.GroupMemberChange.decode(sysMsg.body!.msgContent!)
if (tip.type !== 130) return
this.ctx.logger.info('群成员增加', tip)
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
const operatorUin = await this.ctx.ntUserApi.getUinByUid(tip.adminUid)
const event = new OB11GroupIncreaseEvent(tip.groupCode, +memberUin, +operatorUin)
this.dispatch(event)
} else if (msgType === 34) {
const tip = SysMsg.GroupMemberChange.decode(sysMsg.body!.msgContent!)
if (tip.type !== 130) return // adminUid: 0
this.ctx.logger.info('群成员减少', tip)
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
const userId = Number(memberUin)
const event = new OB11GroupDecreaseEvent(tip.groupCode, userId, userId)
this.dispatch(event)
} }
}) })
} }
@@ -380,6 +430,8 @@ namespace OneBot11Adapter {
heartInterval: number heartInterval: number
token: string token: string
debug: boolean debug: boolean
reportSelfMessage: boolean
msgCacheExpire: number
musicSignUrl?: string musicSignUrl?: string
enableLocalFile2Url: boolean enableLocalFile2Url: boolean
ffmpeg?: string ffmpeg?: string

View File

@@ -1,7 +1,7 @@
import http from 'node:http' import http from 'node:http'
import cors from 'cors' import cors from 'cors'
import crypto from 'node:crypto' import crypto from 'node:crypto'
import express, { Express, Request, Response, NextFunction } from 'express' import express, { Express, Request, Response } from 'express'
import { BaseAction } from '../action/BaseAction' import { BaseAction } from '../action/BaseAction'
import { Context } from 'cordis' import { Context } from 'cordis'
import { llonebotError, selfInfo } from '@/common/globalVars' import { llonebotError, selfInfo } from '@/common/globalVars'
@@ -76,7 +76,7 @@ class OB11Http {
Object.assign(this.config, config) Object.assign(this.config, config)
} }
private authorize(req: Request, res: Response, next: NextFunction) { private authorize(req: Request, res: Response, next: () => void) {
const serverToken = this.config.token const serverToken = this.config.token
if (!serverToken) return next() if (!serverToken) return next()
@@ -95,13 +95,12 @@ class OB11Http {
} }
if (clientToken !== serverToken) { if (clientToken !== serverToken) {
res.status(403).json({ message: 'token verify failed!' }) return res.status(403).json({ message: 'token verify failed!' })
} else { }
next() next()
} }
}
private async handleRequest(req: Request, res: Response, next: NextFunction) { private async handleRequest(req: Request, res: Response, next: () => void) {
if (req.path === '/') return next() if (req.path === '/') return next()
let payload = req.body let payload = req.body
if (req.method === 'GET') { if (req.method === 'GET') {
@@ -134,13 +133,11 @@ namespace OB11Http {
class OB11HttpPost { class OB11HttpPost {
private disposeInterval?: () => void private disposeInterval?: () => void
private activated = false
constructor(protected ctx: Context, public config: OB11HttpPost.Config) { constructor(protected ctx: Context, public config: OB11HttpPost.Config) {
} }
public start() { public start() {
this.activated = true
if (this.config.enableHttpHeart && !this.disposeInterval) { if (this.config.enableHttpHeart && !this.disposeInterval) {
this.disposeInterval = this.ctx.setInterval(() => { this.disposeInterval = this.ctx.setInterval(() => {
// ws的心跳是ws自己维护的 // ws的心跳是ws自己维护的
@@ -150,14 +147,10 @@ class OB11HttpPost {
} }
public stop() { public stop() {
this.activated = false
this.disposeInterval?.() this.disposeInterval?.()
} }
public async emitEvent(event: OB11BaseEvent) { public async emitEvent(event: OB11BaseEvent | OB11Message) {
if (!this.activated || !this.config.hosts.length) {
return
}
const msgStr = JSON.stringify(event) const msgStr = JSON.stringify(event)
const headers: Dict = { const headers: Dict = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -177,8 +170,7 @@ class OB11HttpPost {
}).then( }).then(
async (res) => { async (res) => {
if (event.post_type) { if (event.post_type) {
const eventName = event.post_type + '.' + event[event.post_type + '_type'] this.ctx.logger.info(`HTTP 事件上报: ${host}`, event.post_type, res.status)
this.ctx.logger.info(`HTTP 事件上报: ${host}`, eventName, res.status)
} }
try { try {
const resJson = await res.json() const resJson = await res.json()

View File

@@ -57,12 +57,11 @@ class OB11WebSocket {
}) })
} }
public async emitEvent(event: OB11BaseEvent) { public async emitEvent(event: OB11BaseEvent | OB11Message) {
this.wsClients.forEach(({ socket, emitEvent }) => { this.wsClients.forEach(({ socket, emitEvent }) => {
if (emitEvent && socket.readyState === WebSocket.OPEN) { if (emitEvent && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(event)) socket.send(JSON.stringify(event))
const eventName = event.post_type + '.' + event[event.post_type + '_type'] this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', event.post_type)
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', eventName)
} }
}) })
} }
@@ -194,11 +193,10 @@ class OB11WebSocketReverse {
this.wsClient?.close() this.wsClient?.close()
} }
public emitEvent(event: OB11BaseEvent) { public emitEvent(event: OB11BaseEvent | OB11Message) {
if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) { if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) {
this.wsClient.send(JSON.stringify(event)) this.wsClient.send(JSON.stringify(event))
const eventName = event.post_type + '.' + event[event.post_type + '_type'] this.ctx.logger.info('WebSocket 事件上报', this.wsClient.url ?? '', event.post_type)
this.ctx.logger.info('WebSocket 事件上报', this.wsClient.url ?? '', eventName)
} }
} }
@@ -216,7 +214,7 @@ class OB11WebSocketReverse {
let receive: { action: ActionName | null; params: unknown; echo?: unknown } = { action: null, params: {} } let receive: { action: ActionName | null; params: unknown; echo?: unknown } = { action: null, params: {} }
try { try {
receive = JSON.parse(msg.toString()) receive = JSON.parse(msg.toString())
this.ctx.logger.info('收到反向 Websocket 消息', receive) this.ctx.logger.info('收到反向Websocket消息', receive)
} catch (e) { } catch (e) {
return this.reply(this.wsClient!, OB11Response.error('json解析失败请检查数据格式', 1400, receive.echo)) return this.reply(this.wsClient!, OB11Response.error('json解析失败请检查数据格式', 1400, receive.echo))
} }

View File

@@ -1,3 +1,4 @@
import { XMLParser } from 'fast-xml-parser'
import { import {
OB11Group, OB11Group,
OB11GroupMember, OB11GroupMember,
@@ -19,7 +20,7 @@ import {
Sex, Sex,
TipGroupElementType, TipGroupElementType,
User, User,
SimpleInfo FriendV2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
@@ -29,6 +30,7 @@ import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNotice
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { calcQQLevel } from '../common/utils/misc' import { calcQQLevel } from '../common/utils/misc'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
@@ -37,7 +39,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent' import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit, pick, Dict } from 'cosmokit' import { omit, isNullable, pick, Dict } from 'cosmokit'
import { Context } from 'cordis' import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
@@ -70,7 +72,7 @@ export namespace OB11Entities {
sub_type: 'friend', sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [], message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array', message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfUin === msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
} }
if (debug) { if (debug) {
resMsg.raw = msg resMsg.raw = msg
@@ -78,24 +80,21 @@ export namespace OB11Entities {
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)
// 284840486: 合并转发内部 const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin)
if (msg.peerUin !== '284840486') {
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUid)
if (member) { if (member) {
resMsg.sender.role = groupMemberRole(member.role) resMsg.sender.role = groupMemberRole(member.role)
resMsg.sender.nickname = member.nick resMsg.sender.nickname = member.nick
resMsg.sender.title = member.memberSpecialTitle ?? '' resMsg.sender.title = member.memberSpecialTitle ?? ''
} }
} }
}
else if (msg.chatType === ChatType.C2C) { else if (msg.chatType === ChatType.C2C) {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
resMsg.sender.nickname = (await ctx.ntUserApi.getUserSimpleInfo(msg.senderUid)).nick resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick
} }
else if (msg.chatType === ChatType.TempC2CFromGroup) { else if (msg.chatType === ChatType.TempC2CFromGroup) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
resMsg.temp_source = 0 //群聊 resMsg.temp_source = 0 //群聊
resMsg.sender.nickname = (await ctx.ntUserApi.getUserSimpleInfo(msg.senderUid)).nick resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick
const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType.TempC2CFromGroup, msg.senderUid) const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType.TempC2CFromGroup, msg.senderUid)
if (ret?.result === 0) { if (ret?.result === 0) {
resMsg.sender.group_id = Number(ret.tmpChatInfo?.groupCode) resMsg.sender.group_id = Number(ret.tmpChatInfo?.groupCode)
@@ -121,7 +120,7 @@ export namespace OB11Entities {
name = content.replace('@', '') name = content.replace('@', '')
} }
messageSegment = { messageSegment = {
type: OB11MessageDataType.At, type: OB11MessageDataType.at,
data: { data: {
qq, qq,
name name
@@ -134,7 +133,7 @@ export namespace OB11Entities {
continue continue
} }
messageSegment = { messageSegment = {
type: OB11MessageDataType.Text, type: OB11MessageDataType.text,
data: { data: {
text text
} }
@@ -170,7 +169,7 @@ export namespace OB11Entities {
throw new Error('回复消息验证失败') throw new Error('回复消息验证失败')
} }
messageSegment = { messageSegment = {
type: OB11MessageDataType.Reply, type: OB11MessageDataType.reply,
data: { data: {
id: ctx.store.createMsgShortId(peer, replyMsg ? replyMsg.msgId : records.msgId).toString() id: ctx.store.createMsgShortId(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
} }
@@ -184,7 +183,7 @@ export namespace OB11Entities {
const { picElement } = element const { picElement } = element
const fileSize = picElement.fileSize ?? '0' const fileSize = picElement.fileSize ?? '0'
messageSegment = { messageSegment = {
type: OB11MessageDataType.Image, type: OB11MessageDataType.image,
data: { data: {
file: picElement.fileName, file: picElement.fileName,
subType: picElement.picSubType, subType: picElement.picSubType,
@@ -212,7 +211,7 @@ export namespace OB11Entities {
}, msg.msgId, element.elementId) }, msg.msgId, element.elementId)
const fileSize = videoElement.fileSize ?? '0' const fileSize = videoElement.fileSize ?? '0'
messageSegment = { messageSegment = {
type: OB11MessageDataType.Video, type: OB11MessageDataType.video,
data: { data: {
file: videoElement.fileName, file: videoElement.fileName,
url: videoUrl || pathToFileURL(videoElement.filePath).href, url: videoUrl || pathToFileURL(videoElement.filePath).href,
@@ -236,7 +235,7 @@ export namespace OB11Entities {
const { fileElement } = element const { fileElement } = element
const fileSize = fileElement.fileSize ?? '0' const fileSize = fileElement.fileSize ?? '0'
messageSegment = { messageSegment = {
type: OB11MessageDataType.File, type: OB11MessageDataType.file,
data: { data: {
file: fileElement.fileName, file: fileElement.fileName,
url: pathToFileURL(fileElement.filePath).href, url: pathToFileURL(fileElement.filePath).href,
@@ -261,7 +260,7 @@ export namespace OB11Entities {
const { pttElement } = element const { pttElement } = element
const fileSize = pttElement.fileSize ?? '0' const fileSize = pttElement.fileSize ?? '0'
messageSegment = { messageSegment = {
type: OB11MessageDataType.Record, type: OB11MessageDataType.voice,
data: { data: {
file: pttElement.fileName, file: pttElement.fileName,
url: pathToFileURL(pttElement.filePath).href, url: pathToFileURL(pttElement.filePath).href,
@@ -284,7 +283,7 @@ export namespace OB11Entities {
else if (element.arkElement) { else if (element.arkElement) {
const { arkElement } = element const { arkElement } = element
messageSegment = { messageSegment = {
type: OB11MessageDataType.Json, type: OB11MessageDataType.json,
data: { data: {
data: arkElement.bytesData data: arkElement.bytesData
} }
@@ -295,14 +294,14 @@ export namespace OB11Entities {
const { faceIndex, pokeType } = faceElement const { faceIndex, pokeType } = faceElement
if (faceIndex === FaceIndex.Dice) { if (faceIndex === FaceIndex.Dice) {
messageSegment = { messageSegment = {
type: OB11MessageDataType.Dice, type: OB11MessageDataType.dice,
data: { data: {
result: faceElement.resultId! result: faceElement.resultId!
} }
} }
} else if (faceIndex === FaceIndex.RPS) { } else if (faceIndex === FaceIndex.RPS) {
messageSegment = { messageSegment = {
type: OB11MessageDataType.Rps, type: OB11MessageDataType.RPS,
data: { data: {
result: faceElement.resultId! result: faceElement.resultId!
} }
@@ -314,7 +313,7 @@ export namespace OB11Entities {
}*/ }*/
} else { } else {
messageSegment = { messageSegment = {
type: OB11MessageDataType.Face, type: OB11MessageDataType.face,
data: { data: {
id: faceIndex.toString() id: faceIndex.toString()
} }
@@ -330,7 +329,7 @@ export namespace OB11Entities {
// const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000` // const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000`
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif` const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`
messageSegment = { messageSegment = {
type: OB11MessageDataType.Mface, type: OB11MessageDataType.mface,
data: { data: {
summary: marketFaceElement.faceName!, summary: marketFaceElement.faceName!,
url, url,
@@ -339,19 +338,20 @@ export namespace OB11Entities {
key: marketFaceElement.key key: marketFaceElement.key
} }
} }
//mFaceCache.set(emojiId, element.marketFaceElement.faceName!)
} }
else if (element.markdownElement) { else if (element.markdownElement) {
const { markdownElement } = element const { markdownElement } = element
messageSegment = { messageSegment = {
type: OB11MessageDataType.Markdown, type: OB11MessageDataType.markdown,
data: { data: {
content: markdownElement.content data: markdownElement.content
} }
} }
} }
else if (element.multiForwardMsgElement) { else if (element.multiForwardMsgElement) {
messageSegment = { messageSegment = {
type: OB11MessageDataType.Forward, type: OB11MessageDataType.forward,
data: { data: {
id: msg.msgId id: msg.msgId
} }
@@ -375,23 +375,11 @@ export namespace OB11Entities {
if (msg.chatType !== ChatType.C2C) { if (msg.chatType !== ChatType.C2C) {
return return
} }
if (msg.msgType !== 5) {
return
}
for (const element of msg.elements) { for (const element of msg.elements) {
if (element.grayTipElement) { if (element.grayTipElement) {
const { grayTipElement } = element const { grayTipElement } = element
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') { if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr) const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
if (param) {
return new OB11FriendPokeEvent(
Number(param.templParam.get('uin_str1')),
Number(param.templParam.get('uin_str2')),
json.items
)
}
const pokedetail: Dict[] = json.items const pokedetail: Dict[] = json.items
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
@@ -416,17 +404,38 @@ export namespace OB11Entities {
if (msg.chatType !== ChatType.Group) { if (msg.chatType !== ChatType.Group) {
return return
} }
if (msg.msgType !== 5 && msg.msgType !== 3) { if (msg.senderUin) {
return const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin)
if (member && member.cardName !== msg.sendMemberName) {
const event = new OB11GroupCardEvent(
parseInt(msg.peerUid),
parseInt(msg.senderUin),
msg.sendMemberName!,
member.cardName,
)
member.cardName = msg.sendMemberName!
return event
}
} }
for (const element of msg.elements) { for (const element of msg.elements) {
const grayTipElement = element.grayTipElement const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement const groupElement = grayTipElement?.groupElement
const xmlElement = grayTipElement?.xmlElement
if (groupElement) { if (groupElement) {
if (groupElement.type === TipGroupElementType.Ban) { if (groupElement.type === TipGroupElementType.MemberIncrease) {
ctx.logger.info('收到群成员增加消息', groupElement)
await ctx.sleep(1000)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid)
let memberUin = member?.uin
if (!memberUin) {
memberUin = (await ctx.ntUserApi.getUserDetailInfo(groupElement.memberUid)).uin
}
const adminMember = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid)
if (memberUin) {
const operatorUin = adminMember?.uin || memberUin
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin))
}
}
else if (groupElement.type === TipGroupElementType.Ban) {
ctx.logger.info('收到群成员禁言提示', 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
@@ -478,13 +487,6 @@ export namespace OB11Entities {
) )
} }
} }
else if (groupElement.type === TipGroupElementType.MemberIncrease) {
const { memberUid, adminUid } = groupElement
if (memberUid !== selfInfo.uid) return
ctx.logger.info('收到群成员增加消息', groupElement)
const adminUin = adminUid ? await ctx.ntUserApi.getUinByUid(adminUid) : selfInfo.uin
return new OB11GroupIncreaseEvent(+msg.peerUid, +selfInfo.uin, +adminUin)
}
} }
else if (element.fileElement) { else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(+msg.peerUid, +msg.senderUin!, { return new OB11GroupUploadNoticeEvent(+msg.peerUid, +msg.senderUin!, {
@@ -494,70 +496,81 @@ export namespace OB11Entities {
busid: element.fileElement.fileBizId || 0, busid: element.fileElement.fileBizId || 0,
}) })
} }
else if (xmlElement) {
if (xmlElement.templId === '10382') { if (grayTipElement) {
ctx.logger.info('收到表情回应我的消息', xmlElement.templParam) const xmlElement = grayTipElement.xmlElement
if (xmlElement?.templId === '10382') {
const emojiLikeData = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
}).parse(xmlElement.content)
ctx.logger.info('收到表情回应我的消息', emojiLikeData)
try { try {
const senderUin = xmlElement.templParam.get('jp_uin') const senderUin: string = emojiLikeData.gtip.qq.jp
const msgSeq = xmlElement.templParam.get('msg_seq') const msgSeq: string = emojiLikeData.gtip.url.msgseq
const emojiId = xmlElement.templParam.get('face_id') const emojiId: string = emojiLikeData.gtip.face.id
const peer = { const peer = {
chatType: ChatType.Group, chatType: ChatType.Group,
guildId: '', guildId: '',
peerUid: msg.peerUid, peerUid: msg.peerUid,
} }
const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq!)).msgList const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList
if (!replyMsgList?.length) { if (!replyMsgList?.length) {
return return
} }
const shortId = ctx.store.createMsgShortId(peer, replyMsgList[0].msgId) const shortId = ctx.store.getShortIdByMsgInfo(peer, replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent( return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(senderUin!), parseInt(senderUin),
shortId, shortId!,
[{ [{
emoji_id: emojiId!, emoji_id: emojiId,
count: 1, count: 1,
}] }]
) )
} catch (e) { } catch (e) {
ctx.logger.error('解析表情回应消息失败', (e as Error).stack) ctx.logger.error('解析表情回应消息失败', (e as Error).stack)
} }
} else if (xmlElement.templId == '10179') {
ctx.logger.info('收到新人被邀请进群消息', xmlElement)
const invitor = xmlElement.templParam.get('invitor')
const invitee = xmlElement.templParam.get('invitee')
if (invitor && invitee) {
return new OB11GroupIncreaseEvent(+msg.peerUid, +invitee, +invitor, 'invite')
}
}
} }
if (grayTipElement) { if (
if (grayTipElement.subElementType == GrayTipElementSubType.JSON) { grayTipElement.subElementType == GrayTipElementSubType.XmlMsg &&
xmlElement?.templId == '10179'
) {
ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
if (xmlElement?.content) {
const regex = /jp="(\d+)"/g
const matches: string[] = []
let match: RegExpExecArray | null = null
while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1])
}
// log("新人进群匹配到的QQ号", matches)
if (matches.length === 2) {
const [inviter, invitee] = matches
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite')
}
}
}
else if (grayTipElement.subElementType == GrayTipElementSubType.JSON) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement!.jsonStr) const json = JSON.parse(grayTipElement.jsonGrayTipElement!.jsonStr)
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') { if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
if (param) {
return new OB11GroupPokeEvent(
Number(msg.peerUid),
Number(param.templParam.get('uin_str1')),
Number(param.templParam.get('uin_str2')),
json.items
)
}
const pokedetail: Dict[] = json.items const pokedetail: Dict[] = json.items
//筛选item带有uid的元素 //筛选item带有uid的元素
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 OB11GroupPokeEvent( return new OB11GroupPokeEvent(
Number(msg.peerUid), parseInt(msg.peerUid),
Number(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
Number(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
pokedetail pokedetail
) )
} }
} else if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) { }
if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) {
ctx.logger.info('收到群精华消息', json) ctx.logger.info('收到群精华消息', json)
const searchParams = new URL(json.items[2].jp).searchParams const searchParams = new URL(json.items[2].jp).searchParams
const msgSeq = searchParams.get('seq') const msgSeq = searchParams.get('seq')
@@ -579,16 +592,17 @@ export namespace OB11Entities {
parseInt(essence.items[0]?.msgSenderUin ?? sourceMsg.senderUin), parseInt(essence.items[0]?.msgSenderUin ?? sourceMsg.senderUin),
parseInt(essence.items[0]?.opUin ?? '0'), parseInt(essence.items[0]?.opUin ?? '0'),
) )
} else 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
ctx.logger.info('收到群成员新头衔消息', json) ctx.logger.info('收到群成员新头衔消息', json)
ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNullable(member)) {
member.memberSpecialTitle = title
}
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title) return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
} else if (grayTipElement.jsonGrayTipElement?.busiId === '19217') {
ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
const userId = new URL(json.items[2].jp).searchParams.get('robot_uin')
const operatorId = new URL(json.items[0].jp).searchParams.get('uin')
return new OB11GroupIncreaseEvent(Number(msg.peerUid), Number(userId), Number(operatorId), 'invite')
} }
} }
} }
@@ -600,7 +614,13 @@ export namespace OB11Entities {
msg: RawMessage, msg: RawMessage,
shortId: number shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
const revokeElement = msg.elements[0].grayTipElement?.revokeElement const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.Revoke,
)
if (!msgElement) {
return
}
const revokeElement = msgElement.grayTipElement!.revokeElement
if (msg.chatType === ChatType.Group) { if (msg.chatType === ChatType.Group) {
const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement!.operatorUid) const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement!.operatorUid)
return new OB11GroupRecallNoticeEvent( return new OB11GroupRecallNoticeEvent(
@@ -629,7 +649,7 @@ export namespace OB11Entities {
return friends.map(friend) return friends.map(friend)
} }
export function friendV2(raw: SimpleInfo): OB11User { export function friendV2(raw: FriendV2): OB11User {
return { return {
...omit(raw.baseInfo, ['richBuffer', 'phoneNum']), ...omit(raw.baseInfo, ['richBuffer', 'phoneNum']),
...omit(raw.coreInfo, ['nick']), ...omit(raw.coreInfo, ['nick']),
@@ -641,50 +661,60 @@ export namespace OB11Entities {
} }
} }
export function friendsV2(raw: SimpleInfo[]): OB11User[] { export function friendsV2(raw: FriendV2[]): OB11User[] {
return raw.map(friendV2) return raw.map(friendV2)
} }
export function groupMemberRole(role: number): OB11GroupMemberRole { export function groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return { return {
4: OB11GroupMemberRole.Owner, 4: OB11GroupMemberRole.owner,
3: OB11GroupMemberRole.Admin, 3: OB11GroupMemberRole.admin,
2: OB11GroupMemberRole.Member, 2: OB11GroupMemberRole.member,
}[role] ?? OB11GroupMemberRole.Member }[role]
} }
export function sex(sex: Sex): OB11UserSex { export function sex(sex: Sex): OB11UserSex {
const sexMap = { const sexMap = {
[Sex.Unknown]: OB11UserSex.Unknown, [Sex.male]: OB11UserSex.male,
[Sex.Male]: OB11UserSex.Male, [Sex.female]: OB11UserSex.female,
[Sex.Female]: OB11UserSex.Female, [Sex.unknown]: OB11UserSex.unknown,
[Sex.Hidden]: OB11UserSex.Unknown
} }
return sexMap[sex] ?? OB11UserSex.Unknown return sexMap[sex] || OB11UserSex.unknown
} }
export function groupMember(groupId: number, member: GroupMember): OB11GroupMember { export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
const titleExpireTime = +member.specialTitleExpireTime
const int32Max = 2147483647
return { return {
group_id: groupId, group_id: parseInt(group_id),
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName || member.nick, card: member.cardName,
sex: OB11UserSex.Unknown, sex: sex(member.sex!),
age: 0, age: 0,
area: '', area: '',
level: String(member.memberLevel ?? 0), level: String(member.memberLevel ?? 0),
qq_level: 0, qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: member.joinTime, join_time: member.joinTime,
last_sent_time: member.lastSpeakTime, last_sent_time: member.lastSpeakTime,
title_expire_time: titleExpireTime > int32Max ? 0 : titleExpireTime, title_expire_time: 0,
unfriendly: false, unfriendly: false,
card_changeable: true, card_changeable: true,
is_robot: member.isRobot, is_robot: member.isRobot,
shut_up_timestamp: member.shutUpTime, shut_up_timestamp: member.shutUpTime,
role: groupMemberRole(member.role), role: groupMemberRole(member.role),
title: member.memberSpecialTitle, title: member.memberSpecialTitle || '',
}
}
export function stranger(user: User): OB11User {
return {
...user,
user_id: parseInt(user.uin),
nickname: user.nick,
sex: sex(user.sex!),
age: 0,
qid: user.qid,
login_days: 0,
level: (user.qqLevel && calcQQLevel(user.qqLevel)) || 0,
} }
} }

View File

@@ -9,7 +9,6 @@ export enum EventType {
} }
export abstract class OB11BaseEvent { export abstract class OB11BaseEvent {
[index: string]: any
time = Math.floor(Date.now() / 1000) time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin) self_id = parseInt(selfInfo.uin)
abstract post_type: EventType abstract post_type: EventType

View File

@@ -12,7 +12,7 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') { constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super() super()
this.group_id = groupId this.group_id = groupId
this.operator_id = operatorId this.operator_id = operatorId // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的
this.user_id = userId this.user_id = userId
this.sub_type = subType this.sub_type = subType
} }

View File

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

View File

@@ -15,7 +15,7 @@ import {
} from '../types' } from '../types'
import { decodeCQCode } from '../cqcode' import { decodeCQCode } from '../cqcode'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { SendElement } from '@/ntqqapi/entities' import { SendElementEntities } from '@/ntqqapi/entities'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { Context } from 'cordis' import { Context } from 'cordis'
@@ -33,14 +33,14 @@ export async function createSendElements(
continue continue
} }
switch (sendMsg.type) { switch (sendMsg.type) {
case OB11MessageDataType.Text: { case OB11MessageDataType.text: {
const text = sendMsg.data?.text const text = sendMsg.data?.text
if (text) { if (text) {
sendElements.push(SendElement.text(sendMsg.data!.text)) sendElements.push(SendElementEntities.text(sendMsg.data!.text))
} }
} }
break break
case OB11MessageDataType.At: { case OB11MessageDataType.at: {
if (!peer) { if (!peer) {
continue continue
} }
@@ -56,50 +56,68 @@ export async function createSendElements(
remainAtAllCount = (await ctx.ntGroupApi.getGroupRemainAtTimes(groupCode)).atInfo remainAtAllCount = (await ctx.ntGroupApi.getGroupRemainAtTimes(groupCode)).atInfo
.RemainAtAllCountForUin .RemainAtAllCountForUin
ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount) ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uid) 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) {
} }
} }
if (isAdmin && remainAtAllCount > 0) { if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendElement.at(atQQ, atQQ, AtType.All, '@全体成员')) sendElements.push(SendElementEntities.at(atQQ, atQQ, AtType.All, '@全体成员'))
} }
} }
else if (peer.chatType === ChatType.Group) { else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid) ?? '' const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ)
let display = '' if (atMember) {
if (sendMsg.data.name) { const display = `@${atMember.cardName || atMember.nick}`
display = `@${sendMsg.data.name}` sendElements.push(
SendElementEntities.at(atQQ, atMember.uid, AtType.One, display),
)
} else {
const atNmae = sendMsg.data?.name
const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : ''
sendElements.push(
SendElementEntities.at(atQQ, uid, AtType.One, display),
)
} }
sendElements.push(SendElement.at(atQQ, uid, AtType.One, display))
} }
} }
} }
break break
case OB11MessageDataType.Reply: { case OB11MessageDataType.reply: {
if (sendMsg.data?.id) { if (sendMsg.data?.id) {
const info = await ctx.store.getMsgInfoByShortId(+sendMsg.data.id) const replyMsgId = await ctx.store.getMsgInfoByShortId(+sendMsg.data.id)
if (!info) { if (!replyMsgId) {
ctx.logger.warn('回复消息不存在', info) ctx.logger.warn('回复消息不存在', replyMsgId)
continue continue
} }
const source = (await ctx.ntMsgApi.getMsgsByMsgId(info.peer, [info.msgId])).msgList[0] const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId(
if (source) { replyMsgId.peer,
sendElements.push(SendElement.reply(source.msgSeq, source.msgId, source.senderUin)) [replyMsgId.msgId!]
)).msgList[0]
if (replyMsg) {
sendElements.push(
SendElementEntities.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin!,
replyMsg.senderUin!,
),
)
} }
} }
} }
break break
case OB11MessageDataType.Face: { case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id const faceId = sendMsg.data?.id
if (faceId) { if (faceId) {
sendElements.push(SendElement.face(parseInt(faceId))) sendElements.push(SendElementEntities.face(parseInt(faceId)))
} }
} }
break break
case OB11MessageDataType.Mface: { case OB11MessageDataType.mface: {
sendElements.push( sendElements.push(
SendElement.mface( SendElementEntities.mface(
+sendMsg.data.emoji_package_id, +sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id, sendMsg.data.emoji_id,
sendMsg.data.key, sendMsg.data.key,
@@ -108,10 +126,10 @@ export async function createSendElements(
) )
} }
break break
case OB11MessageDataType.Image: { case OB11MessageDataType.image: {
const res = await SendElement.pic( const res = await SendElementEntities.pic(
ctx, ctx,
(await handleOb11RichMedia(ctx, sendMsg, deleteAfterSentFiles)).path, (await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '', sendMsg.data.summary || '',
sendMsg.data.subType || 0, sendMsg.data.subType || 0,
sendMsg.data.type === 'flash' sendMsg.data.type === 'flash'
@@ -120,50 +138,50 @@ export async function createSendElements(
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.File: { case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11RichMedia(ctx, sendMsg, deleteAfterSentFiles) const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendElement.file(ctx, path, fileName)) sendElements.push(await SendElementEntities.file(ctx, path, fileName))
} }
break break
case OB11MessageDataType.Video: { case OB11MessageDataType.video: {
const { path, fileName } = await handleOb11RichMedia(ctx, 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(ctx, thumb) const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path if (uri2LocalRes.success) thumb = uri2LocalRes.path
} }
const res = await SendElement.video(ctx, path, fileName, thumb) const res = await SendElementEntities.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath) deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.Record: { case OB11MessageDataType.voice: {
const { path } = await handleOb11RichMedia(ctx, sendMsg, deleteAfterSentFiles) const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendElement.ptt(ctx, path)) sendElements.push(await SendElementEntities.ptt(ctx, path))
} }
break break
case OB11MessageDataType.Json: { case OB11MessageDataType.json: {
sendElements.push(SendElement.ark(sendMsg.data.data)) sendElements.push(SendElementEntities.ark(sendMsg.data.data))
} }
break break
case OB11MessageDataType.Dice: { case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendElement.dice(resultId)) sendElements.push(SendElementEntities.dice(resultId))
} }
break break
case OB11MessageDataType.Rps: { case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendElement.rps(resultId)) sendElements.push(SendElementEntities.rps(resultId))
} }
break break
case OB11MessageDataType.Contact: { case OB11MessageDataType.contact: {
const { type, id } = sendMsg.data const { type, id } = sendMsg.data
const data = type === 'qq' ? ctx.ntFriendApi.getBuddyRecommendContact(id) : ctx.ntGroupApi.getGroupRecommendContact(id) const data = type === 'qq' ? ctx.ntFriendApi.getBuddyRecommendContact(id) : ctx.ntGroupApi.getGroupRecommendContact(id)
sendElements.push(SendElement.ark(await data)) sendElements.push(SendElementEntities.ark(await data))
} }
break break
case OB11MessageDataType.Shake: { case OB11MessageDataType.shake: {
sendElements.push(SendElement.shake()) sendElements.push(SendElementEntities.shake())
} }
break break
} }
@@ -175,22 +193,51 @@ export async function createSendElements(
} }
} }
export function message2List(message: OB11MessageMixType, autoEscape = false) { // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
async function handleOb11FileLikeMessage(
ctx: Context,
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: { deleteAfterSentFiles: string[] },
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const {
path,
isLocal,
fileName,
errMsg,
success,
} = (await uri2local(inputdata?.url || inputdata.file))
if (!success) {
ctx.logger.error(errMsg)
throw Error(errMsg)
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
return { path, fileName: inputdata.name || fileName }
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') { if (typeof message === 'string') {
if (autoEscape === true) { if (autoEscape === true) {
return [ message = [
{ {
type: OB11MessageDataType.Text, type: OB11MessageDataType.text,
data: { data: {
text: message, text: message,
}, },
}, },
] as OB11MessageData[] ]
} else {
return decodeCQCode(message)
} }
} else if (!Array.isArray(message)) { else {
return [message] message = decodeCQCode(message.toString())
}
}
else if (!Array.isArray(message)) {
message = [message]
} }
return message return message
} }
@@ -272,18 +319,3 @@ export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode
} }
throw new Error('请指定 group_id 或 user_id') throw new Error('请指定 group_id 或 user_id')
} }
export async function handleOb11RichMedia(ctx: Context, segment: OB11MessageFileBase, deleteAfterSentFiles: string[]) {
const res = await uri2local(ctx, segment.data.url || segment.data.file)
if (!res.success) {
ctx.logger.error(res.errMsg)
throw Error(res.errMsg)
}
if (!res.isLocal) {
deleteAfterSentFiles.push(res.path)
}
return { path: res.path, fileName: segment.data.name || res.fileName }
}

View File

@@ -1,239 +0,0 @@
import { Context } from 'cordis'
import { OB11MessageData, OB11MessageDataType } from '../types'
import { Msg, RichMedia } from '@/ntqqapi/proto/compiled'
import { handleOb11RichMedia } from './createMessage'
import { selfInfo } from '@/common/globalVars'
import { Peer, RichMediaUploadCompleteNotify } from '@/ntqqapi/types'
import { deflateSync } from 'node:zlib'
import faceConfig from '@/ntqqapi/helper/face_config.json'
export class MessageEncoder {
static support = ['text', 'face', 'image', 'markdown', 'forward']
results: Msg.Message[]
children: Msg.Elem[]
deleteAfterSentFiles: string[]
isGroup: boolean
seq: number
tsum: number
preview: string
news: { text: string }[]
name?: string
uin?: number
constructor(private ctx: Context, private peer: Peer) {
this.results = []
this.children = []
this.deleteAfterSentFiles = []
this.isGroup = peer.chatType === 2
this.seq = Math.trunc(Math.random() * 65430)
this.tsum = 0
this.preview = ''
this.news = []
}
async flush() {
if (this.children.length === 0) return
const nick = this.name || selfInfo.nick || 'QQ用户'
if (this.news.length < 4) {
this.news.push({
text: `${nick}: ${this.preview}`
})
}
this.results.push({
routingHead: {
fromUin: this.uin ?? +selfInfo.uin ?? 1094950020,
c2c: this.isGroup ? undefined : {
friendName: nick
},
group: this.isGroup ? {
groupCode: 284840486,
groupCard: nick
} : undefined
},
contentHead: {
msgType: this.isGroup ? 82 : 9,
random: Math.floor(Math.random() * 4294967290),
msgSeq: this.seq,
msgTime: Math.trunc(Date.now() / 1000),
pkgNum: 1,
pkgIndex: 0,
divSeq: 0,
field15: {
field1: 0,
field2: 0,
field3: 0,
field4: '',
field5: ''
}
},
body: {
richText: {
elems: this.children
}
}
})
this.seq++
this.tsum++
this.children = []
this.preview = ''
}
async packImage(data: RichMediaUploadCompleteNotify, busiType: number) {
const imageSize = await this.ctx.ntFileApi.getImageSize(data.filePath)
return {
commonElem: {
serviceType: 48,
pbElem: RichMedia.MsgInfo.encode({
msgInfoBody: [{
index: {
info: {
fileSize: +data.commonFileInfo.fileSize,
md5HexStr: data.commonFileInfo.md5,
sha1HexStr: data.commonFileInfo.sha,
fileName: data.commonFileInfo.fileName,
fileType: {
type: 1,
picFormat: imageSize.type === 'gif' ? 2000 : 1000
},
width: imageSize.width,
height: imageSize.height,
time: 0,
original: 1
},
fileUuid: data.fileId,
storeID: 1,
expire: 2678400
},
pic: {
urlPath: `/download?appid=${this.isGroup ? 1407 : 1406}&fileid=${data.fileId}`,
ext: {
originalParam: '&spec=0',
bigParam: '&spec=720',
thumbParam: '&spec=198'
},
domain: 'multimedia.nt.qq.com.cn'
},
fileExist: true
}],
extBizInfo: {
pic: {
bizType: 0,
summary: ''
},
busiType
}
}).finish(),
businessType: this.isGroup ? 20 : 10
}
}
}
packForwardMessage(resid: string) {
const uuid = crypto.randomUUID()
const content = JSON.stringify({
app: 'com.tencent.multimsg',
config: {
autosize: 1,
forward: 1,
round: 1,
type: 'normal',
width: 300
},
desc: '[聊天记录]',
extra: JSON.stringify({
filename: uuid,
tsum: 0,
}),
meta: {
detail: {
news: [{
text: '查看转发消息'
}],
resid,
source: '聊天记录',
summary: '查看转发消息',
uniseq: uuid,
}
},
prompt: '[聊天记录]',
ver: '0.0.0.5',
view: 'contact'
})
return {
lightApp: {
data: Buffer.concat([Buffer.from([1]), deflateSync(Buffer.from(content, 'utf-8'))])
}
}
}
async visit(segment: OB11MessageData) {
const { type, data } = segment
if (type === OB11MessageDataType.Node) {
await this.render(data.content as OB11MessageData[])
const id = data.uin ?? data.user_id
this.uin = id ? +id : undefined
this.name = data.name ?? data.nickname
await this.flush()
} else if (type === OB11MessageDataType.Text) {
this.children.push({
text: {
str: data.text
}
})
this.preview += data.text
} else if (type === OB11MessageDataType.Face) {
this.children.push({
face: {
index: +data.id
}
})
const face = faceConfig.sysface.find(e => e.QSid === String(data.id))
if (face) {
this.preview += face.QDes
}
} else if (type === OB11MessageDataType.Image) {
const { path } = await handleOb11RichMedia(this.ctx, segment, this.deleteAfterSentFiles)
const data = await this.ctx.ntFileApi.uploadRMFileWithoutMsg(path, this.isGroup ? 4 : 3, this.peer.peerUid)
const busiType = Number(segment.data.subType) || 0
this.children.push(await this.packImage(data, busiType))
this.preview += busiType === 1 ? '[动画表情]' : '[图片]'
} else if (type === OB11MessageDataType.Markdown) {
this.children.push({
commonElem: {
serviceType: 45,
pbElem: Msg.MarkdownElem.encode(data).finish(),
businessType: 1
}
})
} else if (type === OB11MessageDataType.Forward) {
this.children.push(this.packForwardMessage(data.id))
this.preview += '[聊天记录]'
}
}
async render(segments: OB11MessageData[]) {
for (const segment of segments) {
await this.visit(segment)
}
}
async generate(content: any[]) {
await this.render(content)
return {
multiMsgItems: [{
fileName: 'MultiMsg',
buffer: {
msg: this.results
}
}],
tsum: this.tsum,
source: this.isGroup ? '群聊的聊天记录' : '聊天记录',
summary: `查看${this.tsum}条转发消息`,
news: this.news
}
}
}

View File

@@ -2,7 +2,7 @@ import { OB11Message, 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 { GroupRequestOperateTypes } from '@/ntqqapi/types' import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { message2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../helper/createMessage' import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../helper/createMessage'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
import { Context } from 'cordis' import { Context } from 'cordis'
@@ -65,7 +65,7 @@ async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOpera
if (reply) { if (reply) {
let replyMessage: OB11MessageData[] = [] let replyMessage: OB11MessageData[] = []
replyMessage.push({ replyMessage.push({
type: OB11MessageDataType.Reply, type: OB11MessageDataType.reply,
data: { data: {
id: msg.message_id.toString(), id: msg.message_id.toString(),
}, },
@@ -74,14 +74,14 @@ async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOpera
if (msg.message_type == 'group') { if (msg.message_type == 'group') {
if ((quickAction as QuickOperationGroupMessage).at_sender) { if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({ replyMessage.push({
type: OB11MessageDataType.At, type: OB11MessageDataType.at,
data: { data: {
qq: msg.user_id.toString(), qq: msg.user_id.toString(),
}, },
}) })
} }
} }
replyMessage = replyMessage.concat(message2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(ctx, replyMessage, peer) const { sendElements, deleteAfterSentFiles } = await createSendElements(ctx, replyMessage, peer)
sendMsg(ctx, peer, sendElements, deleteAfterSentFiles).catch(e => ctx.logger.error(e)) sendMsg(ctx, peer, sendElements, deleteAfterSentFiles).catch(e => ctx.logger.error(e))
} }

View File

@@ -11,39 +11,41 @@ export interface OB11User {
age?: number age?: number
qid?: string qid?: string
login_days?: number login_days?: number
categroyName?: string
categoryId?: number
} }
export enum OB11UserSex { export enum OB11UserSex {
Male = 'male', male = 'male',
Female = 'female', female = 'female',
Unknown = 'unknown', unknown = 'unknown',
} }
export enum OB11GroupMemberRole { export enum OB11GroupMemberRole {
Owner = 'owner', owner = 'owner',
Admin = 'admin', admin = 'admin',
Member = 'member', member = 'member',
} }
export interface OB11GroupMember { export interface OB11GroupMember {
group_id: number group_id: number
user_id: number user_id: number
nickname: string nickname: string
card: string card?: string
sex: OB11UserSex sex?: OB11UserSex
age: number age?: number
join_time: number join_time?: number
last_sent_time: number last_sent_time?: number
level: string level?: string
qq_level?: number qq_level?: number
role: OB11GroupMemberRole role?: OB11GroupMemberRole
title: string title?: string
area: string area?: string
unfriendly: boolean unfriendly?: boolean
title_expire_time: number title_expire_time?: number
card_changeable: boolean card_changeable?: boolean
// 以下为gocq字段 // 以下为gocq字段
shut_up_timestamp: number shut_up_timestamp?: number
// 以下为扩展字段 // 以下为扩展字段
is_robot?: boolean is_robot?: boolean
qage?: number qage?: number
@@ -77,7 +79,7 @@ export enum OB11MessageType {
export interface OB11Message { export interface OB11Message {
target_id?: number // 自己发送的消息才有此字段 target_id?: number // 自己发送的消息才有此字段
self_id: number self_id?: number
time: number time: number
message_id: number message_id: number
message_seq: number // go-cqhttp字段实际上是message_id message_seq: number // go-cqhttp字段实际上是message_id
@@ -91,7 +93,7 @@ export interface OB11Message {
message_format: 'array' | 'string' message_format: 'array' | 'string'
raw_message: string raw_message: string
font: number font: number
post_type: EventType post_type?: EventType
raw?: RawMessage raw?: RawMessage
temp_source?: 0 | 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 temp_source?: 0 | 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9
} }
@@ -117,30 +119,30 @@ export interface OB11Return<DataType> {
} }
export enum OB11MessageDataType { export enum OB11MessageDataType {
Text = 'text', text = 'text',
Image = 'image', image = 'image',
Music = 'music', music = 'music',
Video = 'video', video = 'video',
Record = 'record', voice = 'record',
File = 'file', file = 'file',
At = 'at', at = 'at',
Reply = 'reply', reply = 'reply',
Json = 'json', json = 'json',
Face = 'face', face = 'face',
Mface = 'mface', // 商城表情 mface = 'mface', // 商城表情
Markdown = 'markdown', markdown = 'markdown',
Node = 'node', // 合并转发消息节点 node = 'node', // 合并转发消息节点
Forward = 'forward', // 合并转发消息,用于上报 forward = 'forward', // 合并转发消息,用于上报
Xml = 'xml', xml = 'xml',
Poke = 'poke', poke = 'poke',
Dice = 'dice', dice = 'dice',
Rps = 'rps', RPS = 'rps',
Contact = 'contact', contact = 'contact',
Shake = 'shake', shake = 'shake',
} }
export interface OB11MessageMFace { export interface OB11MessageMFace {
type: OB11MessageDataType.Mface type: OB11MessageDataType.mface
data: { data: {
emoji_package_id: number emoji_package_id: number
emoji_id: string emoji_id: string
@@ -151,27 +153,27 @@ export interface OB11MessageMFace {
} }
export interface OB11MessageDice { export interface OB11MessageDice {
type: OB11MessageDataType.Dice type: OB11MessageDataType.dice
data: { data: {
result: number /* intended */ | string /* in fact */ result: number /* intended */ | string /* in fact */
} }
} }
export interface OB11MessageRPS { export interface OB11MessageRPS {
type: OB11MessageDataType.Rps type: OB11MessageDataType.RPS
data: { data: {
result: number | string result: number | string
} }
} }
export interface OB11MessageText { export interface OB11MessageText {
type: OB11MessageDataType.Text type: OB11MessageDataType.text
data: { data: {
text: string // 纯文本 text: string // 纯文本
} }
} }
export interface OB11MessagePoke { export interface OB11MessagePoke {
type: OB11MessageDataType.Poke type: OB11MessageDataType.poke
data: { data: {
qq?: number qq?: number
id?: number id?: number
@@ -189,7 +191,7 @@ export interface OB11MessageFileBase {
} }
export interface OB11MessageImage extends OB11MessageFileBase { export interface OB11MessageImage extends OB11MessageFileBase {
type: OB11MessageDataType.Image type: OB11MessageDataType.image
data: OB11MessageFileBase['data'] & { data: OB11MessageFileBase['data'] & {
summary?: string // 图片摘要 summary?: string // 图片摘要
subType?: PicSubType subType?: PicSubType
@@ -198,14 +200,14 @@ export interface OB11MessageImage extends OB11MessageFileBase {
} }
export interface OB11MessageRecord extends OB11MessageFileBase { export interface OB11MessageRecord extends OB11MessageFileBase {
type: OB11MessageDataType.Record type: OB11MessageDataType.voice
data: OB11MessageFileBase['data'] & { data: OB11MessageFileBase['data'] & {
path?: string //扩展 path?: string //扩展
} }
} }
export interface OB11MessageFile extends OB11MessageFileBase { export interface OB11MessageFile extends OB11MessageFileBase {
type: OB11MessageDataType.File type: OB11MessageDataType.file
data: OB11MessageFileBase['data'] & { data: OB11MessageFileBase['data'] & {
file_id?: string file_id?: string
path?: string path?: string
@@ -213,14 +215,14 @@ export interface OB11MessageFile extends OB11MessageFileBase {
} }
export interface OB11MessageVideo extends OB11MessageFileBase { export interface OB11MessageVideo extends OB11MessageFileBase {
type: OB11MessageDataType.Video type: OB11MessageDataType.video
data: OB11MessageFileBase['data'] & { data: OB11MessageFileBase['data'] & {
path?: string //扩展 path?: string //扩展
} }
} }
export interface OB11MessageAt { export interface OB11MessageAt {
type: OB11MessageDataType.At type: OB11MessageDataType.at
data: { data: {
qq: string | 'all' qq: string | 'all'
name?: string name?: string
@@ -228,14 +230,14 @@ export interface OB11MessageAt {
} }
export interface OB11MessageReply { export interface OB11MessageReply {
type: OB11MessageDataType.Reply type: OB11MessageDataType.reply
data: { data: {
id: string id: string
} }
} }
export interface OB11MessageFace { export interface OB11MessageFace {
type: OB11MessageDataType.Face type: OB11MessageDataType.face
data: { data: {
id: string id: string
} }
@@ -244,50 +246,48 @@ export interface OB11MessageFace {
export type OB11MessageMixType = OB11MessageData[] | string | OB11MessageData export type OB11MessageMixType = OB11MessageData[] | string | OB11MessageData
export interface OB11MessageNode { export interface OB11MessageNode {
type: OB11MessageDataType.Node type: OB11MessageDataType.node
data: { data: {
id?: number | string id?: string
content?: OB11MessageMixType user_id?: number
user_id?: number // ob11 nickname: string
nickname?: string // ob11 content: OB11MessageMixType
name?: string // gocq
uin?: number | string // gocq
} }
} }
export interface OB11MessageIdMusic { export interface OB11MessageIdMusic {
type: OB11MessageDataType.Music type: OB11MessageDataType.music
data: IdMusicSignPostData data: IdMusicSignPostData
} }
export interface OB11MessageCustomMusic { export interface OB11MessageCustomMusic {
type: OB11MessageDataType.Music type: OB11MessageDataType.music
data: Omit<CustomMusicSignPostData, 'singer'> & { content?: string } data: Omit<CustomMusicSignPostData, 'singer'> & { content?: string }
} }
export type OB11MessageMusic = OB11MessageIdMusic | OB11MessageCustomMusic export type OB11MessageMusic = OB11MessageIdMusic | OB11MessageCustomMusic
export interface OB11MessageJson { export interface OB11MessageJson {
type: OB11MessageDataType.Json type: OB11MessageDataType.json
data: { data: string /* , config: { token: string } */ } data: { data: string /* , config: { token: string } */ }
} }
export interface OB11MessageMarkdown { export interface OB11MessageMarkdown {
type: OB11MessageDataType.Markdown type: OB11MessageDataType.markdown
data: { data: {
content: string data: string
} }
} }
export interface OB11MessageForward { export interface OB11MessageForward {
type: OB11MessageDataType.Forward type: OB11MessageDataType.forward
data: { data: {
id: string id: string
} }
} }
export interface OB11MessageContact { export interface OB11MessageContact {
type: OB11MessageDataType.Contact type: OB11MessageDataType.contact
data: { data: {
type: 'qq' | 'group' type: 'qq' | 'group'
id: string id: string
@@ -295,7 +295,7 @@ export interface OB11MessageContact {
} }
export interface OB11MessageShake { export interface OB11MessageShake {
type: OB11MessageDataType.Shake type: OB11MessageDataType.shake
data: Record<string, never> data: Record<string, never>
} }

View File

@@ -7,8 +7,11 @@ 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 { contextBridge, ipcRenderer } from 'electron'
const { contextBridge } = require('electron')
const { ipcRenderer } = require('electron')
const llonebot = { const llonebot = {
log: (data: unknown) => { log: (data: unknown) => {
@@ -21,7 +24,8 @@ const llonebot = {
return ipcRenderer.invoke(CHANNEL_UPDATE) return ipcRenderer.invoke(CHANNEL_UPDATE)
}, },
setConfig: async (ask: boolean, config: Config) => { setConfig: async (ask: boolean, config: Config) => {
return ipcRenderer.invoke(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

@@ -3,4 +3,3 @@ export * from './item'
export * from './button' export * from './button'
export * from './switch' export * from './switch'
export * from './select' export * from './select'
export * from './input'

View File

@@ -1,20 +0,0 @@
export const SettingInput = (
key: string,
type: 'port' | 'text',
value: string | number,
placeholder: string | number,
style = ''
) => {
if (type === 'text') {
return `
<div class="q-input" style="${style}">
<input class="q-input__inner" data-config-key="${key}" type="text" value="${value}" placeholder="${placeholder}" />
</div>
`
}
return `
<div class="q-input" style="${style}">
<input class="q-input__inner" data-config-key="${key}" type="number" min="1" max="65534" value="${value}" placeholder="${placeholder}" />
</div>
`
}

View File

@@ -1,5 +1,5 @@
import { CheckVersion, Config } from '../common/types' import { CheckVersion, Config } from '../common/types'
import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect, SettingInput } from './components' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components'
import { version } from '../version' import { version } from '../version'
// @ts-expect-error: Unreachable code error // @ts-expect-error: Unreachable code error
import StyleRaw from './style.css?raw' import StyleRaw from './style.css?raw'
@@ -11,6 +11,7 @@ function isEmpty(value: unknown) {
} }
async function onSettingWindowCreated(view: Element) { async function onSettingWindowCreated(view: Element) {
//window.llonebot.log('setting window created')
const config = await window.llonebot.getConfig() const config = await window.llonebot.getConfig()
const ob11Config = { ...config.ob11 } const ob11Config = { ...config.ob11 }
@@ -25,6 +26,9 @@ async function onSettingWindowCreated(view: Element) {
} else { } else {
Object.assign(config, { [key]: value }) Object.assign(config, { [key]: value })
} }
if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) {
window.llonebot.setConfig(false, config)
}
} }
} }
@@ -45,35 +49,12 @@ async function onSettingWindowCreated(view: Element) {
]), ]),
SettingList([ SettingList([
SettingItem( SettingItem(
'是否启用 LLOneBot重启 QQ 后生效', '是否启用 LLOneBot, 重启 QQ 后生效',
null, null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }), SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
) )]
]), ),
SettingList([ SettingList([
SettingItem(
'是否启用 Satori 协议',
'重启 QQ 后生效',
SettingSwitch('satori.enable', config.satori.enable),
),
SettingItem(
'服务端口',
null,
SettingInput('satori.port', 'port', config.satori.port, config.satori.port),
),
SettingItem(
'服务令牌',
null,
SettingInput('satori.token', 'text', config.satori.token, '未设置', 'width:170px;'),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]),
SettingList([
SettingItem(
'是否启用 OneBot 协议',
'重启 QQ 后生效',
SettingSwitch('ob11.enable', config.ob11.enable),
),
SettingItem( SettingItem(
'启用 HTTP 服务', '启用 HTTP 服务',
null, null,
@@ -82,7 +63,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'HTTP 服务监听端口', 'HTTP 服务监听端口',
null, null,
SettingInput('ob11.httpPort', 'port', config.ob11.httpPort, config.ob11.httpPort), `<div class="q-input"><input class="q-input__inner" data-config-key="ob11.httpPort" type="number" min="1" max="65534" value="${config.ob11.httpPort}" placeholder="${config.ob11.httpPort}" /></div>`,
'config-ob11-httpPort', 'config-ob11-httpPort',
config.ob11.enableHttp, config.ob11.enableHttp,
), ),
@@ -146,14 +127,14 @@ async function onSettingWindowCreated(view: Element) {
<div id="config-ob11-wsHosts-list"></div> <div id="config-ob11-wsHosts-list"></div>
</div>`, </div>`,
SettingItem( SettingItem(
'WebSocket 服务心跳间隔', ' WebSocket 服务心跳间隔',
'控制每隔多久发送一个心跳包,单位为毫秒', '控制每隔多久发送一个心跳包,单位为毫秒',
`<div class="q-input"><input class="q-input__inner" data-config-key="heartInterval" type="number" min="1000" value="${config.heartInterval}" placeholder="${config.heartInterval}" /></div>`, `<div class="q-input"><input class="q-input__inner" data-config-key="heartInterval" type="number" min="1000" value="${config.heartInterval}" placeholder="${config.heartInterval}" /></div>`,
), ),
SettingItem( SettingItem(
'Access token', 'Access token',
null, null,
`<div class="q-input" style="width:170px;"><input class="q-input__inner" data-config-key="token" type="text" value="${config.token}" placeholder="未设置" /></div>`, `<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="token" type="text" value="${config.token}" placeholder="未设置" /></div>`,
), ),
SettingItem( SettingItem(
'新消息上报格式', '新消息上报格式',
@@ -167,24 +148,6 @@ async function onSettingWindowCreated(view: Element) {
config.ob11.messagePostFormat, config.ob11.messagePostFormat,
), ),
), ),
SettingItem(
'HTTP、正向 WebSocket 服务仅监听 127.0.0.1',
'而不是 0.0.0.0',
SettingSwitch('ob11.listenLocalhost', config.ob11.listenLocalhost),
),
SettingItem(
'上报 Bot 自身发送的消息',
'上报 event 为 message_sent',
SettingSwitch('ob11.reportSelfMessage', config.ob11.reportSelfMessage),
),
SettingItem(
'使用 Base64 编码获取文件',
'调用 /get_image、/get_record、/get_file 时,没有 url 时添加 Base64 字段',
SettingSwitch('enableLocalFile2Url', config.enableLocalFile2Url),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save-2', 'primary')),
]),
SettingList([
SettingItem( SettingItem(
'FFmpeg 路径,发送语音、视频需要', 'FFmpeg 路径,发送语音、视频需要',
`<a href="javascript:LiteLoader.api.openExternal(\'https://llonebot.github.io/zh-CN/guide/ffmpeg\');">可点此下载</a>, 路径: <span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定' `<a href="javascript:LiteLoader.api.openExternal(\'https://llonebot.github.io/zh-CN/guide/ffmpeg\');">可点此下载</a>, 路径: <span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'
@@ -197,6 +160,25 @@ async function onSettingWindowCreated(view: Element) {
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="musicSignUrl" type="text" value="${config.musicSignUrl}" placeholder="未设置" /></div>`, `<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="musicSignUrl" type="text" value="${config.musicSignUrl}" placeholder="未设置" /></div>`,
'config-musicSignUrl', 'config-musicSignUrl',
), ),
SettingItem(
'HTTP、正向 WebSocket 服务仅监听 127.0.0.1',
'而不是 0.0.0.0',
SettingSwitch('ob11.listenLocalhost', config.ob11.listenLocalhost),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]),
SettingList([
SettingItem(
'使用 Base64 编码获取文件',
'调用 /get_image、/get_record、/get_file 时,没有 url 时添加 Base64 字段',
SettingSwitch('enableLocalFile2Url', config.enableLocalFile2Url),
),
SettingItem('调试模式', '开启后上报信息会添加 raw 字段以附带原始信息', SettingSwitch('debug', config.debug)),
SettingItem(
'上报 Bot 自身发送的消息',
'上报 event 为 message_sent',
SettingSwitch('reportSelfMessage', config.reportSelfMessage),
),
SettingItem( SettingItem(
'自动删除收到的文件', '自动删除收到的文件',
'在收到文件后的指定时间内删除该文件', '在收到文件后的指定时间内删除该文件',
@@ -222,7 +204,6 @@ async function onSettingWindowCreated(view: Element) {
'单位为秒,可用于获取撤回的消息', '单位为秒,可用于获取撤回的消息',
`<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`, `<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`,
), ),
SettingItem('', null, SettingButton('保存', 'config-ob11-save-3', 'primary')),
]), ]),
SettingList([ SettingList([
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')), SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),
@@ -401,42 +382,26 @@ async function onSettingWindowCreated(view: Element) {
alert('保存成功') alert('保存成功')
}) })
doc.querySelector('#config-ob11-save-2')?.addEventListener('click', () => {
config.ob11 = ob11Config
window.llonebot.setConfig(false, config)
showError().then()
alert('保存成功')
})
doc.querySelector('#config-ob11-save-3')?.addEventListener('click', () => {
config.ob11 = ob11Config
window.llonebot.setConfig(false, config)
showError().then()
alert('保存成功')
})
doc.body.childNodes.forEach((node) => { doc.body.childNodes.forEach((node) => {
view.appendChild(node) view.appendChild(node)
}) })
// 更新逻辑 // 更新逻辑
async function checkVersionFunc(info: CheckVersion) { async function checkVersionFunc(ResultVersion: CheckVersion) {
const titleDom = view.querySelector<HTMLSpanElement>('#llonebot-update-title')! const titleDom = view.querySelector<HTMLSpanElement>('#llonebot-update-title')!
const buttonDom = view.querySelector<HTMLButtonElement>('#llonebot-update-button')! const buttonDom = view.querySelector<HTMLButtonElement>('#llonebot-update-button')!
if (info.version === '') { if (ResultVersion.version === '') {
titleDom.innerHTML = `当前版本为 v${version},检查更新失败` titleDom.innerHTML = `当前版本为 v${version},检查更新失败`
buttonDom.innerHTML = '点击重试' buttonDom.innerHTML = '点击重试'
buttonDom.addEventListener('click', async () => { buttonDom.addEventListener('click', async () => {
window.llonebot.checkVersion().then(checkVersionFunc) window.llonebot.checkVersion().then(checkVersionFunc)
}, { once: true }) })
} else if (!info.result) { } else if (!ResultVersion.result) {
titleDom.innerHTML = '当前已是最新版本 v' + version titleDom.innerHTML = '当前已是最新版本 v' + version
buttonDom.innerHTML = '无需更新' buttonDom.innerHTML = '无需更新'
} else { } else {
titleDom.innerHTML = `当前版本为 v${version},最新版本为 v${info.version}` titleDom.innerHTML = `当前版本为 v${version},最新版本为 v${ResultVersion.version}`
buttonDom.innerHTML = '点击更新' buttonDom.innerHTML = '点击更新'
buttonDom.dataset.type = 'primary' buttonDom.dataset.type = 'primary'
@@ -456,11 +421,9 @@ async function onSettingWindowCreated(view: Element) {
} }
window.llonebot.checkVersion().then(checkVersionFunc) window.llonebot.checkVersion().then(checkVersionFunc)
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
window.llonebot.getConfig().then(oldConfig=>{ if (JSON.stringify(ob11Config) === JSON.stringify(config.ob11)) return
if(JSON.stringify(oldConfig) !== JSON.stringify(config)){ config.ob11 = ob11Config
window.llonebot.setConfig(true, config) window.llonebot.setConfig(true, config)
}
})
}) })
} }

View File

@@ -1,193 +0,0 @@
import * as NT from '@/ntqqapi/types'
import { omit } from 'cosmokit'
import { Event } from '@satorijs/protocol'
import { Service, Context } from 'cordis'
import { SatoriConfig } from '@/common/types'
import { SatoriServer } from './server'
import { selfInfo } from '@/common/globalVars'
import { ObjectToSnake } from 'ts-case-convert'
import { isDeepStrictEqual } from 'node:util'
import { parseMessageCreated, parseMessageDeleted } from './event/message'
import { parseGuildAdded, parseGuildRemoved, parseGuildRequest } from './event/guild'
import { parseGuildMemberAdded, parseGuildMemberRemoved, parseGuildMemberRequest } from './event/member'
import { parseFriendRequest } from './event/user'
declare module 'cordis' {
interface Context {
satori: SatoriAdapter
}
}
class SatoriAdapter extends Service {
static inject = [
'ntMsgApi', 'ntFileApi', 'ntFileCacheApi',
'ntFriendApi', 'ntGroupApi', 'ntUserApi',
'ntWindowApi', 'ntWebApi', 'store',
]
private counter: number
private selfId: string
private server: SatoriServer
constructor(public ctx: Context, public config: SatoriAdapter.Config) {
super(ctx, 'satori', true)
this.counter = 0
this.selfId = selfInfo.uin
this.server = new SatoriServer(ctx, config)
}
async handleMessage(input: NT.RawMessage) {
if (
input.msgType === 5 &&
input.subMsgType === 8 &&
input.elements[0]?.grayTipElement?.groupElement?.type === 1 &&
input.elements[0].grayTipElement.groupElement.memberUid === selfInfo.uid
) {
// 自身主动申请
return await parseGuildAdded(this, input)
}
else if (
input.msgType === 5 &&
input.subMsgType === 12 &&
input.elements[0]?.grayTipElement?.xmlElement?.templId === '10179' &&
input.elements[0].grayTipElement.xmlElement.templParam.get('invitee') === selfInfo.uin
) {
// 自身被邀请
return await parseGuildAdded(this, input)
}
else if (
input.msgType === 5 &&
input.subMsgType === 8 &&
input.elements[0]?.grayTipElement?.groupElement?.type === 3
) {
// 自身被踢出
return await parseGuildRemoved(this, input)
}
else if (
input.msgType === 5 &&
input.subMsgType === 8 &&
input.elements[0]?.grayTipElement?.groupElement?.type === 1
) {
// 他人主动申请
return await parseGuildMemberAdded(this, input)
}
else if (
input.msgType === 5 &&
input.subMsgType === 12 &&
input.elements[0]?.grayTipElement?.xmlElement?.templId === '10179'
) {
// 他人被邀请
return await parseGuildMemberAdded(this, input)
}
else if (
input.msgType === 5 &&
input.subMsgType === 12 &&
input.elements[0]?.grayTipElement?.jsonGrayTipElement?.busiId === '19217'
) {
// 机器人被邀请
return await parseGuildMemberAdded(this, input, true)
}
else if (
input.msgType === 5 &&
input.subMsgType === 12 &&
input.elements[0]?.grayTipElement?.xmlElement?.templId === '10382'
) {
}
else {
// 普通的消息
return await parseMessageCreated(this, input)
}
}
async handleGroupNotify(input: NT.GroupNotify, doubt: boolean) {
if (
input.type === NT.GroupNotifyType.InvitedByMember &&
input.status === NT.GroupNotifyStatus.Unhandle
) {
// 自身被邀请,需自身同意
return await parseGuildRequest(this, input)
}
else if (
input.type === NT.GroupNotifyType.MemberLeaveNotifyAdmin ||
input.type === NT.GroupNotifyType.KickMemberNotifyAdmin
) {
// 他人主动退出或被踢
return await parseGuildMemberRemoved(this, input)
}
else if (
input.type === NT.GroupNotifyType.RequestJoinNeedAdminiStratorPass &&
input.status === NT.GroupNotifyStatus.Unhandle
) {
// 他人主动申请,需管理员同意
return await parseGuildMemberRequest(this, input, doubt)
}
else if (
input.type === NT.GroupNotifyType.InvitedNeedAdminiStratorPass &&
input.status === NT.GroupNotifyStatus.Unhandle
) {
// 他人被邀请,需管理员同意
return await parseGuildMemberRequest(this, input, doubt)
}
}
start() {
this.server.start()
this.ctx.on('nt/message-created', async input => {
const event = await this.handleMessage(input)
.catch(e => this.ctx.logger.error(e))
event && this.server.dispatch(event)
})
this.ctx.on('nt/group-notify', async input => {
const { doubt, notify } = input
const event = await this.handleGroupNotify(notify, doubt)
.catch(e => this.ctx.logger.error(e))
event && this.server.dispatch(event)
})
this.ctx.on('nt/message-deleted', async input => {
const event = await parseMessageDeleted(this, input)
.catch(e => this.ctx.logger.error(e))
event && this.server.dispatch(event)
})
this.ctx.on('nt/friend-request', async input => {
const event = await parseFriendRequest(this, input)
.catch(e => this.ctx.logger.error(e))
event && this.server.dispatch(event)
})
this.ctx.on('llob/config-updated', async input => {
const old = omit(this.config, ['ffmpeg'])
if (!isDeepStrictEqual(old, input.satori)) {
await this.server.stop()
this.server.updateConfig(input.satori)
this.server.start()
}
Object.assign(this.config, {
...input.satori,
ffmpeg: input.ffmpeg
})
})
}
event(type: string, data: Partial<ObjectToSnake<Event>>): ObjectToSnake<Event> {
return {
id: ++this.counter,
type,
self_id: this.selfId,
platform: 'llonebot',
timestamp: Date.now(),
...data
}
}
}
namespace SatoriAdapter {
export interface Config extends SatoriConfig {
ffmpeg?: string
}
}
export default SatoriAdapter

View File

@@ -1,11 +0,0 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
interface Payload {
channel_id: string
}
export const deleteChannel: Handler<Dict<never>, Payload> = async (ctx, payload) => {
await ctx.ntGroupApi.quitGroup(payload.channel_id)
return {}
}

View File

@@ -1,15 +0,0 @@
import { Channel } from '@satorijs/protocol'
import { Handler } from '../index'
interface Payload {
channel_id: string
}
export const getChannel: Handler<Channel, Payload> = async (ctx, payload) => {
const info = await ctx.ntGroupApi.getGroupAllInfo(payload.channel_id)
return {
id: payload.channel_id,
type: Channel.Type.TEXT,
name: info.groupAll.groupName
}
}

View File

@@ -1,18 +0,0 @@
import { Channel, List } from '@satorijs/protocol'
import { Handler } from '../index'
interface Payload {
guild_id: string
next?: string
}
export const getChannelList: Handler<List<Channel>, Payload> = async (ctx, payload) => {
const info = await ctx.ntGroupApi.getGroupAllInfo(payload.guild_id)
return {
data: [{
id: payload.guild_id,
type: Channel.Type.TEXT,
name: info.groupAll.groupName
}]
}
}

View File

@@ -1,12 +0,0 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
interface Payload {
channel_id: string
duration: number
}
export const muteChannel: Handler<Dict<never>, Payload> = async (ctx, payload) => {
await ctx.ntGroupApi.banGroup(payload.channel_id, payload.duration !== 0)
return {}
}

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