Compare commits

...

18 Commits

Author SHA1 Message Date
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
94 changed files with 2603 additions and 1179 deletions

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.10", "version": "4.0.1",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -18,16 +18,19 @@
"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.0",
"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": {

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: '',
@@ -35,8 +36,14 @@ export class ConfigUtil {
enableHttpHeart: false, enableHttpHeart: false,
listenLocalhost: false listenLocalhost: 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: '',

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
@@ -18,13 +19,15 @@ export interface OB11Config {
listenLocalhost: boolean listenLocalhost: 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
@@ -45,6 +48,13 @@ export interface Config {
hosts?: string[] hosts?: string[]
/** @deprecated */ /** @deprecated */
wsPort?: string wsPort?: string
/** @deprecated */
enableLLOB?: boolean
}
export interface CheckVersion {
result: boolean
version: string
} }
export interface LLOneBotError { export interface LLOneBotError {

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 { hookNTQQApiCall, hookNTQQApiReceive } 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,45 @@ 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!,
reportSelfMessage: config.reportSelfMessage!,
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()
} }

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,
@@ -223,21 +209,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 +241,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' {
@@ -25,15 +23,11 @@ export class NTQQFriendApi extends Service {
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,
afterFirstCmd: false,
}
)
const _friends: Friend[] = [] const _friends: Friend[] = []
for (const item of data.data) { for (const item of data.data) {
_friends.push(...item.buddyList) _friends.push(...item.buddyList)
@@ -42,25 +36,16 @@ export class NTQQFriendApi extends Service {
} }
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 +103,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

@@ -14,10 +14,7 @@ import {
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 {
@@ -28,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)
} }
@@ -51,49 +46,37 @@ export class NTQQGroupApi extends Service {
} }
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> { async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession() const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>> const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }])
if (session) { if (data.errCode !== 0) {
const groupService = session.getGroupService() throw new Error('获取群成员列表出错,' + data.errMsg)
const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow')
result = await groupService.getNextMemberList(sceneId, undefined, num)
} else {
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)) { 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() {
@@ -105,11 +88,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,
@@ -122,168 +106,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: GroupFileInfo }>( const data = await invoke<{ fileInfo: GroupFileInfo }>(
'nodeIKernelRichMediaService/getGroupFileList', 'nodeIKernelRichMediaService/getGroupFileList',
[ [{
{ groupId,
groupId, fileListForm
fileListForm }],
},
null,
],
{ {
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate', cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false, afterFirstCmd: false,
@@ -296,17 +209,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
} }
@@ -317,7 +230,7 @@ export class NTQQGroupApi extends Service {
msgSeq: +msgSeq, msgSeq: +msgSeq,
msgRandom: +msgRandom msgRandom: +msgRandom
} }
}, null]) }])
} }
async getGroupHonorList(groupCode: string) { async getGroupHonorList(groupCode: string) {
@@ -326,31 +239,33 @@ 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) { async getGroupBulletinList(groupCode: string) {
invoke('nodeIKernelGroupListener/onGetGroupBulletinListResult', [], { registerEvent: true }) invoke('nodeIKernelGroupListener/onGetGroupBulletinListResult', [], {
registerEvent: true
})
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<{ return await invoke<{
@@ -377,4 +292,8 @@ export class NTQQGroupApi extends Service {
} }
) )
} }
async setGroupAvatar(groupCode: string, path: string) {
return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }])
}
} }

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,32 @@ 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', [])
}
} }

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,51 +88,25 @@ 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) {
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
for (const member of membersList.values()) {
if (member.uin === uin) {
uid = member.uid
break
}
}
if (uid) break
}
}
if (!uid) { if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换 const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') === -1) { if (unveifyUid.indexOf('*') === -1) {
@@ -153,48 +117,43 @@ export class NTQQUserApi extends Service {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转 const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid uid = friends.find(item => item.uin === uin)?.uid
} }
if (!uid && groupCode) {
const members = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
uid = Array.from(members.values()).find(e => e.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 +161,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 +187,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 +205,7 @@ export class NTQQUserApi extends Service {
extStatus, extStatus,
batteryStatus, batteryStatus,
} }
}, null]) }])
} }
async getProfileLike(uid: string) { async getProfileLike(uid: string) {
@@ -279,6 +220,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,9 +12,11 @@ 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'
@@ -24,28 +25,34 @@ declare module 'cordis' {
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
constructor(protected ctx: Context, public config: Core.Config) { constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true) super(ctx, 'app', true)
} }
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 +124,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 +170,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 +187,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)) {
@@ -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

@@ -23,9 +23,7 @@ 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,
}, },
} }
} }
@@ -253,19 +251,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

@@ -26,7 +26,6 @@ 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 NTReturnData = [
@@ -96,7 +95,7 @@ export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi') const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
if (!isLogger) { if (!isLogger) {
try { try {
logHook && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', args)
} catch (e) { } } catch (e) { }
if (!onlyLog) { if (!onlyLog) {
try { try {

View File

@@ -13,7 +13,8 @@ import {
NodeIKernelUixConvertService, NodeIKernelUixConvertService,
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService NodeIKernelTipOffService,
NodeIKernelRobotService
} from './services' } from './services'
export enum NTClass { export enum NTClass {
@@ -39,7 +40,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 +93,7 @@ interface NTService {
nodeIKernelRichMediaService: NodeIKernelRichMediaService nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
} }
interface InvokeOptions<ReturnType> { interface InvokeOptions<ReturnType> {

View File

@@ -123,4 +123,6 @@ 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>
} }

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

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,4 @@ export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelRobotService'

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,6 +288,8 @@ 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
@@ -241,7 +297,6 @@ export interface GrayTipElement {
} }
} }
export enum FaceIndex { export enum FaceIndex {
Dice = 358, Dice = 358,
RPS = 359, // 石头剪刀布 RPS = 359, // 石头剪刀布
@@ -268,6 +323,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 +385,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 +416,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 +476,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

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

@@ -105,7 +105,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 +221,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 +250,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

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

View File

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

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

@@ -18,7 +18,9 @@ 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)) { if (isNullable(member.sex)) {
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid) const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)

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'
@@ -71,6 +71,8 @@ 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 { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
import { DeleteFriend } from './go-cqhttp/DeleteFriend'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -87,6 +89,8 @@ 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),
// onebot11 // onebot11
new SendLike(adapter), new SendLike(adapter),
new GetMsg(adapter), new GetMsg(adapter),
@@ -117,7 +121,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
@@ -147,6 +150,7 @@ export function initActionMap(adapter: Adapter) {
new GetGroupFilesByFolder(adapter), new GetGroupFilesByFolder(adapter),
new GetGroupFileUrl(adapter), new GetGroupFileUrl(adapter),
new GetGroupNotice(adapter), new GetGroupNotice(adapter),
new DeleteFriend(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

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

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

@@ -25,6 +25,8 @@ export enum ActionName {
FetchCustomFace = 'fetch_custom_face', FetchCustomFace = 'fetch_custom_face',
GetFriendMsgHistory = 'get_friend_msg_history', GetFriendMsgHistory = 'get_friend_msg_history',
SendForwardMsg = 'send_forward_msg', SendForwardMsg = 'send_forward_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
GetRobotUinRange = 'get_robot_uin_range',
// onebot 11 // onebot 11
SendLike = 'send_like', SendLike = 'send_like',
GetLoginInfo = 'get_login_info', GetLoginInfo = 'get_login_info',
@@ -38,7 +40,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',
@@ -58,7 +59,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_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',
@@ -84,4 +85,5 @@ export enum ActionName {
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_GetGroupNotice = '_get_group_notice',
GoCQHTTP_DeleteFriend = 'delete_friend',
} }

View File

@@ -4,10 +4,7 @@ import {
GroupNotify, GroupNotify,
GroupNotifyType, GroupNotifyType,
RawMessage, RawMessage,
BuddyReqType,
FriendRequest, FriendRequest,
GroupMember,
GroupMemberRole,
GroupNotifyStatus GroupNotifyStatus
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest'
@@ -23,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'
@@ -35,10 +31,10 @@ 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', 'store'
public startTime = 0 ]
private ob11WebSocket: OB11WebSocket private ob11WebSocket: OB11WebSocket
private ob11WebSocketReverseManager: OB11WebSocketReverseManager private ob11WebSocketReverseManager: OB11WebSocketReverseManager
private ob11Http: OB11Http private ob11Http: OB11Http
@@ -74,24 +70,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)
@@ -108,115 +86,113 @@ 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 && !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.message(this.ctx, message) OB11Entities.groupEvent(this.ctx, message).then(groupEvent => {
.then((msg) => { if (groupEvent) {
if (!msg) { this.dispatch(groupEvent)
return }
} })
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)
} }
}) })
OB11Entities.privateEvent(this.ctx, message).then((privateEvent) => {
if (privateEvent) {
this.dispatch(privateEvent)
}
})
}
} }
private handleRecallMsg(message: RawMessage) { private handleRecallMsg(message: RawMessage) {
@@ -235,30 +211,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) {
@@ -350,31 +318,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()
} }
@@ -387,7 +331,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 => {
@@ -397,7 +341,7 @@ 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]) this.handleMsg(input)
}) })
this.ctx.on('nt/group-notify', input => { this.ctx.on('nt/group-notify', input => {
this.handleGroupNotify(input) this.handleGroupNotify(input)
@@ -405,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,7 +371,6 @@ namespace OneBot11Adapter {
token: string token: string
debug: boolean debug: boolean
reportSelfMessage: boolean reportSelfMessage: boolean
msgCacheExpire: number
musicSignUrl?: string musicSignUrl?: string
enableLocalFile2Url: boolean enableLocalFile2Url: boolean
ffmpeg?: string ffmpeg?: string

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

@@ -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,10 +539,9 @@ 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')
} }
} }
} }
@@ -569,8 +559,7 @@ export namespace OB11Entities {
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 +581,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 +602,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 +631,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,7 +643,7 @@ export namespace OB11Entities {
} }
} }
export function friendsV2(raw: FriendV2[]): OB11User[] { export function friendsV2(raw: SimpleInfo[]): OB11User[] {
return raw.map(friendV2) return raw.map(friendV2)
} }

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,34 @@ 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) ?? ''
if (atMember) { const atNmae = sendMsg.data?.name
const display = `@${atMember.cardName || atMember.nick}` const display = atNmae ? `@${atNmae}` : ''
sendElements.push( sendElements.push(SendElement.at(atQQ, uid, AtType.One, display))
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),
)
}
} }
} }
} }
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 +91,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 +107,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 +120,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 +186,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

@@ -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('reportSelfMessage', config.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 {}
}

View File

@@ -0,0 +1,20 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
import { getPeer } from '../../utils'
interface Payload {
channel_id: string
message_id: string
emoji: string
user_id?: string
}
export const deleteReaction: 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, false)
return {}
}

View File

@@ -0,0 +1,27 @@
import { List, User } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeUser, getPeer } from '../../utils'
import { filterNullable } from '@/common/utils/misc'
interface Payload {
channel_id: string
message_id: string
emoji: string
next?: string
}
export const getReactionList: Handler<List<User>, 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('无法获取该消息')
}
const emojiType = payload.emoji.length > 3 ? '2' : '1'
const count = msgList[0].emojiLikesList.find(e => e.emojiId === payload.emoji)?.likesCnt ?? '50'
const data = await ctx.ntMsgApi.getMsgEmojiLikesList(peer, msgList[0].msgSeq, payload.emoji, emojiType, +count)
const uids = await Promise.all(data.emojiLikesList.map(e => ctx.ntUserApi.getUidByUin(e.tinyId, peer.peerUid)))
const raw = await ctx.ntUserApi.getCoreAndBaseInfo(filterNullable(uids))
return {
data: Array.from(raw.values()).map(e => decodeUser(e.coreInfo))
}
}

View File

@@ -0,0 +1,26 @@
import { GuildRole, List } from '@satorijs/protocol'
import { Handler } from '../index'
interface Payload {
guild_id: string
next?: string
}
export const getGuildRoleList: Handler<List<Partial<GuildRole>>, Payload> = () => {
return {
data: [
{
id: '4',
name: 'owner'
},
{
id: '3',
name: 'admin'
},
{
id: '2',
name: 'member'
}
]
}
}

View File

@@ -0,0 +1,20 @@
import { Handler } from '../../index'
import { Dict } from 'cosmokit'
interface Payload {
guild_id: string
user_id: string
role_id: string
}
export const setGuildMemberRole: Handler<Dict<never>, Payload> = async (ctx, payload) => {
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id, payload.guild_id)
if (!uid) {
throw new Error('无法获取用户信息')
}
if (payload.role_id !== '2' && payload.role_id !== '3') {
throw new Error('role_id 仅可以为 2 或 3')
}
await ctx.ntGroupApi.setMemberRole(payload.guild_id, uid, +payload.role_id)
return {}
}

View File

@@ -0,0 +1,25 @@
import { Handler } from '../index'
import { Dict } from 'cosmokit'
import { ChatType } from '@/ntqqapi/types'
interface Payload {
message_id: string
approve: boolean
comment?: string
}
export const handleFriendRequest: Handler<Dict<never>, Payload> = async (ctx, payload) => {
const data = payload.message_id.split('|')
if (data.length < 2) {
throw new Error('无效的 message_id')
}
const uid = data[0]
const reqTime = data[1]
await ctx.ntFriendApi.handleFriendRequest(uid, reqTime, payload.approve)
await ctx.ntMsgApi.activateChat({
peerUid: uid,
chatType: ChatType.C2C,
guildId: ''
})
return {}
}

View File

@@ -0,0 +1,19 @@
import { User } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeUser } from '../../utils'
interface Payload {
user_id: string
}
export const getUser: Handler<User, Payload> = async (ctx, payload) => {
const uin = payload.user_id
const uid = await ctx.ntUserApi.getUidByUin(uin)
if (!uid) throw new Error('无法获取用户信息')
const data = await ctx.ntUserApi.getUserSimpleInfo(uid)
const ranges = await ctx.ntUserApi.getRobotUinRange()
return {
...decodeUser(data),
is_bot: ranges.some(e => uin >= e.minUin && uin <= e.maxUin)
}
}

View File

@@ -0,0 +1,22 @@
import { User, List } from '@satorijs/protocol'
import { Handler } from '../index'
import { decodeUser } from '../../utils'
import { getBuildVersion } from '@/common/utils/misc'
interface Payload {
next?: string
}
export const getFriendList: Handler<List<User>, Payload> = async (ctx) => {
if (getBuildVersion() >= 26702) {
const friends = await ctx.ntFriendApi.getBuddyV2()
return {
data: friends.map(e => decodeUser(e.coreInfo))
}
} else {
const friends = await ctx.ntFriendApi.getFriends()
return {
data: friends.map(e => decodeUser(e))
}
}
}

32
src/satori/event/guild.ts Normal file
View File

@@ -0,0 +1,32 @@
import SatoriAdapter from '../adapter'
import { RawMessage, GroupNotify } from '@/ntqqapi/types'
import { decodeGuild } from '../utils'
export async function parseGuildAdded(bot: SatoriAdapter, input: RawMessage) {
const { groupAll } = await bot.ctx.ntGroupApi.getGroupAllInfo(input.peerUid)
return bot.event('guild-added', {
guild: decodeGuild(groupAll)
})
}
export async function parseGuildRemoved(bot: SatoriAdapter, input: RawMessage) {
const { groupAll } = await bot.ctx.ntGroupApi.getGroupAllInfo(input.peerUid)
return bot.event('guild-removed', {
guild: decodeGuild(groupAll)
})
}
export async function parseGuildRequest(bot: SatoriAdapter, notify: GroupNotify) {
const groupCode = notify.group.groupCode
const flag = groupCode + '|' + notify.seq + '|' + notify.type
return bot.event('guild-request', {
guild: decodeGuild(notify.group),
message: {
id: flag,
content: notify.postscript
}
})
}

View File

@@ -0,0 +1,60 @@
import SatoriAdapter from '../adapter'
import { RawMessage, GroupNotify } from '@/ntqqapi/types'
import { decodeGuild, decodeUser } from '../utils'
export async function parseGuildMemberAdded(bot: SatoriAdapter, input: RawMessage, isBot = false) {
const { groupAll } = await bot.ctx.ntGroupApi.getGroupAllInfo(input.peerUid)
let memberUid: string | undefined
if (input.elements[0].grayTipElement?.groupElement) {
memberUid = input.elements[0].grayTipElement.groupElement.memberUid
} else if (input.elements[0].grayTipElement?.jsonGrayTipElement) {
const json = JSON.parse(input.elements[0].grayTipElement.jsonGrayTipElement.jsonStr)
const uin = new URL(json.items[2].jp).searchParams.get('robot_uin')
if (!uin) return
memberUid = await bot.ctx.ntUserApi.getUidByUin(uin)
} else {
const iterator = input.elements[0].grayTipElement?.xmlElement?.members.keys()
iterator?.next()
memberUid = iterator?.next().value
}
if (!memberUid) return
const user = decodeUser(await bot.ctx.ntUserApi.getUserSimpleInfo(memberUid))
user.is_bot = isBot
return bot.event('guild-member-added', {
guild: decodeGuild(groupAll),
user,
member: {
user,
nick: user.name
}
})
}
export async function parseGuildMemberRemoved(bot: SatoriAdapter, input: GroupNotify) {
const user = decodeUser(await bot.ctx.ntUserApi.getUserSimpleInfo(input.user1.uid))
return bot.event('guild-member-removed', {
guild: decodeGuild(input.group),
user,
member: {
user,
nick: user.name
}
})
}
export async function parseGuildMemberRequest(bot: SatoriAdapter, input: GroupNotify) {
const groupCode = input.group.groupCode
const flag = groupCode + '|' + input.seq + '|' + input.type
return bot.event('guild-member-request', {
guild: decodeGuild(input.group),
message: {
id: flag,
content: input.postscript
}
})
}

View File

@@ -0,0 +1,35 @@
import SatoriAdapter from '../adapter'
import { RawMessage } from '@/ntqqapi/types'
import { decodeMessage, decodeUser } from '../utils'
import { omit } from 'cosmokit'
export async function parseMessageCreated(bot: SatoriAdapter, input: RawMessage) {
const message = await decodeMessage(bot.ctx, input)
if (!message) return
return bot.event('message-created', {
message: omit(message, ['member', 'user', 'channel', 'guild']),
member: message.member,
user: message.user,
channel: message.channel,
guild: message.guild
})
}
export async function parseMessageDeleted(bot: SatoriAdapter, input: RawMessage) {
const origin = bot.ctx.store.getMsgCache(input.msgId)
if (!origin) return
const message = await decodeMessage(bot.ctx, origin)
if (!message) return
const operatorUid = input.elements[0].grayTipElement!.revokeElement!.operatorUid
const user = await bot.ctx.ntUserApi.getUserSimpleInfo(operatorUid)
return bot.event('message-deleted', {
message: omit(message, ['member', 'user', 'channel', 'guild']),
member: message.member,
user: message.user,
channel: message.channel,
guild: message.guild,
operator: omit(decodeUser(user), ['is_bot'])
})
}

16
src/satori/event/user.ts Normal file
View File

@@ -0,0 +1,16 @@
import SatoriAdapter from '../adapter'
import { FriendRequest } from '@/ntqqapi/types'
import { decodeUser } from '../utils'
export async function parseFriendRequest(bot: SatoriAdapter, input: FriendRequest) {
const flag = input.friendUid + '|' + input.reqTime
const user = await bot.ctx.ntUserApi.getUserSimpleInfo(input.friendUid)
return bot.event('friend-request', {
user: decodeUser(user),
message: {
id: flag,
content: input.extWords
}
})
}

307
src/satori/message.ts Normal file
View File

@@ -0,0 +1,307 @@
import h from '@satorijs/element'
import pathLib from 'node:path'
import * as NT from '@/ntqqapi/types'
import { Context } from 'cordis'
import { Message } from '@satorijs/protocol'
import { SendElement } from '@/ntqqapi/entities'
import { decodeMessage, getPeer } from './utils'
import { ObjectToSnake } from 'ts-case-convert'
import { uri2local } from '@/common/utils'
import { unlink } from 'node:fs/promises'
import { selfInfo } from '@/common/globalVars'
class State {
children: (NT.SendMessageElement | string)[] = []
constructor(public type: 'message' | 'multiForward') { }
}
export class MessageEncoder {
public errors: Error[] = []
public results: ObjectToSnake<Message>[] = []
private elements: NT.SendMessageElement[] = []
private deleteAfterSentFiles: string[] = []
private stack: State[] = [new State('message')]
private peer?: NT.Peer
constructor(private ctx: Context, private channelId: string) { }
async flush() {
if (this.elements.length === 0) return
if (this.stack[0].type === 'multiForward') {
this.stack[0].children.push(...this.elements)
this.elements = []
return
}
this.peer ??= await getPeer(this.ctx, this.channelId)
const sent = await this.ctx.ntMsgApi.sendMsg(this.peer, this.elements)
if (sent) {
this.ctx.logger.info('消息发送', this.peer)
const result = await decodeMessage(this.ctx, sent)
result && this.results.push(result)
}
this.deleteAfterSentFiles.forEach(path => unlink(path))
this.deleteAfterSentFiles = []
this.elements = []
}
private async fetchFile(url: string) {
const res = await uri2local(this.ctx, url)
if (!res.success) {
this.ctx.logger.error(res.errMsg)
throw Error(res.errMsg)
}
if (!res.isLocal) {
this.deleteAfterSentFiles.push(res.path)
}
return res.path
}
private async getPeerFromMsgId(msgId: string): Promise<NT.Peer | undefined> {
this.peer ??= await getPeer(this.ctx, this.channelId)
const msg = (await this.ctx.ntMsgApi.getMsgsByMsgId(this.peer, [msgId])).msgList
if (msg.length > 0) {
return this.peer
} else {
const cacheMsg = this.ctx.store.getMsgCache(msgId)
if (cacheMsg) {
return {
peerUid: cacheMsg.peerUid,
chatType: cacheMsg.chatType
}
}
const c2cMsg = await this.ctx.ntMsgApi.queryMsgsById(NT.ChatType.C2C, msgId)
if (c2cMsg.msgList.length) {
return {
peerUid: c2cMsg.msgList[0].peerUid,
chatType: c2cMsg.msgList[0].chatType
}
}
const groupMsg = await this.ctx.ntMsgApi.queryMsgsById(NT.ChatType.Group, msgId)
if (groupMsg.msgList.length) {
return {
peerUid: groupMsg.msgList[0].peerUid,
chatType: groupMsg.msgList[0].chatType
}
}
}
}
private async forward(msgId: string, srcPeer: NT.Peer, destPeer: NT.Peer) {
const list = await this.ctx.ntMsgApi.forwardMsg(srcPeer, destPeer, [msgId])
return list[0]
}
private async multiForward() {
if (!this.stack[0].children.length) return
const selfPeer = {
chatType: NT.ChatType.C2C,
peerUid: selfInfo.uid,
}
const nodeMsgIds: { msgId: string, peer: NT.Peer }[] = []
for (const node of this.stack[0].children) {
if (typeof node === 'string') {
if (node.length !== 19) {
this.ctx.logger.warn('转发消息失败,消息 ID 不合法', node)
continue
}
const peer = await this.getPeerFromMsgId(node)
if (!peer) {
this.ctx.logger.warn('转发消息失败,未找到消息', node)
continue
}
nodeMsgIds.push({ msgId: node, peer })
} else {
try {
const sent = await this.ctx.ntMsgApi.sendMsg(selfPeer, [node])
if (!sent) {
this.ctx.logger.warn('转发节点生成失败', node)
continue
}
nodeMsgIds.push({ msgId: sent.msgId, peer: selfPeer })
await this.ctx.sleep(100)
} catch (e) {
this.ctx.logger.error('生成转发消息节点失败', e)
}
}
}
let srcPeer: NT.Peer
let needSendSelf = false
for (const { peer } of nodeMsgIds) {
srcPeer ??= { chatType: peer.chatType, peerUid: peer.peerUid }
if (srcPeer.peerUid !== peer.peerUid) {
needSendSelf = true
break
}
}
let retMsgIds: string[] = []
if (needSendSelf) {
for (const { msgId, peer } of nodeMsgIds) {
const srcPeer = {
peerUid: peer.peerUid,
chatType: peer.chatType
}
const clonedMsg = await this.forward(msgId, srcPeer, selfPeer)
if (clonedMsg) {
retMsgIds.push(clonedMsg.msgId)
}
await this.ctx.sleep(100)
}
srcPeer = selfPeer
} else {
retMsgIds = nodeMsgIds.map(e => e.msgId)
}
if (retMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
if (this.stack[1].type === 'multiForward') {
this.peer ??= await getPeer(this.ctx, this.channelId)
const msg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, selfPeer, retMsgIds)
this.stack[1].children.push(...msg.elements as NT.SendMessageElement[])
} else {
this.peer ??= await getPeer(this.ctx, this.channelId)
await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, this.peer, retMsgIds)
this.ctx.logger.info('消息发送', this.peer)
}
}
async visit(element: h) {
const { type, attrs, children } = element
if (type === 'text') {
this.elements.push(SendElement.text(attrs.content))
} else if (type === 'at') {
if (attrs.type === 'all') {
this.elements.push(SendElement.at('', '', NT.AtType.All, '@全体成员'))
} else {
const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id) ?? ''
const display = attrs.name ? '@' + attrs.name : ''
this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display))
}
} else if (type === 'a') {
await this.render(children)
const prev = this.elements.at(-1)
if (prev?.elementType === 1 && prev.textElement.atType === 0) {
prev.textElement.content += ` ( ${attrs.href} )`
}
} else if (type === 'img' || type === 'image') {
const url = attrs.src ?? attrs.url
const path = await this.fetchFile(url)
const element = await SendElement.pic(this.ctx, path)
this.deleteAfterSentFiles.push(element.picElement.sourcePath!)
this.elements.push(element)
} else if (type === 'audio') {
await this.flush()
const url = attrs.src ?? attrs.url
const path = await this.fetchFile(url)
this.elements.push(await SendElement.ptt(this.ctx, path))
await this.flush()
} else if (type === 'video') {
await this.flush()
const url = attrs.src ?? attrs.url
const path = await this.fetchFile(url)
let thumb: string | undefined
if (attrs.poster) {
thumb = await this.fetchFile(attrs.poster)
}
const element = await SendElement.video(this.ctx, path, undefined, thumb)
this.deleteAfterSentFiles.push(element.videoElement.filePath)
this.elements.push(element)
await this.flush()
} else if (type === 'file') {
await this.flush()
const url = attrs.src ?? attrs.url
const path = await this.fetchFile(url)
const fileName = attrs.title ?? pathLib.basename(path)
this.elements.push(await SendElement.file(this.ctx, path, fileName))
await this.flush()
} else if (type === 'br') {
this.elements.push(SendElement.text('\n'))
} else if (type === 'p') {
const prev = this.elements.at(-1)
if (prev?.elementType === 1 && prev.textElement.atType === 0) {
if (!prev.textElement.content.endsWith('\n')) {
prev.textElement.content += '\n'
}
} else if (prev) {
this.elements.push(SendElement.text('\n'))
}
await this.render(children)
const last = this.elements.at(-1)
if (last?.elementType === 1 && last.textElement.atType === 0) {
if (!last.textElement.content.endsWith('\n')) {
last.textElement.content += '\n'
}
} else {
this.elements.push(SendElement.text('\n'))
}
} else if (type === 'message') {
if (attrs.id && attrs.forward) {
await this.flush()
const srcPeer = await this.getPeerFromMsgId(attrs.id)
if (srcPeer) {
this.peer ??= await getPeer(this.ctx, this.channelId)
const sent = await this.forward(attrs.id, srcPeer, this.peer)
if (sent) {
this.ctx.logger.info('消息发送', this.peer)
const result = await decodeMessage(this.ctx, sent)
result && this.results.push(result)
}
}
} else if (attrs.forward) {
await this.flush()
this.stack.unshift(new State('multiForward'))
await this.render(children)
await this.flush()
await this.multiForward()
this.stack.shift()
} else if (attrs.id && this.stack[0].type === 'multiForward') {
this.stack[0].children.push(attrs.id)
} else {
await this.render(children)
await this.flush()
}
} else if (type === 'quote') {
this.peer ??= await getPeer(this.ctx, this.channelId)
const source = (await this.ctx.ntMsgApi.getMsgsByMsgId(this.peer, [attrs.id])).msgList[0]
if (source) {
this.elements.push(SendElement.reply(source.msgSeq, source.msgId, source.senderUin))
}
} else if (type === 'face') {
this.elements.push(SendElement.face(+attrs.id, +attrs.type))
} else if (type === 'mface') {
this.elements.push(SendElement.mface(
+attrs.emojiPackageId,
attrs.emojiId,
attrs.key,
attrs.summary
))
} else {
await this.render(children)
}
}
async render(elements: h[], flush?: boolean) {
for (const element of elements) {
await this.visit(element)
}
if (flush) {
await this.flush()
}
}
async send(content: h.Fragment) {
const elements = h.normalize(content)
await this.render(elements)
await this.flush()
if (this.errors.length) {
throw new AggregateError(this.errors)
} else {
return this.results
}
}
}

143
src/satori/server.ts Normal file
View File

@@ -0,0 +1,143 @@
import * as Universal from '@satorijs/protocol'
import express, { Express, Request, Response } from 'express'
import { Server } from 'node:http'
import { Context } from 'cordis'
import { handlers } from './api'
import { WebSocket, WebSocketServer } from 'ws'
import { promisify } from 'node:util'
import { ObjectToSnake } from 'ts-case-convert'
import { selfInfo } from '@/common/globalVars'
export class SatoriServer {
private express: Express
private httpServer?: Server
private wsServer?: WebSocketServer
private wsClients: WebSocket[] = []
constructor(private ctx: Context, private config: SatoriServer.Config) {
this.express = express()
this.express.use(express.json({ limit: '50mb' }))
}
public start() {
this.express.get('/v1/:name', async (req, res) => {
res.status(405).send('Please use POST method to send requests.')
})
this.express.post('/v1/:name', async (req, res) => {
const method = Universal.Methods[req.params.name]
if (!method) {
res.status(404).send('method not found')
return
}
if (this.checkAuth(req, res)) return
const selfId = req.headers['satori-user-id'] ?? req.headers['x-self-id']
const platform = req.headers['satori-platform'] ?? req.headers['x-platform']
if (selfId !== selfInfo.uin || !platform) {
res.status(403).send('login not found')
return
}
const handle = handlers[method.name]
if (!handle) {
res.status(404).send('method not found')
return
}
try {
const result = await handle(this.ctx, req.body)
res.json(result)
} catch (e) {
this.ctx.logger.error(e)
throw e
}
})
const { listen, port } = this.config
this.httpServer = this.express.listen(port, listen, () => {
this.ctx.logger.info(`HTTP server started ${listen}:${port}`)
})
this.wsServer = new WebSocketServer({
server: this.httpServer
})
this.wsServer.on('connection', (socket, req) => {
const url = req.url?.split('?').shift()
if (!['/v1/events', '/v1/events/'].includes(url!)) {
return socket.close()
}
socket.addEventListener('message', async (event) => {
let payload: Universal.ClientPayload
try {
payload = JSON.parse(event.data.toString())
} catch (error) {
return socket.close(4000, 'invalid message')
}
if (payload.op === Universal.Opcode.IDENTIFY) {
if (this.config.token && payload.body?.token !== this.config.token) {
return socket.close(4004, 'invalid token')
}
this.ctx.logger.info('ws connect', url)
socket.send(JSON.stringify({
op: Universal.Opcode.READY,
body: {
logins: [await handlers.getLogin(this.ctx, {})]
}
} as Universal.ServerPayload))
this.wsClients.push(socket)
} else if (payload.op === Universal.Opcode.PING) {
socket.send(JSON.stringify({
op: Universal.Opcode.PONG,
body: {},
} as Universal.ServerPayload))
}
})
})
}
public async stop() {
if (this.wsServer) {
const close = promisify(this.wsServer.close)
await close.call(this.wsServer)
}
if (this.httpServer) {
const close = promisify(this.httpServer.close)
await close.call(this.httpServer)
}
}
private checkAuth(req: Request, res: Response) {
if (!this.config.token) return
if (req.headers.authorization !== `Bearer ${this.config.token}`) {
res.status(403).send('invalid token')
return true
}
}
public async dispatch(body: ObjectToSnake<Universal.Event>) {
this.wsClients.forEach(socket => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
op: Universal.Opcode.EVENT,
body
} as ObjectToSnake<Universal.ServerPayload>))
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', body.type)
}
})
}
public updateConfig(config: SatoriServer.Config) {
Object.assign(this.config, config)
}
}
namespace SatoriServer {
export interface Config {
port: number
listen: string
token: string
}
}

221
src/satori/utils.ts Normal file
View File

@@ -0,0 +1,221 @@
import h from '@satorijs/element'
import * as NT from '@/ntqqapi/types'
import * as Universal from '@satorijs/protocol'
import { Context } from 'cordis'
import { ObjectToSnake } from 'ts-case-convert'
import { pick } from 'cosmokit'
import { pathToFileURL } from 'node:url'
export function decodeUser(user: NT.User): ObjectToSnake<Universal.User> {
return {
id: user.uin,
name: user.nick,
nick: user.remark || user.nick,
avatar: `http://q.qlogo.cn/headimg_dl?dst_uin=${user.uin}&spec=640`,
is_bot: false
}
}
function decodeGuildChannelId(data: NT.RawMessage) {
if (data.chatType === NT.ChatType.Group) {
return [data.peerUin, data.peerUin]
} else {
return [undefined, 'private:' + data.peerUin]
}
}
function decodeMessageUser(data: NT.RawMessage) {
return {
id: data.senderUin,
name: data.sendNickName,
nick: data.sendRemarkName || data.sendNickName,
avatar: `http://q.qlogo.cn/headimg_dl?dst_uin=${data.senderUin}&spec=640`
}
}
async function decodeElement(ctx: Context, data: NT.RawMessage, quoted = false) {
const buffer: h[] = []
for (const v of data.elements) {
if (v.textElement && v.textElement.atType !== NT.AtType.Unknown) {
// at
const { atNtUid, atUid, atType, content } = v.textElement
if (atType === NT.AtType.All) {
buffer.push(h.at(undefined, { type: 'all' }))
} else if (atType === NT.AtType.One) {
let id: string
if (atUid && atUid !== '0') {
id = atUid
} else {
id = await ctx.ntUserApi.getUinByUid(atNtUid)
}
buffer.push(h.at(id, { name: content.replace('@', '') }))
}
} else if (v.textElement && v.textElement.content) {
// text
buffer.push(h.text(v.textElement.content))
} else if (v.replyElement && !quoted) {
// quote
const peer = {
chatType: data.chatType,
peerUid: data.peerUid,
guildId: ''
}
const { replayMsgSeq, replyMsgTime, sourceMsgIdInRecords } = v.replyElement
const records = data.records.find(msgRecord => msgRecord.msgId === sourceMsgIdInRecords)
const senderUid = v.replyElement.senderUidStr || records?.senderUid
if (!records || !replyMsgTime || !senderUid) {
ctx.logger.error('找不到引用消息', v.replyElement)
continue
}
if (data.multiTransInfo) {
buffer.push(h.quote(records.msgId))
continue
}
try {
const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUid])
let replyMsg: NT.RawMessage | undefined
if (records.msgRandom !== '0') {
replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom)
} else {
ctx.logger.info('msgRandom is missing', v.replyElement, records)
replyMsg = msgList[0]
}
if (!replyMsg) {
ctx.logger.info('queryMsgs', msgList.map(e => pick(e, ['msgSeq', 'msgRandom'])), records.msgRandom)
throw new Error('回复消息验证失败')
}
const elements = await decodeElement(ctx, replyMsg, true)
buffer.push(h('quote', { id: replyMsg.msgId }, elements))
} catch (e) {
ctx.logger.error('获取不到引用的消息', v.replyElement, (e as Error).stack)
}
} else if (v.picElement) {
// img
const src = await ctx.ntFileApi.getImageUrl(v.picElement)
buffer.push(h.img(src, {
width: v.picElement.picWidth,
height: v.picElement.picHeight,
subType: v.picElement.picSubType
}))
} else if (v.pttElement) {
// audio
const src = pathToFileURL(v.pttElement.filePath).href
buffer.push(h.audio(src, { duration: v.pttElement.duration }))
} else if (v.videoElement) {
// video
const src = (await ctx.ntFileApi.getVideoUrl({
chatType: data.chatType,
peerUid: data.peerUid,
}, data.msgId, v.elementId)) || pathToFileURL(v.videoElement.filePath).href
buffer.push(h.video(src))
} else if (v.marketFaceElement) {
// mface
const { emojiId, supportSize } = v.marketFaceElement
const { width = 300, height = 300 } = supportSize?.[0] ?? {}
const dir = emojiId.substring(0, 2)
const src = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw${width}.gif`
buffer.push(h('mface', {
emojiPackageId: v.marketFaceElement.emojiPackageId,
emojiId,
key: v.marketFaceElement.key,
summary: v.marketFaceElement.faceName
}, [h.image(src, { width, height })]))
} else if (v.faceElement) {
// face
const { faceIndex, faceType } = v.faceElement
buffer.push(h('face', {
id: String(faceIndex),
type: String(faceType),
platform: 'llonebot'
}))
}
}
return buffer
}
export async function decodeMessage(
ctx: Context,
data: NT.RawMessage,
message: ObjectToSnake<Universal.Message> = {}
) {
if (!data.senderUin || data.senderUin === '0') return //跳过空消息
const [guildId, channelId] = decodeGuildChannelId(data)
const elements = await decodeElement(ctx, data)
if (elements.length === 0) return
message.id = data.msgId
message.content = elements.join('')
message.channel = {
id: channelId!,
name: data.peerName,
type: guildId ? Universal.Channel.Type.TEXT : Universal.Channel.Type.DIRECT
}
message.user = decodeMessageUser(data)
message.created_at = +data.msgTime * 1000
if (message.channel.type === Universal.Channel.Type.DIRECT) {
const info = await ctx.ntUserApi.getUserSimpleInfo(data.senderUid)
message.channel.name = info.nick
message.user.name = info.nick
message.user.nick = info.remark || info.nick
}
if (guildId) {
let nick = data.sendMemberName || data.sendNickName
if (!data.sendNickName) {
const info = await ctx.ntGroupApi.getGroupMember(guildId, data.senderUid)
message.user.name = info.nick
message.user.nick = info.remark || info.nick
nick = info.cardName || info.nick
}
message.guild = {
id: guildId,
name: data.peerName,
avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`
}
message.member = {
user: message.user,
nick
}
}
return message
}
export function decodeGuildMember(data: NT.GroupMember): ObjectToSnake<Universal.GuildMember> {
return {
user: {
...decodeUser(data),
is_bot: data.isRobot
},
nick: data.cardName || data.nick,
avatar: `http://q.qlogo.cn/headimg_dl?dst_uin=${data.uin}&spec=640`,
joined_at: data.joinTime * 1000
}
}
export function decodeGuild(data: Record<'groupCode' | 'groupName', string>): ObjectToSnake<Universal.Guild> {
return {
id: data.groupCode,
name: data.groupName,
avatar: `https://p.qlogo.cn/gh/${data.groupCode}/${data.groupCode}/640`
}
}
export async function getPeer(ctx: Context, channelId: string): Promise<NT.Peer> {
let peerUid = channelId
let chatType: NT.ChatType = NT.ChatType.Group
if (peerUid.includes('private:')) {
const uin = channelId.replace('private:', '')
const uid = await ctx.ntUserApi.getUidByUin(uin)
if (!uid) throw new Error('无法获取用户信息')
const isBuddy = await ctx.ntFriendApi.isBuddy(uid)
chatType = isBuddy ? NT.ChatType.C2C : NT.ChatType.TempC2CFromGroup
peerUid = uid
}
return {
chatType,
peerUid
}
}

View File

@@ -1 +1 @@
export const version = '3.33.10' export const version = '4.0.1'