mirror of
https://github.com/LLOneBot/LLOneBot.git
synced 2024-11-22 01:56:33 +00:00
Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2f74de667e | ||
![]() |
2a67ffae24 | ||
![]() |
78def9ebf8 | ||
![]() |
c6dddcd664 | ||
![]() |
5b90a25f8f | ||
![]() |
364dfe8b93 | ||
![]() |
0fe725eb32 | ||
![]() |
8b89fd7a0b | ||
![]() |
1b0c9ad57c | ||
![]() |
2910b8f4e6 | ||
![]() |
2453509734 | ||
![]() |
8239e9a243 | ||
![]() |
50e5f89f4f | ||
![]() |
be2119a1e6 | ||
![]() |
951afea794 | ||
![]() |
0946d9652e | ||
![]() |
a66e48dfb0 | ||
![]() |
029842ca08 | ||
![]() |
39fda24799 | ||
![]() |
a4beeba528 | ||
![]() |
c837e970df | ||
![]() |
4ced7fa3cf | ||
![]() |
dbd71d4376 | ||
![]() |
d43612b2a3 | ||
![]() |
31ad0195d8 | ||
![]() |
9b32140f87 | ||
![]() |
dc5982c6b2 | ||
![]() |
ce46c99330 | ||
![]() |
d3abaf806f | ||
![]() |
e10a67ce05 | ||
![]() |
c8e897abdb | ||
![]() |
e07c06f3e9 | ||
![]() |
9c694a11b5 | ||
![]() |
6e3bb7c9cf | ||
![]() |
0d8d3ac24f | ||
![]() |
c96d032820 | ||
![]() |
837f48b63a | ||
![]() |
9d0f9e7096 | ||
![]() |
801d79d79d | ||
![]() |
0d5640046c | ||
![]() |
e988908784 | ||
![]() |
1cfa736dd5 | ||
![]() |
0081b0b124 | ||
![]() |
ba565e7c38 | ||
![]() |
abb468c3f8 | ||
![]() |
433a175809 | ||
![]() |
b40c81c5cb | ||
![]() |
ddf7ffcabe | ||
![]() |
2b0aa6249b | ||
![]() |
6bb4a8fe69 | ||
![]() |
91d78f22f7 | ||
![]() |
457ffc0922 | ||
![]() |
e3a2303e45 | ||
![]() |
8465c47d41 | ||
![]() |
41822eb052 | ||
![]() |
b5578d6278 | ||
![]() |
fecb4c4655 | ||
![]() |
c82b849ead |
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -1,7 +0,0 @@
|
||||
* text eol=lf
|
||||
|
||||
*.png -text
|
||||
*.jpg -text
|
||||
*.ico -text
|
||||
*.gif -text
|
||||
*.webp -text
|
@@ -4,7 +4,7 @@
|
||||
"name": "LLOneBot",
|
||||
"slug": "LLOneBot",
|
||||
"description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.13",
|
||||
"icon": "./icon.webp",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
|
||||
"format": "prettier -cw .",
|
||||
"check": "tsc",
|
||||
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
|
||||
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto groupMemberChange.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
@@ -24,7 +24,7 @@
|
||||
"cordis": "^3.18.1",
|
||||
"cors": "^2.8.5",
|
||||
"cosmokit": "^1.6.3",
|
||||
"express": "^5.0.0",
|
||||
"express": "^5.0.1",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"minato": "^3.6.0",
|
||||
@@ -42,8 +42,8 @@
|
||||
"electron": "^31.4.0",
|
||||
"electron-vite": "^2.3.0",
|
||||
"protobufjs-cli": "^1.1.3",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-cp": "^4.0.8"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
|
@@ -1,6 +1,5 @@
|
||||
export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
|
||||
export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
|
||||
export const CHANNEL_SET_CONFIG_CONFIRMED = 'llonebot_set_config_confirmed'
|
||||
export const CHANNEL_LOG = 'llonebot_log'
|
||||
export const CHANNEL_ERROR = 'llonebot_error'
|
||||
export const CHANNEL_UPDATE = 'llonebot_update'
|
||||
|
@@ -34,7 +34,8 @@ export class ConfigUtil {
|
||||
enableWsReverse: false,
|
||||
messagePostFormat: 'array',
|
||||
enableHttpHeart: false,
|
||||
listenLocalhost: false
|
||||
listenLocalhost: false,
|
||||
reportSelfMessage: false
|
||||
}
|
||||
const satoriDefault: SatoriConfig = {
|
||||
enable: true,
|
||||
@@ -43,14 +44,14 @@ export class ConfigUtil {
|
||||
token: ''
|
||||
}
|
||||
const defaultConfig: Config = {
|
||||
enableLLOB: true,
|
||||
satori: satoriDefault,
|
||||
ob11: ob11Default,
|
||||
heartInterval: 60000,
|
||||
token: '',
|
||||
enableLocalFile2Url: false,
|
||||
debug: false,
|
||||
log: false,
|
||||
reportSelfMessage: false,
|
||||
log: true,
|
||||
autoDeleteFile: false,
|
||||
autoDeleteFileSecond: 60,
|
||||
musicSignUrl: '',
|
||||
@@ -73,6 +74,7 @@ export class ConfigUtil {
|
||||
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
|
||||
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
|
||||
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
|
||||
this.checkOldConfig(jsonData.ob11, jsonData, 'reportSelfMessage', 'reportSelfMessage')
|
||||
this.config = jsonData
|
||||
return this.config
|
||||
}
|
||||
@@ -86,8 +88,8 @@ export class ConfigUtil {
|
||||
private checkOldConfig(
|
||||
currentConfig: OB11Config,
|
||||
oldConfig: Config,
|
||||
currentKey: 'httpPort' | 'httpHosts' | 'wsPort',
|
||||
oldKey: 'http' | 'hosts' | 'wsPort',
|
||||
currentKey: 'httpPort' | 'httpHosts' | 'wsPort' | 'reportSelfMessage',
|
||||
oldKey: 'http' | 'hosts' | 'wsPort' | 'reportSelfMessage',
|
||||
) {
|
||||
// 迁移旧的配置到新配置,避免用户重新填写配置
|
||||
const oldValue = oldConfig[oldKey]
|
||||
|
@@ -17,6 +17,7 @@ export interface OB11Config {
|
||||
*/
|
||||
enableQOAutoQuote?: boolean
|
||||
listenLocalhost: boolean
|
||||
reportSelfMessage: boolean
|
||||
}
|
||||
|
||||
export interface SatoriConfig {
|
||||
@@ -27,13 +28,13 @@ export interface SatoriConfig {
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
enableLLOB: boolean
|
||||
satori: SatoriConfig
|
||||
ob11: OB11Config
|
||||
token?: string
|
||||
heartInterval: number // ms
|
||||
enableLocalFile2Url?: boolean // 开启后,本地文件路径图片会转成http链接, 语音会转成base64
|
||||
debug?: boolean
|
||||
reportSelfMessage?: boolean
|
||||
log?: boolean
|
||||
autoDeleteFile?: boolean
|
||||
autoDeleteFileSecond?: number
|
||||
@@ -49,7 +50,7 @@ export interface Config {
|
||||
/** @deprecated */
|
||||
wsPort?: string
|
||||
/** @deprecated */
|
||||
enableLLOB?: boolean
|
||||
reportSelfMessage?: boolean
|
||||
}
|
||||
|
||||
export interface CheckVersion {
|
||||
|
@@ -53,49 +53,44 @@ function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?
|
||||
}
|
||||
|
||||
export async function encodeSilk(ctx: Context, filePath: string) {
|
||||
try {
|
||||
const file = await fsPromise.readFile(filePath)
|
||||
if (!isSilk(file)) {
|
||||
ctx.logger.info(`语音文件${filePath}需要转换成silk`)
|
||||
let result: EncodeResult
|
||||
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
|
||||
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
|
||||
result = await encode(file, 0)
|
||||
} else {
|
||||
const input = await convert(ctx, filePath, {
|
||||
output: [
|
||||
'-ar 24000',
|
||||
'-ac 1',
|
||||
'-f s16le'
|
||||
]
|
||||
})
|
||||
result = await encode(input, 24000)
|
||||
}
|
||||
const pttPath = path.join(TEMP_DIR, randomUUID())
|
||||
await fsPromise.writeFile(pttPath, result.data)
|
||||
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
|
||||
return {
|
||||
converted: true,
|
||||
path: pttPath,
|
||||
duration: result.duration / 1000,
|
||||
}
|
||||
const file = await fsPromise.readFile(filePath)
|
||||
if (!isSilk(file)) {
|
||||
ctx.logger.info(`语音文件${filePath}需要转换成silk`)
|
||||
let result: EncodeResult
|
||||
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
|
||||
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
|
||||
result = await encode(file, 0)
|
||||
} else {
|
||||
const silk = file
|
||||
let duration = 1
|
||||
try {
|
||||
duration = getDuration(silk) / 1000
|
||||
} catch (e) {
|
||||
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
|
||||
}
|
||||
return {
|
||||
converted: false,
|
||||
path: filePath,
|
||||
duration,
|
||||
}
|
||||
const input = await convert(ctx, filePath, {
|
||||
output: [
|
||||
'-ar 24000',
|
||||
'-ac 1',
|
||||
'-f s16le'
|
||||
]
|
||||
})
|
||||
result = await encode(input, 24000)
|
||||
}
|
||||
const pttPath = path.join(TEMP_DIR, randomUUID())
|
||||
await fsPromise.writeFile(pttPath, result.data)
|
||||
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
|
||||
return {
|
||||
converted: true,
|
||||
path: pttPath,
|
||||
duration: result.duration / 1000,
|
||||
}
|
||||
} else {
|
||||
const silk = file
|
||||
let duration = 1
|
||||
try {
|
||||
duration = getDuration(silk) / 1000
|
||||
} catch (e) {
|
||||
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
|
||||
}
|
||||
return {
|
||||
converted: false,
|
||||
path: filePath,
|
||||
duration,
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.logger.error('convert silk failed', (err as Error).stack)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
154
src/main/main.ts
154
src/main/main.ts
@@ -15,10 +15,9 @@ import {
|
||||
CHANNEL_LOG,
|
||||
CHANNEL_SELECT_FILE,
|
||||
CHANNEL_SET_CONFIG,
|
||||
CHANNEL_UPDATE,
|
||||
CHANNEL_SET_CONFIG_CONFIRMED
|
||||
CHANNEL_UPDATE
|
||||
} from '../common/channels'
|
||||
import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
|
||||
import { startHook } from '../ntqqapi/hook'
|
||||
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
|
||||
import { getConfigUtil } from '../common/config'
|
||||
import { checkFfmpeg } from '../common/utils/video'
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
NTQQWebApi,
|
||||
NTQQWindowApi
|
||||
} from '../ntqqapi/api'
|
||||
import { mkdir } from 'node:fs/promises'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
|
||||
declare module 'cordis' {
|
||||
@@ -56,6 +54,29 @@ function onLoad() {
|
||||
mkdirSync(LOG_DIR)
|
||||
}
|
||||
|
||||
if (!existsSync(TEMP_DIR)) {
|
||||
mkdirSync(TEMP_DIR)
|
||||
}
|
||||
|
||||
const dbDir = path.join(DATA_DIR, 'database')
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir)
|
||||
}
|
||||
|
||||
const ctx = new Context()
|
||||
|
||||
ctx.plugin(NTQQFileApi)
|
||||
ctx.plugin(NTQQFileCacheApi)
|
||||
ctx.plugin(NTQQFriendApi)
|
||||
ctx.plugin(NTQQGroupApi)
|
||||
ctx.plugin(NTQQMsgApi)
|
||||
ctx.plugin(NTQQUserApi)
|
||||
ctx.plugin(NTQQWebApi)
|
||||
ctx.plugin(NTQQWindowApi)
|
||||
ctx.plugin(Database)
|
||||
|
||||
let started = false
|
||||
|
||||
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
|
||||
return checkNewVersion()
|
||||
})
|
||||
@@ -114,7 +135,9 @@ function onLoad() {
|
||||
if (!ask) {
|
||||
getConfigUtil().setConfig(config)
|
||||
log('配置已更新', config)
|
||||
checkFfmpeg(config.ffmpeg).then()
|
||||
if (started) {
|
||||
ctx.parallel('llob/config-updated', config)
|
||||
}
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
@@ -131,7 +154,9 @@ function onLoad() {
|
||||
if (result.response === 0) {
|
||||
getConfigUtil().setConfig(config)
|
||||
log('配置已更新', config)
|
||||
checkFfmpeg(config.ffmpeg).then()
|
||||
if (started) {
|
||||
ctx.parallel('llob/config-updated', config)
|
||||
}
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
@@ -146,63 +171,7 @@ function onLoad() {
|
||||
log(arg)
|
||||
})
|
||||
|
||||
async function start() {
|
||||
log('process pid', process.pid)
|
||||
const config = getConfigUtil().getConfig()
|
||||
if (!existsSync(TEMP_DIR)) {
|
||||
await mkdir(TEMP_DIR)
|
||||
}
|
||||
const dbDir = path.join(DATA_DIR, 'database')
|
||||
if (!existsSync(dbDir)) {
|
||||
await mkdir(dbDir)
|
||||
}
|
||||
const ctx = new Context()
|
||||
ctx.plugin(Log, {
|
||||
enable: config.log!,
|
||||
filename: logFileName
|
||||
})
|
||||
ctx.plugin(NTQQFileApi)
|
||||
ctx.plugin(NTQQFileCacheApi)
|
||||
ctx.plugin(NTQQFriendApi)
|
||||
ctx.plugin(NTQQGroupApi)
|
||||
ctx.plugin(NTQQMsgApi)
|
||||
ctx.plugin(NTQQUserApi)
|
||||
ctx.plugin(NTQQWebApi)
|
||||
ctx.plugin(NTQQWindowApi)
|
||||
ctx.plugin(Core, config)
|
||||
ctx.plugin(Database)
|
||||
ctx.plugin(SQLiteDriver, {
|
||||
path: path.join(dbDir, `${selfInfo.uin}.db`)
|
||||
})
|
||||
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()
|
||||
llonebotError.otherError = ''
|
||||
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
|
||||
ctx.parallel('llob/config-updated', config)
|
||||
})
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
const intervalId = setInterval(async () => {
|
||||
const self = Object.assign(selfInfo, {
|
||||
uin: globalThis.authData?.uin,
|
||||
uid: globalThis.authData?.uid,
|
||||
@@ -210,26 +179,57 @@ function onLoad() {
|
||||
})
|
||||
if (self.uin) {
|
||||
clearInterval(intervalId)
|
||||
start()
|
||||
log('process pid', process.pid)
|
||||
|
||||
const config = getConfigUtil().getConfig()
|
||||
|
||||
if (config.enableLLOB && (config.satori.enable || config.ob11.enable)) {
|
||||
startHook()
|
||||
await ctx.sleep(300)
|
||||
} else {
|
||||
llonebotError.otherError = 'LLOneBot 未启动'
|
||||
log('LLOneBot 开关设置为关闭,不启动 LLOneBot')
|
||||
return
|
||||
}
|
||||
|
||||
ctx.plugin(Log, {
|
||||
enable: config.log!,
|
||||
filename: logFileName
|
||||
})
|
||||
ctx.plugin(SQLiteDriver, {
|
||||
path: path.join(dbDir, `${selfInfo.uin}.db`)
|
||||
})
|
||||
ctx.plugin(Store, {
|
||||
msgCacheExpire: config.msgCacheExpire! * 1000
|
||||
})
|
||||
ctx.plugin(Core, config)
|
||||
if (config.ob11.enable) {
|
||||
ctx.plugin(OneBot11Adapter, {
|
||||
...config.ob11,
|
||||
heartInterval: config.heartInterval,
|
||||
token: config.token!,
|
||||
debug: config.debug!,
|
||||
musicSignUrl: config.musicSignUrl,
|
||||
enableLocalFile2Url: config.enableLocalFile2Url!,
|
||||
ffmpeg: config.ffmpeg,
|
||||
})
|
||||
}
|
||||
if (config.satori.enable) {
|
||||
ctx.plugin(SatoriAdapter, {
|
||||
...config.satori,
|
||||
ffmpeg: config.ffmpeg,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.start()
|
||||
started = true
|
||||
llonebotError.otherError = ''
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
|
||||
// 创建窗口时触发
|
||||
function onBrowserWindowCreated(window: BrowserWindow) {
|
||||
if (![2, 4, 6].includes(window.id)) {
|
||||
return
|
||||
}
|
||||
if (window.id === 2) {
|
||||
mainWindow = window
|
||||
}
|
||||
//log('window create', window.webContents.getURL().toString())
|
||||
try {
|
||||
hookNTQQApiCall(window, window.id !== 2)
|
||||
hookNTQQApiReceive(window, window.id !== 2)
|
||||
} catch (e) {
|
||||
log('LLOneBot hook error: ', String(e))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
@@ -62,9 +62,10 @@ class Store extends Service {
|
||||
}
|
||||
|
||||
createMsgShortId(peer: Peer, msgId: string): number {
|
||||
// OneBot 11 要求 message_id 为 int32
|
||||
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
|
||||
const hash = createHash('md5').update(cacheKey).digest()
|
||||
hash[0] &= 0x7f //设置第一个bit为0 保证shortId为正数
|
||||
hash[0] &= 0x7f //保证shortId为正数
|
||||
const shortId = hash.readInt32BE()
|
||||
this.cache.set(cacheKey, shortId)
|
||||
this.ctx.database.upsert('message', [{
|
||||
|
@@ -16,7 +16,7 @@ import path from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { ReceiveCmdS } from '../hook'
|
||||
import { RkeyManager } from '@/ntqqapi/helper/rkey'
|
||||
import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
|
||||
import { RichMediaDownloadCompleteNotify, Peer } from '@/ntqqapi/types/msg'
|
||||
import { calculateFileMD5 } from '@/common/utils/file'
|
||||
import { copyFile, stat, unlink } from 'node:fs/promises'
|
||||
import { Time } from 'cosmokit'
|
||||
@@ -113,7 +113,7 @@ export class NTQQFileApi extends Service {
|
||||
return sourcePath
|
||||
}
|
||||
}
|
||||
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
|
||||
const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
|
||||
'nodeIKernelMsgService/downloadRichMedia',
|
||||
[{
|
||||
getReq: {
|
||||
@@ -185,7 +185,7 @@ export class NTQQFileApi extends Service {
|
||||
}
|
||||
|
||||
async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) {
|
||||
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
|
||||
const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
|
||||
'nodeIKernelRichMediaService/downloadFileForModelId',
|
||||
[{
|
||||
peer,
|
||||
@@ -201,6 +201,16 @@ export class NTQQFileApi extends Service {
|
||||
)
|
||||
return data.notifyInfo.filePath
|
||||
}
|
||||
|
||||
async ocrImage(path: string) {
|
||||
return await invoke(
|
||||
'nodeIKernelNodeMiscService/wantWinScreenOCR',
|
||||
[
|
||||
{ url: path },
|
||||
{ timeout: 5000 }
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class NTQQFileCacheApi extends Service {
|
||||
|
@@ -16,7 +16,7 @@ export class NTQQFriendApi extends Service {
|
||||
|
||||
/** 大于或等于 26702 应使用 getBuddyV2 */
|
||||
async getFriends() {
|
||||
const data = await invoke<{
|
||||
const res = await invoke<{
|
||||
data: {
|
||||
categoryId: number
|
||||
categroyName: string
|
||||
@@ -28,11 +28,7 @@ export class NTQQFriendApi extends Service {
|
||||
cbCmd: ReceiveCmdS.FRIENDS,
|
||||
afterFirstCmd: false
|
||||
})
|
||||
const _friends: Friend[] = []
|
||||
for (const item of data.data) {
|
||||
_friends.push(...item.buddyList)
|
||||
}
|
||||
return _friends
|
||||
return res.data.flatMap(e => e.buddyList)
|
||||
}
|
||||
|
||||
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
|
||||
@@ -62,7 +58,7 @@ export class NTQQFriendApi extends Service {
|
||||
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
|
||||
}
|
||||
|
||||
/** uid => uin */
|
||||
/** uid -> uin */
|
||||
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
|
||||
const retMap: Map<string, string> = new Map()
|
||||
const data = await invoke<{
|
||||
@@ -116,4 +112,14 @@ export class NTQQFriendApi extends Service {
|
||||
remarkParams: { uid, remark }
|
||||
}])
|
||||
}
|
||||
|
||||
async delBuddy(friendUid: string) {
|
||||
return await invoke('nodeIKernelBuddyService/delBuddy', [{
|
||||
delInfo: {
|
||||
friendUid,
|
||||
tempBlock: false,
|
||||
tempBothDel: true
|
||||
}
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
@@ -45,8 +45,11 @@ export class NTQQGroupApi extends Service {
|
||||
return result.groupList
|
||||
}
|
||||
|
||||
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
|
||||
async getGroupMembers(groupCode: string, num = 3000) {
|
||||
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
|
||||
groupCode,
|
||||
scene: 'groupMemberList_MainWindow'
|
||||
}])
|
||||
const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }])
|
||||
if (data.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + data.errMsg)
|
||||
@@ -55,7 +58,7 @@ export class NTQQGroupApi extends Service {
|
||||
}
|
||||
|
||||
async getGroupMember(groupCode: string, uid: string, forceUpdate = false) {
|
||||
invoke('nodeIKernelGroupListener/onMemberInfoChange', [], {
|
||||
await invoke('nodeIKernelGroupListener/onMemberInfoChange', [], {
|
||||
registerEvent: true
|
||||
})
|
||||
|
||||
@@ -296,4 +299,44 @@ export class NTQQGroupApi extends Service {
|
||||
async setGroupAvatar(groupCode: string, path: string) {
|
||||
return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }])
|
||||
}
|
||||
|
||||
async searchMember(groupCode: string, keyword: string) {
|
||||
await invoke('nodeIKernelGroupListener/onSearchMemberChange', [], {
|
||||
registerEvent: true
|
||||
})
|
||||
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
|
||||
groupCode,
|
||||
scene: 'groupMemberList_MainWindow'
|
||||
}])
|
||||
const data = await invoke<{
|
||||
sceneId: string
|
||||
keyword: string
|
||||
infos: Map<string, GroupMember>
|
||||
}>(
|
||||
'nodeIKernelGroupService/searchMember',
|
||||
[{ sceneId, keyword }],
|
||||
{
|
||||
cbCmd: 'nodeIKernelGroupListener/onSearchMemberChange',
|
||||
cmdCB: payload => {
|
||||
return payload.sceneId === sceneId && payload.keyword === keyword
|
||||
},
|
||||
afterFirstCmd: false
|
||||
}
|
||||
)
|
||||
return data.infos
|
||||
}
|
||||
|
||||
async getGroupFileCount(groupId: string) {
|
||||
return await invoke(
|
||||
'nodeIKernelRichMediaService/batchGetGroupFileCount',
|
||||
[{ groupIds: [groupId] }]
|
||||
)
|
||||
}
|
||||
|
||||
async getGroupFileSpace(groupId: string) {
|
||||
return await invoke(
|
||||
'nodeIKernelRichMediaService/getGroupSpace',
|
||||
[{ groupId }]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ export class NTQQMsgApi extends Service {
|
||||
}
|
||||
|
||||
async activateChatAndGetHistory(peer: Peer, cnt: number) {
|
||||
// 消息从旧到新
|
||||
return await invoke(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt, msgId: '0', queryOrder: true }])
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ export class NTQQMsgApi extends Service {
|
||||
}
|
||||
|
||||
async getMsgHistory(peer: Peer, msgId: string, cnt: number, queryOrder = false) {
|
||||
// 默认情况下消息时间从旧到新
|
||||
// 默认情况下消息时间从新到旧
|
||||
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder }])
|
||||
}
|
||||
|
||||
@@ -61,21 +62,35 @@ export class NTQQMsgApi extends Service {
|
||||
|
||||
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
|
||||
const uniqueId = await this.generateMsgUniqueId(peer.chatType)
|
||||
peer.guildId = uniqueId
|
||||
const msgAttributeInfos = new Map()
|
||||
msgAttributeInfos.set(0, {
|
||||
attrType: 0,
|
||||
attrId: uniqueId,
|
||||
vasMsgInfo: {
|
||||
msgNamePlateInfo: {},
|
||||
bubbleInfo: {},
|
||||
avatarPendantInfo: {},
|
||||
vasFont: {},
|
||||
iceBreakInfo: {}
|
||||
}
|
||||
})
|
||||
|
||||
let sentMsgId: string
|
||||
const data = await invoke<{ msgList: RawMessage[] }>(
|
||||
'nodeIKernelMsgService/sendMsg',
|
||||
[{
|
||||
msgId: '0',
|
||||
peer,
|
||||
msgElements,
|
||||
msgAttributeInfos: new Map()
|
||||
msgAttributeInfos
|
||||
}],
|
||||
{
|
||||
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
|
||||
afterFirstCmd: false,
|
||||
cmdCB: payload => {
|
||||
for (const msgRecord of payload.msgList) {
|
||||
if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
|
||||
if (msgRecord.msgAttrs.get(0)?.attrId === uniqueId && msgRecord.sendStatus === 2) {
|
||||
sentMsgId = msgRecord.msgId
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -84,15 +99,15 @@ export class NTQQMsgApi extends Service {
|
||||
timeout
|
||||
}
|
||||
)
|
||||
delete peer.guildId
|
||||
return data.msgList.find(msgRecord => msgRecord.guildId === uniqueId)
|
||||
|
||||
return data.msgList.find(msgRecord => msgRecord.msgId === sentMsgId)
|
||||
}
|
||||
|
||||
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
|
||||
const uniqueId = await this.generateMsgUniqueId(destPeer.chatType)
|
||||
destPeer.guildId = uniqueId
|
||||
const data = await invoke<{ msgList: RawMessage[] }>(
|
||||
'nodeIKernelMsgService/forwardMsg',
|
||||
'nodeIKernelMsgService/forwardMsgWithComment',
|
||||
[{
|
||||
msgIds,
|
||||
srcContact: srcPeer,
|
||||
@@ -110,7 +125,8 @@ export class NTQQMsgApi extends Service {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
timeout: 3000
|
||||
}
|
||||
)
|
||||
delete destPeer.guildId
|
||||
@@ -262,4 +278,8 @@ export class NTQQMsgApi extends Service {
|
||||
async getServerTime() {
|
||||
return await invoke('nodeIKernelMSFService/getServerTime', [])
|
||||
}
|
||||
|
||||
async fetchUnitedCommendConfig(groups: string[]) {
|
||||
return await invoke('nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', [{ groups }])
|
||||
}
|
||||
}
|
||||
|
@@ -108,18 +108,23 @@ export class NTQQUserApi extends Service {
|
||||
async getUidByUinV1(uin: string, groupCode?: string) {
|
||||
let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
|
||||
if (!uid) {
|
||||
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
|
||||
if (unveifyUid.indexOf('*') === -1) {
|
||||
uid = unveifyUid
|
||||
}
|
||||
}
|
||||
if (!uid) {
|
||||
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
|
||||
const friends = await this.ctx.ntFriendApi.getFriends()
|
||||
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
|
||||
let member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
|
||||
if (member.size === 0) {
|
||||
await this.ctx.ntGroupApi.getGroupMembers(groupCode, 1)
|
||||
await this.ctx.sleep(30)
|
||||
member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
|
||||
}
|
||||
uid = Array.from(member.values()).find(e => e.uin === uin)?.uid
|
||||
}
|
||||
if (!uid) {
|
||||
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid
|
||||
if (!unveifyUid.includes('*')) {
|
||||
uid = unveifyUid
|
||||
}
|
||||
}
|
||||
return uid
|
||||
}
|
||||
@@ -168,12 +173,12 @@ export class NTQQUserApi extends Service {
|
||||
|
||||
async getUinByUidV2(uid: string) {
|
||||
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uidList: [uid] }])).uins.get(uid)
|
||||
if (uin) return uin
|
||||
if (uin && uin !== '0') 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
|
||||
uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
|
||||
uin = (await this.ctx.ntFriendApi.getBuddyIdMap()).get(uid)
|
||||
if (uin) return uin
|
||||
uin = (await this.getUserDetailInfo(uid)).uin
|
||||
return uin
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
import { selfInfo, llonebotError } from '../common/globalVars'
|
||||
import { version } from '../version'
|
||||
import { invoke } from './ntcall'
|
||||
import { Native } from './native/index'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
@@ -38,17 +39,14 @@ declare module 'cordis' {
|
||||
class Core extends Service {
|
||||
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store']
|
||||
public startTime = 0
|
||||
public native
|
||||
|
||||
constructor(protected ctx: Context, public config: Core.Config) {
|
||||
super(ctx, 'app', true)
|
||||
this.native = new Native(ctx)
|
||||
}
|
||||
|
||||
public start() {
|
||||
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.ctx.logger.info(`LLOneBot/${version}`)
|
||||
@@ -124,10 +122,15 @@ class Core extends Service {
|
||||
activatedPeerUids.push(contact.id)
|
||||
const peer = { peerUid: contact.id, chatType: contact.chatType }
|
||||
if (contact.chatType === ChatType.TempC2CFromGroup) {
|
||||
this.ctx.ntMsgApi.activateChatAndGetHistory(peer, 1).then(res => {
|
||||
const lastTempMsg = res.msgList[0]
|
||||
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) {
|
||||
this.ctx.parallel('nt/message-created', lastTempMsg!)
|
||||
this.ctx.ntMsgApi.activateChatAndGetHistory(peer, 2).then(res => {
|
||||
for (const msg of res.msgList) {
|
||||
if (Date.now() / 1000 - Number(msg.msgTime) > 3) {
|
||||
continue
|
||||
}
|
||||
if (msg.senderUin && msg.senderUin !== '0') {
|
||||
this.ctx.store.addMsgCache(msg)
|
||||
}
|
||||
this.ctx.parallel('nt/message-created', msg)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -165,9 +168,10 @@ class Core extends Service {
|
||||
})
|
||||
|
||||
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
|
||||
const startTime = this.startTime / 1000
|
||||
for (const message of payload.msgList) {
|
||||
// 过滤启动之前的消息
|
||||
if (parseInt(message.msgTime) < this.startTime / 1000) {
|
||||
if (parseInt(message.msgTime) < startTime) {
|
||||
continue
|
||||
}
|
||||
if (message.senderUin && message.senderUin !== '0') {
|
||||
@@ -194,19 +198,18 @@ class Core extends Service {
|
||||
this.ctx.parallel('nt/message-deleted', msg)
|
||||
} else if (sentMsgIds.get(msg.msgId)) {
|
||||
sentMsgIds.delete(msg.msgId)
|
||||
this.ctx.parallel('nt/message-sent', msg)
|
||||
if (msg.sendStatus === 2) {
|
||||
this.ctx.parallel('nt/message-sent', msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
|
||||
if (!this.config.reportSelfMessage) {
|
||||
return
|
||||
}
|
||||
sentMsgIds.set(payload.msgRecord.msgId, true)
|
||||
})
|
||||
|
||||
const groupNotifyFlags: string[] = []
|
||||
const groupNotifyIgnore: string[] = []
|
||||
registerReceiveHook<{
|
||||
doubt: boolean
|
||||
oldestUnreadSeq: string
|
||||
@@ -220,13 +223,11 @@ class Core extends Service {
|
||||
return
|
||||
}
|
||||
for (const notify of notifies) {
|
||||
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
|
||||
const notifyTime = parseInt(notify.seq) / 1000
|
||||
if (groupNotifyFlags.includes(flag) || notifyTime < this.startTime) {
|
||||
const notifyTime = Math.trunc(+notify.seq / 1000)
|
||||
if (groupNotifyIgnore.includes(notify.seq) || notifyTime < this.startTime) {
|
||||
continue
|
||||
}
|
||||
groupNotifyFlags.shift()
|
||||
groupNotifyFlags.push(flag)
|
||||
groupNotifyIgnore.push(notify.seq)
|
||||
this.ctx.parallel('nt/group-notify', notify)
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import {
|
||||
SendTextElement,
|
||||
SendVideoElement,
|
||||
} from './types'
|
||||
import { stat, writeFile, copyFile, unlink } from 'node:fs/promises'
|
||||
import { stat, writeFile, copyFile, unlink, access } from 'node:fs/promises'
|
||||
import { calculateFileMD5 } from '../common/utils/file'
|
||||
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
|
||||
import { encodeSilk } from '../common/utils/audio'
|
||||
@@ -115,25 +115,17 @@ export namespace SendElement {
|
||||
}
|
||||
|
||||
export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
|
||||
try {
|
||||
await stat(filePath)
|
||||
} catch (e) {
|
||||
throw `文件${filePath}异常,不存在`
|
||||
}
|
||||
ctx.logger.info('复制视频到QQ目录', filePath)
|
||||
await access(filePath)
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video)
|
||||
|
||||
ctx.logger.info('复制视频到QQ目录完成', path)
|
||||
if (fileSize === 0) {
|
||||
throw '文件异常,大小为0'
|
||||
throw new Error('文件异常,大小为 0')
|
||||
}
|
||||
const maxMB = 100;
|
||||
const maxMB = 100
|
||||
if (fileSize > 1024 * 1024 * maxMB) {
|
||||
throw `视频过大,最大支持${maxMB}MB,当前文件大小${fileSize}B`
|
||||
throw new Error(`视频过大,最大支持${maxMB}MB,当前文件大小${fileSize}B`)
|
||||
}
|
||||
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
|
||||
thumbDir = pathLib.dirname(thumbDir)
|
||||
// log("thumb 目录", thumb)
|
||||
const thumbDir = pathLib.dirname(path.replaceAll('\\', '/').replace(`/Ori/`, `/Thumb/`))
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
@@ -194,7 +186,6 @@ export namespace SendElement {
|
||||
const _thumbPath = await createThumb
|
||||
ctx.logger.info('生成视频缩略图', _thumbPath)
|
||||
const thumbSize = (await stat(_thumbPath)).size
|
||||
// log("生成缩略图", _thumbPath)
|
||||
thumbPath.set(0, _thumbPath)
|
||||
const thumbMd5 = await calculateFileMD5(_thumbPath)
|
||||
const element: SendVideoElement = {
|
||||
@@ -219,13 +210,9 @@ export namespace SendElement {
|
||||
|
||||
export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
|
||||
if (!silkPath) {
|
||||
throw '语音转换失败, 请检查语音文件是否正常'
|
||||
}
|
||||
// log("生成语音", silkPath, duration);
|
||||
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt)
|
||||
if (fileSize === 0) {
|
||||
throw '文件异常,大小为0'
|
||||
throw new Error('文件异常,大小为 0')
|
||||
}
|
||||
if (converted) {
|
||||
unlink(silkPath)
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import { NTClass, NTMethod } from './ntcall'
|
||||
import { NTMethod } from './ntcall'
|
||||
import { log } from '@/common/utils'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { Dict } from 'cosmokit'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
export const hookApiCallbacks: Record<string, (res: any) => void> = {}
|
||||
|
||||
@@ -28,118 +27,76 @@ export enum ReceiveCmdS {
|
||||
MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete',
|
||||
}
|
||||
|
||||
type NTReturnData = [
|
||||
{
|
||||
type: 'request'
|
||||
eventName: NTClass
|
||||
callbackId?: string
|
||||
},
|
||||
{
|
||||
cmdName: ReceiveCmdS
|
||||
cmdType: 'event'
|
||||
payload: unknown
|
||||
}[]
|
||||
]
|
||||
|
||||
const logHook = false
|
||||
|
||||
const receiveHooks: Array<{
|
||||
const receiveHooks: Map<string, {
|
||||
method: ReceiveCmdS[]
|
||||
hookFunc: (payload: any) => void | Promise<void>
|
||||
id: string
|
||||
}> = []
|
||||
}> = new Map()
|
||||
|
||||
const callHooks: Array<{
|
||||
method: NTMethod[]
|
||||
hookFunc: (callParams: unknown[]) => void | Promise<void>
|
||||
}> = []
|
||||
|
||||
export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
|
||||
window.webContents.send = new Proxy(window.webContents.send, {
|
||||
apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
|
||||
try {
|
||||
if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) {
|
||||
log('received ntqq api message', args)
|
||||
}
|
||||
} catch { }
|
||||
if (!onlyLog) {
|
||||
if (args[2] instanceof Array) {
|
||||
for (const receiveData of args[2]) {
|
||||
const ntMethodName = receiveData.cmdName
|
||||
for (const hook of receiveHooks) {
|
||||
if (hook.method.includes(ntMethodName)) {
|
||||
Promise.resolve(hook.hookFunc(receiveData.payload))
|
||||
export function startHook() {
|
||||
const senderExclude = Symbol()
|
||||
|
||||
ipcMain.emit = new Proxy(ipcMain.emit, {
|
||||
apply(target, thisArg, args: [eventName: string, ...args: any]) {
|
||||
if (args[2]?.eventName.startsWith('ns-LoggerApi')) {
|
||||
return target.apply(thisArg, args)
|
||||
}
|
||||
if (logHook) {
|
||||
log('request', args)
|
||||
}
|
||||
|
||||
const event = args[1]
|
||||
if (event.sender && !event.sender[senderExclude]) {
|
||||
event.sender[senderExclude] = true
|
||||
event.sender.send = new Proxy(event.sender.send, {
|
||||
apply(target, thisArg, args: any[]) {
|
||||
if (args[1].eventName?.startsWith('ns-LoggerApi')) {
|
||||
return target.apply(thisArg, args)
|
||||
}
|
||||
if (logHook) {
|
||||
log('received', args)
|
||||
}
|
||||
|
||||
const callbackId = args[1].callbackId
|
||||
if (callbackId) {
|
||||
if (hookApiCallbacks[callbackId]) {
|
||||
Promise.resolve(hookApiCallbacks[callbackId](args[2]))
|
||||
delete hookApiCallbacks[callbackId]
|
||||
}
|
||||
} else if (args[2]) {
|
||||
for (const receiveData of args[2]) {
|
||||
for (const hook of receiveHooks.values()) {
|
||||
if (hook.method.includes(receiveData.cmdName)) {
|
||||
Promise.resolve(hook.hookFunc(receiveData.payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return target.apply(thisArg, args)
|
||||
}
|
||||
}
|
||||
if (args[1]?.callbackId) {
|
||||
const callbackId = args[1].callbackId
|
||||
if (hookApiCallbacks[callbackId]) {
|
||||
Promise.resolve(hookApiCallbacks[callbackId](args[2]))
|
||||
delete hookApiCallbacks[callbackId]
|
||||
})
|
||||
}
|
||||
|
||||
if (args[3]?.length) {
|
||||
const method = args[3][0]
|
||||
const callParams = args[3].slice(1)
|
||||
for (const hook of callHooks) {
|
||||
if (hook.method.includes(method)) {
|
||||
Promise.resolve(hook.hookFunc(callParams))
|
||||
}
|
||||
}
|
||||
}
|
||||
return target.apply(thisArg, args)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
|
||||
const webContents = window.webContents as Dict
|
||||
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
|
||||
|
||||
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
|
||||
apply(target, thisArg, args) {
|
||||
const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
|
||||
if (!isLogger) {
|
||||
try {
|
||||
logHook && log('call NTQQ api', args)
|
||||
} catch (e) { }
|
||||
if (!onlyLog) {
|
||||
try {
|
||||
const _args: unknown[] = args[3][1]
|
||||
const cmdName = _args[0] as NTMethod
|
||||
const callParams = _args.slice(1)
|
||||
callHooks.forEach((hook) => {
|
||||
if (hook.method.includes(cmdName)) {
|
||||
Promise.resolve(hook.hookFunc(callParams))
|
||||
}
|
||||
})
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
return target.apply(thisArg, args)
|
||||
},
|
||||
})
|
||||
if (webContents._events['-ipc-message']?.[0]) {
|
||||
webContents._events['-ipc-message'][0] = proxyIpcMsg
|
||||
} else {
|
||||
webContents._events['-ipc-message'] = proxyIpcMsg
|
||||
}
|
||||
|
||||
/*const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
|
||||
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
|
||||
apply(target, thisArg, args) {
|
||||
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
|
||||
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
|
||||
apply(sendtarget, sendthisArg, sendargs) {
|
||||
sendtarget.apply(sendthisArg, sendargs)
|
||||
},
|
||||
})
|
||||
const ret = target.apply(thisArg, args)
|
||||
//HOOK_LOG && log('call NTQQ invoke api return', ret)
|
||||
return ret
|
||||
},
|
||||
})
|
||||
if (webContents._events['-ipc-invoke']?.[0]) {
|
||||
webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
|
||||
} else {
|
||||
webContents._events['-ipc-invoke'] = proxyIpcInvoke
|
||||
}*/
|
||||
}
|
||||
|
||||
export function registerReceiveHook<PayloadType>(
|
||||
method: string | string[],
|
||||
hookFunc: (payload: PayloadType) => void,
|
||||
@@ -148,10 +105,9 @@ export function registerReceiveHook<PayloadType>(
|
||||
if (!Array.isArray(method)) {
|
||||
method = [method]
|
||||
}
|
||||
receiveHooks.push({
|
||||
receiveHooks.set(id, {
|
||||
method: method as ReceiveCmdS[],
|
||||
hookFunc,
|
||||
id,
|
||||
})
|
||||
return id
|
||||
}
|
||||
@@ -170,6 +126,5 @@ export function registerCallHook(
|
||||
}
|
||||
|
||||
export function removeReceiveHook(id: string) {
|
||||
const index = receiveHooks.findIndex((h) => h.id === id)
|
||||
receiveHooks.splice(index, 1)
|
||||
receiveHooks.delete(id)
|
||||
}
|
||||
|
BIN
src/ntqqapi/native/external/crychic-win32-x64.node
vendored
Normal file
BIN
src/ntqqapi/native/external/crychic-win32-x64.node
vendored
Normal file
Binary file not shown.
65
src/ntqqapi/native/index.ts
Normal file
65
src/ntqqapi/native/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Context } from 'cordis'
|
||||
import { Dict } from 'cosmokit'
|
||||
import { getBuildVersion } from '@/common/utils/misc'
|
||||
import { TEMP_DIR } from '@/common/globalVars'
|
||||
import { copyFile } from 'fs/promises'
|
||||
import path from 'node:path'
|
||||
import addon from './external/crychic-win32-x64.node?asset'
|
||||
|
||||
export class Native {
|
||||
private crychic?: Dict
|
||||
|
||||
constructor(private ctx: Context) {
|
||||
ctx.on('ready', () => {
|
||||
this.start()
|
||||
})
|
||||
}
|
||||
|
||||
checkPlatform() {
|
||||
return process.platform === 'win32' && process.arch === 'x64'
|
||||
}
|
||||
|
||||
checkVersion() {
|
||||
const version = getBuildVersion()
|
||||
// 27333—27597
|
||||
return version >= 27333 && version < 28060
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.crychic) {
|
||||
return
|
||||
}
|
||||
if (!this.checkPlatform()) {
|
||||
return
|
||||
}
|
||||
if (!this.checkVersion()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const fileName = path.basename(addon)
|
||||
const dest = path.join(TEMP_DIR, fileName)
|
||||
try {
|
||||
await copyFile(addon, dest)
|
||||
} catch (e) {
|
||||
// resource busy or locked?
|
||||
this.ctx.logger.warn(e)
|
||||
}
|
||||
this.crychic = require(dest)
|
||||
this.crychic!.init()
|
||||
} catch (e) {
|
||||
this.ctx.logger.warn('crychic 加载失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async sendFriendPoke(uin: number) {
|
||||
if (!this.crychic) return
|
||||
this.crychic.sendFriendPoke(uin)
|
||||
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
|
||||
}
|
||||
|
||||
async sendGroupPoke(groupCode: number, memberUin: number) {
|
||||
if (!this.crychic) return
|
||||
this.crychic.sendGroupPoke(memberUin, groupCode)
|
||||
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
|
||||
}
|
||||
}
|
@@ -14,7 +14,8 @@ import {
|
||||
NodeIKernelRichMediaService,
|
||||
NodeIKernelTicketService,
|
||||
NodeIKernelTipOffService,
|
||||
NodeIKernelRobotService
|
||||
NodeIKernelRobotService,
|
||||
NodeIKernelNodeMiscService
|
||||
} from './services'
|
||||
|
||||
export enum NTClass {
|
||||
@@ -94,6 +95,7 @@ interface NTService {
|
||||
nodeIKernelTicketService: NodeIKernelTicketService
|
||||
nodeIKernelTipOffService: NodeIKernelTipOffService
|
||||
nodeIKernelRobotService: NodeIKernelRobotService
|
||||
nodeIKernelNodeMiscService: NodeIKernelNodeMiscService
|
||||
}
|
||||
|
||||
interface InvokeOptions<ReturnType> {
|
||||
@@ -122,9 +124,14 @@ export function invoke<
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
const apiArgs = [method, ...args]
|
||||
const callbackId = randomUUID()
|
||||
let eventId: string
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
|
||||
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
|
||||
if (eventId) {
|
||||
removeReceiveHook(eventId)
|
||||
}
|
||||
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, args)
|
||||
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${JSON.stringify(args)}`)
|
||||
}, timeout)
|
||||
|
||||
if (!options.cbCmd) {
|
||||
@@ -138,19 +145,15 @@ export function invoke<
|
||||
let result: unknown
|
||||
// 这里的callback比较特殊,QQ后端先返回是否调用成功,再返回一条结果数据
|
||||
const secondCallback = () => {
|
||||
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
|
||||
eventId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
|
||||
if (options.cmdCB) {
|
||||
if (options.cmdCB(payload, result)) {
|
||||
removeReceiveHook(hookId)
|
||||
clearTimeout(timeoutId)
|
||||
resolve(payload)
|
||||
if (!options.cmdCB(payload, result)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
removeReceiveHook(hookId)
|
||||
clearTimeout(timeoutId)
|
||||
resolve(payload)
|
||||
}
|
||||
removeReceiveHook(eventId)
|
||||
clearTimeout(timeoutId)
|
||||
resolve(payload)
|
||||
})
|
||||
}
|
||||
!afterFirstCmd && secondCallback()
|
||||
@@ -160,9 +163,12 @@ export function invoke<
|
||||
afterFirstCmd && secondCallback()
|
||||
}
|
||||
else {
|
||||
log('ntqq api call failed,', method, args, res)
|
||||
clearTimeout(timeoutId)
|
||||
reject(`ntqq api call failed, ${method}, ${res?.errMsg}`)
|
||||
if (eventId) {
|
||||
removeReceiveHook(eventId)
|
||||
}
|
||||
log('ntqq api call failed,', method, args, res)
|
||||
reject(`ntqq api call failed, ${method}, ${JSON.stringify(res)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
84
src/ntqqapi/proto/compiled.d.ts
vendored
84
src/ntqqapi/proto/compiled.d.ts
vendored
@@ -64,11 +64,8 @@ export namespace SysMsg {
|
||||
/** Properties of a SystemMessageHeader. */
|
||||
interface ISystemMessageHeader {
|
||||
|
||||
/** SystemMessageHeader peerNumber */
|
||||
peerNumber?: (number|null);
|
||||
|
||||
/** SystemMessageHeader peerString */
|
||||
peerString?: (string|null);
|
||||
/** SystemMessageHeader peerUin */
|
||||
peerUin?: (number|null);
|
||||
|
||||
/** SystemMessageHeader uin */
|
||||
uin?: (number|null);
|
||||
@@ -86,11 +83,8 @@ export namespace SysMsg {
|
||||
*/
|
||||
constructor(properties?: SysMsg.ISystemMessageHeader);
|
||||
|
||||
/** SystemMessageHeader peerNumber. */
|
||||
public peerNumber: number;
|
||||
|
||||
/** SystemMessageHeader peerString. */
|
||||
public peerString: string;
|
||||
/** SystemMessageHeader peerUin. */
|
||||
public peerUin: number;
|
||||
|
||||
/** SystemMessageHeader uin. */
|
||||
public uin: number;
|
||||
@@ -160,10 +154,10 @@ export namespace SysMsg {
|
||||
public msgType: number;
|
||||
|
||||
/** SystemMessageMsgSpec subType. */
|
||||
public subType: number;
|
||||
public subType?: (number|null);
|
||||
|
||||
/** SystemMessageMsgSpec subSubType. */
|
||||
public subSubType: number;
|
||||
public subSubType?: (number|null);
|
||||
|
||||
/** SystemMessageMsgSpec msgSeq. */
|
||||
public msgSeq: number;
|
||||
@@ -172,7 +166,7 @@ export namespace SysMsg {
|
||||
public time: number;
|
||||
|
||||
/** SystemMessageMsgSpec other. */
|
||||
public other: number;
|
||||
public other?: (number|null);
|
||||
|
||||
/**
|
||||
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer.
|
||||
@@ -466,4 +460,68 @@ export namespace SysMsg {
|
||||
*/
|
||||
public static getTypeUrl(typeUrlPrefix?: string): string;
|
||||
}
|
||||
|
||||
/** Properties of a GroupMemberChange. */
|
||||
interface IGroupMemberChange {
|
||||
|
||||
/** GroupMemberChange groupCode */
|
||||
groupCode?: (number|null);
|
||||
|
||||
/** GroupMemberChange memberUid */
|
||||
memberUid?: (string|null);
|
||||
|
||||
/** GroupMemberChange type */
|
||||
type?: (number|null);
|
||||
|
||||
/** GroupMemberChange adminUid */
|
||||
adminUid?: (string|null);
|
||||
}
|
||||
|
||||
/** Represents a GroupMemberChange. */
|
||||
class GroupMemberChange implements IGroupMemberChange {
|
||||
|
||||
/**
|
||||
* Constructs a new GroupMemberChange.
|
||||
* @param [properties] Properties to set
|
||||
*/
|
||||
constructor(properties?: SysMsg.IGroupMemberChange);
|
||||
|
||||
/** GroupMemberChange groupCode. */
|
||||
public groupCode: number;
|
||||
|
||||
/** GroupMemberChange memberUid. */
|
||||
public memberUid: string;
|
||||
|
||||
/** GroupMemberChange type. */
|
||||
public type: number;
|
||||
|
||||
/** GroupMemberChange adminUid. */
|
||||
public adminUid: string;
|
||||
|
||||
/**
|
||||
* Decodes a GroupMemberChange message from the specified reader or buffer.
|
||||
* @param reader Reader or buffer to decode from
|
||||
* @param [length] Message length if known beforehand
|
||||
* @returns GroupMemberChange
|
||||
* @throws {Error} If the payload is not a reader or valid buffer
|
||||
* @throws {$protobuf.util.ProtocolError} If required fields are missing
|
||||
*/
|
||||
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.GroupMemberChange;
|
||||
|
||||
/**
|
||||
* Decodes a GroupMemberChange message from the specified reader or buffer, length delimited.
|
||||
* @param reader Reader or buffer to decode from
|
||||
* @returns GroupMemberChange
|
||||
* @throws {Error} If the payload is not a reader or valid buffer
|
||||
* @throws {$protobuf.util.ProtocolError} If required fields are missing
|
||||
*/
|
||||
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.GroupMemberChange;
|
||||
|
||||
/**
|
||||
* Gets the default type url for GroupMemberChange
|
||||
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
|
||||
* @returns The default type url
|
||||
*/
|
||||
public static getTypeUrl(typeUrlPrefix?: string): string;
|
||||
}
|
||||
}
|
||||
|
@@ -150,8 +150,7 @@ export const SysMsg = $root.SysMsg = (() => {
|
||||
* Properties of a SystemMessageHeader.
|
||||
* @memberof SysMsg
|
||||
* @interface ISystemMessageHeader
|
||||
* @property {number|null} [peerNumber] SystemMessageHeader peerNumber
|
||||
* @property {string|null} [peerString] SystemMessageHeader peerString
|
||||
* @property {number|null} [peerUin] SystemMessageHeader peerUin
|
||||
* @property {number|null} [uin] SystemMessageHeader uin
|
||||
* @property {string|null} [uid] SystemMessageHeader uid
|
||||
*/
|
||||
@@ -172,20 +171,12 @@ export const SysMsg = $root.SysMsg = (() => {
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemMessageHeader peerNumber.
|
||||
* @member {number} peerNumber
|
||||
* SystemMessageHeader peerUin.
|
||||
* @member {number} peerUin
|
||||
* @memberof SysMsg.SystemMessageHeader
|
||||
* @instance
|
||||
*/
|
||||
SystemMessageHeader.prototype.peerNumber = 0;
|
||||
|
||||
/**
|
||||
* SystemMessageHeader peerString.
|
||||
* @member {string} peerString
|
||||
* @memberof SysMsg.SystemMessageHeader
|
||||
* @instance
|
||||
*/
|
||||
SystemMessageHeader.prototype.peerString = "";
|
||||
SystemMessageHeader.prototype.peerUin = 0;
|
||||
|
||||
/**
|
||||
* SystemMessageHeader uin.
|
||||
@@ -231,11 +222,7 @@ export const SysMsg = $root.SysMsg = (() => {
|
||||
let tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
message.peerNumber = reader.uint32();
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
message.peerString = reader.string();
|
||||
message.peerUin = reader.uint32();
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
@@ -327,19 +314,19 @@ export const SysMsg = $root.SysMsg = (() => {
|
||||
|
||||
/**
|
||||
* SystemMessageMsgSpec subType.
|
||||
* @member {number} subType
|
||||
* @member {number|null|undefined} subType
|
||||
* @memberof SysMsg.SystemMessageMsgSpec
|
||||
* @instance
|
||||
*/
|
||||
SystemMessageMsgSpec.prototype.subType = 0;
|
||||
SystemMessageMsgSpec.prototype.subType = null;
|
||||
|
||||
/**
|
||||
* SystemMessageMsgSpec subSubType.
|
||||
* @member {number} subSubType
|
||||
* @member {number|null|undefined} subSubType
|
||||
* @memberof SysMsg.SystemMessageMsgSpec
|
||||
* @instance
|
||||
*/
|
||||
SystemMessageMsgSpec.prototype.subSubType = 0;
|
||||
SystemMessageMsgSpec.prototype.subSubType = null;
|
||||
|
||||
/**
|
||||
* SystemMessageMsgSpec msgSeq.
|
||||
@@ -359,11 +346,32 @@ export const SysMsg = $root.SysMsg = (() => {
|
||||
|
||||
/**
|
||||
* SystemMessageMsgSpec other.
|
||||
* @member {number} other
|
||||
* @member {number|null|undefined} other
|
||||
* @memberof SysMsg.SystemMessageMsgSpec
|
||||
* @instance
|
||||
*/
|
||||
SystemMessageMsgSpec.prototype.other = 0;
|
||||
SystemMessageMsgSpec.prototype.other = null;
|
||||
|
||||
// OneOf field names bound to virtual getters and setters
|
||||
let $oneOfFields;
|
||||
|
||||
// Virtual OneOf for proto3 optional field
|
||||
Object.defineProperty(SystemMessageMsgSpec.prototype, "_subType", {
|
||||
get: $util.oneOfGetter($oneOfFields = ["subType"]),
|
||||
set: $util.oneOfSetter($oneOfFields)
|
||||
});
|
||||
|
||||
// Virtual OneOf for proto3 optional field
|
||||
Object.defineProperty(SystemMessageMsgSpec.prototype, "_subSubType", {
|
||||
get: $util.oneOfGetter($oneOfFields = ["subSubType"]),
|
||||
set: $util.oneOfSetter($oneOfFields)
|
||||
});
|
||||
|
||||
// Virtual OneOf for proto3 optional field
|
||||
Object.defineProperty(SystemMessageMsgSpec.prototype, "_other", {
|
||||
get: $util.oneOfGetter($oneOfFields = ["other"]),
|
||||
set: $util.oneOfSetter($oneOfFields)
|
||||
});
|
||||
|
||||
/**
|
||||
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer.
|
||||
@@ -1007,6 +1015,141 @@ export const SysMsg = $root.SysMsg = (() => {
|
||||
return ProfileLikeTip;
|
||||
})();
|
||||
|
||||
SysMsg.GroupMemberChange = (function() {
|
||||
|
||||
/**
|
||||
* Properties of a GroupMemberChange.
|
||||
* @memberof SysMsg
|
||||
* @interface IGroupMemberChange
|
||||
* @property {number|null} [groupCode] GroupMemberChange groupCode
|
||||
* @property {string|null} [memberUid] GroupMemberChange memberUid
|
||||
* @property {number|null} [type] GroupMemberChange type
|
||||
* @property {string|null} [adminUid] GroupMemberChange adminUid
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constructs a new GroupMemberChange.
|
||||
* @memberof SysMsg
|
||||
* @classdesc Represents a GroupMemberChange.
|
||||
* @implements IGroupMemberChange
|
||||
* @constructor
|
||||
* @param {SysMsg.IGroupMemberChange=} [properties] Properties to set
|
||||
*/
|
||||
function GroupMemberChange(properties) {
|
||||
if (properties)
|
||||
for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i)
|
||||
if (properties[keys[i]] != null)
|
||||
this[keys[i]] = properties[keys[i]];
|
||||
}
|
||||
|
||||
/**
|
||||
* GroupMemberChange groupCode.
|
||||
* @member {number} groupCode
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @instance
|
||||
*/
|
||||
GroupMemberChange.prototype.groupCode = 0;
|
||||
|
||||
/**
|
||||
* GroupMemberChange memberUid.
|
||||
* @member {string} memberUid
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @instance
|
||||
*/
|
||||
GroupMemberChange.prototype.memberUid = "";
|
||||
|
||||
/**
|
||||
* GroupMemberChange type.
|
||||
* @member {number} type
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @instance
|
||||
*/
|
||||
GroupMemberChange.prototype.type = 0;
|
||||
|
||||
/**
|
||||
* GroupMemberChange adminUid.
|
||||
* @member {string} adminUid
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @instance
|
||||
*/
|
||||
GroupMemberChange.prototype.adminUid = "";
|
||||
|
||||
/**
|
||||
* Decodes a GroupMemberChange message from the specified reader or buffer.
|
||||
* @function decode
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @static
|
||||
* @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
|
||||
* @param {number} [length] Message length if known beforehand
|
||||
* @returns {SysMsg.GroupMemberChange} GroupMemberChange
|
||||
* @throws {Error} If the payload is not a reader or valid buffer
|
||||
* @throws {$protobuf.util.ProtocolError} If required fields are missing
|
||||
*/
|
||||
GroupMemberChange.decode = function decode(reader, length) {
|
||||
if (!(reader instanceof $Reader))
|
||||
reader = $Reader.create(reader);
|
||||
let end = length === undefined ? reader.len : reader.pos + length, message = new $root.SysMsg.GroupMemberChange();
|
||||
while (reader.pos < end) {
|
||||
let tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
message.groupCode = reader.uint32();
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
message.memberUid = reader.string();
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
message.type = reader.uint32();
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
message.adminUid = reader.string();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodes a GroupMemberChange message from the specified reader or buffer, length delimited.
|
||||
* @function decodeDelimited
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @static
|
||||
* @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
|
||||
* @returns {SysMsg.GroupMemberChange} GroupMemberChange
|
||||
* @throws {Error} If the payload is not a reader or valid buffer
|
||||
* @throws {$protobuf.util.ProtocolError} If required fields are missing
|
||||
*/
|
||||
GroupMemberChange.decodeDelimited = function decodeDelimited(reader) {
|
||||
if (!(reader instanceof $Reader))
|
||||
reader = new $Reader(reader);
|
||||
return this.decode(reader, reader.uint32());
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the default type url for GroupMemberChange
|
||||
* @function getTypeUrl
|
||||
* @memberof SysMsg.GroupMemberChange
|
||||
* @static
|
||||
* @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
|
||||
* @returns {string} The default type url
|
||||
*/
|
||||
GroupMemberChange.getTypeUrl = function getTypeUrl(typeUrlPrefix) {
|
||||
if (typeUrlPrefix === undefined) {
|
||||
typeUrlPrefix = "type.googleapis.com";
|
||||
}
|
||||
return typeUrlPrefix + "/SysMsg.GroupMemberChange";
|
||||
};
|
||||
|
||||
return GroupMemberChange;
|
||||
})();
|
||||
|
||||
return SysMsg;
|
||||
})();
|
||||
|
||||
|
9
src/ntqqapi/proto/groupMemberChange.proto
Normal file
9
src/ntqqapi/proto/groupMemberChange.proto
Normal file
@@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
package SysMsg;
|
||||
|
||||
message GroupMemberChange {
|
||||
uint32 groupCode = 1;
|
||||
string memberUid = 3;
|
||||
uint32 type = 4; // 130:主动 131:被动
|
||||
string adminUid = 5;
|
||||
}
|
@@ -8,23 +8,22 @@ message SystemMessage {
|
||||
}
|
||||
|
||||
message SystemMessageHeader {
|
||||
uint32 peerNumber = 1;
|
||||
string peerString = 2;
|
||||
uint32 peerUin = 1;
|
||||
//string peerUid = 2;
|
||||
uint32 uin = 5;
|
||||
optional string uid = 6;
|
||||
}
|
||||
|
||||
message SystemMessageMsgSpec {
|
||||
uint32 msgType = 1;
|
||||
uint32 subType = 2;
|
||||
uint32 subSubType = 3;
|
||||
optional uint32 subType = 2;
|
||||
optional uint32 subSubType = 3;
|
||||
uint32 msgSeq = 5;
|
||||
uint32 time = 6;
|
||||
//uint64 msgId = 12;
|
||||
uint32 other = 13;
|
||||
optional uint32 other = 13;
|
||||
}
|
||||
|
||||
message SystemMessageBodyWrapper {
|
||||
bytes body = 2;
|
||||
// Find the first [08], or ignore the first 7 bytes?
|
||||
}
|
||||
}
|
||||
|
@@ -125,4 +125,6 @@ export interface NodeIKernelGroupService {
|
||||
removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
|
||||
|
||||
setHeader(args: unknown[]): Promise<GeneralCallResult>
|
||||
|
||||
searchMember(sceneId: string, keyword: string): Promise<void>
|
||||
}
|
||||
|
@@ -95,4 +95,6 @@ export interface NodeIKernelMsgService {
|
||||
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
|
||||
|
||||
getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi>
|
||||
|
||||
sendSsoCmdReqByContend(ssoCmd: string, content: string): Promise<GeneralCallResult & { rsp: string }>
|
||||
}
|
||||
|
15
src/ntqqapi/services/NodeIKernelNodeMiscService.ts
Normal file
15
src/ntqqapi/services/NodeIKernelNodeMiscService.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface NodeIKernelNodeMiscService {
|
||||
wantWinScreenOCR(...args: unknown[]): Promise<{
|
||||
code: number
|
||||
errMsg: string
|
||||
result: {
|
||||
text: string
|
||||
[key: `pt${number}`]: {
|
||||
x: string
|
||||
y: string
|
||||
}
|
||||
charBox: unknown[]
|
||||
score: ''
|
||||
}[]
|
||||
}>
|
||||
}
|
@@ -39,5 +39,21 @@ export interface NodeIKernelRichMediaService {
|
||||
failFileIdList: Array<unknown>
|
||||
}
|
||||
}>
|
||||
|
||||
batchGetGroupFileCount(groupIds: string[]): Promise<GeneralCallResult & {
|
||||
groupCodes: string[]
|
||||
groupFileCounts: number[]
|
||||
}>
|
||||
|
||||
getGroupSpace(groupId: string): Promise<GeneralCallResult & {
|
||||
groupSpaceResult: {
|
||||
retCode: number
|
||||
retMsg: string
|
||||
clientWording: string
|
||||
totalSpace: string
|
||||
usedSpace: string
|
||||
allUpload: boolean
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
|
@@ -10,3 +10,4 @@ export * from './NodeIKernelRichMediaService'
|
||||
export * from './NodeIKernelTicketService'
|
||||
export * from './NodeIKernelTipOffService'
|
||||
export * from './NodeIKernelRobotService'
|
||||
export * from './NodeIKernelNodeMiscService'
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { QQLevel, Sex } from './user'
|
||||
|
||||
export enum GroupListUpdateType {
|
||||
REFRESHALL,
|
||||
GETALL,
|
||||
@@ -35,36 +33,54 @@ export interface Group {
|
||||
memberUin: string
|
||||
memberUid: string
|
||||
}
|
||||
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export enum GroupMemberRole {
|
||||
normal = 2,
|
||||
admin = 3,
|
||||
owner = 4,
|
||||
Normal = 2,
|
||||
Admin = 3,
|
||||
Owner = 4,
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
memberSpecialTitle?: string
|
||||
avatarPath: string
|
||||
cardName: string
|
||||
cardType: number
|
||||
isDelete: boolean
|
||||
nick: string
|
||||
uid: string
|
||||
qid: string
|
||||
uin: string
|
||||
nick: string
|
||||
remark: string
|
||||
role: GroupMemberRole // 群主:4, 管理员:3,群员:2
|
||||
shutUpTime: number // 禁言时间,单位是什么暂时不清楚
|
||||
uid: string // 加密的字符串
|
||||
uin: string // QQ号
|
||||
cardType: number
|
||||
cardName: string
|
||||
role: GroupMemberRole
|
||||
avatarPath: string
|
||||
shutUpTime: number
|
||||
isDelete: boolean
|
||||
isSpecialConcerned: boolean
|
||||
isSpecialShield: boolean
|
||||
isRobot: boolean
|
||||
sex?: Sex
|
||||
qqLevel?: QQLevel
|
||||
isChangeRole: boolean
|
||||
groupHonor: Uint8Array
|
||||
memberRealLevel: number
|
||||
memberLevel: number
|
||||
globalGroupLevel: number
|
||||
globalGroupPoint: number
|
||||
memberTitleId: number
|
||||
memberSpecialTitle: string
|
||||
specialTitleExpireTime: string
|
||||
userShowFlag: number
|
||||
userShowFlagNew: number
|
||||
richFlag: number
|
||||
mssVipType: number
|
||||
bigClubLevel: number
|
||||
bigClubFlag: number
|
||||
autoRemark: string
|
||||
creditLevel: number
|
||||
joinTime: number
|
||||
lastSpeakTime: number
|
||||
memberLevel: number
|
||||
memberFlag: number
|
||||
memberFlagExt: number
|
||||
memberMobileFlag: number
|
||||
memberFlagExt2: number
|
||||
isSpecialShielded: boolean
|
||||
cardNameId: number
|
||||
}
|
||||
|
||||
export interface PublishGroupBulletinReq {
|
||||
|
@@ -294,6 +294,9 @@ export interface GrayTipElement {
|
||||
jsonGrayTipElement?: {
|
||||
busiId: string
|
||||
jsonStr: string
|
||||
xmlToJsonParam?: {
|
||||
templParam: Map<string, string>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +438,10 @@ export interface RawMessage {
|
||||
likesCnt: string
|
||||
isClicked: boolean
|
||||
}[]
|
||||
msgAttrs: Map<number, {
|
||||
attrType: number
|
||||
attrId: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface Peer {
|
||||
@@ -476,7 +483,7 @@ export interface MessageElement {
|
||||
actionBarElement?: unknown
|
||||
}
|
||||
|
||||
export interface OnRichMediaDownloadCompleteParams {
|
||||
export interface RichMediaDownloadCompleteNotify {
|
||||
fileModelId: string
|
||||
msgElementId: string
|
||||
msgId: string
|
||||
|
@@ -1,7 +1,8 @@
|
||||
export enum Sex {
|
||||
male = 0,
|
||||
female = 2,
|
||||
unknown = 255,
|
||||
Unknown = 0,
|
||||
Male = 1,
|
||||
Female = 2,
|
||||
Hidden = 255
|
||||
}
|
||||
|
||||
export interface QQLevel {
|
||||
@@ -67,6 +68,7 @@ export interface User {
|
||||
recommendImgFlag?: number
|
||||
disableEmojiShortCuts?: number
|
||||
pendantId?: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
export interface SelfInfo extends User {
|
||||
@@ -100,7 +102,7 @@ export interface BaseInfo {
|
||||
birthday_month: number
|
||||
birthday_day: number
|
||||
age: number
|
||||
sex: number
|
||||
sex: Sex
|
||||
eMail: string
|
||||
phoneNum: string
|
||||
categoryId: number
|
||||
@@ -295,7 +297,7 @@ export interface UserDetailInfoByUin {
|
||||
birthday_year: number
|
||||
birthday_month: number
|
||||
birthday_day: number
|
||||
sex: number //0
|
||||
sex: number
|
||||
topTime: string
|
||||
constellation: number
|
||||
shengXiao: number
|
||||
|
21
src/onebot11/action/go-cqhttp/DeleteFriend.ts
Normal file
21
src/onebot11/action/go-cqhttp/DeleteFriend.ts
Normal 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
|
||||
}
|
||||
}
|
32
src/onebot11/action/go-cqhttp/GetGroupFileSystemInfo.ts
Normal file
32
src/onebot11/action/go-cqhttp/GetGroupFileSystemInfo.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { BaseAction, Schema } from '../BaseAction'
|
||||
import { ActionName } from '../types'
|
||||
|
||||
interface Payload {
|
||||
group_id: number | string
|
||||
}
|
||||
|
||||
interface Response {
|
||||
file_count: number
|
||||
limit_count: number
|
||||
used_space: number
|
||||
total_space: number
|
||||
}
|
||||
|
||||
export class GetGroupFileSystemInfo extends BaseAction<Payload, Response> {
|
||||
actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo
|
||||
payloadSchema = Schema.object({
|
||||
group_id: Schema.union([Number, String]).required()
|
||||
})
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const groupId = payload.group_id.toString()
|
||||
const { groupFileCounts } = await this.ctx.ntGroupApi.getGroupFileCount(groupId)
|
||||
const { groupSpaceResult } = await this.ctx.ntGroupApi.getGroupFileSpace(groupId)
|
||||
return {
|
||||
file_count: groupFileCounts[0],
|
||||
limit_count: 10000,
|
||||
used_space: +groupSpaceResult.usedSpace,
|
||||
total_space: +groupSpaceResult.totalSpace,
|
||||
}
|
||||
}
|
||||
}
|
@@ -30,12 +30,12 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
|
||||
const { count, reverseOrder } = payload
|
||||
const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() }
|
||||
let msgList: RawMessage[]
|
||||
if (!payload.message_seq || payload.message_seq === '0') {
|
||||
if (!payload.message_seq || +payload.message_seq === 0) {
|
||||
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
|
||||
} else {
|
||||
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
|
||||
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
|
||||
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
|
||||
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count, true)).msgList
|
||||
}
|
||||
if (!msgList?.length) throw new Error('未找到消息')
|
||||
if (reverseOrder) msgList.reverse()
|
||||
|
@@ -3,59 +3,51 @@ import { OB11User } from '../../types'
|
||||
import { OB11Entities } from '../../entities'
|
||||
import { ActionName } from '../types'
|
||||
import { getBuildVersion } from '@/common/utils'
|
||||
import { OB11UserSex } from '../../types'
|
||||
import { calcQQLevel } from '@/common/utils/misc'
|
||||
|
||||
interface Payload {
|
||||
user_id: number | string
|
||||
}
|
||||
|
||||
export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
|
||||
interface Response extends OB11User {
|
||||
reg_time: number
|
||||
long_nick: string
|
||||
}
|
||||
|
||||
export class GetStrangerInfo extends BaseAction<Payload, Response> {
|
||||
actionName = ActionName.GoCQHTTP_GetStrangerInfo
|
||||
payloadSchema = Schema.object({
|
||||
user_id: Schema.union([Number, String]).required()
|
||||
})
|
||||
|
||||
protected async _handle(payload: Payload): Promise<OB11User> {
|
||||
if (!(getBuildVersion() >= 26702)) {
|
||||
const user_id = payload.user_id.toString()
|
||||
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUin(user_id)
|
||||
const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
|
||||
if (!uid || uid.indexOf('*') != -1) {
|
||||
const ret = {
|
||||
...extendData,
|
||||
user_id: parseInt(extendData.info.uin) || 0,
|
||||
nickname: extendData.info.nick,
|
||||
sex: OB11UserSex.unknown,
|
||||
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
|
||||
qid: extendData.info.qid,
|
||||
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
|
||||
login_days: 0,
|
||||
uid: ''
|
||||
}
|
||||
return ret
|
||||
protected async _handle(payload: Payload) {
|
||||
const uin = payload.user_id.toString()
|
||||
if (getBuildVersion() >= 26702) {
|
||||
const data = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(uin)
|
||||
return {
|
||||
user_id: parseInt(data.detail.uin) || 0,
|
||||
nickname: data.detail.simpleInfo.coreInfo.nick,
|
||||
sex: OB11Entities.sex(data.detail.simpleInfo.baseInfo.sex),
|
||||
age: data.detail.simpleInfo.baseInfo.age,
|
||||
qid: data.detail.simpleInfo.baseInfo.qid,
|
||||
level: data.detail.commonExt.qqLevel && calcQQLevel(data.detail.commonExt.qqLevel) || 0,
|
||||
login_days: 0,
|
||||
reg_time: data.detail.commonExt.regTime,
|
||||
long_nick: data.detail.simpleInfo.baseInfo.longNick
|
||||
}
|
||||
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
|
||||
return OB11Entities.stranger(data)
|
||||
} else {
|
||||
const user_id = payload.user_id.toString()
|
||||
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id)
|
||||
const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
|
||||
if (!uid || uid.indexOf('*') != -1) {
|
||||
const ret = {
|
||||
...extendData,
|
||||
user_id: parseInt(extendData.detail.uin) || 0,
|
||||
nickname: extendData.detail.simpleInfo.coreInfo.nick,
|
||||
sex: OB11UserSex.unknown,
|
||||
age: 0,
|
||||
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
|
||||
login_days: 0,
|
||||
uid: ''
|
||||
}
|
||||
return ret
|
||||
const data = await this.ctx.ntUserApi.getUserDetailInfoByUin(uin)
|
||||
return {
|
||||
user_id: parseInt(data.info.uin) || 0,
|
||||
nickname: data.info.nick,
|
||||
sex: OB11Entities.sex(data.info.sex),
|
||||
age: data.info.birthday_year === 0 ? 0 : new Date().getFullYear() - data.info.birthday_year,
|
||||
qid: data.info.qid,
|
||||
level: data.info.qqLevel && calcQQLevel(data.info.qqLevel) || 0,
|
||||
login_days: 0,
|
||||
reg_time: data.info.regTime,
|
||||
long_nick: data.info.longNick
|
||||
}
|
||||
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
|
||||
return OB11Entities.stranger(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
63
src/onebot11/action/go-cqhttp/OCRImage.ts
Normal file
63
src/onebot11/action/go-cqhttp/OCRImage.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { BaseAction, Schema } from '../BaseAction'
|
||||
import { ActionName } from '../types'
|
||||
import { uri2local } from '@/common/utils/file'
|
||||
import { access, unlink } from 'node:fs/promises'
|
||||
|
||||
interface Payload {
|
||||
image: string
|
||||
}
|
||||
|
||||
interface TextDetection {
|
||||
text: string
|
||||
confidence: number
|
||||
coordinates: {
|
||||
x: number //int32
|
||||
y: number
|
||||
}[]
|
||||
}
|
||||
|
||||
interface Response {
|
||||
texts: TextDetection[]
|
||||
language: string
|
||||
}
|
||||
|
||||
export class OCRImage extends BaseAction<Payload, Response> {
|
||||
actionName = ActionName.GoCQHTTP_OCRImage
|
||||
payloadSchema = Schema.object({
|
||||
image: Schema.string().required()
|
||||
})
|
||||
|
||||
protected async _handle(payload: Payload) {
|
||||
const { errMsg, isLocal, path, success } = await uri2local(this.ctx, payload.image, true)
|
||||
if (!success) {
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
await access(path)
|
||||
|
||||
const data = await this.ctx.ntFileApi.ocrImage(path)
|
||||
if (!isLocal) {
|
||||
unlink(path)
|
||||
}
|
||||
|
||||
const texts = data.result.map(item => {
|
||||
const ret: TextDetection = {
|
||||
text: item.text,
|
||||
confidence: 1,
|
||||
coordinates: []
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const pt = item[`pt${i + 1}`]
|
||||
ret.coordinates.push({
|
||||
x: parseInt(pt.x),
|
||||
y: parseInt(pt.y)
|
||||
})
|
||||
}
|
||||
return ret
|
||||
})
|
||||
|
||||
return {
|
||||
texts,
|
||||
language: ''
|
||||
}
|
||||
}
|
||||
}
|
@@ -27,7 +27,11 @@ export class UploadGroupFile extends BaseAction<Payload, null> {
|
||||
if (!success) {
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
const file = await SendElement.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id)
|
||||
const name = payload.name || fileName
|
||||
if (name.includes('/') || name.includes('\\')) {
|
||||
throw new Error(`文件名 ${name} 不合法`)
|
||||
}
|
||||
const file = await SendElement.file(this.ctx, path, name, payload.folder ?? payload.folder_id)
|
||||
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
|
||||
await sendMsg(this.ctx, peer, [file], [])
|
||||
return null
|
||||
|
@@ -23,7 +23,11 @@ export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null
|
||||
if (!success) {
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
const sendFileEle = await SendElement.file(this.ctx, path, payload.name || fileName)
|
||||
const name = payload.name || fileName
|
||||
if (name.includes('/') || name.includes('\\')) {
|
||||
throw new Error(`文件名 ${name} 不合法`)
|
||||
}
|
||||
const sendFileEle = await SendElement.file(this.ctx, path, name)
|
||||
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
|
||||
await sendMsg(this.ctx, peer, [sendFileEle], [])
|
||||
return null
|
||||
|
@@ -2,34 +2,36 @@ import { BaseAction, Schema } from '../BaseAction'
|
||||
import { OB11GroupMember } from '../../types'
|
||||
import { OB11Entities } from '../../entities'
|
||||
import { ActionName } from '../types'
|
||||
import { isNullable } from 'cosmokit'
|
||||
import { calcQQLevel, parseBool } from '@/common/utils/misc'
|
||||
|
||||
interface Payload {
|
||||
group_id: number | string
|
||||
user_id: number | string
|
||||
no_cache: boolean
|
||||
}
|
||||
|
||||
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
|
||||
actionName = ActionName.GetGroupMemberInfo
|
||||
payloadSchema = Schema.object({
|
||||
group_id: Schema.union([Number, String]).required(),
|
||||
user_id: Schema.union([Number, String]).required()
|
||||
user_id: Schema.union([Number, String]).required(),
|
||||
no_cache: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
|
||||
})
|
||||
|
||||
protected async _handle(payload: Payload) {
|
||||
const groupCode = payload.group_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)
|
||||
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid, payload.no_cache)
|
||||
if (member) {
|
||||
if (isNullable(member.sex)) {
|
||||
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
|
||||
Object.assign(member, info)
|
||||
}
|
||||
const ret = OB11Entities.groupMember(groupCode, member)
|
||||
const date = Math.round(Date.now() / 1000)
|
||||
const ret = OB11Entities.groupMember(+groupCode, member)
|
||||
const date = Math.trunc(Date.now() / 1000)
|
||||
ret.last_sent_time ??= date
|
||||
ret.join_time ??= date
|
||||
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
|
||||
ret.sex = OB11Entities.sex(info.sex!)
|
||||
ret.qq_level = info.qqLevel && calcQQLevel(info.qqLevel) || 0
|
||||
ret.age = info.age ?? 0
|
||||
return ret
|
||||
}
|
||||
throw new Error(`群成员${payload.user_id}不存在`)
|
||||
|
@@ -17,19 +17,26 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
|
||||
protected async _handle(payload: Payload) {
|
||||
const groupCode = payload.group_id.toString()
|
||||
let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
|
||||
if (groupMembers.size === 0) {
|
||||
await this.ctx.sleep(100)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (groupMembers.size > 0) {
|
||||
break
|
||||
}
|
||||
await this.ctx.sleep(60)
|
||||
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
|
||||
}
|
||||
const groupMembersArr = Array.from(groupMembers.values())
|
||||
const date = Math.round(Date.now() / 1000)
|
||||
|
||||
return groupMembersArr.map(item => {
|
||||
const member = OB11Entities.groupMember(groupCode, item)
|
||||
const date = Math.trunc(Date.now() / 1000)
|
||||
const groupId = Number(payload.group_id)
|
||||
const ret: OB11GroupMember[] = []
|
||||
|
||||
for (const item of groupMembers.values()) {
|
||||
const member = OB11Entities.groupMember(groupId, item)
|
||||
member.join_time ??= date
|
||||
member.last_sent_time ??= date
|
||||
return member
|
||||
})
|
||||
ret.push(member)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,7 @@ export default class SetGroupAdmin extends BaseAction<Payload, null> {
|
||||
await this.ctx.ntGroupApi.setMemberRole(
|
||||
groupCode,
|
||||
uid,
|
||||
payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal
|
||||
payload.enable ? GroupMemberRole.Admin : GroupMemberRole.Normal
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
@@ -72,6 +72,11 @@ import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile'
|
||||
import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl'
|
||||
import { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
|
||||
import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
|
||||
import { DeleteFriend } from './go-cqhttp/DeleteFriend'
|
||||
import { OCRImage } from './go-cqhttp/OCRImage'
|
||||
import { GroupPoke } from './llonebot/GroupPoke'
|
||||
import { FriendPoke } from './llonebot/FriendPoke'
|
||||
import { GetGroupFileSystemInfo } from './go-cqhttp/GetGroupFileSystemInfo'
|
||||
|
||||
export function initActionMap(adapter: Adapter) {
|
||||
const actionHandlers = [
|
||||
@@ -90,6 +95,8 @@ export function initActionMap(adapter: Adapter) {
|
||||
new FetchCustomFace(adapter),
|
||||
new SetMsgEmojiLike(adapter),
|
||||
new GetRobotUinRange(adapter),
|
||||
new GroupPoke(adapter),
|
||||
new FriendPoke(adapter),
|
||||
// onebot11
|
||||
new SendLike(adapter),
|
||||
new GetMsg(adapter),
|
||||
@@ -149,6 +156,9 @@ export function initActionMap(adapter: Adapter) {
|
||||
new GetGroupFilesByFolder(adapter),
|
||||
new GetGroupFileUrl(adapter),
|
||||
new GetGroupNotice(adapter),
|
||||
new DeleteFriend(adapter),
|
||||
new OCRImage(adapter),
|
||||
new GetGroupFileSystemInfo(adapter),
|
||||
]
|
||||
const actionMap = new Map<string, BaseAction<any, unknown>>()
|
||||
for (const action of actionHandlers) {
|
||||
|
25
src/onebot11/action/llonebot/FriendPoke.ts
Normal file
25
src/onebot11/action/llonebot/FriendPoke.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BaseAction, Schema } from '../BaseAction'
|
||||
import { ActionName } from '../types'
|
||||
import { getBuildVersion } from '@/common/utils/misc'
|
||||
|
||||
interface Payload {
|
||||
user_id: number | string
|
||||
}
|
||||
|
||||
export class FriendPoke extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.FriendPoke
|
||||
payloadSchema = Schema.object({
|
||||
user_id: Schema.union([Number, String]).required()
|
||||
})
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
if (!this.ctx.app.native.checkPlatform()) {
|
||||
throw new Error('当前系统平台或架构不支持')
|
||||
}
|
||||
if (!this.ctx.app.native.checkVersion()) {
|
||||
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
|
||||
}
|
||||
await this.ctx.app.native.sendFriendPoke(+payload.user_id)
|
||||
return null
|
||||
}
|
||||
}
|
27
src/onebot11/action/llonebot/GroupPoke.ts
Normal file
27
src/onebot11/action/llonebot/GroupPoke.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseAction, Schema } from '../BaseAction'
|
||||
import { ActionName } from '../types'
|
||||
import { getBuildVersion } from '@/common/utils/misc'
|
||||
|
||||
interface Payload {
|
||||
group_id: number | string
|
||||
user_id: number | string
|
||||
}
|
||||
|
||||
export class GroupPoke extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.GroupPoke
|
||||
payloadSchema = Schema.object({
|
||||
group_id: Schema.union([Number, String]).required(),
|
||||
user_id: Schema.union([Number, String]).required()
|
||||
})
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
if (!this.ctx.app.native.checkPlatform()) {
|
||||
throw new Error('当前系统平台或架构不支持')
|
||||
}
|
||||
if (!this.ctx.app.native.checkVersion()) {
|
||||
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
|
||||
}
|
||||
await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id)
|
||||
return null
|
||||
}
|
||||
}
|
@@ -27,6 +27,8 @@ export enum ActionName {
|
||||
SendForwardMsg = 'send_forward_msg',
|
||||
SetMsgEmojiLike = 'set_msg_emoji_like',
|
||||
GetRobotUinRange = 'get_robot_uin_range',
|
||||
GroupPoke = 'group_poke',
|
||||
FriendPoke = 'friend_poke',
|
||||
// onebot 11
|
||||
SendLike = 'send_like',
|
||||
GetLoginInfo = 'get_login_info',
|
||||
@@ -85,4 +87,7 @@ export enum ActionName {
|
||||
GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder',
|
||||
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url',
|
||||
GoCQHTTP_GetGroupNotice = '_get_group_notice',
|
||||
GoCQHTTP_DeleteFriend = 'delete_friend',
|
||||
GoCQHTTP_OCRImage = 'ocr_image',
|
||||
GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info',
|
||||
}
|
||||
|
@@ -23,11 +23,11 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
|
||||
if (payload.remark) {
|
||||
await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark)
|
||||
}
|
||||
await this.ctx.ntMsgApi.activateChat({
|
||||
/*await this.ctx.ntMsgApi.activateChat({
|
||||
peerUid: uid,
|
||||
chatType: ChatType.C2C,
|
||||
guildId: ''
|
||||
})
|
||||
})*/
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import { llonebotError } from '../common/globalVars'
|
||||
import { OB11GroupAdminNoticeEvent } from './event/notice/OB11GroupAdminNoticeEvent'
|
||||
import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent'
|
||||
import { SysMsg } from '@/ntqqapi/proto/compiled'
|
||||
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
@@ -33,12 +34,13 @@ declare module 'cordis' {
|
||||
class OneBot11Adapter extends Service {
|
||||
static inject = [
|
||||
'ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi',
|
||||
'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store'
|
||||
'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi',
|
||||
'store', 'app'
|
||||
]
|
||||
private ob11WebSocket: OB11WebSocket
|
||||
private ob11WebSocketReverseManager: OB11WebSocketReverseManager
|
||||
private ob11Http: OB11Http
|
||||
private ob11HttpPost: OB11HttpPost
|
||||
private ob11WebSocket
|
||||
private ob11WebSocketReverseManager
|
||||
private ob11Http
|
||||
private ob11HttpPost
|
||||
|
||||
constructor(public ctx: Context, public config: OneBot11Adapter.Config) {
|
||||
super(ctx, 'onebot', true)
|
||||
@@ -173,26 +175,23 @@ class OneBot11Adapter extends Service {
|
||||
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()))
|
||||
}).catch(e => this.ctx.logger.error('handling incoming messages', e))
|
||||
|
||||
OB11Entities.groupEvent(this.ctx, message).then(groupEvent => {
|
||||
if (groupEvent) {
|
||||
this.dispatch(groupEvent)
|
||||
}
|
||||
})
|
||||
}).catch(e => this.ctx.logger.error('handling incoming group events', e))
|
||||
|
||||
OB11Entities.privateEvent(this.ctx, message).then(privateEvent => {
|
||||
if (privateEvent) {
|
||||
this.dispatch(privateEvent)
|
||||
}
|
||||
})
|
||||
}).catch(e => this.ctx.logger.error('handling incoming buddy events', e))
|
||||
}
|
||||
|
||||
private handleRecallMsg(message: RawMessage) {
|
||||
@@ -310,7 +309,6 @@ class OneBot11Adapter extends Service {
|
||||
heartInterval: config.heartInterval,
|
||||
token: config.token,
|
||||
debug: config.debug,
|
||||
reportSelfMessage: config.reportSelfMessage,
|
||||
msgCacheExpire: config.msgCacheExpire,
|
||||
musicSignUrl: config.musicSignUrl,
|
||||
enableLocalFile2Url: config.enableLocalFile2Url,
|
||||
@@ -341,6 +339,9 @@ class OneBot11Adapter extends Service {
|
||||
this.handleRecallMsg(input)
|
||||
})
|
||||
this.ctx.on('nt/message-sent', input => {
|
||||
if (!this.config.reportSelfMessage) {
|
||||
return
|
||||
}
|
||||
this.handleMsg(input)
|
||||
})
|
||||
this.ctx.on('nt/group-notify', input => {
|
||||
@@ -349,7 +350,7 @@ class OneBot11Adapter extends Service {
|
||||
this.ctx.on('nt/friend-request', input => {
|
||||
this.handleFriendRequest(input)
|
||||
})
|
||||
this.ctx.on('nt/system-message-created', input => {
|
||||
this.ctx.on('nt/system-message-created', async input => {
|
||||
const sysMsg = SysMsg.SystemMessage.decode(input)
|
||||
const { msgType, subType, subSubType } = sysMsg.msgSpec[0] ?? {}
|
||||
if (msgType === 528 && subType === 39 && subSubType === 39) {
|
||||
@@ -358,9 +359,25 @@ class OneBot11Adapter extends Service {
|
||||
const detail = tip.content?.msg?.detail
|
||||
if (!detail) return
|
||||
const [times] = detail.txt?.match(/\d+/) ?? ['0']
|
||||
const profileLikeEvent = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
|
||||
this.dispatch(profileLikeEvent)
|
||||
}
|
||||
const event = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
|
||||
this.dispatch(event)
|
||||
} else if (msgType === 33) {
|
||||
const tip = SysMsg.GroupMemberChange.decode(sysMsg.bodyWrapper!.body!)
|
||||
if (tip.type !== 130) return
|
||||
this.ctx.logger.info('群成员增加', tip)
|
||||
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
|
||||
const operatorUin = await this.ctx.ntUserApi.getUinByUid(tip.adminUid)
|
||||
const event = new OB11GroupIncreaseEvent(tip.groupCode, +memberUin, +operatorUin)
|
||||
this.dispatch(event)
|
||||
}/* else if (msgType === 34) {
|
||||
const tip = SysMsg.GroupMemberChange.decode(sysMsg.bodyWrapper!.body!)
|
||||
this.ctx.logger.info('群成员减少', tip)
|
||||
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
|
||||
const operatorUin = await this.ctx.ntUserApi.getUinByUid(tip.adminUid) //0
|
||||
const subType = tip.type === 130 ? 'leave' : 'kick'
|
||||
const event = new OB11GroupDecreaseEvent(tip.groupCode, +memberUin, +operatorUin, subType)
|
||||
this.dispatch(event)
|
||||
}*/
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -370,7 +387,6 @@ namespace OneBot11Adapter {
|
||||
heartInterval: number
|
||||
token: string
|
||||
debug: boolean
|
||||
reportSelfMessage: boolean
|
||||
musicSignUrl?: string
|
||||
enableLocalFile2Url: boolean
|
||||
ffmpeg?: string
|
||||
|
@@ -134,11 +134,13 @@ namespace OB11Http {
|
||||
|
||||
class OB11HttpPost {
|
||||
private disposeInterval?: () => void
|
||||
private activated = false
|
||||
|
||||
constructor(protected ctx: Context, public config: OB11HttpPost.Config) {
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.activated = true
|
||||
if (this.config.enableHttpHeart && !this.disposeInterval) {
|
||||
this.disposeInterval = this.ctx.setInterval(() => {
|
||||
// ws的心跳是ws自己维护的
|
||||
@@ -148,10 +150,14 @@ class OB11HttpPost {
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.activated = false
|
||||
this.disposeInterval?.()
|
||||
}
|
||||
|
||||
public async emitEvent(event: OB11BaseEvent | OB11Message) {
|
||||
if (!this.activated || !this.config.hosts.length) {
|
||||
return
|
||||
}
|
||||
const msgStr = JSON.stringify(event)
|
||||
const headers: Dict = {
|
||||
'Content-Type': 'application/json',
|
||||
|
@@ -71,7 +71,7 @@ export namespace OB11Entities {
|
||||
sub_type: 'friend',
|
||||
message: messagePostFormat === 'string' ? '' : [],
|
||||
message_format: messagePostFormat === 'string' ? 'string' : 'array',
|
||||
post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
|
||||
post_type: selfUin === msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
|
||||
}
|
||||
if (debug) {
|
||||
resMsg.raw = msg
|
||||
@@ -79,11 +79,14 @@ export namespace OB11Entities {
|
||||
if (msg.chatType === ChatType.Group) {
|
||||
resMsg.sub_type = 'normal'
|
||||
resMsg.group_id = parseInt(msg.peerUin)
|
||||
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUid)
|
||||
if (member) {
|
||||
resMsg.sender.role = groupMemberRole(member.role)
|
||||
resMsg.sender.nickname = member.nick
|
||||
resMsg.sender.title = member.memberSpecialTitle ?? ''
|
||||
// 284840486: 合并转发内部
|
||||
if (msg.peerUin !== '284840486') {
|
||||
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUid)
|
||||
if (member) {
|
||||
resMsg.sender.role = groupMemberRole(member.role)
|
||||
resMsg.sender.nickname = member.nick
|
||||
resMsg.sender.title = member.memberSpecialTitle ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (msg.chatType === ChatType.C2C) {
|
||||
@@ -373,11 +376,23 @@ export namespace OB11Entities {
|
||||
if (msg.chatType !== ChatType.C2C) {
|
||||
return
|
||||
}
|
||||
if (msg.msgType !== 5) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const element of msg.elements) {
|
||||
if (element.grayTipElement) {
|
||||
const { grayTipElement } = element
|
||||
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
|
||||
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
|
||||
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
|
||||
if (param) {
|
||||
return new OB11FriendPokeEvent(
|
||||
Number(param.templParam.get('uin_str1')),
|
||||
Number(param.templParam.get('uin_str2')),
|
||||
json.items
|
||||
)
|
||||
}
|
||||
const pokedetail: Dict[] = json.items
|
||||
//筛选item带有uid的元素
|
||||
const poke_uid = pokedetail.filter(item => item.uid)
|
||||
@@ -402,31 +417,15 @@ export namespace OB11Entities {
|
||||
if (msg.chatType !== ChatType.Group) {
|
||||
return
|
||||
}
|
||||
/**if (msg.senderUin) {
|
||||
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin)
|
||||
if (member && member.cardName !== msg.sendMemberName) {
|
||||
const event = new OB11GroupCardEvent(
|
||||
parseInt(msg.peerUid),
|
||||
parseInt(msg.senderUin),
|
||||
msg.sendMemberName!,
|
||||
member.cardName,
|
||||
)
|
||||
member.cardName = msg.sendMemberName!
|
||||
return event
|
||||
}
|
||||
}*/
|
||||
if (msg.msgType !== 5 && msg.msgType !== 3) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const element of msg.elements) {
|
||||
const grayTipElement = element.grayTipElement
|
||||
const groupElement = grayTipElement?.groupElement
|
||||
if (groupElement) {
|
||||
if (groupElement.type === TipGroupElementType.MemberIncrease) {
|
||||
ctx.logger.info('收到群成员增加消息', groupElement)
|
||||
const { memberUid, adminUid } = groupElement
|
||||
const memberUin = await ctx.ntUserApi.getUinByUid(memberUid)
|
||||
const operatorUin = adminUid ? await ctx.ntUserApi.getUinByUid(adminUid) : memberUin
|
||||
return new OB11GroupIncreaseEvent(+msg.peerUid, +memberUin, +operatorUin)
|
||||
}
|
||||
else if (groupElement.type === TipGroupElementType.Ban) {
|
||||
if (groupElement.type === TipGroupElementType.Ban) {
|
||||
ctx.logger.info('收到群成员禁言提示', groupElement)
|
||||
const memberUid = groupElement.shutUp?.member.uid
|
||||
const adminUid = groupElement.shutUp?.admin.uid
|
||||
@@ -548,14 +547,23 @@ export namespace OB11Entities {
|
||||
else if (grayTipElement.subElementType == GrayTipElementSubType.JSON) {
|
||||
const json = JSON.parse(grayTipElement.jsonGrayTipElement!.jsonStr)
|
||||
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
|
||||
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
|
||||
if (param) {
|
||||
return new OB11GroupPokeEvent(
|
||||
Number(msg.peerUid),
|
||||
Number(param.templParam.get('uin_str1')),
|
||||
Number(param.templParam.get('uin_str2')),
|
||||
json.items
|
||||
)
|
||||
}
|
||||
const pokedetail: Dict[] = json.items
|
||||
//筛选item带有uid的元素
|
||||
const poke_uid = pokedetail.filter(item => item.uid)
|
||||
if (poke_uid.length == 2) {
|
||||
return new OB11GroupPokeEvent(
|
||||
parseInt(msg.peerUid),
|
||||
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
|
||||
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
|
||||
Number(msg.peerUid),
|
||||
Number(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
|
||||
Number(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
|
||||
pokedetail
|
||||
)
|
||||
}
|
||||
@@ -647,56 +655,46 @@ export namespace OB11Entities {
|
||||
return raw.map(friendV2)
|
||||
}
|
||||
|
||||
export function groupMemberRole(role: number): OB11GroupMemberRole | undefined {
|
||||
export function groupMemberRole(role: number): OB11GroupMemberRole {
|
||||
return {
|
||||
4: OB11GroupMemberRole.owner,
|
||||
3: OB11GroupMemberRole.admin,
|
||||
2: OB11GroupMemberRole.member,
|
||||
}[role]
|
||||
4: OB11GroupMemberRole.Owner,
|
||||
3: OB11GroupMemberRole.Admin,
|
||||
2: OB11GroupMemberRole.Member,
|
||||
}[role] ?? OB11GroupMemberRole.Member
|
||||
}
|
||||
|
||||
export function sex(sex: Sex): OB11UserSex {
|
||||
const sexMap = {
|
||||
[Sex.male]: OB11UserSex.male,
|
||||
[Sex.female]: OB11UserSex.female,
|
||||
[Sex.unknown]: OB11UserSex.unknown,
|
||||
[Sex.Unknown]: OB11UserSex.Unknown,
|
||||
[Sex.Male]: OB11UserSex.Male,
|
||||
[Sex.Female]: OB11UserSex.Female,
|
||||
[Sex.Hidden]: OB11UserSex.Unknown
|
||||
}
|
||||
return sexMap[sex] || OB11UserSex.unknown
|
||||
return sexMap[sex] ?? OB11UserSex.Unknown
|
||||
}
|
||||
|
||||
export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
|
||||
export function groupMember(groupId: number, member: GroupMember): OB11GroupMember {
|
||||
const titleExpireTime = +member.specialTitleExpireTime
|
||||
const int32Max = 2147483647
|
||||
return {
|
||||
group_id: parseInt(group_id),
|
||||
group_id: groupId,
|
||||
user_id: parseInt(member.uin),
|
||||
nickname: member.nick,
|
||||
card: member.cardName,
|
||||
sex: sex(member.sex!),
|
||||
card: member.cardName || member.nick,
|
||||
sex: OB11UserSex.Unknown,
|
||||
age: 0,
|
||||
area: '',
|
||||
level: String(member.memberLevel ?? 0),
|
||||
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
|
||||
qq_level: 0,
|
||||
join_time: member.joinTime,
|
||||
last_sent_time: member.lastSpeakTime,
|
||||
title_expire_time: 0,
|
||||
title_expire_time: titleExpireTime > int32Max ? 0 : titleExpireTime,
|
||||
unfriendly: false,
|
||||
card_changeable: true,
|
||||
is_robot: member.isRobot,
|
||||
shut_up_timestamp: member.shutUpTime,
|
||||
role: groupMemberRole(member.role),
|
||||
title: member.memberSpecialTitle || '',
|
||||
}
|
||||
}
|
||||
|
||||
export function stranger(user: User): OB11User {
|
||||
return {
|
||||
...user,
|
||||
user_id: parseInt(user.uin),
|
||||
nickname: user.nick,
|
||||
sex: sex(user.sex!),
|
||||
age: 0,
|
||||
qid: user.qid,
|
||||
login_days: 0,
|
||||
level: (user.qqLevel && calcQQLevel(user.qqLevel)) || 0,
|
||||
title: member.memberSpecialTitle,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,7 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
|
||||
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
|
||||
super()
|
||||
this.group_id = groupId
|
||||
this.operator_id = operatorId // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的
|
||||
this.operator_id = operatorId
|
||||
this.user_id = userId
|
||||
this.sub_type = subType
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ export async function createSendElements(
|
||||
.RemainAtAllCountForUin
|
||||
ctx.logger.info(`群${groupCode}剩余at全体次数`, remainAtAllCount)
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -66,9 +66,11 @@ export async function createSendElements(
|
||||
}
|
||||
}
|
||||
else if (peer.chatType === ChatType.Group) {
|
||||
const uid = await ctx.ntUserApi.getUidByUin(atQQ) ?? ''
|
||||
const atNmae = sendMsg.data?.name
|
||||
const display = atNmae ? `@${atNmae}` : ''
|
||||
const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid) ?? ''
|
||||
let display = ''
|
||||
if (sendMsg.data.name) {
|
||||
display = `@${sendMsg.data.name}`
|
||||
}
|
||||
sendElements.push(SendElement.at(atQQ, uid, AtType.One, display))
|
||||
}
|
||||
}
|
||||
|
@@ -11,41 +11,39 @@ export interface OB11User {
|
||||
age?: number
|
||||
qid?: string
|
||||
login_days?: number
|
||||
categroyName?: string
|
||||
categoryId?: number
|
||||
}
|
||||
|
||||
export enum OB11UserSex {
|
||||
male = 'male',
|
||||
female = 'female',
|
||||
unknown = 'unknown',
|
||||
Male = 'male',
|
||||
Female = 'female',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export enum OB11GroupMemberRole {
|
||||
owner = 'owner',
|
||||
admin = 'admin',
|
||||
member = 'member',
|
||||
Owner = 'owner',
|
||||
Admin = 'admin',
|
||||
Member = 'member',
|
||||
}
|
||||
|
||||
export interface OB11GroupMember {
|
||||
group_id: number
|
||||
user_id: number
|
||||
nickname: string
|
||||
card?: string
|
||||
sex?: OB11UserSex
|
||||
age?: number
|
||||
join_time?: number
|
||||
last_sent_time?: number
|
||||
level?: string
|
||||
card: string
|
||||
sex: OB11UserSex
|
||||
age: number
|
||||
join_time: number
|
||||
last_sent_time: number
|
||||
level: string
|
||||
qq_level?: number
|
||||
role?: OB11GroupMemberRole
|
||||
title?: string
|
||||
area?: string
|
||||
unfriendly?: boolean
|
||||
title_expire_time?: number
|
||||
card_changeable?: boolean
|
||||
role: OB11GroupMemberRole
|
||||
title: string
|
||||
area: string
|
||||
unfriendly: boolean
|
||||
title_expire_time: number
|
||||
card_changeable: boolean
|
||||
// 以下为gocq字段
|
||||
shut_up_timestamp?: number
|
||||
shut_up_timestamp: number
|
||||
// 以下为扩展字段
|
||||
is_robot?: boolean
|
||||
qage?: number
|
||||
|
@@ -7,11 +7,8 @@ import {
|
||||
CHANNEL_SELECT_FILE,
|
||||
CHANNEL_SET_CONFIG,
|
||||
CHANNEL_UPDATE,
|
||||
CHANNEL_SET_CONFIG_CONFIRMED,
|
||||
} from './common/channels'
|
||||
|
||||
const { contextBridge } = require('electron')
|
||||
const { ipcRenderer } = require('electron')
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
const llonebot = {
|
||||
log: (data: unknown) => {
|
||||
@@ -24,8 +21,7 @@ const llonebot = {
|
||||
return ipcRenderer.invoke(CHANNEL_UPDATE)
|
||||
},
|
||||
setConfig: async (ask: boolean, config: Config) => {
|
||||
const isSuccess = await ipcRenderer.invoke(CHANNEL_SET_CONFIG, ask, config)
|
||||
if (isSuccess) ipcRenderer.send(CHANNEL_SET_CONFIG_CONFIRMED, config)
|
||||
return ipcRenderer.invoke(CHANNEL_SET_CONFIG, ask, config)
|
||||
},
|
||||
getConfig: async (): Promise<Config> => {
|
||||
return ipcRenderer.invoke(CHANNEL_GET_CONFIG)
|
||||
|
@@ -25,9 +25,6 @@ async function onSettingWindowCreated(view: Element) {
|
||||
} else {
|
||||
Object.assign(config, { [key]: value })
|
||||
}
|
||||
if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) {
|
||||
window.llonebot.setConfig(false, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +43,13 @@ async function onSettingWindowCreated(view: Element) {
|
||||
SettingButton('请稍候', 'llonebot-update-button', 'secondary'),
|
||||
),
|
||||
]),
|
||||
SettingList([
|
||||
SettingItem(
|
||||
'是否启用 LLOneBot,重启 QQ 后生效',
|
||||
null,
|
||||
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
|
||||
)
|
||||
]),
|
||||
SettingList([
|
||||
SettingItem(
|
||||
'是否启用 Satori 协议',
|
||||
@@ -171,13 +175,14 @@ async function onSettingWindowCreated(view: Element) {
|
||||
SettingItem(
|
||||
'上报 Bot 自身发送的消息',
|
||||
'上报 event 为 message_sent',
|
||||
SettingSwitch('reportSelfMessage', config.reportSelfMessage),
|
||||
SettingSwitch('ob11.reportSelfMessage', config.ob11.reportSelfMessage),
|
||||
),
|
||||
SettingItem(
|
||||
'使用 Base64 编码获取文件',
|
||||
'调用 /get_image、/get_record、/get_file 时,没有 url 时添加 Base64 字段',
|
||||
SettingSwitch('enableLocalFile2Url', config.enableLocalFile2Url),
|
||||
),
|
||||
SettingItem('', null, SettingButton('保存', 'config-ob11-save-2', 'primary')),
|
||||
]),
|
||||
SettingList([
|
||||
SettingItem(
|
||||
@@ -217,6 +222,7 @@ async function onSettingWindowCreated(view: Element) {
|
||||
'单位为秒,可用于获取撤回的消息',
|
||||
`<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`,
|
||||
),
|
||||
SettingItem('', null, SettingButton('保存', 'config-ob11-save-3', 'primary')),
|
||||
]),
|
||||
SettingList([
|
||||
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),
|
||||
@@ -395,6 +401,22 @@ async function onSettingWindowCreated(view: Element) {
|
||||
alert('保存成功')
|
||||
})
|
||||
|
||||
doc.querySelector('#config-ob11-save-2')?.addEventListener('click', () => {
|
||||
config.ob11 = ob11Config
|
||||
|
||||
window.llonebot.setConfig(false, config)
|
||||
showError().then()
|
||||
alert('保存成功')
|
||||
})
|
||||
|
||||
doc.querySelector('#config-ob11-save-3')?.addEventListener('click', () => {
|
||||
config.ob11 = ob11Config
|
||||
|
||||
window.llonebot.setConfig(false, config)
|
||||
showError().then()
|
||||
alert('保存成功')
|
||||
})
|
||||
|
||||
doc.body.childNodes.forEach((node) => {
|
||||
view.appendChild(node)
|
||||
})
|
||||
|
@@ -175,10 +175,14 @@ export class MessageEncoder {
|
||||
if (type === 'text') {
|
||||
this.elements.push(SendElement.text(attrs.content))
|
||||
} else if (type === 'at') {
|
||||
this.peer ??= await getPeer(this.ctx, this.channelId)
|
||||
if (this.peer.chatType !== NT.ChatType.Group) {
|
||||
return
|
||||
}
|
||||
if (attrs.type === 'all') {
|
||||
this.elements.push(SendElement.at('', '', NT.AtType.All, '@全体成员'))
|
||||
} else {
|
||||
const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id) ?? ''
|
||||
const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id, this.peer.peerUid) ?? ''
|
||||
const display = attrs.name ? '@' + attrs.name : ''
|
||||
this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display))
|
||||
}
|
||||
|
@@ -56,7 +56,7 @@ export class SatoriServer {
|
||||
|
||||
const { listen, port } = this.config
|
||||
this.httpServer = this.express.listen(port, listen, () => {
|
||||
this.ctx.logger.info(`HTTP server started ${listen}:${port}`)
|
||||
this.ctx.logger.info(`server started ${listen}:${port}`)
|
||||
})
|
||||
this.wsServer = new WebSocketServer({
|
||||
server: this.httpServer
|
||||
@@ -65,7 +65,7 @@ export class SatoriServer {
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
const url = req.url?.split('?').shift()
|
||||
if (!['/v1/events', '/v1/events/'].includes(url!)) {
|
||||
return socket.close()
|
||||
return socket.close(1008, 'invalid address')
|
||||
}
|
||||
|
||||
socket.addEventListener('message', async (event) => {
|
||||
@@ -99,6 +99,15 @@ export class SatoriServer {
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.wsClients.length > 0) {
|
||||
for (const socket of this.wsClients) {
|
||||
try {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.close(1000)
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
if (this.wsServer) {
|
||||
const close = promisify(this.wsServer.close)
|
||||
await close.call(this.wsServer)
|
||||
|
@@ -33,13 +33,6 @@ function decodeMessageUser(data: NT.RawMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
function decodeMessageMember(user: Universal.User, data: NT.RawMessage) {
|
||||
return {
|
||||
user: user,
|
||||
nick: data.sendMemberName || data.sendNickName
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeElement(ctx: Context, data: NT.RawMessage, quoted = false) {
|
||||
const buffer: h[] = []
|
||||
for (const v of data.elements) {
|
||||
@@ -136,6 +129,11 @@ async function decodeElement(ctx: Context, data: NT.RawMessage, quoted = false)
|
||||
type: String(faceType),
|
||||
platform: 'llonebot'
|
||||
}))
|
||||
} else if (v.arkElement) {
|
||||
// llonebot:ark
|
||||
buffer.push(h('llonebot:ark', {
|
||||
data: v.arkElement.bytesData
|
||||
}))
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
@@ -162,11 +160,13 @@ export async function decodeMessage(
|
||||
}
|
||||
message.user = decodeMessageUser(data)
|
||||
message.created_at = +data.msgTime * 1000
|
||||
if (message.channel.type === Universal.Channel.Type.DIRECT) {
|
||||
if (!message.user.name) {
|
||||
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 (message.channel.type === Universal.Channel.Type.DIRECT) {
|
||||
message.channel.name = info.nick
|
||||
}
|
||||
}
|
||||
if (guildId) {
|
||||
message.guild = {
|
||||
@@ -174,7 +174,10 @@ export async function decodeMessage(
|
||||
name: data.peerName,
|
||||
avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`
|
||||
}
|
||||
message.member = decodeMessageMember(message.user, data)
|
||||
message.member = {
|
||||
user: message.user,
|
||||
nick: data.sendMemberName || message.user.name
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
|
@@ -1 +1 @@
|
||||
export const version = '4.0.0'
|
||||
export const version = '4.0.13'
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2023",
|
||||
"module": "CommonJS",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
@@ -20,7 +20,10 @@
|
||||
"./src/ntqqapi/*"
|
||||
]
|
||||
},
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"electron-vite/node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
|
Reference in New Issue
Block a user