Compare commits

..

55 Commits

Author SHA1 Message Date
idranme
9b32140f87 Merge pull request #470 from LLOneBot/dev
release: 4.0.8
2024-10-13 16:36:12 +08:00
idranme
dc5982c6b2 chore: bump versions 2024-10-13 16:34:35 +08:00
idranme
ce46c99330 fix 2024-10-13 16:32:39 +08:00
idranme
d3abaf806f optimize 2024-10-13 16:32:18 +08:00
idranme
e10a67ce05 Merge pull request #468 from LLOneBot/dev
release: 4.0.7
2024-10-13 01:30:01 +08:00
idranme
c8e897abdb chore: bump versions 2024-10-13 01:29:19 +08:00
idranme
e07c06f3e9 Merge pull request #467 from LLOneBot/dev
...
2024-10-13 01:17:06 +08:00
idranme
9c694a11b5 fix: poke 2024-10-13 01:12:12 +08:00
idranme
6e3bb7c9cf fix 2024-10-13 01:10:39 +08:00
idranme
0d8d3ac24f Merge pull request #466 from LLOneBot/dev
release: 4.0.6
2024-10-13 00:48:36 +08:00
idranme
c96d032820 chore: bump versions 2024-10-13 00:47:31 +08:00
idranme
837f48b63a fix: poke 2024-10-13 00:45:30 +08:00
idranme
9d0f9e7096 Merge pull request #465 from LLOneBot/dev
release: 4.0.5
2024-10-12 23:58:07 +08:00
idranme
801d79d79d chore: bump versions 2024-10-12 23:55:32 +08:00
idranme
0d5640046c feat: poke 2024-10-12 23:50:58 +08:00
idranme
e988908784 Merge pull request #463 from LLOneBot/dev
release: 4.0.4
2024-10-11 18:22:37 +08:00
idranme
1cfa736dd5 chore: bump versions 2024-10-11 18:18:51 +08:00
idranme
0081b0b124 refactor 2024-10-11 18:17:52 +08:00
idranme
ba565e7c38 feat(onebot): ocr_image API 2024-10-11 17:42:19 +08:00
idranme
abb468c3f8 optimize 2024-10-11 13:38:59 +08:00
idranme
433a175809 fix: at element 2024-10-11 12:47:12 +08:00
idranme
b40c81c5cb Merge pull request #462 from LLOneBot/dev
release: 4.0.3
2024-10-11 00:52:33 +08:00
idranme
ddf7ffcabe chore: bump versions 2024-10-11 00:50:56 +08:00
idranme
2b0aa6249b fix: at element 2024-10-11 00:50:06 +08:00
idranme
6bb4a8fe69 optimize 2024-10-11 00:31:11 +08:00
idranme
91d78f22f7 refactor 2024-10-09 02:55:12 +08:00
idranme
457ffc0922 Merge pull request #461 from LLOneBot/dev
release: 4.0.2
2024-10-08 21:26:39 +08:00
idranme
e3a2303e45 chore: bump versions 2024-10-08 21:25:23 +08:00
idranme
8465c47d41 fix 2024-10-08 21:22:04 +08:00
idranme
41822eb052 Merge pull request #460 from LLOneBot/dev
release: 4.0.1
2024-10-08 20:46:09 +08:00
idranme
b5578d6278 chore: bump versions 2024-10-08 20:43:56 +08:00
idranme
fecb4c4655 feat(onebot): delete_friend API 2024-10-08 20:40:02 +08:00
idranme
c82b849ead fix 2024-10-08 20:07:12 +08:00
idranme
0bc6e23343 Merge pull request #459 from LLOneBot/dev
release: 4.0.0
2024-10-07 20:26:59 +08:00
idranme
8e9523602b chore: v4.0.0 2024-10-07 20:23:54 +08:00
idranme
48588817fb chore 2024-10-07 19:10:38 +08:00
idranme
4cd9adde1d feat: satori protocol 2024-10-06 10:37:06 +08:00
idranme
8c0cc8beba refactor 2024-10-06 10:28:52 +08:00
idranme
9ec09c6eee Merge pull request #457 from LLOneBot/dev
release: 3.34.1
2024-10-03 15:18:47 +08:00
idranme
4d816b498a chore: v3.34.1 2024-10-03 15:17:57 +08:00
idranme
464efe819d fix 2024-10-03 15:16:48 +08:00
idranme
0876e4645f Merge pull request #456 from LLOneBot/dev
release: 3.34.0
2024-10-01 21:32:24 +08:00
idranme
a2f9128623 chore: v3.34.0 2024-10-01 21:25:19 +08:00
idranme
e313b2b3e6 feat 2024-10-01 21:16:39 +08:00
idranme
a7d86f8fe0 refactor 2024-10-01 21:09:27 +08:00
idranme
496d56f297 feat 2024-09-30 00:49:58 +08:00
idranme
ed2f554d4e refactor 2024-09-28 22:00:05 +08:00
idranme
36d990e328 Merge pull request #452 from LLOneBot/dev
release: 3.33.10
2024-09-28 14:40:11 +08:00
idranme
0ceef4d4c0 chore: v3.33.10 2024-09-28 14:37:44 +08:00
idranme
35bf4f001b feat: _get_group_notice API 2024-09-28 14:35:06 +08:00
idranme
544682fe41 fix 2024-09-28 12:54:02 +08:00
idranme
3da49fbfba optimize 2024-09-27 18:37:47 +08:00
idranme
d5875c9e5b Merge pull request #451 from LLOneBot/dev
release: 3.33.9
2024-09-27 16:53:44 +08:00
idranme
7895644156 chore: v3.33.9 2024-09-27 16:51:49 +08:00
idranme
f092626ede fix 2024-09-27 16:22:50 +08:00
116 changed files with 3312 additions and 1522 deletions

7
.gitattributes vendored
View File

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

View File

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

View File

@@ -3,8 +3,8 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发",
"version": "3.33.8", "version": "4.0.8",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -18,29 +18,32 @@
"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.2", "cosmokit": "^1.6.3",
"express": "^5.0.0", "express": "^5.0.1",
"fast-xml-parser": "^4.5.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.1", "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": "^4.17.21", "@types/express": "^5.0.0",
"@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.2", "typescript": "^5.6.3",
"vite": "^5.4.7", "vite": "^5.4.8",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.5.0" "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 协议,用于 QQ 机器人开发', description: '实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [

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 } from './types' import { Config, OB11Config, SatoriConfig } 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,6 +22,7 @@ export class ConfigUtil {
reloadConfig(): Config { reloadConfig(): Config {
const ob11Default: OB11Config = { const ob11Default: OB11Config = {
enable: true,
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: '', httpSecret: '',
@@ -33,17 +34,23 @@ 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, satori: satoriDefault,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
token: '', token: '',
enableLocalFile2Url: false, enableLocalFile2Url: false,
debug: false, debug: false,
log: false, log: false,
reportSelfMessage: false,
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
@@ -66,6 +73,7 @@ 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
} }
@@ -79,8 +87,8 @@ export class ConfigUtil {
private checkOldConfig( private checkOldConfig(
currentConfig: OB11Config, currentConfig: OB11Config,
oldConfig: Config, oldConfig: Config,
currentKey: 'httpPort' | 'httpHosts' | 'wsPort', currentKey: 'httpPort' | 'httpHosts' | 'wsPort' | 'reportSelfMessage',
oldKey: 'http' | 'hosts' | 'wsPort', oldKey: 'http' | 'hosts' | 'wsPort' | 'reportSelfMessage',
) { ) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]

View File

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

View File

@@ -53,49 +53,44 @@ 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`) let result: EncodeResult
let result: EncodeResult const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) { result = await encode(file, 0)
result = await encode(file, 0)
} else {
const input = await convert(ctx, filePath, {
output: [
'-ar 24000',
'-ac 1',
'-f s16le'
]
})
result = await encode(input, 24000)
}
const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data)
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: result.duration / 1000,
}
} else { } else {
const silk = file const input = await convert(ctx, filePath, {
let duration = 1 output: [
try { '-ar 24000',
duration = getDuration(silk) / 1000 '-ac 1',
} catch (e) { '-f s16le'
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack) ]
} })
return { result = await encode(input, 24000)
converted: false, }
path: filePath, const pttPath = path.join(TEMP_DIR, randomUUID())
duration, await fsPromise.writeFile(pttPath, result.data)
} ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: result.duration / 1000,
}
} else {
const silk = file
let duration = 1
try {
duration = getDuration(silk) / 1000
} catch (e) {
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
}
return {
converted: false,
path: filePath,
duration,
} }
} catch (err) {
ctx.logger.error('convert silk failed', (err as Error).stack)
return {}
} }
} }

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 { fileTypeFromFile } from 'file-type' import { Context } from 'cordis'
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
@@ -118,7 +118,7 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, filename?: string, needExt?: boolean): Promise<Uri2LocalRes> { export async function uri2local(ctx: Context, uri: string, needExt?: boolean): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri) const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) { if (type === FileUriType.FileURL) {
@@ -136,15 +136,16 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
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 fileTypeFromFile(filePath))?.ext const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`
@@ -157,12 +158,12 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
} }
if (type === FileUriType.OneBotBase64) { if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID() let 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 fileTypeFromFile(filePath))?.ext const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`
@@ -174,12 +175,12 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
// 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) {
filename ??= randomUUID() let 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 fileTypeFromFile(filePath))?.ext const ext = (await ctx.ntFileApi.getFileType(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,24 +2,7 @@ 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 { Dict } from 'cosmokit' import { inspect } from 'node:util'
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, '-')
@@ -29,12 +12,16 @@ 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 += JSON.stringify(truncateString(msgItem)) + ' ' logMsg += inspect(msgItem, {
continue depth: 10,
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,25 +3,19 @@ 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 latestVersionText = await getRemoteVersion() const latestVersion = await getRemoteVersion()
const latestVersion = latestVersionText.split('.')
log('LLOneBot latest version', latestVersion) log('LLOneBot latest version', latestVersion)
const currentVersion = version.split('.') if (latestVersion === '') {
//log('llonebot current version', currentVersion) return { result: false, version: latestVersion }
for (const k of [0, 1, 2]) { }
const latest = parseInt(latestVersion[k]) if (compare(latestVersion, version, '>')) {
const current = parseInt(currentVersion[k]) return { result: true, version: latestVersion }
if (latest > current) {
log('')
return { result: true, version: latestVersionText }
} else if (latest < current) {
break
}
} }
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('llonebot/config-updated', input => { ctx.on('llob/config-updated', input => {
enable = input.log! enable = input.log!
}) })
} }

View File

@@ -2,6 +2,10 @@ 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 {
@@ -14,12 +18,10 @@ import {
CHANNEL_UPDATE, CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED CHANNEL_SET_CONFIG_CONFIRMED
} from '../common/channels' } from '../common/channels'
import { getBuildVersion } from '../common/utils' import { startHook } from '../ntqqapi/hook'
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'
@@ -35,13 +37,10 @@ import {
} from '../ntqqapi/api' } from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises' 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 {
'llonebot/config-updated': (input: LLOBConfig) => void 'llob/config-updated': (input: LLOBConfig) => void
} }
} }
@@ -150,11 +149,6 @@ function onLoad() {
async function start() { async function start() {
log('process pid', process.pid) log('process pid', process.pid)
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
if (!existsSync(TEMP_DIR)) { if (!existsSync(TEMP_DIR)) {
await mkdir(TEMP_DIR) await mkdir(TEMP_DIR)
} }
@@ -176,37 +170,44 @@ function onLoad() {
ctx.plugin(NTQQWebApi) ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi) ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config) ctx.plugin(Core, config)
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
ctx.plugin(Database) ctx.plugin(Database)
ctx.plugin(SQLiteDriver, { ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`) path: path.join(dbDir, `${selfInfo.uin}.db`)
}) })
ctx.plugin(Store) ctx.plugin(Store, {
msgCacheExpire: config.msgCacheExpire! * 1000
})
if (config.ob11.enable) {
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
}
if (config.satori.enable) {
ctx.plugin(SatoriAdapter, {
...config.satori,
ffmpeg: config.ffmpeg,
})
}
ctx.start() ctx.start()
llonebotError.otherError = ''
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => { ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config) ctx.parallel('llob/config-updated', config)
}) })
} }
const buildVersion = getBuildVersion()
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const self = Object.assign(selfInfo, { const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin, uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid, uid: globalThis.authData?.uid,
online: true online: true
}) })
if (self.uin && (buildVersion >= 27187 || getSession())) { if (self.uin) {
clearInterval(intervalId) clearInterval(intervalId)
start() start()
} }
@@ -215,23 +216,11 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
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 {
onLoad() onLoad()
startHook()
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }

View File

@@ -1,4 +1,4 @@
import { Peer } from '@/ntqqapi/types' import { Peer, RawMessage } 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,13 +24,15 @@ interface MsgInfo {
peer: Peer peer: Peer
} }
export default class Store extends Service { 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) { constructor(protected ctx: Context, public config: Store.Config) {
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()
} }
@@ -60,9 +62,10 @@ export default 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 //设置第一个bit为0 保证shortId为正数 hash[0] &= 0x7f //保证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', [{
@@ -123,4 +126,29 @@ export default 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,10 +16,8 @@ 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 { getSession } from '@/ntqqapi/wrapper' import { RichMediaDownloadCompleteNotify, Peer } from '@/ntqqapi/types/msg'
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'
@@ -39,39 +37,33 @@ 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) { async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string | undefined> {
const session = getSession() const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
if (session) { peer,
return (await session.getRichMediaService().getVideoPlayUrlV2( msgId,
peer, elemId: elementId,
msgId, videoCodecFormat: 0,
elementId, params: {
0, downSourceType: 1,
{ downSourceType: 1, triggerType: 1 } triggerType: 1
)).urlResult.domainUrl[0]?.url
} else {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer,
msgId,
elemId: elementId,
videoCodecFormat: 0,
exParams: {
downSourceType: 1,
triggerType: 1
},
}, null])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url }])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url
} }
async getFileType(filePath: string) { async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return await invoke<{
ext: string
mime: string
}>(NTMethod.FILE_TYPE, [filePath], {
className: NTClass.FS_API
})
} }
// 上传文件到QQ的文件夹 /** 上传文件到 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)
@@ -121,25 +113,22 @@ export class NTQQFileApi extends Service {
return sourcePath return sourcePath
} }
} }
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>( const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
'nodeIKernelMsgService/downloadRichMedia', 'nodeIKernelMsgService/downloadRichMedia',
[ [{
{ getReq: {
getReq: { fileModelId: '0',
fileModelId: '0', downloadSourceType: 0,
downloadSourceType: 0, triggerType: 1,
triggerType: 1, msgId: msgId,
msgId: msgId, chatType: chatType,
chatType: chatType, peerUid: peerUid,
peerUid: peerUid, elementId: elementId,
elementId: elementId, thumbSize: 0,
thumbSize: 0, 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,
@@ -174,8 +163,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 isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid) const isNTPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) { if (isNTPic) {
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
@@ -196,16 +185,13 @@ 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: OnRichMediaDownloadCompleteParams }>( const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
'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,
@@ -215,6 +201,16 @@ 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 }
]
)
}
} }
export class NTQQFileCacheApi extends Service { export class NTQQFileCacheApi extends Service {
@@ -223,21 +219,19 @@ export class NTQQFileCacheApi extends Service {
} }
async setCacheSilentScan(isSilent: boolean = true) { async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }, null]) return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }])
} }
getCacheSessionPathList() { getCacheSessionPathList() {
return invoke< return invoke<Array<{
{ 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, [null, null], { timeout: 300 * Time.second }) return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [], { timeout: 300 * Time.second })
} }
getHotUpdateCachePath() { getHotUpdateCachePath() {
@@ -257,13 +251,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,8 +1,6 @@
import { Friend, FriendV2, SimpleInfo, CategoryFriend, BuddyListReqType } from '../types' import { Friend, SimpleInfo, CategoryFriend } 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' {
@@ -18,49 +16,32 @@ export class NTQQFriendApi extends Service {
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() { async getFriends() {
const data = await invoke<{ const res = 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,
[], cbCmd: ReceiveCmdS.FRIENDS,
{ afterFirstCmd: false
className: NTClass.NODE_STORE_API, })
cbCmd: ReceiveCmdS.FRIENDS, return res.data.flatMap(e => e.buddyList)
afterFirstCmd: false,
}
)
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() return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
if (session) { approvalInfo: {
return session.getBuddyService().approvalFriendRequest({
friendUid, friendUid,
reqTime, reqTime,
accept accept,
}) },
} else { }])
return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
approvalInfo: {
friendUid,
reqTime,
accept,
},
}])
}
} }
async getBuddyV2(refresh = false): Promise<FriendV2[]> { async getBuddyV2(refresh = false): Promise<SimpleInfo[]> {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
@@ -118,22 +99,27 @@ export class NTQQFriendApi extends Service {
} }
async isBuddy(uid: string): Promise<boolean> { async isBuddy(uid: string): Promise<boolean> {
const session = getSession() return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }])
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 }, null]) const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }])
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

@@ -6,17 +6,15 @@ import {
GroupNotifies, GroupNotifies,
GroupRequestOperateTypes, GroupRequestOperateTypes,
GetFileListParam, GetFileListParam,
OnGroupFileInfoUpdateParams,
PublishGroupBulletinReq, PublishGroupBulletinReq,
GroupAllInfo GroupAllInfo,
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 {
@@ -27,8 +25,6 @@ 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)
} }
@@ -49,50 +45,41 @@ export class NTQQGroupApi extends Service {
return result.groupList return result.groupList
} }
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> { async getGroupMembers(groupCode: string, num = 3000) {
const session = getSession() const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>> groupCode,
if (session) { scene: 'groupMemberList_MainWindow'
const groupService = session.getGroupService() }])
const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow') const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }])
result = await groupService.getNextMemberList(sceneId, undefined, num) if (data.errCode !== 0) {
} else { throw new Error('获取群成员列表出错,' + data.errMsg)
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
} }
if (result.errCode !== 0) { return data.result.infos
throw ('获取群成员列表出错,' + result.errMsg)
}
return result.result.infos
} }
async getGroupMember(groupCode: string, memberUinOrUid: string) { async getGroupMember(groupCode: string, uid: string, forceUpdate = false) {
if (!this.groupMembers.has(groupCode)) { await invoke('nodeIKernelGroupListener/onMemberInfoChange', [], {
try { registerEvent: true
// 更新群成员列表 })
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 return data.members.get(uid)!
}
}
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() {
@@ -104,11 +91,12 @@ export class NTQQGroupApi extends Service {
) )
} }
async getSingleScreenNotifies(num: number) { async getSingleScreenNotifies(number: number, startSeq = '') {
invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true }) invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true })
return (await invoke<GroupNotifies>( return (await invoke<GroupNotifies>(
'nodeIKernelGroupService/getSingleScreenNotifies', 'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt: false, startSeq: '', number: num }, null], [{ doubt: false, startSeq, number }],
{ {
cbCmd: ReceiveCmdS.GROUP_NOTIFY, cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false, afterFirstCmd: false,
@@ -121,168 +109,97 @@ 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 session = getSession() return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
if (session) { doubt: false,
return session.getGroupService().operateSysNotify(false, { operateMsg: {
operateType, // 2 拒绝 operateType,
targetMsg: { targetMsg: {
seq, // 通知序列号 seq,
type, type,
groupCode, groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
} else {
return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
doubt: false,
operateMsg: {
operateType,
targetMsg: {
seq,
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
}, },
}, null]) },
} }])
} }
async quitGroup(groupCode: string) { async quitGroup(groupCode: string) {
const session = getSession() return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }])
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() return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
if (session) {
return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
} else {
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为解除禁言 return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
const session = getSession()
if (session) {
return session.getGroupService().setMemberShutUp(groupCode, memList)
} else {
return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
}
} }
async banGroup(groupCode: string, shutUp: boolean) { async banGroup(groupCode: string, shutUp: boolean) {
const session = getSession() return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }])
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) {
const session = getSession() return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }])
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) {
const session = getSession() return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }])
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) {
const session = getSession() return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }])
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 }, null]) return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }])
} }
async removeGroupEssence(groupCode: string, msgId: string) { async removeGroupEssence(groupCode: string, msgId: string) {
const session = getSession() const ntMsgApi = this.ctx.get('ntMsgApi')!
if (session) { const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false) return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
return session.getGroupService().removeGroupEssence({ req: {
groupCode: groupCode, groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom), msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq) msgSeq: Number(data?.msgList[0].msgSeq)
}) }
} else { }])
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
} }
async addGroupEssence(groupCode: string, msgId: string) { async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession() const ntMsgApi = this.ctx.get('ntMsgApi')!
if (session) { const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false) return await invoke('nodeIKernelGroupService/addGroupEssence', [{
return session.getGroupService().addGroupEssence({ req: {
groupCode: groupCode, groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom), msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq) msgSeq: Number(data?.msgList[0].msgSeq)
}) }
} else { }])
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/addGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
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 }, null]) return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }])
} }
async deleteGroupFileFolder(groupId: string, folderId: string) { async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null]) return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }])
} }
async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) { async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }, null]) return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }])
} }
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: OnGroupFileInfoUpdateParams }>( 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,
@@ -295,17 +212,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 }, null]) return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }])
} }
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 }, null]) return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }])
} }
async getGroupRecommendContact(groupCode: string) { async getGroupRecommendContact(groupCode: string) {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null]) const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }])
return ret.arkJson return ret.arkJson
} }
@@ -316,7 +233,7 @@ export class NTQQGroupApi extends Service {
msgSeq: +msgSeq, msgSeq: +msgSeq,
msgRandom: +msgRandom msgRandom: +msgRandom
} }
}, null]) }])
} }
async getGroupHonorList(groupCode: string) { async getGroupHonorList(groupCode: string) {
@@ -325,26 +242,87 @@ export class NTQQGroupApi extends Service {
req: { req: {
groupCode: [+groupCode] groupCode: [+groupCode]
} }
}, null]) }])
} }
async getGroupAllInfo(groupCode: string, timeout = 1000) { async getGroupAllInfo(groupCode: string) {
invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], { registerEvent: true }) invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], {
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
}
} }

View File

@@ -1,7 +1,5 @@
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'
@@ -19,98 +17,66 @@ export class NTQQMsgApi extends Service {
} }
async getTempChatInfo(chatType: ChatType, peerUid: string) { async getTempChatInfo(chatType: ChatType, peerUid: string) {
const session = getSession() return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }])
if (session) {
return session.getMsgService().getTempChatInfo(chatType, peerUid)
} else {
return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
}
} }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) { async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean) {
// 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'
if (session) { return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }])
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) {
const session = getSession() return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }])
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<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }, null]) return await invoke(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }])
} }
async activateChatAndGetHistory(peer: Peer) { async activateChatAndGetHistory(peer: Peer, cnt: number) {
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 }, null]) return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }])
} }
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { async getMsgsByMsgId(peer: Peer, msgIds: string[]) {
if (!peer) throw new Error('peer is not allowed') if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed') if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession() return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }])
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, isReverseOrder: boolean = false) { async getMsgHistory(peer: Peer, msgId: string, cnt: number, queryOrder = 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[]) {
const session = getSession() return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }])
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 msgId = await this.generateMsgUniqueId(peer.chatType) const uniqueId = await this.generateMsgUniqueId(peer.chatType)
peer.guildId = msgId peer.guildId = uniqueId
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: 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 (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true return true
} }
} }
@@ -119,22 +85,37 @@ export class NTQQMsgApi extends Service {
timeout timeout
} }
) )
return data.msgList.find(msgRecord => msgRecord.guildId === msgId) delete peer.guildId
return data.msgList.find(msgRecord => msgRecord.guildId === uniqueId)
} }
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession() const uniqueId = await this.generateMsgUniqueId(destPeer.chatType)
if (session) { destPeer.guildId = uniqueId
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], []) const data = await invoke<{ msgList: RawMessage[] }>(
} else { 'nodeIKernelMsgService/forwardMsg',
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
}
}
)
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> {
@@ -145,27 +126,24 @@ 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 (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { if (msgRecord.peerUid === destPeer.peerUid && msgRecord.senderUid === selfUid) {
return true return true
} }
} }
return false return false
}, }
} }
) )
for (const msg of data.msgList) { for (const msg of data.msgList) {
@@ -174,37 +152,18 @@ export class NTQQMsgApi extends Service {
continue continue
} }
const forwardData = JSON.parse(arkElement.arkElement!.bytesData) const forwardData = JSON.parse(arkElement.arkElement!.bytesData)
if (forwardData.app != 'com.tencent.multimsg') { if (forwardData.app !== 'com.tencent.multimsg') {
continue continue
} }
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) { if (msg.peerUid === destPeer.peerUid && msg.senderUid === selfUid) {
return msg return msg
} }
} }
throw new Error('转发消息超时') 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) {
const session = getSession() return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }])
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) {
@@ -222,7 +181,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[] = []) {
@@ -240,11 +199,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 }, null]) return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }])
} }
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) { async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
@@ -254,7 +213,7 @@ export class NTQQMsgApi extends Service {
emojiId, emojiId,
emojiType, emojiType,
cnt: count cnt: count
}, null]) }])
} }
async fetchFavEmojiList(count: number) { async fetchFavEmojiList(count: number) {
@@ -263,11 +222,12 @@ 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 uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }]) const time = await this.getServerTime()
const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType, time }])
if (typeof uniqueId === 'string') { if (typeof uniqueId === 'string') {
return uniqueId return uniqueId
} else { } else {
@@ -275,4 +235,36 @@ 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,9 +1,8 @@
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg, UserDetailSource, ProfileBizType } from '../types' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfo, UserDetailSource, ProfileBizType, SimpleInfo } from '../types'
import { invoke } 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 { Time } from 'cosmokit' import { isNullable, pick, Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -20,31 +19,25 @@ export class NTQQUserApi extends Service {
super(ctx, 'ntUserApi', true) super(ctx, 'ntUserApi', true)
} }
async setQQAvatar(path: string) { async setSelfAvatar(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: UserDetailInfoListenerArg }>( const result = await invoke<{ info: UserDetailInfo }>(
'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,
@@ -70,13 +63,10 @@ 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,
@@ -98,103 +88,77 @@ export class NTQQUserApi extends Service {
} }
async getPSkey(domains: string[]) { async getPSkey(domains: string[]) {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null]) return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }])
} }
async like(uid: string, count = 1) { async like(uid: string, count = 1) {
const session = getSession() return await invoke(
if (session) { 'nodeIKernelProfileLikeService/setBuddyProfileLike',
return session.getProfileLikeService().setBuddyProfileLike({ [{
friendUid: uid, doLikeUserInfo: {
sourceId: 71, friendUid: uid,
doLikeCount: count, sourceId: 71,
doLikeTollCount: 0 doLikeCount: count,
}) doLikeTollCount: 0
} else { }
return await invoke( }]
'nodeIKernelProfileLikeService/setBuddyProfileLike', )
[
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
)
}
} }
async getUidByUinV1(uin: string) { async getUidByUinV1(uin: string, groupCode?: string) {
const session = getSession() let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) { if (!uid) {
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转 const friends = await this.ctx.ntFriendApi.getFriends()
for (const member of membersList.values()) { uid = friends.find(item => item.uin === uin)?.uid
if (member.uin === uin) { }
uid = member.uid if (!uid && groupCode) {
break let member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
} if (member.size === 0) {
} await this.ctx.ntGroupApi.getGroupMembers(groupCode, 1)
if (uid) break 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) { if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换 const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid
if (unveifyUid.indexOf('*') === -1) { if (!unveifyUid.includes('*')) {
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, groupCode?: string) { async getUidByUinV2(uin: string) {
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin) let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uinList: [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
if (groupCode) { return unveifyUid
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, groupCode) return this.getUidByUinV2(uin)
} }
return this.getUidByUinV1(uin) return this.getUidByUinV1(uin, groupCode)
} }
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,
],
) )
} }
@@ -202,31 +166,21 @@ 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 //从QQ Native 转换 uin = (await this.getUserDetailInfo(uid)).uin
} }
return uin return uin
} }
async getUinByUidV2(uid: string) { async getUinByUidV2(uid: string) {
const session = getSession() let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uidList: [uid] }])).uins.get(uid)
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)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin
return uin return uin
} }
@@ -238,21 +192,13 @@ export class NTQQUserApi extends Service {
} }
async forceFetchClientKey() { async forceFetchClientKey() {
const session = getSession() return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }])
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }, null])
}
} }
async getSelfNick(refresh = false) { async getSelfNick(refresh = true) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) { if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const userInfo = await this.getUserDetailInfo(selfInfo.uid) const data = await this.getUserSimpleInfo(selfInfo.uid)
if (userInfo) { selfInfo.nick = data.nick
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
} }
return selfInfo.nick return selfInfo.nick
} }
@@ -264,7 +210,7 @@ export class NTQQUserApi extends Service {
extStatus, extStatus,
batteryStatus, batteryStatus,
} }
}, null]) }])
} }
async getProfileLike(uid: string) { async getProfileLike(uid: string) {
@@ -279,6 +225,67 @@ 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
} }
} }

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,7 +53,6 @@ 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,7 +2,6 @@ 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 {
@@ -13,39 +12,50 @@ import {
GroupMember, GroupMember,
CategoryFriend, CategoryFriend,
SimpleInfo, SimpleInfo,
ChatType ChatType,
BuddyReqType,
GrayTipElementSubType
} from './types' } from './types'
import { selfInfo } from '../common/globalVars' import { selfInfo, llonebotError } 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: GroupNotify[]) => void 'nt/group-notify': (input: GroupNotify) => void
'nt/friend-request': (input: FriendRequest[]) => void 'nt/friend-request': (input: FriendRequest) => void
'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/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'] static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store']
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() {
llonebotError.otherError = '' if (!this.config.ob11.enable && !this.config.satori.enable) {
llonebotError.otherError = 'LLOneBot 未启动'
this.ctx.logger.info('LLOneBot 开关设置为关闭,不启动 LLOneBot')
return
}
this.startTime = Date.now()
this.registerListener() this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`) this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => { this.ctx.on('llob/config-updated', input => {
Object.assign(this.config, input) Object.assign(this.config, input)
}) })
} }
@@ -117,16 +127,18 @@ 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).then(() => { this.ctx.ntMsgApi.activateChatAndGetHistory(peer, 2).then(res => {
this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => { for (const msg of res.msgList) {
const lastTempMsg = msgList.at(-1) if (Date.now() / 1000 - Number(msg.msgTime) > 3) {
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) { continue
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)
} }
} }
@@ -161,7 +173,16 @@ 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 => {
this.ctx.parallel('nt/message-created', payload.msgList) for (const message of payload.msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < this.startTime / 1000) {
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>()
@@ -169,7 +190,14 @@ 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 (msg.recallTime !== '0' && !recallMsgIds.includes(msg.msgId)) { if (
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)) {
@@ -180,9 +208,6 @@ class Core extends Service {
}) })
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)
}) })
@@ -195,24 +220,33 @@ class Core extends Service {
if (payload.unreadCount) { if (payload.unreadCount) {
let notifies: GroupNotify[] let notifies: GroupNotify[]
try { try {
notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount) notifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(payload.unreadCount)
} catch (e) { } catch (e) {
return return
} }
const list = notifies.filter(v => { for (const notify of notifies) {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (groupNotifyFlags.includes(flag)) { const notifyTime = parseInt(notify.seq) / 1000
return false if (groupNotifyFlags.includes(flag) || notifyTime < this.startTime) {
continue
} }
groupNotifyFlags.shift()
groupNotifyFlags.push(flag) groupNotifyFlags.push(flag)
return true this.ctx.parallel('nt/group-notify', notify)
}) }
this.ctx.parallel('nt/group-notify', list)
} }
}) })
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs) for (const req of 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,16 +16,14 @@ import {
SendTextElement, SendTextElement,
SendVideoElement, SendVideoElement,
} from './types' } from './types'
import { stat, writeFile, copyFile, unlink } from 'node:fs/promises' import { stat, writeFile, copyFile, unlink, access } 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 const mFaceCache = new Map<string, string>() // emojiId -> faceName export namespace SendElement {
export namespace SendElementEntities {
export function text(content: string): SendTextElement { export function text(content: string): SendTextElement {
return { return {
elementType: ElementType.Text, elementType: ElementType.Text,
@@ -54,15 +52,15 @@ export namespace SendElementEntities {
} }
} }
export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { export function reply(msgSeq: string, msgId: string, senderUin: string): SendReplyElement {
return { return {
elementType: ElementType.Reply, elementType: ElementType.Reply,
elementId: '', elementId: '',
replyElement: { replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq replayMsgSeq: msgSeq,
replayMsgId: msgId, // raw.msgId replayMsgId: msgId,
senderUin: senderUin, senderUin: senderUin,
senderUinStr: senderUinStr, senderUinStr: senderUin,
}, },
} }
} }
@@ -117,25 +115,17 @@ export namespace SendElementEntities {
} }
export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
try { await access(filePath)
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 '文件异常大小为0' throw new Error('文件异常,大小为 0')
} }
const maxMB = 100; const maxMB = 100
if (fileSize > 1024 * 1024 * maxMB) { if (fileSize > 1024 * 1024 * maxMB) {
throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw new Error(`视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`)
} }
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) const thumbDir = pathLib.dirname(path.replaceAll('\\', '/').replace(`/Ori/`, `/Thumb/`))
thumbDir = pathLib.dirname(thumbDir)
// log("thumb 目录", thumb)
let videoInfo = { let videoInfo = {
width: 1920, width: 1920,
height: 1080, height: 1080,
@@ -196,7 +186,6 @@ export namespace SendElementEntities {
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 = {
@@ -221,13 +210,9 @@ export namespace SendElementEntities {
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 '文件异常大小为0' throw new Error('文件异常,大小为 0')
} }
if (converted) { if (converted) {
unlink(silkPath) unlink(silkPath)
@@ -253,19 +238,19 @@ export namespace SendElementEntities {
} }
} }
export function face(faceId: number): SendFaceElement { export function face(faceId: number, faceType?: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji const face = sysFaces.find(face => face.QSid === String(faceId))
const face = sysFaces.find((face) => face.QSid === faceId.toString()) if (!faceType) {
faceId = parseInt(faceId.toString()) if (faceId < 222) {
// let faceType = parseInt(faceId.toString().substring(0, 1)); faceType = 1
let faceType = 1 } else {
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,5 +1,15 @@
{ {
"sysface": [ "sysface": [
{
"QSid": "419",
"QDes": "/火车",
"IQLid": "419",
"AQLid": "419",
"EMCode": "10419",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "47"
},
{ {
"QSid": "392", "QSid": "392",
"QDes": "/龙年快乐", "QDes": "/龙年快乐",
@@ -3662,4 +3672,4 @@
"EMCode": "401016" "EMCode": "401016"
} }
] ]
} }

View File

@@ -31,30 +31,18 @@ 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() { async fetchServerRkey(): Promise<ServerRkeyData> {
return new Promise<ServerRkeyData>((resolve, reject) => { const response = await fetch(this.serverUrl)
fetch(this.serverUrl) if (!response.ok) {
.then(response => { throw new Error(response.statusText)
if (!response.ok) { }
return reject(response.statusText) // 请求失败,返回错误信息 return response.json()
}
return response.json() // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data)
})
.catch(error => {
reject(error)
})
})
} }
} }

View File

@@ -1,8 +1,7 @@
import type { BrowserWindow } from 'electron' import { NTMethod } from './ntcall'
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 { Dict } from 'cosmokit' import { ipcMain } from 'electron'
export const hookApiCallbacks: Record<string, (res: any) => void> = {} export const hookApiCallbacks: Record<string, (res: any) => void> = {}
@@ -26,22 +25,8 @@ 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: Array<{ const receiveHooks: Array<{
@@ -55,92 +40,64 @@ const callHooks: Array<{
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) { export function startHook() {
window.webContents.send = new Proxy(window.webContents.send, { const senderExclude = Symbol()
apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
try { ipcMain.emit = new Proxy(ipcMain.emit, {
if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) { apply(target, thisArg, args: [eventName: string, ...args: any]) {
log('received ntqq api message', args) if (args[2]?.eventName.startsWith('ns-LoggerApi')) {
} return target.apply(thisArg, args)
} catch { } }
if (!onlyLog) { if (logHook) {
if (args[2] instanceof Array) { log('request', args)
for (const receiveData of args[2]) { }
const ntMethodName = receiveData.cmdName
for (const hook of receiveHooks) { const event = args[1]
if (hook.method.includes(ntMethodName)) { if (event.sender && !event.sender[senderExclude]) {
Promise.resolve(hook.hookFunc(receiveData.payload)) 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]) {
for (const receiveData of args[2]) {
for (const hook of receiveHooks) {
if (hook.method.includes(receiveData.cmdName)) {
Promise.resolve(hook.hookFunc(receiveData.payload))
}
}
} }
} }
return target.apply(thisArg, args)
} }
} })
if (args[1]?.callbackId) { }
const callbackId = args[1].callbackId
if (hookApiCallbacks[callbackId]) { if (args[3]?.length) {
Promise.resolve(hookApiCallbacks[callbackId](args[2])) const method = args[3][0]
delete hookApiCallbacks[callbackId] const callParams = args[3].slice(1)
for (const hook of callHooks) {
if (hook.method.includes(method)) {
Promise.resolve(hook.hookFunc(callParams))
} }
} }
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
}, }
}) })
} }
export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
const webContents = window.webContents as Dict
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
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))
}
})
} catch { }
}
}
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>(
method: string | string[], method: string | string[],
hookFunc: (payload: PayloadType) => void, hookFunc: (payload: PayloadType) => void,

Binary file not shown.

View File

@@ -0,0 +1,54 @@
import { Context } from 'cordis'
import { Dict } from 'cosmokit'
import { getBuildVersion } from '@/common/utils/misc'
import addon from './external/crychic-win32-x64.node?asset'
export class Native {
private crychic?: Dict
constructor(private ctx: Context) {
ctx.on('ready', () => {
this.start()
})
}
checkPlatform() {
return process.platform === 'win32' && process.arch === 'x64'
}
checkVersion() {
const version = getBuildVersion()
// 27187—27597
return version >= 27187 && version < 28060
}
start() {
if (this.crychic) {
return
}
if (!this.checkPlatform()) {
return
}
if (!this.checkVersion()) {
return
}
try {
this.crychic = require(addon)
this.crychic!.init()
} 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'])
}
}

View File

@@ -13,7 +13,9 @@ import {
NodeIKernelUixConvertService, NodeIKernelUixConvertService,
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService NodeIKernelTipOffService,
NodeIKernelRobotService,
NodeIKernelNodeMiscService
} from './services' } from './services'
export enum NTClass { export enum NTClass {
@@ -39,7 +41,6 @@ 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',
@@ -93,6 +94,8 @@ interface NTService {
nodeIKernelRichMediaService: NodeIKernelRichMediaService nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
nodeIKernelNodeMiscService: NodeIKernelNodeMiscService
} }
interface InvokeOptions<ReturnType> { interface InvokeOptions<ReturnType> {
@@ -121,9 +124,14 @@ 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(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs) if (eventId) {
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`) removeReceiveHook(eventId)
}
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) {
@@ -137,16 +145,16 @@ export function invoke<
let result: unknown let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => { eventId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
if (options.cmdCB) { if (options.cmdCB) {
if (options.cmdCB(payload, result)) { if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId) removeReceiveHook(eventId)
clearTimeout(timeoutId) clearTimeout(timeoutId)
resolve(payload) resolve(payload)
} }
} }
else { else {
removeReceiveHook(hookId) removeReceiveHook(eventId)
clearTimeout(timeoutId) clearTimeout(timeoutId)
resolve(payload) resolve(payload)
} }
@@ -159,9 +167,9 @@ export function invoke<
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
log('ntqq api call failed,', method, res) log('ntqq api call failed,', method, args, res)
clearTimeout(timeoutId) clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`) reject(`ntqq api call failed, ${method}, ${res?.errMsg}`)
} }
} }
} }

View File

@@ -123,4 +123,8 @@ 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,7 +10,9 @@ 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>
@@ -18,6 +20,10 @@ 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[] }>
@@ -71,7 +77,7 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown downloadRichMedia(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown setMsgEmojiLikes(...args: unknown[]): Promise<GeneralCallResult>
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
@@ -89,4 +95,6 @@ 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

@@ -0,0 +1,15 @@
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[]): { result: number, errMsg: string, succCounts: number } setBuddyProfileLike(...args: unknown[]): GeneralCallResult & { succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & { getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
info: { info: {

View File

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

View File

@@ -9,3 +9,5 @@ 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,5 +1,3 @@
import { QQLevel, Sex } from './user'
export enum GroupListUpdateType { export enum GroupListUpdateType {
REFRESHALL, REFRESHALL,
GETALL, GETALL,
@@ -22,9 +20,9 @@ export interface Group {
hasModifyConfGroupName: boolean hasModifyConfGroupName: boolean
remarkName: string remarkName: string
hasMemo: boolean hasMemo: boolean
groupShutupExpireTime: string //"0", groupShutupExpireTime: string
personShutupExpireTime: string //"0", personShutupExpireTime: string
discussToGroupUin: string //"0", discussToGroupUin: string
discussToGroupMaxMsgSeq: number discussToGroupMaxMsgSeq: number
discussToGroupTime: number discussToGroupTime: number
groupFlagExt: number //1073938496, groupFlagExt: number //1073938496,
@@ -32,39 +30,57 @@ export interface Group {
groupCreditLevel: number //0, groupCreditLevel: number //0,
groupFlagExt3: number //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
memberUin: string //"0", memberUin: string
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string
} }
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 {
memberSpecialTitle?: string uid: string
avatarPath: string
cardName: string
cardType: number
isDelete: boolean
nick: string
qid: string qid: string
uin: string
nick: string
remark: string remark: string
role: GroupMemberRole // 群主:4, 管理员:3群员:2 cardType: number
shutUpTime: number // 禁言时间,单位是什么暂时不清楚 cardName: string
uid: string // 加密的字符串 role: GroupMemberRole
uin: string // QQ号 avatarPath: string
shutUpTime: number
isDelete: boolean
isSpecialConcerned: boolean
isSpecialShield: boolean
isRobot: boolean isRobot: boolean
sex?: Sex groupHonor: Uint8Array
qqLevel?: QQLevel memberRealLevel: number
isChangeRole: boolean memberLevel: number
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
memberLevel: number memberFlag: number
memberFlagExt: number
memberMobileFlag: number
memberFlagExt2: number
isSpecialShielded: boolean
cardNameId: number
} }
export interface PublishGroupBulletinReq { export interface PublishGroupBulletinReq {
@@ -120,3 +136,55 @@ 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

@@ -197,6 +197,60 @@ 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,
@@ -234,14 +288,18 @@ 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, // 石头剪刀布
@@ -268,6 +326,10 @@ 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 {
@@ -326,58 +388,6 @@ 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
@@ -409,11 +419,25 @@ 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
}[]
} }
export interface Peer { export interface Peer {
@@ -455,7 +479,7 @@ export interface MessageElement {
actionBarElement?: unknown actionBarElement?: unknown
} }
export interface OnRichMediaDownloadCompleteParams { export interface RichMediaDownloadCompleteNotify {
fileModelId: string fileModelId: string
msgElementId: string msgElementId: string
msgId: string msgId: string
@@ -478,7 +502,7 @@ export interface OnRichMediaDownloadCompleteParams {
userUsedSpacePerDay: unknown userUsedSpacePerDay: unknown
} }
export interface OnGroupFileInfoUpdateParams { export interface GroupFileInfo {
retCode: number retCode: number
retMsg: string retMsg: string
clientWording: string clientWording: string

View File

@@ -1,19 +1,19 @@
export enum GroupNotifyType { export enum GroupNotifyType {
INVITED_BY_MEMBER = 1, InvitedByMember = 1,
REFUSE_INVITED, RefuseInvited,
REFUSED_BY_ADMINI_STRATOR, RefusedByAdminiStrator,
AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群 AgreedTojoinDirect, // 有人接受了邀请入群
INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群 InvitedNeedAdminiStratorPass, // 有人邀请了别人入群
AGREED_TO_JOIN_BY_ADMINI_STRATOR, AgreedToJoinByAdminiStrator,
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS, RequestJoinNeedAdminiStratorPass,
SET_ADMIN, SetAdmin,
KICK_MEMBER_NOTIFY_ADMIN, KickMemberNotifyAdmin,
KICK_MEMBER_NOTIFY_KICKED, KickMemberNotifyKicked,
MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出 MemberLeaveNotifyAdmin, // 主动退出
CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员 CancelAdminNotifyCanceled, // 我被取消管理员
CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员 CancelAdminNotifyAdmin, // 其他人取消管理员
TRANSFER_GROUP_NOTIFY_OLDOWNER, TransferGroupNotifyOldowner,
TRANSFER_GROUP_NOTIFY_ADMIN TransferGroupNotifyAdmin
} }
export interface GroupNotifies { export interface GroupNotifies {
@@ -23,11 +23,11 @@ export interface GroupNotifies {
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
KINIT, // 初始化 Init, // 初始化
KUNHANDLE, // 未处理 Unhandle, // 未处理
KAGREED, // 同意 Agreed, // 同意
KREFUSED, // 拒绝 Refused, // 拒绝
KIGNORED // 忽略 Ignored // 忽略
} }
export interface GroupNotify { export interface GroupNotify {
@@ -56,20 +56,8 @@ export enum GroupRequestOperateTypes {
} }
export enum BuddyReqType { export enum BuddyReqType {
KMEINITIATOR, MsgInfo = 12,
KPEERINITIATOR, MeInitiatorWaitPeerConfirm = 13,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
@@ -91,41 +79,3 @@ 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

@@ -67,6 +67,7 @@ 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 {
@@ -105,7 +106,7 @@ export interface BaseInfo {
phoneNum: string phoneNum: string
categoryId: number categoryId: number
richTime: number richTime: number
richBuffer: string richBuffer: Uint8Array
} }
interface MusicInfo { interface MusicInfo {
@@ -221,11 +222,6 @@ 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
@@ -255,7 +251,7 @@ interface PhotoWall {
picList: Pic[] picList: Pic[]
} }
export interface UserDetailInfoListenerArg { export interface UserDetailInfo {
uid: string uid: string
uin: string uin: string
simpleInfo: SimpleInfo simpleInfo: SimpleInfo

View File

@@ -1,54 +0,0 @@
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

@@ -0,0 +1,21 @@
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

@@ -34,25 +34,23 @@ export class GetForwardMsg extends BaseAction<Payload, Response> {
if (data?.result !== 0) { if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg) throw Error('找不到相关的聊天记录' + data?.errMsg)
} }
const msgList = data.msgList const messages: (OB11ForwardMessage | undefined)[] = await Promise.all(
const messages = await Promise.all( data.msgList.map(async (msg) => {
msgList.map(async (msg) => { const res = await OB11Entities.message(this.ctx, msg)
const resMsg = await OB11Entities.message(this.ctx, msg) if (res) {
if (!resMsg) return return {
resMsg.message_id = this.ctx.store.createMsgShortId({ content: res.message,
chatType: msg.chatType, sender: {
peerUid: msg.peerUid, nickname: res.sender.nickname,
}, msg.msgId) user_id: res.sender.user_id
return resMsg },
time: res.time,
message_format: res.message_format,
message_type: res.message_type
}
}
}) })
) )
const forwardMessages = filterNullable(messages) return { messages: filterNullable(messages) }
.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
return msg as OB11ForwardMessage
})
return { messages: forwardMessages }
} }
} }

View File

@@ -2,6 +2,7 @@ import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { GroupFileInfo } from '@/ntqqapi/types'
export interface Payload { export interface Payload {
group_id: number | string group_id: number | string
@@ -30,23 +31,7 @@ export class GetGroupFileUrl extends BaseAction<Payload, Response> {
} }
} else { } else {
const groupId = payload.group_id.toString() const groupId = payload.group_id.toString()
let modelId: string | undefined const modelId = await this.search(groupId, payload.file_id)
let nextIndex: number | undefined
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 50,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
const file = res.item.find(item => item.fileInfo?.fileId === payload.file_id)
if (file) {
modelId = file.fileInfo?.fileModelId
break
}
nextIndex = res.nextIndex
}
if (modelId) { if (modelId) {
const peer = { const peer = {
chatType: ChatType.Group, chatType: ChatType.Group,
@@ -61,4 +46,37 @@ export class GetGroupFileUrl extends BaseAction<Payload, Response> {
throw new Error('file not found') throw new Error('file not found')
} }
} }
private async search(groupId: string, fileId: string, folderId?: string) {
let modelId: string | undefined
let nextIndex: number | undefined
const folders: GroupFileInfo['item'] = []
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId
})
const file = res.item.find(item => item.fileInfo?.fileId === fileId)
if (file) {
modelId = file.fileInfo?.fileModelId
break
}
folders.push(...res.item.filter(item => item.folderInfo?.totalFileCount))
nextIndex = res.nextIndex
}
if (!modelId) {
for (const item of folders) {
const res = await this.search(groupId, fileId, item.folderInfo?.folderId)
if (res) {
modelId = res
break
}
}
}
return modelId
}
} }

View File

@@ -1,7 +1,7 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types' import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
import { OnGroupFileInfoUpdateParams } from '@/ntqqapi/types' import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -22,7 +22,7 @@ export class GetGroupFilesByFolder extends BaseAction<Payload, Response> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const groupId = payload.group_id.toString() const groupId = payload.group_id.toString()
const data: OnGroupFileInfoUpdateParams['item'] = [] const data: GroupFileInfo['item'] = []
let nextIndex: number | undefined let nextIndex: number | undefined
while (nextIndex !== 0) { while (nextIndex !== 0) {

View File

@@ -0,0 +1,48 @@
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

@@ -1,7 +1,7 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types' import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
import { OnGroupFileInfoUpdateParams } from '@/ntqqapi/types' import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -20,7 +20,7 @@ export class GetGroupRootFiles extends BaseAction<Payload, Response> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const groupId = payload.group_id.toString() const groupId = payload.group_id.toString()
const data: OnGroupFileInfoUpdateParams['item'] = [] const data: GroupFileInfo['item'] = []
let nextIndex: number | undefined let nextIndex: number | undefined
while (nextIndex !== 0) { while (nextIndex !== 0) {

View File

@@ -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.KUNHANDLE, checked: notify.status !== GroupNotifyStatus.Unhandle,
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.KUNHANDLE, checked: notify.status !== GroupNotifyStatus.Unhandle,
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

@@ -26,7 +26,7 @@ export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
...extendData, ...extendData,
user_id: parseInt(extendData.info.uin) || 0, user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick, nickname: extendData.info.nick,
sex: OB11UserSex.unknown, sex: OB11UserSex.Unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid, qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
@@ -46,7 +46,7 @@ export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
...extendData, ...extendData,
user_id: parseInt(extendData.detail.uin) || 0, user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick, nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown, sex: OB11UserSex.Unknown,
age: 0, age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0, level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0, login_days: 0,

View File

@@ -0,0 +1,63 @@
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

@@ -10,7 +10,8 @@ import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePee
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'
} }
@@ -20,15 +21,20 @@ interface Response {
} }
export class SendForwardMsg extends BaseAction<Payload, Response> { export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_SendForwardMsg actionName = ActionName.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()).required(), messages: Schema.array(Schema.any()),
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
@@ -36,7 +42,7 @@ 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 msg = await this.handleForwardNode(peer, messages)
const msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId) const msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
return { message_id: msgShortId } return { message_id: msgShortId }
} }

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(payload.image, undefined, true) const { path, isLocal, success, errMsg } = await uri2local(this.ctx, payload.image, 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 { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } 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,11 +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(payload.file) const { success, errMsg, path, fileName } = await uri2local(this.ctx, payload.file)
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const file = await SendElementEntities.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id) const file = await SendElement.file(this.ctx, path, payload.name || fileName, 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 { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } 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,11 +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(payload.file) const { success, errMsg, path, fileName } = await uri2local(this.ctx, payload.file)
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const sendFileEle = await SendElementEntities.file(this.ctx, path, payload.name || fileName) const sendFileEle = await SendElement.file(this.ctx, path, payload.name || fileName)
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,7 +2,7 @@ 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 { isNullable } from 'cosmokit' import { calcQQLevel } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -18,16 +18,18 @@ class GetGroupMemberInfo 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()
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, payload.user_id.toString()) const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取用户信息')
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid)
if (member) { if (member) {
if (isNullable(member.sex)) {
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
Object.assign(member, info)
}
const ret = OB11Entities.groupMember(groupCode, member) const ret = OB11Entities.groupMember(groupCode, member)
const date = Math.round(Date.now() / 1000) 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,7 +17,10 @@ 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)
if (groupMembers.size === 0) { for (let i = 0; i < 5; i++) {
if (groupMembers.size > 0) {
break
}
await this.ctx.sleep(100) await this.ctx.sleep(100)
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode) groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
} }

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 duration: number | string
} }
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.number().default(30 * 60) duration: Schema.union([Number, String]).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 './msg/SetMsgEmojiLike' import { SetMsgEmojiLike } from './llonebot/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,6 +70,12 @@ 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'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -86,6 +92,10 @@ 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),
@@ -116,7 +126,6 @@ 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),
// go-cqhttp // go-cqhttp
@@ -144,7 +153,10 @@ 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),
] ]
const actionMap = new Map<string, BaseAction<any, unknown>>() const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@@ -16,10 +16,8 @@ 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 = method.apply(ntqqApiClass, payload.args) const result = await method.apply(ntqqApiClass, payload.args)
if (method.constructor.name === 'AsyncFunction') { this.ctx.logger.info('debug', result)
return await result
}
return result return result
} }
} }

View File

@@ -0,0 +1,25 @@
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()} 不支持,可尝试其他版本 27187—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.KUNHANDLE) const notifies = data.notifies.filter(notify => notify.status === GroupNotifyStatus.Unhandle)
const returnData: OB11GroupRequestNotify[] = [] const returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,27 @@
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()} 不支持,可尝试其他版本 27187—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(payload.file) const { path, isLocal, errMsg } = await uri2local(this.ctx, 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.setQQAvatar(path) const ret = await this.ctx.ntUserApi.setSelfAvatar(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.result !== 0) { if (ret.length === 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`) throw new Error(`转发消息失败`)
} }
return null return null
} }

View File

@@ -25,14 +25,11 @@ 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.adapter.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0] const msg = this.ctx.store.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

@@ -24,6 +24,11 @@ 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',
@@ -37,7 +42,6 @@ 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',
@@ -57,8 +61,7 @@ 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',
// 以下为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',
@@ -82,5 +85,8 @@ 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',
} }

View File

@@ -4,11 +4,7 @@ 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'
@@ -24,7 +20,6 @@ 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 { SysMsg } from '@/ntqqapi/proto/compiled' import { SysMsg } from '@/ntqqapi/proto/compiled'
@@ -36,14 +31,15 @@ declare module 'cordis' {
} }
class OneBot11Adapter extends Service { class OneBot11Adapter extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store'] static inject = [
'ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi',
public messages: Map<string, RawMessage> = new Map() 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi',
public startTime = 0 'store', 'app'
private ob11WebSocket: OB11WebSocket ]
private ob11WebSocketReverseManager: OB11WebSocketReverseManager private ob11WebSocket
private ob11Http: OB11Http private ob11WebSocketReverseManager
private ob11HttpPost: OB11HttpPost private ob11Http
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)
@@ -75,24 +71,6 @@ class OneBot11Adapter extends Service {
}) })
} }
/** 缓存近期消息内容 */
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) { public dispatch(event: OB11BaseEvent | OB11Message) {
if (this.config.enableWs) { if (this.config.enableWs) {
this.ob11WebSocket.emitEvent(event) this.ob11WebSocket.emitEvent(event)
@@ -109,115 +87,110 @@ class OneBot11Adapter extends Service {
} }
} }
private async handleGroupNotify(notifies: GroupNotify[]) { private async handleGroupNotify(notify: GroupNotify) {
for (const notify of notifies) { try {
try { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
const notifyTime = parseInt(notify.seq) / 1000 if ([GroupNotifyType.MemberLeaveNotifyAdmin, GroupNotifyType.KickMemberNotifyAdmin].includes(notify.type)) {
if (notifyTime < this.startTime) { this.ctx.logger.info('有成员退出通知', notify)
continue const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
} let operatorId = member1Uin
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type let subType: GroupDecreaseSubType = 'leave'
if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) { if (notify.user2.uid) {
this.ctx.logger.info('有成员退出通知', notify) // 是被踢的
const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
let operatorId = member1Uin if (member2Uin) {
let subType: GroupDecreaseSubType = 'leave' operatorId = member2Uin
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick'
} }
const event = new OB11GroupDecreaseEvent( subType = 'kick'
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
)
this.dispatch(event)
} }
else if (notify.type === GroupNotifyType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) { const event = new OB11GroupDecreaseEvent(
this.ctx.logger.info('有加群请求') parseInt(notify.group.groupCode),
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) parseInt(member1Uin),
const event = new OB11GroupRequestEvent( parseInt(operatorId),
parseInt(notify.group.groupCode), subType,
parseInt(requestUin) || 0, )
flag, this.dispatch(event)
notify.postscript,
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_BY_MEMBER && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript,
undefined,
'invite'
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
}
} catch (e) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack)
} }
else if (notify.type === GroupNotifyType.RequestJoinNeedAdminiStratorPass && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('有加群请求')
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestUin) || 0,
flag,
notify.postscript,
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.InvitedByMember && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript,
undefined,
'invite'
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.InvitedNeedAdminiStratorPass && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
}
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) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack)
} }
} }
private handleMsg(msgList: RawMessage[]) { private handleMsg(message: RawMessage) {
for (const message of msgList) { OB11Entities.message(this.ctx, message).then(msg => {
// 过滤启动之前的消息 if (!msg) {
if (parseInt(message.msgTime) < this.startTime / 1000) { return
continue
} }
this.addMsgCache(message) if (!this.config.debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
this.dispatch(msg)
}).catch(e => this.ctx.logger.error('handling incoming messages', e))
OB11Entities.message(this.ctx, message) OB11Entities.groupEvent(this.ctx, message).then(groupEvent => {
.then((msg) => { if (groupEvent) {
if (!msg) { this.dispatch(groupEvent)
return }
} }).catch(e => this.ctx.logger.error('handling incoming group events', e))
if (!this.config.debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
this.dispatch(msg)
})
.catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString()))
OB11Entities.groupEvent(this.ctx, message).then((groupEvent) => { OB11Entities.privateEvent(this.ctx, message).then(privateEvent => {
if (groupEvent) { if (privateEvent) {
this.dispatch(groupEvent) this.dispatch(privateEvent)
} }
}) }).catch(e => this.ctx.logger.error('handling incoming buddy events', e))
OB11Entities.privateEvent(this.ctx, message).then((privateEvent) => {
if (privateEvent) {
this.dispatch(privateEvent)
}
})
}
} }
private handleRecallMsg(message: RawMessage) { private handleRecallMsg(message: RawMessage) {
@@ -236,30 +209,22 @@ class OneBot11Adapter extends Service {
}) })
} }
private async handleFriendRequest(buddyReqs: FriendRequest[]) { private async handleFriendRequest(req: FriendRequest) {
for (const req of buddyReqs) { let userId = 0
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) { try {
continue const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
} userId = parseInt(requesterUin)
if (+req.reqTime < this.startTime / 1000) { } catch (e) {
continue this.ctx.logger.error('获取加好友者QQ号失败', e)
}
let userId = 0
try {
const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin)
} catch (e) {
this.ctx.logger.error('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
this.dispatch(friendRequestEvent)
} }
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
this.dispatch(friendRequestEvent)
} }
private async handleConfigUpdated(config: LLOBConfig) { private async handleConfigUpdated(config: LLOBConfig) {
@@ -343,7 +308,6 @@ 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,
@@ -351,31 +315,7 @@ 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()
} }
@@ -388,7 +328,7 @@ class OneBot11Adapter extends Service {
if (this.config.enableHttpPost) { if (this.config.enableHttpPost) {
this.ob11HttpPost.start() this.ob11HttpPost.start()
} }
this.ctx.on('llonebot/config-updated', input => { this.ctx.on('llob/config-updated', input => {
this.handleConfigUpdated(input) this.handleConfigUpdated(input)
}) })
this.ctx.on('nt/message-created', input => { this.ctx.on('nt/message-created', input => {
@@ -398,7 +338,10 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input) this.handleRecallMsg(input)
}) })
this.ctx.on('nt/message-sent', input => { this.ctx.on('nt/message-sent', input => {
this.handleMsg([input]) if (!this.config.reportSelfMessage) {
return
}
this.handleMsg(input)
}) })
this.ctx.on('nt/group-notify', input => { this.ctx.on('nt/group-notify', input => {
this.handleGroupNotify(input) this.handleGroupNotify(input)
@@ -406,9 +349,6 @@ class OneBot11Adapter extends Service {
this.ctx.on('nt/friend-request', input => { this.ctx.on('nt/friend-request', input => {
this.handleFriendRequest(input) this.handleFriendRequest(input)
}) })
this.ctx.on('nt/group-member-info-updated', input => {
this.handleGroupMemberInfoUpdated(input.groupCode, input.members)
})
this.ctx.on('nt/system-message-created', input => { this.ctx.on('nt/system-message-created', input => {
const sysMsg = SysMsg.SystemMessage.decode(input) const sysMsg = SysMsg.SystemMessage.decode(input)
const { msgType, subType, subSubType } = sysMsg.msgSpec[0] ?? {} const { msgType, subType, subSubType } = sysMsg.msgSpec[0] ?? {}
@@ -430,8 +370,6 @@ 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 } from 'express' import express, { Express, Request, Response, NextFunction } 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: () => void) { private authorize(req: Request, res: Response, next: NextFunction) {
const serverToken = this.config.token const serverToken = this.config.token
if (!serverToken) return next() if (!serverToken) return next()
@@ -95,12 +95,13 @@ class OB11Http {
} }
if (clientToken !== serverToken) { if (clientToken !== serverToken) {
return res.status(403).json({ message: 'token verify failed!' }) res.status(403).json({ message: 'token verify failed!' })
} else {
next()
} }
next()
} }
private async handleRequest(req: Request, res: Response, next: () => void) { private async handleRequest(req: Request, res: Response, next: NextFunction) {
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') {

View File

@@ -214,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

@@ -72,7 +72,3 @@ export function encodeCQCode(input: OB11MessageData) {
result += ']' result += ']'
return result return result
} }
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好")
// console.log(JSON.stringify(result))

View File

@@ -20,7 +20,7 @@ import {
Sex, Sex,
TipGroupElementType, TipGroupElementType,
User, User,
FriendV2 SimpleInfo
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
@@ -30,7 +30,6 @@ 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'
@@ -39,7 +38,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, isNullable, pick, Dict } from 'cosmokit' import { omit, 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'
@@ -80,7 +79,7 @@ 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)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin) 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
@@ -89,12 +88,12 @@ export namespace OB11Entities {
} }
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.getUserDetailInfo(msg.senderUid)).nick resMsg.sender.nickname = (await ctx.ntUserApi.getUserSimpleInfo(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.getUserDetailInfo(msg.senderUid)).nick resMsg.sender.nickname = (await ctx.ntUserApi.getUserSimpleInfo(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)
@@ -338,7 +337,6 @@ 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
@@ -404,7 +402,7 @@ export namespace OB11Entities {
if (msg.chatType !== ChatType.Group) { if (msg.chatType !== ChatType.Group) {
return return
} }
if (msg.senderUin) { /**if (msg.senderUin) {
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin) const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin)
if (member && member.cardName !== msg.sendMemberName) { if (member && member.cardName !== msg.sendMemberName) {
const event = new OB11GroupCardEvent( const event = new OB11GroupCardEvent(
@@ -416,24 +414,17 @@ export namespace OB11Entities {
member.cardName = msg.sendMemberName! member.cardName = msg.sendMemberName!
return event 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
if (groupElement) { if (groupElement) {
if (groupElement.type === TipGroupElementType.MemberIncrease) { if (groupElement.type === TipGroupElementType.MemberIncrease) {
ctx.logger.info('收到群成员增加消息', groupElement) ctx.logger.info('收到群成员增加消息', groupElement)
await ctx.sleep(1000) const { memberUid, adminUid } = groupElement
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid) const memberUin = await ctx.ntUserApi.getUinByUid(memberUid)
let memberUin = member?.uin const operatorUin = adminUid ? await ctx.ntUserApi.getUinByUid(adminUid) : memberUin
if (!memberUin) { return new OB11GroupIncreaseEvent(+msg.peerUid, +memberUin, +operatorUin)
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) { else if (groupElement.type === TipGroupElementType.Ban) {
ctx.logger.info('收到群成员禁言提示', groupElement) ctx.logger.info('收到群成员禁言提示', groupElement)
@@ -519,11 +510,11 @@ export namespace OB11Entities {
if (!replyMsgList?.length) { if (!replyMsgList?.length) {
return return
} }
const shortId = ctx.store.getShortIdByMsgInfo(peer, replyMsgList[0].msgId) const shortId = ctx.store.createMsgShortId(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,
@@ -548,29 +539,36 @@ export namespace OB11Entities {
while ((match = regex.exec(xmlElement.content)) !== null) { while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1]) matches.push(match[1])
} }
// log("新人进群匹配到的QQ号", matches)
if (matches.length === 2) { if (matches.length === 2) {
const [inviter, invitee] = matches const [invitor, invitee] = matches
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite') return new OB11GroupIncreaseEvent(+msg.peerUid, +invitee, +invitor, 'invite')
} }
} }
} }
else if (grayTipElement.subElementType == GrayTipElementSubType.JSON) { 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(
parseInt(msg.peerUid), Number(msg.peerUid),
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0), Number(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0), Number(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')
@@ -592,17 +590,16 @@ 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')
} }
} }
} }
@@ -614,13 +611,7 @@ export namespace OB11Entities {
msg: RawMessage, msg: RawMessage,
shortId: number shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
const msgElement = msg.elements.find( const revokeElement = msg.elements[0].grayTipElement?.revokeElement
(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(
@@ -649,7 +640,7 @@ export namespace OB11Entities {
return friends.map(friend) return friends.map(friend)
} }
export function friendV2(raw: FriendV2): OB11User { export function friendV2(raw: SimpleInfo): OB11User {
return { return {
...omit(raw.baseInfo, ['richBuffer', 'phoneNum']), ...omit(raw.baseInfo, ['richBuffer', 'phoneNum']),
...omit(raw.coreInfo, ['nick']), ...omit(raw.coreInfo, ['nick']),
@@ -661,47 +652,49 @@ export namespace OB11Entities {
} }
} }
export function friendsV2(raw: FriendV2[]): OB11User[] { export function friendsV2(raw: SimpleInfo[]): OB11User[] {
return raw.map(friendV2) return raw.map(friendV2)
} }
export function groupMemberRole(role: number): OB11GroupMemberRole | undefined { export function groupMemberRole(role: number): OB11GroupMemberRole {
return { return {
4: OB11GroupMemberRole.owner, 4: OB11GroupMemberRole.Owner,
3: OB11GroupMemberRole.admin, 3: OB11GroupMemberRole.Admin,
2: OB11GroupMemberRole.member, 2: OB11GroupMemberRole.Member,
}[role] }[role] ?? OB11GroupMemberRole.Member
} }
export function sex(sex: Sex): OB11UserSex { export function sex(sex: Sex): OB11UserSex {
const sexMap = { const sexMap = {
[Sex.male]: OB11UserSex.male, [Sex.male]: OB11UserSex.Male,
[Sex.female]: OB11UserSex.female, [Sex.female]: OB11UserSex.Female,
[Sex.unknown]: OB11UserSex.unknown, [Sex.unknown]: OB11UserSex.Unknown,
} }
return sexMap[sex] || OB11UserSex.unknown return sexMap[sex] || OB11UserSex.Unknown
} }
export function groupMember(group_id: string, member: GroupMember): OB11GroupMember { export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
const titleExpireTime = +member.specialTitleExpireTime
const int32Max = Math.pow(2, 31) - 1
return { return {
group_id: parseInt(group_id), group_id: parseInt(group_id),
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName, card: member.cardName || member.nick,
sex: sex(member.sex!), sex: OB11UserSex.Unknown,
age: 0, age: 0,
area: '', area: '',
level: String(member.memberLevel ?? 0), level: String(member.memberLevel ?? 0),
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, qq_level: 0,
join_time: member.joinTime, join_time: member.joinTime,
last_sent_time: member.lastSpeakTime, last_sent_time: member.lastSpeakTime,
title_expire_time: 0, title_expire_time: titleExpireTime > int32Max ? 0 : titleExpireTime,
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,
} }
} }

View File

@@ -8,17 +8,15 @@ 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[], sub_type?: 'ban' | 'lift_ban') { constructor(groupId: number, userId: number, messageId: number, likes: MsgEmojiLike[]) {
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 { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } 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'
@@ -36,7 +36,7 @@ export async function createSendElements(
case OB11MessageDataType.text: { case OB11MessageDataType.text: {
const text = sendMsg.data?.text const text = sendMsg.data?.text
if (text) { if (text) {
sendElements.push(SendElementEntities.text(sendMsg.data!.text)) sendElements.push(SendElement.text(sendMsg.data!.text))
} }
} }
break break
@@ -56,54 +56,36 @@ 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.uin) const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uid)
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(SendElementEntities.at(atQQ, atQQ, AtType.All, '@全体成员')) sendElements.push(SendElement.at(atQQ, atQQ, AtType.All, '@全体成员'))
} }
} }
else if (peer.chatType === ChatType.Group) { else if (peer.chatType === ChatType.Group) {
const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ) const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid) ?? ''
if (atMember) { let display = ''
const display = `@${atMember.cardName || atMember.nick}` if (sendMsg.data.name) {
sendElements.push( display = `@${sendMsg.data.name}`
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 replyMsgId = await ctx.store.getMsgInfoByShortId(+sendMsg.data.id) const info = await ctx.store.getMsgInfoByShortId(+sendMsg.data.id)
if (!replyMsgId) { if (!info) {
ctx.logger.warn('回复消息不存在', replyMsgId) ctx.logger.warn('回复消息不存在', info)
continue continue
} }
const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId( const source = (await ctx.ntMsgApi.getMsgsByMsgId(info.peer, [info.msgId])).msgList[0]
replyMsgId.peer, if (source) {
[replyMsgId.msgId!] sendElements.push(SendElement.reply(source.msgSeq, source.msgId, source.senderUin))
)).msgList[0]
if (replyMsg) {
sendElements.push(
SendElementEntities.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin!,
replyMsg.senderUin!,
),
)
} }
} }
} }
@@ -111,13 +93,13 @@ export async function createSendElements(
case OB11MessageDataType.face: { case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id const faceId = sendMsg.data?.id
if (faceId) { if (faceId) {
sendElements.push(SendElementEntities.face(parseInt(faceId))) sendElements.push(SendElement.face(parseInt(faceId)))
} }
} }
break break
case OB11MessageDataType.mface: { case OB11MessageDataType.mface: {
sendElements.push( sendElements.push(
SendElementEntities.mface( SendElement.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,
@@ -127,7 +109,7 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.image: { case OB11MessageDataType.image: {
const res = await SendElementEntities.pic( const res = await SendElement.pic(
ctx, ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path, (await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '', sendMsg.data.summary || '',
@@ -140,48 +122,48 @@ export async function createSendElements(
break break
case OB11MessageDataType.file: { case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendElementEntities.file(ctx, path, fileName)) sendElements.push(await SendElement.file(ctx, path, fileName))
} }
break break
case OB11MessageDataType.video: { case OB11MessageDataType.video: {
const { path, fileName } = await handleOb11FileLikeMessage(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(thumb) const uri2LocalRes = await uri2local(ctx, thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path if (uri2LocalRes.success) thumb = uri2LocalRes.path
} }
const res = await SendElementEntities.video(ctx, path, fileName, thumb) const res = await SendElement.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath) deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendElementEntities.ptt(ctx, path)) sendElements.push(await SendElement.ptt(ctx, path))
} }
break break
case OB11MessageDataType.json: { case OB11MessageDataType.json: {
sendElements.push(SendElementEntities.ark(sendMsg.data.data)) sendElements.push(SendElement.ark(sendMsg.data.data))
} }
break break
case OB11MessageDataType.dice: { case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendElementEntities.dice(resultId)) sendElements.push(SendElement.dice(resultId))
} }
break break
case OB11MessageDataType.RPS: { case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendElementEntities.rps(resultId)) sendElements.push(SendElement.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(SendElementEntities.ark(await data)) sendElements.push(SendElement.ark(await data))
} }
break break
case OB11MessageDataType.shake: { case OB11MessageDataType.shake: {
sendElements.push(SendElementEntities.shake()) sendElements.push(SendElement.shake())
} }
break break
} }
@@ -206,7 +188,7 @@ async function handleOb11FileLikeMessage(
fileName, fileName,
errMsg, errMsg,
success, success,
} = (await uri2local(inputdata?.url || inputdata.file)) } = (await uri2local(ctx, inputdata.url || inputdata.file))
if (!success) { if (!success) {
ctx.logger.error(errMsg) ctx.logger.error(errMsg)

View File

@@ -16,36 +16,36 @@ export interface OB11User {
} }
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
@@ -98,8 +98,15 @@ export interface OB11Message {
temp_source?: 0 | 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 temp_source?: 0 | 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9
} }
export interface OB11ForwardMessage extends OB11Message { export interface OB11ForwardMessage {
content: OB11MessageData[] | string content: OB11MessageData[] | string
sender: {
nickname: string
user_id: number
}
time: number
message_format: string //扩展
message_type: string //扩展
} }
export interface OB11Return<DataType> { export interface OB11Return<DataType> {

View File

@@ -3,3 +3,4 @@ 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

@@ -0,0 +1,20 @@
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 } from './components' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect, SettingInput } 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,7 +11,6 @@ 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 }
@@ -49,12 +48,28 @@ async function onSettingWindowCreated(view: Element) {
]), ]),
SettingList([ SettingList([
SettingItem( SettingItem(
'是否启用 LLOneBot, 重启 QQ 后生效', '是否启用 Satori 协议',
'重启 QQ 后生效',
SettingSwitch('satori.enable', config.satori.enable),
),
SettingItem(
'服务端口',
null, null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }), 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([ SettingList([
SettingItem(
'是否启用 OneBot 协议',
'重启 QQ 后生效',
SettingSwitch('ob11.enable', config.ob11.enable),
),
SettingItem( SettingItem(
'启用 HTTP 服务', '启用 HTTP 服务',
null, null,
@@ -63,7 +78,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'HTTP 服务监听端口', 'HTTP 服务监听端口',
null, null,
`<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>`, SettingInput('ob11.httpPort', 'port', config.ob11.httpPort, config.ob11.httpPort),
'config-ob11-httpPort', 'config-ob11-httpPort',
config.ob11.enableHttp, config.ob11.enableHttp,
), ),
@@ -127,14 +142,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:210px;"><input class="q-input__inner" data-config-key="token" type="text" value="${config.token}" placeholder="未设置" /></div>`, `<div class="q-input" style="width:170px;"><input class="q-input__inner" data-config-key="token" type="text" value="${config.token}" placeholder="未设置" /></div>`,
), ),
SettingItem( SettingItem(
'新消息上报格式', '新消息上报格式',
@@ -148,6 +163,23 @@ 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),
),
]),
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 : '未指定'
@@ -160,25 +192,6 @@ 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(
'自动删除收到的文件', '自动删除收到的文件',
'在收到文件后的指定时间内删除该文件', '在收到文件后的指定时间内删除该文件',
@@ -386,22 +399,22 @@ async function onSettingWindowCreated(view: Element) {
view.appendChild(node) view.appendChild(node)
}) })
// 更新逻辑 // 更新逻辑
async function checkVersionFunc(ResultVersion: CheckVersion) { async function checkVersionFunc(info: 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 (ResultVersion.version === '') { if (info.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 (!ResultVersion.result) { } else if (!info.result) {
titleDom.innerHTML = '当前已是最新版本 v' + version titleDom.innerHTML = '当前已是最新版本 v' + version
buttonDom.innerHTML = '无需更新' buttonDom.innerHTML = '无需更新'
} else { } else {
titleDom.innerHTML = `当前版本为 v${version},最新版本为 v${ResultVersion.version}` titleDom.innerHTML = `当前版本为 v${version},最新版本为 v${info.version}`
buttonDom.innerHTML = '点击更新' buttonDom.innerHTML = '点击更新'
buttonDom.dataset.type = 'primary' buttonDom.dataset.type = 'primary'

192
src/satori/adapter.ts Normal file
View File

@@ -0,0 +1,192 @@
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) {
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)
}
else if (
input.type === NT.GroupNotifyType.InvitedNeedAdminiStratorPass &&
input.status === NT.GroupNotifyStatus.Unhandle
) {
// 他人被邀请,需管理员同意
return await parseGuildMemberRequest(this, input)
}
}
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 event = await this.handleGroupNotify(input)
.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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,12 @@
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 {}
}

View File

@@ -0,0 +1,16 @@
import { Channel } from '@satorijs/protocol'
import { Handler } from '../index'
import { ObjectToSnake } from 'ts-case-convert'
import { Dict } from 'cosmokit'
interface Payload {
channel_id: string
data: ObjectToSnake<Channel>
}
export const updateChannel: Handler<Dict<never>, Payload> = async (ctx, payload) => {
if (payload.data.name) {
await ctx.ntGroupApi.setGroupName(payload.channel_id, payload.data.name)
}
return {}
}

View File

@@ -0,0 +1,14 @@
import { Channel } from '@satorijs/protocol'
import { Handler } from '../../index'
interface Payload {
user_id: string
guild_id?: string
}
export const createDirectChannel: Handler<Channel, Payload> = async (ctx, payload) => {
return {
id: 'private:' + payload.user_id,
type: Channel.Type.DIRECT
}
}

View File

@@ -0,0 +1,18 @@
import { Handler } from '../index'
import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { Dict } from 'cosmokit'
interface Payload {
message_id: string
approve: boolean
comment: string
}
export const handleGuildRequest: Handler<Dict<never>, Payload> = async (ctx, payload) => {
await ctx.ntGroupApi.handleGroupRequest(
payload.message_id,
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.comment
)
return {}
}

View File

@@ -0,0 +1,12 @@
import { Guild } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeGuild } from '../../utils'
interface Payload {
guild_id: string
}
export const getGuild: Handler<Guild, Payload> = async (ctx, payload) => {
const info = await ctx.ntGroupApi.getGroupAllInfo(payload.guild_id)
return decodeGuild(info.groupAll)
}

View File

@@ -0,0 +1,14 @@
import { Guild, List } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeGuild } from '../../utils'
interface Payload {
next?: string
}
export const getGuildList: Handler<List<Guild>, Payload> = async (ctx) => {
const groups = await ctx.ntGroupApi.getGroups()
return {
data: groups.map(decodeGuild)
}
}

73
src/satori/api/index.ts Normal file
View File

@@ -0,0 +1,73 @@
import { Context } from 'cordis'
import { Awaitable, Dict } from 'cosmokit'
import { ObjectToSnake } from 'ts-case-convert'
import { getChannel } from './channel/get'
import { getChannelList } from './channel/list'
import { updateChannel } from './channel/update'
import { deleteChannel } from './channel/delete'
import { muteChannel } from './channel/mute'
import { createDirectChannel } from './channel/user/create'
import { getGuild } from './guild/get'
import { getGuildList } from './guild/list'
import { handleGuildRequest } from './guild/approve'
import { getLogin } from './login/get'
import { getGuildMember } from './member/get'
import { getGuildMemberList } from './member/list'
import { kickGuildMember } from './member/kick'
import { muteGuildMember } from './member/mute'
import { handleGuildMemberRequest } from './member/approve'
import { createMessage } from './message/create'
import { getMessage } from './message/get'
import { deleteMessage } from './message/delete'
import { getMessageList } from './message/list'
import { createReaction } from './reaction/create'
import { deleteReaction } from './reaction/delete'
import { getReactionList } from './reaction/list'
import { setGuildMemberRole } from './role/member/set'
import { getGuildRoleList } from './role/list'
import { getUser } from './user/get'
import { getFriendList } from './user/list'
import { handleFriendRequest } from './user/approve'
export type Handler<
R extends Dict = Dict,
P extends Dict = any
> = (ctx: Context, payload: P) => Awaitable<ObjectToSnake<R>>
export const handlers: Record<string, Handler> = {
// 频道 (Channel)
getChannel,
getChannelList,
updateChannel,
deleteChannel,
muteChannel,
createDirectChannel,
// 群组 (Guild)
getGuild,
getGuildList,
handleGuildRequest,
// 登录信息 (Login)
getLogin,
// 群组成员 (GuildMember)
getGuildMember,
getGuildMemberList,
kickGuildMember,
muteGuildMember,
handleGuildMemberRequest,
// 消息 (Message)
createMessage,
getMessage,
deleteMessage,
getMessageList,
// 表态 (Reaction)
createReaction,
deleteReaction,
getReactionList,
// 群组角色 (GuildRole)
setGuildMemberRole,
getGuildRoleList,
// 用户 (User)
getUser,
getFriendList,
handleFriendRequest,
}

View File

@@ -0,0 +1,24 @@
import { Login, Status, Methods } from '@satorijs/protocol'
import { decodeUser } from '../../utils'
import { selfInfo } from '@/common/globalVars'
import { Handler } from '../index'
import { handlers } from '../index'
export const getLogin: Handler<Login> = async (ctx) => {
const features: string[] = []
for (const [feature, info] of Object.entries(Methods)) {
if (info.name in handlers) {
features.push(feature)
}
}
features.push('guild.plain')
await ctx.ntUserApi.getSelfNick()
return {
user: decodeUser(selfInfo),
adapter: 'llonebot',
platform: 'llonebot',
status: selfInfo.online ? Status.ONLINE : Status.OFFLINE,
features,
proxy_urls: []
}
}

View File

@@ -0,0 +1,18 @@
import { Handler } from '../index'
import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { Dict } from 'cosmokit'
interface Payload {
message_id: string
approve: boolean
comment?: string
}
export const handleGuildMemberRequest: Handler<Dict<never>, Payload> = async (ctx, payload) => {
await ctx.ntGroupApi.handleGroupRequest(
payload.message_id,
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.comment
)
return {}
}

View File

@@ -0,0 +1,18 @@
import { GuildMember } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeGuildMember } from '../../utils'
interface Payload {
guild_id: string
user_id: string
}
export const getGuildMember: Handler<GuildMember, Payload> = async (ctx, payload) => {
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id)
if (!uid) throw new Error('无法获取用户信息')
const info = await ctx.ntGroupApi.getGroupMember(payload.guild_id, uid)
if (!info) {
throw new Error(`群成员${payload.user_id}不存在`)
}
return decodeGuildMember(info)
}

View File

@@ -0,0 +1,15 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
interface Payload {
guild_id: string
user_id: string
permanent?: boolean
}
export const kickGuildMember: Handler<Dict<never>, Payload> = async (ctx, payload) => {
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id, payload.guild_id)
if (!uid) throw new Error('无法获取用户信息')
await ctx.ntGroupApi.kickMember(payload.guild_id, [uid], Boolean(payload.permanent))
return {}
}

View File

@@ -0,0 +1,19 @@
import { GuildMember, List } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeGuildMember } from '../../utils'
interface Payload {
guild_id: string
next?: string
}
export const getGuildMemberList: Handler<List<GuildMember>, Payload> = async (ctx, payload) => {
let members = await ctx.ntGroupApi.getGroupMembers(payload.guild_id)
if (members.size === 0) {
await ctx.sleep(100)
members = await ctx.ntGroupApi.getGroupMembers(payload.guild_id)
}
return {
data: Array.from(members.values()).map(decodeGuildMember)
}
}

View File

@@ -0,0 +1,17 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
interface Payload {
guild_id: string
user_id: string
duration: number //毫秒
}
export const muteGuildMember: Handler<Dict<never>, Payload> = async (ctx, payload) => {
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id, payload.guild_id)
if (!uid) throw new Error('无法获取用户信息')
await ctx.ntGroupApi.banMember(payload.guild_id, [
{ uid, timeStamp: payload.duration / 1000 }
])
return {}
}

View File

@@ -0,0 +1,13 @@
import { Message } from '@satorijs/protocol'
import { MessageEncoder } from '../../message'
import { Handler } from '../index'
interface Payload {
channel_id: string
content: string
}
export const createMessage: Handler<Message[], Payload> = (ctx, payload) => {
const encoder = new MessageEncoder(ctx, payload.channel_id)
return encoder.send(payload.content)
}

View File

@@ -0,0 +1,18 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
import { getPeer } from '../../utils'
interface Payload {
channel_id: string
message_id: string
}
export const deleteMessage: Handler<Dict<never>, Payload> = async (ctx, payload) => {
const peer = await getPeer(ctx, payload.channel_id)
const data = await ctx.ntMsgApi.recallMsg(peer, [payload.message_id])
if (data.result !== 0) {
ctx.logger.error('message.delete', payload.message_id, data)
throw new Error(`消息撤回失败`)
}
return {}
}

View File

@@ -0,0 +1,18 @@
import { Message } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeMessage, getPeer } from '../../utils'
interface Payload {
channel_id: string
message_id: string
}
export const getMessage: Handler<Message, Payload> = async (ctx, payload) => {
const peer = await getPeer(ctx, payload.channel_id)
const raw = ctx.store.getMsgCache(payload.message_id) ?? (await ctx.ntMsgApi.getMsgsByMsgId(peer, [payload.message_id])).msgList[0]
const result = await decodeMessage(ctx, raw)
if (!result) {
throw new Error('消息为空')
}
return result
}

View File

@@ -0,0 +1,30 @@
import { Direction, Message, Order, TwoWayList } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeMessage, getPeer } from '../../utils'
import { RawMessage } from '@/ntqqapi/types'
import { filterNullable } from '@/common/utils/misc'
interface Payload {
channel_id: string
next?: string
direction?: Direction
limit?: number
order?: Order
}
export const getMessageList: Handler<TwoWayList<Message>, Payload> = async (ctx, payload) => {
const count = payload.limit ?? 50
const peer = await getPeer(ctx, payload.channel_id)
let msgList: RawMessage[]
if (!payload.next) {
msgList = (await ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, count)).msgList
} else {
msgList = (await ctx.ntMsgApi.getMsgHistory(peer, payload.next, count)).msgList
}
const data = filterNullable(await Promise.all(msgList.map(e => decodeMessage(ctx, e))))
if (payload.order === 'desc') data.reverse()
return {
data,
next: msgList.at(-1)?.msgId
}
}

View File

@@ -0,0 +1,19 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
import { getPeer } from '../../utils'
interface Payload {
channel_id: string
message_id: string
emoji: string
}
export const createReaction: Handler<Dict<never>, Payload> = async (ctx, payload) => {
const peer = await getPeer(ctx, payload.channel_id)
const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [payload.message_id])
if (!msgList.length || !msgList[0].msgSeq) {
throw new Error('无法获取该消息')
}
await ctx.ntMsgApi.setEmojiLike(peer, msgList[0].msgSeq, payload.emoji, true)
return {}
}

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