Compare commits

..

64 Commits

Author SHA1 Message Date
idranme
936b1d911c Merge pull request #428 from LLOneBot/dev
release: 3.33.0
2024-09-18 20:59:57 +08:00
idranme
58817d1c02 chore: v3.33.0 2024-09-18 20:53:28 +08:00
idranme
2c24422478 feat: support setting remark when agreeing to a friend request 2024-09-18 20:47:45 +08:00
idranme
c2a723380a fix: get_group_member_list API 2024-09-18 19:35:58 +08:00
idranme
156bbaea33 feat: get_group_files_by_folder API 2024-09-18 17:22:09 +08:00
idranme
6c485634e1 feat: get_friend_msg_history API 2024-09-18 16:56:15 +08:00
idranme
f39a9aeafb feat: fetch_custom_face API 2024-09-18 16:11:08 +08:00
idranme
1160cd4b26 feat: fetch_emoji_like API 2024-09-18 15:49:37 +08:00
idranme
9a7ff523dd optimize 2024-09-18 14:07:42 +08:00
idranme
f49995ea97 refactor 2024-09-17 21:04:36 +08:00
idranme
1876dd29ac Merge pull request #423 from LLOneBot/dev
release: 3.32.8
2024-09-17 11:59:57 +08:00
idranme
9944b53266 chore: v3.32.8 2024-09-17 11:55:50 +08:00
idranme
9a791e3a21 fix 2024-09-17 02:17:16 +08:00
idranme
64c5eb6c04 Merge pull request #422 from LLOneBot/dev
release: 3.32.7
2024-09-16 20:48:15 +08:00
idranme
e5750786cb chore: v3.32.7 2024-09-16 20:46:14 +08:00
idranme
18cb46ade5 fix 2024-09-16 20:43:18 +08:00
idranme
e39c89a441 fix 2024-09-16 19:01:59 +08:00
idranme
476d498e44 Merge pull request #417 from LLOneBot/dev
release: 3.32.6
2024-09-15 17:48:35 +08:00
idranme
55446538de chore: v3.32.6 2024-09-15 17:43:10 +08:00
idranme
b965f50653 fix: friend_add event 2024-09-15 16:14:36 +08:00
idranme
2d354c5eda optimize 2024-09-15 14:08:02 +08:00
idranme
536999f296 feat: support for sending contact message segment 2024-09-14 20:13:45 +08:00
idranme
cad09b2ed1 fix 2024-09-14 19:56:46 +08:00
idranme
6be0c11ca2 refactor 2024-09-13 22:58:21 +08:00
idranme
b03bcf9a7c Merge pull request #415 from LLOneBot/dev
release: 3.32.5
2024-09-13 18:59:37 +08:00
idranme
d4f9629af2 chore: v3.32.5 2024-09-13 18:54:56 +08:00
idranme
6d47e2ee80 chore 2024-09-13 18:52:45 +08:00
idranme
506bddb21a chore: style 2024-09-13 17:30:56 +08:00
idranme
91c689baf8 fix 2024-09-13 14:56:30 +08:00
idranme
b7938aaab8 refactor 2024-09-12 22:39:14 +08:00
idranme
b1a892cf4e chore 2024-09-12 18:29:18 +08:00
idranme
9284fc7e8a Merge pull request #413 from LLOneBot/dev
3.32.4
2024-09-12 18:14:23 +08:00
idranme
ceb063143a chore: v3.32.4 2024-09-12 18:11:01 +08:00
idranme
ed55a5a54c optimize 2024-09-12 17:52:21 +08:00
idranme
2c4fdbfa6a fix: upload_group_file API 2024-09-12 16:46:38 +08:00
idranme
1132495eb3 fix: check for updates 2024-09-12 01:11:17 +08:00
idranme
2ac2c68435 chore 2024-09-11 22:13:11 +08:00
idranme
6477366ba6 chore
chore
2024-09-11 21:35:50 +08:00
idranme
1d63473a04 Merge pull request #411 from LLOneBot/dev
3.32.3
2024-09-11 20:52:52 +08:00
idranme
692ba5163e chore: v3.32.3 2024-09-11 20:52:16 +08:00
idranme
8bcea090ec Merge pull request #410 from LLOneBot/main
merge branch
2024-09-11 20:50:38 +08:00
idranme
67568a662d feat: profile_like event 2024-09-11 20:48:32 +08:00
linyuchen
66b3706524 ♻️refactor: rkey server url 2024-09-10 17:35:14 +08:00
idranme
6d82dd1dad chore 2024-09-10 17:16:47 +08:00
idranme
20f2e66b11 chore
chore

chore
2024-09-10 16:59:12 +08:00
idranme
efefb963c0 Merge pull request #409 from LLOneBot/main
merge branch
2024-09-10 15:32:58 +08:00
linyuchen
92d780cf70 fix: download file add referer 2024-09-10 15:01:50 +08:00
idranme
ce6886011f Merge pull request #407 from LLOneBot/dev
3.32.1
2024-09-10 13:44:03 +08:00
idranme
319b275e4f chore: v3.32.1 2024-09-10 13:39:12 +08:00
idranme
9738c3f63c feat: GetProfileLike 2024-09-10 13:36:00 +08:00
idranme
3de054600c chore 2024-09-09 17:06:33 +08:00
idranme
da0ebd3f80 refactor 2024-09-08 23:37:20 +08:00
idranme
d5682a9788 Merge pull request #406 from LLOneBot/dev
3.32.0
2024-09-08 21:11:52 +08:00
idranme
a1298b1c93 optimize 2024-09-08 21:09:03 +08:00
idranme
6fac490d66 chore: v3.32.0 2024-09-08 20:54:32 +08:00
idranme
dc25d83778 feat 2024-09-08 20:50:22 +08:00
idranme
1bcdbba29a feat 2024-09-08 15:56:05 +08:00
idranme
9ecfb6ea0c optimize 2024-09-08 01:18:14 +08:00
idranme
5c68d4de84 Merge pull request #404 from LLOneBot/dev
3.31.10
2024-09-07 21:55:04 +08:00
idranme
c1c22e872e chore: v3.31.10 2024-09-07 21:52:34 +08:00
idranme
709c0b6f7b fix 2024-09-07 21:51:17 +08:00
idranme
dd34286b43 refactor 2024-09-07 17:45:36 +08:00
idranme
80487e31f5 chore 2024-09-07 03:03:33 +08:00
idranme
bdf1c7fd5f chore 2024-09-07 02:56:59 +08:00
125 changed files with 3070 additions and 1772 deletions

7
.gitattributes vendored Normal file
View File

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

View File

@@ -7,9 +7,6 @@ body:
attributes: attributes:
value: | value: |
欢迎来到 LLOneBot 的 Issue Tracker请填写以下表格来提交 Bug。 欢迎来到 LLOneBot 的 Issue Tracker请填写以下表格来提交 Bug。
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
- type: input - type: input
id: system-version id: system-version
attributes: attributes:
@@ -40,8 +37,6 @@ body:
label: OneBot 客户端 label: OneBot 客户端
description: 连接至 LLOneBot 的客户端版本信息 description: 连接至 LLOneBot 的客户端版本信息
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
validations:
required: true
- type: textarea - type: textarea
id: what-happened id: what-happened
attributes: attributes:

3
.gitignore vendored
View File

@@ -11,5 +11,6 @@ node_modules
dist dist
out out
.idea/
.DS_Store .DS_Store
.idea
.vscode

View File

@@ -31,7 +31,6 @@ const config: ElectronViteConfig = {
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
}, },
}, },
plugins: [ plugins: [

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.31.9", "version": "3.33.0",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -7,25 +7,27 @@
"scripts": { "scripts": {
"build": "electron-vite build", "build": "electron-vite build",
"build-mac": "npm run build && npm run deploy-mac", "build-mac": "npm run build && npm run deploy-mac",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/Documents/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win", "build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"", "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .", "format": "prettier -cw .",
"check": "tsc" "check": "tsc",
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.5.0", "@minatojs/driver-sqlite": "^4.6.0",
"compressing": "^1.10.1", "compressing": "^1.10.1",
"cordis": "^3.18.0", "cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.2", "cosmokit": "^1.6.2",
"express": "^4.19.2", "express": "^5.0.0",
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"file-type": "^19.4.1", "file-type": "^19.5.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.1", "minato": "^3.6.0",
"protobufjs": "^7.4.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
@@ -37,8 +39,9 @@
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^31.4.0", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "protobufjs-cli": "^1.1.3",
"vite": "^5.4.3", "typescript": "^5.6.2",
"vite": "^5.4.6",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.1" "packageManager": "yarn@4.4.1"

View File

@@ -35,4 +35,4 @@ const manifest = {
} }
} }
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2)) writeFileSync('manifest.json', JSON.stringify(manifest, null, 2))

View File

@@ -33,7 +33,6 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
enableQOAutoQuote: false,
listenLocalhost: false listenLocalhost: false
} }
const defaultConfig: Config = { const defaultConfig: Config = {

View File

@@ -19,4 +19,4 @@ export const selfInfo: SelfInfo = {
uin: '', uin: '',
nick: '', nick: '',
online: true, online: true,
} }

View File

@@ -10,7 +10,11 @@ export interface OB11Config {
enableWsReverse?: boolean enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string' messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息 /**
* 快速操作回复自动引用原消息
* @deprecated
*/
enableQOAutoQuote?: boolean
listenLocalhost: boolean listenLocalhost: boolean
} }
@@ -70,4 +74,4 @@ export interface FileCacheV2 {
chatType: number chatType: number
elementId: string elementId: string
elementType: number elementType: number
} }

View File

@@ -101,7 +101,7 @@ export async function encodeSilk(ctx: Context, filePath: string) {
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
export async function decodeSilk(ctx: Context, inputFilePath: string, outFormat: OutFormat = 'mp3') { export async function decodeSilk(ctx: Context, inputFilePath: string, outFormat: OutFormat) {
const silk = await fsPromise.readFile(inputFilePath) const silk = await fsPromise.readFile(inputFilePath)
const { data } = await decode(silk, 24000) const { data } = await decode(silk, 24000)
const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath)) const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath))
@@ -115,4 +115,4 @@ export async function decodeSilk(ctx: Context, inputFilePath: string, outFormat:
'-ac 1' '-ac 1'
] ]
}, outFilePath) }, outFilePath)
} }

View File

@@ -1,234 +0,0 @@
import { NodeIQQNTWrapperSession } from '@/ntqqapi/wrapper'
import { randomUUID } from 'node:crypto'
interface Internal_MapKey {
timeout: number
createtime: number
func: (...arg: any[]) => unknown
checker?: (...args: any[]) => boolean
}
export class ListenerClassBase {
[key: string]: string
}
export interface ListenerIBase {
new(listener: unknown): ListenerClassBase
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
public initialised = false
constructor() {
}
createProxyDispatch(ListenerMainName: string) {
const current = this
return new Proxy({}, {
get(target: any, prop: string, receiver: unknown) {
// console.log('get', prop, typeof target[prop])
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: unknown[]) => {
current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver)
}
})
}
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession
this.initialised = true
}
createEventFunction<T extends (...args: any) => unknown>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
}
if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '')
const eventName = eventNameArr[1]
//getNodeIKernelGroupListener,GroupService
//console.log('2', eventName)
const services = (this.WrapperSession as unknown as eventType)[serviceName]()
let event = services[eventName]
//重新绑定this
event = event.bind(services)
if (event) {
return event as T
}
return undefined
}
}
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.createEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
}
return Listener as T
}
//统一回调清理事件
async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: unknown[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid)
return
}
if (task.checker && task.checker(...args)) {
task.func(...args)
}
})
}
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any>>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
reject(new Error('NTEvent EventName:' + EventName + ' timeout'))
}
}, timeout)
const retData = await EventFunc!(...args)
complete = true
resolve(retData)
})
}
async RegisterListen<ListenerType extends (...args: any[]) => void>(ListenerName = '', waitTimes = 1, timeout = 5000, checker: (...args: Parameters<ListenerType>) => boolean) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
const databack = () => {
if (complete == 0) {
reject(new Error(' ListenerName:' + ListenerName + ' timeout'))
} else {
resolve(retData!)
}
}
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++
retData = args
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.createListenerFunction(ListenerMainName)
})
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
let retEvent = {}
const databack = () => {
if (complete == 0) {
reject(new Error('Timeout: NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'))
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!])
}
}
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: unknown[]) => {
complete++
//console.log('func', ...args)
retData = args as Parameters<ListenerType>
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.createListenerFunction(ListenerMainName)
const EventFunc = this.createEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...args)
})
}
}
export const NTEventDispatch = new NTEventWrapper()
// 示例代码 快速创建事件
// let NTEvent = new NTEventWrapper()
// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise<Number>>('NodeIKernelProfileLikeService/GetTest')
// if (TestEvent) {
// TestEvent(true)
// }
// 示例代码 快速创建监听Listener类
// let NTEvent = new NTEventWrapper()
// NTEvent.CreatListenerFunction<NodeIKernelMsgListener>('NodeIKernelMsgListener', 'core')
// 调用接口
//let NTEvent = new NTEventWrapper()
//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise<Number>, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true)
// 注册监听 解除监听
// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb)
// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core')
// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode)
// GetTest('test')
// always模式
// NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode,(...args:any[])=>{ console.log(args) })

View File

@@ -4,6 +4,7 @@ import path from 'node:path'
import { TEMP_DIR } from '../globalVars' import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { fileTypeFromFile } from 'file-type'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4) const buffer = Buffer.alloc(4)
@@ -90,17 +91,26 @@ interface FetchFileRes {
} }
export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> { export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> {
const headers: Record<string, string> = { const headers = new Headers({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname, 'Host': new URL(url).hostname,
...headersInit ...headersInit
} })
const raw = await fetch(url, { headers }).catch((err) => { let raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) { if (err.cause) {
throw err.cause throw err.cause
} }
throw err throw err
}) })
if (raw.status === 403 && !headers.has('Referer')) {
headers.set('Referer', url)
raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
}
if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`) if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return { return {
data: Buffer.from(await raw.arrayBuffer()), data: Buffer.from(await raw.arrayBuffer()),
@@ -116,7 +126,7 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, filename?: string): Promise<Uri2LocalRes> { export async function uri2local(uri: string, filename?: string, needExt?: boolean): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri) const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) { if (type === FileUriType.FileURL) {
@@ -139,8 +149,14 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
} else { } else {
filename ??= randomUUID() filename ??= randomUUID()
} }
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data) await fsPromise.writeFile(filePath, res.data)
if (needExt && !path.extname(filePath)) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e) { } catch (e) {
const errMsg = `${uri} 下载失败, ${(e as Error).message}` const errMsg = `${uri} 下载失败, ${(e as Error).message}`
@@ -150,9 +166,15 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
if (type === FileUriType.OneBotBase64) { if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID() filename ??= randomUUID()
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '') const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
@@ -162,8 +184,14 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
if (capture) { if (capture) {
filename ??= randomUUID() filename ??= randomUUID()
const [, _type, base64] = capture const [, _type, base64] = capture
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
} }

View File

@@ -10,154 +10,153 @@ import { DATA_DIR } from '../globalVars'
import { FileCacheV2 } from '../types' import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables { interface SQLiteTables extends Tables {
message: { message: {
shortId: number shortId: number
msgId: string msgId: string
chatType: number chatType: number
peerUid: string peerUid: string
} }
file_v2: FileCacheV2 file_v2: FileCacheV2
} }
interface MsgIdAndPeerByShortId { interface MsgIdAndPeerByShortId {
MsgId: string MsgId: string
Peer: Peer Peer: Peer
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper { class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number> private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number> private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) { constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap) this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap) this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
} }
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
}
async init(uin: string) { async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const dbDir = path.join(DATA_DIR, 'database') const heads = this.msgIdMap.getHeads(size)
if (!fs.existsSync(dbDir)) { if (!heads) {
await fsPromise.mkdir(dbDir) return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
} }
const database = new Database<SQLiteTables>() }
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
} }
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> { getShortIdByMsgId(msgId: string): number | undefined {
const heads = this.msgIdMap.getHeads(size) return this.msgIdMap.getValue(msgId)
if (!heads) { }
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number { async getPeerByMsgId(msgId: string) {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}` const shortId = this.msgIdMap.getValue(msgId)
const hash = createHash('md5').update(key).digest() if (!shortId) return undefined
//设置第一个bit为0 保证shortId为正数 return await this.getMsgIdAndPeerByShortId(shortId)
hash[0] &= 0x7f }
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> { resize(maxSize: number): void {
const data = this.msgDataMap.getKey(shortId) this.msgIdMap.resize(maxSize)
if (data) { this.msgDataMap.resize(maxSize)
const [msgId, chatTypeStr, peerUid] = data.split('|') }
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined { addFileCache(data: FileCacheV2) {
return this.msgIdMap.getValue(msgId) return this.db?.upsert('file_v2', [data], 'fileUuid')
} }
async getPeerByMsgId(msgId: string) { getFileCacheByName(fileName: string) {
const shortId = this.msgIdMap.getValue(msgId) return this.db?.get('file_v2', { fileName }, {
if (!shortId) return undefined sort: { msgTime: 'desc' }
return await this.getMsgIdAndPeerByShortId(shortId) })
} }
resize(maxSize: number): void { getFileCacheById(fileUuid: string) {
this.msgIdMap.resize(maxSize) return this.db?.get('file_v2', { fileUuid })
this.msgDataMap.resize(maxSize) }
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
} }
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper() export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -1,5 +1,5 @@
import { QQLevel } from '@/ntqqapi/types' import { QQLevel } from '@/ntqqapi/types'
import { Dict } from 'cosmokit' import { Dict, isNullable } from 'cosmokit'
export function isNumeric(str: string) { export function isNumeric(str: string) {
return /^\d+$/.test(str) return /^\d+$/.test(str)
@@ -32,4 +32,8 @@ export function mergeNewProperties(newObj: Dict, oldObj: Dict) {
} }
} }
}) })
} }
export function filterNullable<T>(array: T[]) {
return array.filter(e => !isNullable(e)) as NonNullable<T>[]
}

View File

@@ -1,72 +1,71 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> { export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map() private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map() private valueToKey: Map<V, K> = new Map()
private maxSize: number private maxSize: number
constructor(maxSize: number) { constructor(maxSize: number) {
this.maxSize = maxSize this.maxSize = maxSize
} }
resize(count: number) { resize(count: number) {
this.maxSize = count this.maxSize = count
} }
set(key: K, value: V): void { set(key: K, value: V): void {
this.keyToValue.set(key, value) this.keyToValue.set(key, value)
this.valueToKey.set(value, key) this.valueToKey.set(value, key)
while (this.keyToValue.size !== this.valueToKey.size) { while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom') this.keyToValue.clear()
this.keyToValue.clear() this.valueToKey.clear()
this.valueToKey.clear()
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value
this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey)
}
} }
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value!
this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey)
}
}
getValue(key: K): V | undefined { getValue(key: K): V | undefined {
return this.keyToValue.get(key) return this.keyToValue.get(key)
} }
getKey(value: V): K | undefined { getKey(value: V): K | undefined {
return this.valueToKey.get(value) return this.valueToKey.get(value)
} }
deleteByValue(value: V): void { deleteByValue(value: V): void {
const key = this.valueToKey.get(value) const key = this.valueToKey.get(value)
if (key !== undefined) { if (key !== undefined) {
this.keyToValue.delete(key) this.keyToValue.delete(key)
this.valueToKey.delete(value) this.valueToKey.delete(value)
}
} }
}
deleteByKey(key: K): void { deleteByKey(key: K): void {
const value = this.keyToValue.get(key) const value = this.keyToValue.get(key)
if (value !== undefined) { if (value !== undefined) {
this.keyToValue.delete(key) this.keyToValue.delete(key)
this.valueToKey.delete(value) this.valueToKey.delete(value)
}
} }
}
getKeyList(): K[] { getKeyList(): K[] {
return Array.from(this.keyToValue.keys()) return Array.from(this.keyToValue.keys())
} }
//获取最近刚写入的几个值 //获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined { getHeads(size: number): { key: K, value: V }[] | undefined {
const keyList = this.getKeyList() const keyList = this.getKeyList()
if (keyList.length === 0) { if (keyList.length === 0) {
return undefined return undefined
}
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
} }
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
}
} }

View File

@@ -5,20 +5,22 @@ import { version } from '../../version'
import { copyFolder, log, fetchFile } from '.' import { copyFolder, log, fetchFile } from '.'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars' import { PLUGIN_DIR, TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/'] const downloadMirrorHosts = ['https://ghp.ci/']
const checkVersionMirrorHosts = ['https://kkgithub.com'] const releasesMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion() const latestVersionText = await getRemoteVersion()
const latestVersion = latestVersionText.split('.') const latestVersion = latestVersionText.split('.')
//log('llonebot last version', latestVersion) log('LLOneBot latest version', latestVersion)
const currentVersion: string[] = version.split('.') const currentVersion = version.split('.')
//log('llonebot current version', currentVersion) //log('llonebot current version', currentVersion)
for (const k of [0, 1, 2]) { for (const k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) { const latest = parseInt(latestVersion[k])
const current = parseInt(currentVersion[k])
if (latest > current) {
log('') log('')
return { result: true, version: latestVersionText } return { result: true, version: latestVersionText }
} else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) { } else if (latest < current) {
break break
} }
} }
@@ -28,7 +30,7 @@ export async function checkNewVersion() {
export async function upgradeLLOneBot() { export async function upgradeLLOneBot() {
const latestVersion = await getRemoteVersion() const latestVersion = await getRemoteVersion()
if (latestVersion && latestVersion != '') { if (latestVersion && latestVersion != '') {
const downloadUrl = 'https://github.com/LLOneBot/LLOneBot/releases/download/v' + latestVersion + '/LLOneBot.zip' const downloadUrl = `https://github.com/LLOneBot/LLOneBot/releases/download/v${latestVersion}/LLOneBot.zip`
const filePath = path.join(TEMP_DIR, './update-' + latestVersion + '.zip') const filePath = path.join(TEMP_DIR, './update-' + latestVersion + '.zip')
let downloadSuccess = false let downloadSuccess = false
// 多镜像下载 // 多镜像下载
@@ -73,26 +75,34 @@ export async function upgradeLLOneBot() {
} }
export async function getRemoteVersion() { export async function getRemoteVersion() {
let Version = '' for (const mirror of releasesMirrorHosts) {
for (let i = 0; i < checkVersionMirrorHosts.length; i++) { const version = await getRemoteVersionByReleasesMirror(mirror)
const mirrorGithub = checkVersionMirrorHosts[i] if (version) {
const tVersion = await getRemoteVersionByMirror(mirrorGithub) return version
if (tVersion && tVersion != '') { }
Version = tVersion }
break for (const mirror of downloadMirrorHosts) {
const version = await getRemoteVersionByDownloadMirror(mirror)
if (version) {
return version
} }
} }
return Version
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = 'error'
try {
releasePage = (await fetchFile(mirrorGithub + '/LLOneBot/LLOneBot/releases')).data.toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch { }
return '' return ''
} }
export async function getRemoteVersionByDownloadMirror(mirrorGithub: string) {
try {
const source = 'https://raw.githubusercontent.com/LLOneBot/LLOneBot/main/src/version.ts'
const page = (await fetchFile(mirrorGithub + source)).data.toString()
return page.match(/(\d+\.\d+\.\d+)/)?.[0]
} catch (e) {
log(e?.toString())
}
}
export async function getRemoteVersionByReleasesMirror(mirrorGithub: string) {
try {
const page = (await fetchFile(mirrorGithub + '/LLOneBot/LLOneBot/releases')).data.toString()
return page.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch { }
}

2
src/global.d.ts vendored
View File

@@ -6,4 +6,4 @@ declare global {
var LiteLoader: Dict var LiteLoader: Dict
var authData: Dict | undefined var authData: Dict | undefined
var navigation: Dict | undefined var navigation: Dict | undefined
} }

View File

@@ -14,25 +14,22 @@ export default class Log {
constructor(ctx: Context, cfg: Config) { constructor(ctx: Context, cfg: Config) {
Logger.targets.splice(0, Logger.targets.length) Logger.targets.splice(0, Logger.targets.length)
if (!cfg.enable) { let enable = cfg.enable
return
}
const file = path.join(LOG_DIR, cfg.filename) const file = path.join(LOG_DIR, cfg.filename)
/*const refreshNick = ctx.debounce(() => {
const ntUserApi = ctx.get('ntUserApi')
if (ntUserApi && !selfInfo.nick) {
ntUserApi.getSelfNick(true)
}
}, 1000)*/
const target: Logger.Target = { const target: Logger.Target = {
colors: 0, colors: 0,
record: (record: Logger.Record) => { record: (record: Logger.Record) => {
if (!enable) {
return
}
const dateTime = new Date(record.timestamp).toLocaleString() const dateTime = new Date(record.timestamp).toLocaleString()
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' const content = `${dateTime} [${record.type}] ${selfInfo.nick}(${selfInfo.uin}) | ${record.name} ${record.content}\n\n`
const content = `${dateTime} [${record.type}] ${userInfo} | ${record.name} ${record.content}\n\n`
appendFile(file, content, noop) appendFile(file, content, noop)
}, },
} }
Logger.targets.push(target) Logger.targets.push(target)
ctx.on('llonebot/config-updated', input => {
enable = input.log!
})
} }
} }

View File

@@ -202,7 +202,7 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (![2, 4].includes(window.id)) { if (![2, 4, 6].includes(window.id)) {
return return
} }
if (window.id === 2) { if (window.id === 2) {

View File

@@ -13,16 +13,14 @@ import {
PicElement, PicElement,
} from '../types' } from '../types'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import { existsSync } from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { RkeyManager } from '@/ntqqapi/helper/rkey' import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg' import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type' import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises' import { copyFile, stat, unlink } from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { TEMP_DIR } from '@/common/globalVars' import { TEMP_DIR } from '@/common/globalVars'
@@ -39,7 +37,7 @@ export class NTQQFileApi extends Service {
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntFileApi', true) super(ctx, 'ntFileApi', true)
this.rkeyManager = new RkeyManager(ctx, 'http://napcat-sign.wumiao.wang:2082/rkey') this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey')
} }
async getVideoUrl(peer: Peer, msgId: string, elementId: string) { async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
@@ -112,8 +110,8 @@ export class NTQQFileApi extends Service {
}, },
}]) }])
} }
await fsPromise.copyFile(filePath, mediaPath) await copyFile(filePath, mediaPath)
const fileSize = (await fsPromise.stat(filePath)).size const fileSize = (await stat(filePath)).size
return { return {
md5: fileMd5, md5: fileMd5,
fileName, fileName,
@@ -134,85 +132,41 @@ export class NTQQFileApi extends Service {
force = false force = false
) { ) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && existsSync(sourcePath)) {
if (force) { if (force) {
try { try {
await fsPromise.unlink(sourcePath) await unlink(sourcePath)
} catch { } } catch { }
} else { } else {
return sourcePath return sourcePath
} }
} }
let filePath: string const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
if (NTEventDispatch.initialised) { 'nodeIKernelMsgService/downloadRichMedia',
const data = await NTEventDispatch.CallNormalEvent< [
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{ {
fileModelId: '0', getReq: {
downloadSourceType: 0, fileModelId: '0',
triggerType: 1, downloadSourceType: 0,
msgId: msgId, triggerType: 1,
chatType: chatType, msgId: msgId,
peerUid: peerUid, chatType: chatType,
elementId: elementId, peerUid: peerUid,
thumbSize: 0, elementId: elementId,
downloadType: 1, thumbSize: 0,
filePath: thumbPath downloadType: 1,
} filePath: thumbPath,
)
filePath = data[1].filePath
} else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
'nodeIKernelMsgService/downloadRichMedia',
[
{
getReq: {
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
}, },
null, },
], null,
{ ],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, {
cmdCB: payload => payload.notifyInfo.msgId === msgId, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
timeout cmdCB: payload => payload.notifyInfo.msgId === msgId,
} timeout
) }
filePath = data.notifyInfo.filePath )
} let filePath = data.notifyInfo.filePath
if (filePath.startsWith('\\')) { if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath) filePath = path.join(downloadPath, filePath)
@@ -238,7 +192,7 @@ export class NTQQFileApi extends Service {
const url: string = element.originImageUrl! // 没有域名 const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr const fileMd5 = element.md5HexStr
if (url) { if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = parsedUrl.searchParams.get('appid') const imageAppid = parsedUrl.searchParams.get('appid')

View File

@@ -2,8 +2,7 @@ import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType } from '../services'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { Dict, pick } from 'cosmokit' import { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@@ -19,7 +18,7 @@ export class NTQQFriendApi extends Service {
} }
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends(_forced = false) { async getFriends() {
const data = await invoke<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
@@ -43,13 +42,7 @@ export class NTQQFriendApi extends Service {
return _friends return _friends
} }
async handleFriendRequest(flag: string, accept: boolean) { async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
const data = flag.split('|')
if (data.length < 2) {
return
}
const friendUid = data[0]
const reqTime = data[1]
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getBuddyService().approvalFriendRequest({ return session.getBuddyService().approvalFriendRequest({
@@ -75,9 +68,7 @@ export class NTQQFriendApi extends Service {
const buddyService = session.getBuddyService() const buddyService = session.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids)) uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values()) return Array.from(data.values())
} else { } else {
const data = await invoke<{ const data = await invoke<{
@@ -92,11 +83,8 @@ export class NTQQFriendApi extends Service {
afterFirstCmd: false, afterFirstCmd: false,
} }
) )
const categoryUids: Map<number, string[]> = new Map() const uids = data.buddyCategory.flatMap(item => item.buddyUids)
for (const item of data.buddyCategory) { return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
categoryUids.set(item.categoryId, item.buddyUids)
}
return Object.values(data.userSimpleInfos).filter(v => v.baseInfo && categoryUids.get(v.baseInfo.categoryId)?.includes(v.uid!))
} }
} }
@@ -106,12 +94,10 @@ export class NTQQFriendApi extends Service {
const session = getSession() const session = getSession()
if (session) { if (session) {
const uids: string[] = [] const uids: string[] = []
const buddyService = session?.getBuddyService() const buddyService = session.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids)) uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
for (const [, item] of data) { for (const [, item] of data) {
if (retMap.size > 5000) { if (retMap.size > 5000) {
break break
@@ -155,9 +141,7 @@ export class NTQQFriendApi extends Service {
}) })
return item.buddyUids return item.buddyUids
})) }))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => { return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key) const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
@@ -199,4 +183,15 @@ export class NTQQFriendApi extends Service {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null]) return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
} }
} }
async getBuddyRecommendContact(uin: string) {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }, null])
return ret.arkMsg
}
async setBuddyRemark(uid: string, remark: string) {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark }
}, null])
}
} }

View File

@@ -1,10 +1,18 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify, GetFileListParam } from '../types' import {
Group,
GroupMember,
GroupMemberRole,
GroupNotifies,
GroupRequestOperateTypes,
GetFileListParam,
OnGroupFileInfoUpdateParams,
PublishGroupBulletinReq
} from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window' import { NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { OnGroupFileInfoUpdateParams } from '../listeners'
import { NodeIKernelGroupService } from '../services' import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc' import { isNumeric } from '@/common/utils/misc'
@@ -24,7 +32,7 @@ export class NTQQGroupApi extends Service {
super(ctx, 'ntGroupApi', true) super(ctx, 'ntGroupApi', true)
} }
async getGroups(forced = false): Promise<Group[]> { async getGroups(): Promise<Group[]> {
const result = await invoke<{ const result = await invoke<{
updateType: number updateType: number
groupList: Group[] groupList: Group[]
@@ -117,17 +125,15 @@ export class NTQQGroupApi extends Service {
const type = parseInt(flagitem[2]) const type = parseInt(flagitem[2])
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().operateSysNotify( return session.getGroupService().operateSysNotify(false, {
false, operateType, // 2 拒绝
{ targetMsg: {
'operateType': operateType, // 2 拒绝 seq, // 通知序列号
'targetMsg': { type,
'seq': seq, // 通知序列号 groupCode,
'type': type, postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
'groupCode': groupCode, }
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 })
}
})
} else { } else {
return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{ return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
doubt: false, doubt: false,
@@ -265,8 +271,8 @@ export class NTQQGroupApi extends Service {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null]) return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null])
} }
async deleteGroupFile(groupId: string, fileIdList: string[]) { async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList: [102], fileIdList }, null]) return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }, null])
} }
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) { async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
@@ -288,4 +294,21 @@ export class NTQQGroupApi extends Service {
) )
return data.fileInfo.item return data.fileInfo.item
} }
async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }, null])
}
async uploadGroupBulletinPic(groupCode: string, path: string) {
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }, null])
}
async getGroupRecommendContact(groupCode: string) {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null])
return ret.arkJson
}
} }

View File

@@ -183,7 +183,7 @@ export class NTQQMsgApi extends Service {
if (!arkElement) { if (!arkElement) {
continue continue
} }
const forwardData = JSON.parse(arkElement.arkElement.bytesData) const forwardData = JSON.parse(arkElement.arkElement!.bytesData)
if (forwardData.app != 'com.tencent.multimsg') { if (forwardData.app != 'com.tencent.multimsg') {
continue continue
} }
@@ -252,4 +252,27 @@ export class NTQQMsgApi extends Service {
} }
}, null]) }, null])
} }
async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null])
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
return await invoke('nodeIKernelMsgService/getMsgEmojiLikesList', [{
peer,
msgSeq,
emojiId,
emojiType,
cnt: count
}, null])
}
async fetchFavEmojiList(count: number) {
return await invoke('nodeIKernelMsgService/fetchFavEmojiList', [{
resId: '',
count,
backwardFetch: true,
forceRefresh: true
}, null])
}
} }

View File

@@ -65,7 +65,7 @@ export class NTQQUserApi extends Service {
return ret return ret
} }
async getUserDetailInfo(uid: string, _getLevel = false) { async getUserDetailInfo(uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return this.fetchUserDetailInfo(uid)
} }
@@ -87,17 +87,6 @@ export class NTQQUserApi extends Service {
return result.info return result.info
} }
async getSkey(): Promise<string> {
const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败')
}
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey
}
async getCookies(domain: string) { async getCookies(domain: string) {
const clientKeyData = await this.forceFetchClientKey() const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) { if (clientKeyData?.result !== 0) {
@@ -109,6 +98,10 @@ export class NTQQUserApi extends Service {
return cookies return cookies
} }
async getPSkey(domains: string[]) {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null])
}
genBkn(sKey: string) { genBkn(sKey: string) {
sKey = sKey || '' sKey = sKey || ''
let hash = 5381 let hash = 5381
@@ -292,4 +285,19 @@ export class NTQQUserApi extends Service {
} }
}, null]) }, null])
} }
async getProfileLike(uid: string) {
return await invoke('nodeIKernelProfileLikeService/getBuddyProfileLike', [{
req: {
friendUids: [uid],
basic: 1,
vote: 1,
favorite: 0,
userProfile: 1,
type: 2,
start: 0,
limit: 20,
}
}, null])
}
} }

View File

@@ -1,6 +1,6 @@
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { ReceiveCmd } from '../hook' import { ReceiveCmdS } from '../hook'
import { BrowserWindow } from 'electron' import { BrowserWindow } from 'electron'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@@ -39,7 +39,7 @@ export class NTQQWindowApi extends Service {
async openWindow<R = GeneralCallResult>( async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: unknown[], args: unknown[],
cbCmd: ReceiveCmd | undefined, cbCmd: ReceiveCmdS | undefined,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await invoke<R>( const result = await invoke<R>(

View File

@@ -2,8 +2,6 @@ import fs from 'node:fs'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook' import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { MessageUnique } from '../common/utils/messageUnique' import { MessageUnique } from '../common/utils/messageUnique'
import { NTEventDispatch } from '../common/utils/eventTask'
import { wrapperConstructor, getSession } from './wrapper'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars' import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc' import { isNumeric } from '../common/utils/misc'
@@ -21,6 +19,7 @@ import {
} from './types' } from './types'
import { selfInfo } from '../common/globalVars' import { selfInfo } from '../common/globalVars'
import { version } from '../version' import { version } from '../version'
import { invoke } from './ntcall'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@@ -28,11 +27,12 @@ declare module 'cordis' {
} }
interface Events { interface Events {
'nt/message-created': (input: RawMessage[]) => void 'nt/message-created': (input: RawMessage[]) => void
'nt/message-deleted': (input: RawMessage[]) => void 'nt/message-deleted': (input: RawMessage) => void
'nt/message-sent': (input: RawMessage[]) => void 'nt/message-sent': (input: RawMessage) => void
'nt/group-notify': (input: GroupNotify[]) => void 'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest[]) => void 'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string; members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/system-message-created': (input: Uint8Array) => void
} }
} }
@@ -45,10 +45,6 @@ class Core extends Service {
public start() { public start() {
llonebotError.otherError = '' llonebotError.otherError = ''
const WrapperSession = getSession()
if (WrapperSession) {
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession })
}
MessageUnique.init(selfInfo.uin) MessageUnique.init(selfInfo.uin)
this.registerListener() this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`) this.ctx.logger.info(`LLOneBot/${version}`)
@@ -100,7 +96,7 @@ class Core extends Service {
} }
for (const path of pathList) { for (const path of pathList) {
if (path) { if (path) {
fs.unlink(picPath, () => { fs.unlink(path, () => {
this.ctx.logger.info('删除文件成功', path) this.ctx.logger.info('删除文件成功', path)
}) })
} }
@@ -178,29 +174,26 @@ class Core extends Service {
this.ctx.parallel('nt/message-created', payload.msgList) this.ctx.parallel('nt/message-created', payload.msgList)
}) })
const sentMsgIds = new Map<string, boolean>()
const recallMsgIds: string[] = [] // 避免重复上报 const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => { registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
const list = payload.msgList.filter(v => { for (const msg of payload.msgList) {
if (recallMsgIds.includes(v.msgId)) { if (msg.recallTime !== '0' && !recallMsgIds.includes(msg.msgId)) {
return false recallMsgIds.push(msg.msgId)
this.ctx.parallel('nt/message-deleted', msg)
} else if (sentMsgIds.get(msg.msgId)) {
sentMsgIds.delete(msg.msgId)
this.ctx.parallel('nt/message-sent', msg)
} }
recallMsgIds.push(v.msgId) }
return true
})
this.ctx.parallel('nt/message-deleted', list)
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
const { msgId, chatType, peerUid } = payload.msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
if (!this.config.reportSelfMessage) { if (!this.config.reportSelfMessage) {
return return
} }
this.ctx.parallel('nt/message-sent', [payload.msgRecord]) sentMsgIds.set(payload.msgRecord.msgId, true)
}) })
const groupNotifyFlags: string[] = [] const groupNotifyFlags: string[] = []
@@ -231,6 +224,14 @@ class Core extends Service {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs) this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
}) })
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { classNameIsRegister: true })
registerReceiveHook<{
msgBuf: number[]
}>('nodeIKernelMsgListener/onRecvSysMsg', payload => {
this.ctx.parallel('nt/system-message-created', Uint8Array.from(payload.msgBuf))
})
} }
} }
@@ -239,4 +240,4 @@ namespace Core {
} }
} }
export default Core export default Core

View File

@@ -99,19 +99,20 @@ export namespace SendElementEntities {
} }
} }
export async function file(ctx: Context, filePath: string, fileName = '', folderId = ''): Promise<SendFileElement> { export async function file(ctx: Context, filePath: string, fileName: string, folderId = ''): Promise<SendFileElement> {
const { fileName: _fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(filePath, ElementType.FILE) const fileSize = (await stat(filePath)).size.toString()
if (fileSize === 0) { if (fileSize === '0') {
throw '文件异常,大小为 0' ctx.logger.warn(`文件${fileName}异常,大小为 0`)
throw new Error('文件异常,大小为 0')
} }
const element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName: fileName || _fileName, fileName,
folderId: folderId, folderId,
filePath: path!, filePath,
fileSize: fileSize.toString(), fileSize,
}, },
} }
return element return element
@@ -201,7 +202,7 @@ export namespace SendElementEntities {
// log("生成缩略图", _thumbPath) // log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
let element: SendVideoElement = { const element: SendVideoElement = {
elementType: ElementType.VIDEO, elementType: ElementType.VIDEO,
elementId: '', elementId: '',
videoElement: { videoElement: {

View File

@@ -1,4 +1,4 @@
import { Context } from "cordis" import { Context } from 'cordis'
interface ServerRkeyData { interface ServerRkeyData {
group_rkey: string group_rkey: string

View File

@@ -6,48 +6,46 @@ import { Dict } from 'cosmokit'
export const hookApiCallbacks: Record<string, (res: any) => void> = {} export const hookApiCallbacks: Record<string, (res: any) => void> = {}
export const ReceiveCmdS = { export enum ReceiveCmdS {
RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2', RECENT_CONTACT = 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2',
UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate', UPDATE_MSG = 'nodeIKernelMsgListener/onMsgInfoListUpdate',
UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate', UPDATE_ACTIVE_MSG = 'nodeIKernelMsgListener/onActiveMsgInfoUpdate',
NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, NEW_MSG = 'nodeIKernelMsgListener/onRecvMsg',
NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`, NEW_ACTIVE_MSG = 'nodeIKernelMsgListener/onRecvActiveMsg',
SELF_SEND_MSG: 'nodeIKernelMsgListener/onAddSendMsg', SELF_SEND_MSG = 'nodeIKernelMsgListener/onAddSendMsg',
USER_INFO: 'nodeIKernelProfileListener/onProfileSimpleChanged', USER_INFO = 'nodeIKernelProfileListener/onProfileSimpleChanged',
USER_DETAIL_INFO: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', USER_DETAIL_INFO = 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
GROUPS: 'nodeIKernelGroupListener/onGroupListUpdate', GROUPS = 'nodeIKernelGroupListener/onGroupListUpdate',
GROUPS_STORE: 'onGroupListUpdate', GROUPS_STORE = 'onGroupListUpdate',
GROUP_MEMBER_INFO_UPDATE: 'nodeIKernelGroupListener/onMemberInfoChange', GROUP_MEMBER_INFO_UPDATE = 'nodeIKernelGroupListener/onMemberInfoChange',
FRIENDS: 'onBuddyListChange', FRIENDS = 'onBuddyListChange',
MEDIA_DOWNLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaDownloadComplete', MEDIA_DOWNLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaDownloadComplete',
UNREAD_GROUP_NOTIFY: 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated', UNREAD_GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated',
GROUP_NOTIFY: 'nodeIKernelGroupListener/onGroupSingleScreenNotifies', GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupSingleScreenNotifies',
FRIEND_REQUEST: 'nodeIKernelBuddyListener/onBuddyReqChange', FRIEND_REQUEST = 'nodeIKernelBuddyListener/onBuddyReqChange',
SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate', SKEY_UPDATE = 'onSkeyUpdate',
} as const }
export type ReceiveCmd = string type NTReturnData = [
{
interface NTQQApiReturnData extends Array<unknown> {
0: {
type: 'request' type: 'request'
eventName: NTClass eventName: NTClass
callbackId?: string callbackId?: string
} },
1: { {
cmdName: ReceiveCmd cmdName: ReceiveCmdS
cmdType: 'event' cmdType: 'event'
payload: unknown payload: unknown
}[] }[]
} ]
const logHook = false const logHook = false
const receiveHooks: Array<{ const receiveHooks: Array<{
method: ReceiveCmd[] method: ReceiveCmdS[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
id: string id: string
}> = [] }> = []
@@ -58,62 +56,44 @@ const callHooks: Array<{
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) { export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
const originalSend = window.webContents.send window.webContents.send = new Proxy(window.webContents.send, {
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
try { try {
const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) {
if (logHook && !isLogger) { log('received ntqq api message', args)
log(`received ntqq api message: ${channel}`, args) }
} } catch { }
} catch { } if (!onlyLog) {
if (!onlyLog) { if (args[2] instanceof Array) {
if (args?.[1] instanceof Array) { for (const receiveData of args[2]) {
for (const receiveData of args[1]) { const ntMethodName = receiveData.cmdName
const ntQQApiMethodName = receiveData.cmdName for (const hook of receiveHooks) {
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) if (hook.method.includes(ntMethodName)) {
for (const hook of receiveHooks) { Promise.resolve(hook.hookFunc(receiveData.payload))
if (hook.method.includes(ntQQApiMethodName)) { }
new Promise(resolve => {
try {
hook.hookFunc(receiveData.payload)
} catch (e) {
log('hook error', ntQQApiMethodName, (e as Error).stack?.toString())
}
resolve(undefined)
}).then()
} }
} }
} }
} if (args[1]?.callbackId) {
if (args[0]?.callbackId) { const callbackId = args[1].callbackId
// log("hookApiCallback", hookApiCallbacks, args) if (hookApiCallbacks[callbackId]) {
const callbackId = args[0].callbackId Promise.resolve(hookApiCallbacks[callbackId](args[2]))
if (hookApiCallbacks[callbackId]) { delete hookApiCallbacks[callbackId]
new Promise(resolve => { }
hookApiCallbacks[callbackId](args[1])
resolve(undefined)
}).then()
delete hookApiCallbacks[callbackId]
} }
} }
} return target.apply(thisArg, args)
originalSend.call(window.webContents, channel, ...args) },
} })
window.webContents.send = patchSend
} }
export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) { export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
// 监听调用NTQQApi
const webContents = window.webContents as Dict const webContents = window.webContents as Dict
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message'] const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
const proxyIpcMsg = new Proxy(ipc_message_proxy, { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(thisArg, args); const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
let isLogger = false
try {
isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) { }
if (!isLogger) { if (!isLogger) {
try { try {
logHook && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', thisArg, args)
@@ -121,21 +101,14 @@ export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
if (!onlyLog) { if (!onlyLog) {
try { try {
const _args: unknown[] = args[3][1] const _args: unknown[] = args[3][1]
const cmdName: NTMethod = _args[0] as NTMethod const cmdName = _args[0] as NTMethod
const callParams = _args.slice(1) const callParams = _args.slice(1)
callHooks.forEach((hook) => { callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) { if (hook.method.includes(cmdName)) {
new Promise(resolve => { Promise.resolve(hook.hookFunc(callParams))
try {
hook.hookFunc(callParams)
} catch (e) {
log('hook call error', e, _args)
}
resolve(undefined)
}).then()
} }
}) })
} catch (e) { } } catch { }
} }
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
@@ -147,7 +120,7 @@ export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
webContents._events['-ipc-message'] = proxyIpcMsg webContents._events['-ipc-message'] = proxyIpcMsg
} }
const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke'] /*const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args) //HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
@@ -157,9 +130,7 @@ export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
}, },
}) })
const ret = target.apply(thisArg, args) const ret = target.apply(thisArg, args)
/*try { //HOOK_LOG && log('call NTQQ invoke api return', ret)
HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { }*/
return ret return ret
}, },
}) })
@@ -167,11 +138,11 @@ export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
webContents._events['-ipc-invoke'][0] = proxyIpcInvoke webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
} else { } else {
webContents._events['-ipc-invoke'] = proxyIpcInvoke webContents._events['-ipc-invoke'] = proxyIpcInvoke
} }*/
} }
export function registerReceiveHook<PayloadType>( export function registerReceiveHook<PayloadType>(
method: ReceiveCmd | ReceiveCmd[], method: string | string[],
hookFunc: (payload: PayloadType) => void, hookFunc: (payload: PayloadType) => void,
): string { ): string {
const id = randomUUID() const id = randomUUID()
@@ -179,7 +150,7 @@ export function registerReceiveHook<PayloadType>(
method = [method] method = [method]
} }
receiveHooks.push({ receiveHooks.push({
method, method: method as ReceiveCmdS[],
hookFunc, hookFunc,
id, id,
}) })
@@ -202,4 +173,4 @@ export function registerCallHook(
export function removeReceiveHook(id: string) { export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex((h) => h.id === id) const index = receiveHooks.findIndex((h) => h.id === id)
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1)
} }

View File

@@ -1,58 +0,0 @@
import { Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/ntqqapi/types'
export interface IGroupListener {
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): void
onGroupExtListUpdate(...args: unknown[]): void
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): void
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): void
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): void
onGroupDetailInfoChange(...args: unknown[]): void
onGroupAllInfoChange(...args: unknown[]): void
onGroupsMsgMaskResult(...args: unknown[]): void
onGroupConfMemberChange(...args: unknown[]): void
onGroupBulletinChange(...args: unknown[]): void
onGetGroupBulletinListResult(...args: unknown[]): void
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>,
finish: boolean,
hasRobot: boolean
}): void
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>): void
onSearchMemberChange(...args: unknown[]): void
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): void
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): void
onGroupStatisticInfoChange(...args: unknown[]): void
onJoinGroupNotify(...args: unknown[]): void
onShutUpMemberListChanged(...args: unknown[]): void
onGroupBulletinRemindNotify(...args: unknown[]): void
onGroupFirstBulletinNotify(...args: unknown[]): void
onJoinGroupNoVerifyFlag(...args: unknown[]): void
onGroupArkInviteStateResult(...args: unknown[]): void
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void
}

View File

@@ -1,268 +0,0 @@
import { ChatType, RawMessage } from '@/ntqqapi/types'
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string,
msgElementId: string,
msgId: string,
fileId: string,
fileProgress: string, // '0'
fileSpeed: string, // '0'
fileErrCode: string, // '0'
fileErrMsg: string,
fileDownType: number, // 暂时未知
thumbSize: number,
filePath: string,
totalSize: string,
trasferStatus: number,
step: number,
commonFileInfo: unknown | null,
fileSrvErrCode: string,
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown | null,
userUsedSpacePerDay: unknown | null
}
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
}
// {
// sessionType: 1,
// chatType: 100,
// peerUid: 'u_PVQ3tl6K78xxxx',
// groupCode: '809079648',
// fromNick: '拾xxxx,
// sig: '0x'
// }
export interface TempOnRecvParams {
sessionType: number,//1
chatType: ChatType,//100
peerUid: string,//uid
groupCode: string,//gc
fromNick: string,//gc name
sig: string,
}
export interface IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage): void
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): void
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): void
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): void
onContactUnreadCntUpdate(hashMap: unknown): void
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): void
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): void
onEmojiDownloadComplete(emojiNotifyInfo: unknown): void
onEmojiResourceUpdate(emojiResourceInfo: unknown): void
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFileMsgCome(arrayList: unknown): void
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFirstViewGroupGuildMapping(arrayList: unknown): void
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): void
onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: OnGroupFileInfoUpdateParams): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
onGroupTransferInfoAdd(groupItem: unknown): void
onGroupTransferInfoUpdate(groupFileListResult: unknown): void
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): void
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): void
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): void
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): void
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): void
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): void
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): void
onInputStatusPush(inputStatusInfo: unknown): void
onKickedOffLine(kickedInfo: unknown): void
onLineDev(arrayList: unknown): void
onLogLevelChanged(j2: unknown): void
onMsgAbstractUpdate(arrayList: unknown): void
onMsgBoxChanged(arrayList: unknown): void
onMsgDelete(contact: unknown, arrayList: unknown): void
onMsgEventListUpdate(hashMap: unknown): void
onMsgInfoListAdd(arrayList: unknown): void
onMsgInfoListUpdate(msgList: RawMessage[]): void
onMsgQRCodeStatusChanged(i2: unknown): void
onMsgRecall(i2: unknown, str: unknown, j2: unknown): void
onMsgSecurityNotify(msgRecord: unknown): void
onMsgSettingUpdate(msgSetting: unknown): void
onNtFirstViewMsgSyncEnd(): void
onNtMsgSyncEnd(): void
onNtMsgSyncStart(): void
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onRecvGroupGuildFlag(i2: unknown): void
onRecvMsg(...arrayList: unknown[]): void
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): void
onRecvOnlineFileMsg(arrayList: unknown): void
onRecvS2CMsg(arrayList: unknown): void
onRecvSysMsg(arrayList: unknown): void
onRecvUDCFlag(i2: unknown): void
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): void
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): void
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): void
onSearchGroupFileInfoUpdate(searchGroupFileResult:
{
result: {
retCode: number,
retMsg: string,
clientWording: string
},
syncCookie: string,
totalMatchCount: number,
ownerMatchCount: number,
isEnd: boolean,
reqId: number,
item: Array<{
groupCode: string,
groupName: string,
uploaderUin: string,
uploaderName: string,
matchUin: string,
matchWords: Array<unknown>,
fileNameHits: Array<{
start: number,
end: number
}>,
fileModelId: string,
fileId: string,
fileName: string,
fileSize: string,
busId: number,
uploadTime: number,
modifyTime: number,
deadTime: number,
downloadTimes: number,
localPath: string
}>
}): void
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): void
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): void
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): void
onUnreadCntAfterFirstView(hashMap: unknown): void
onUnreadCntUpdate(hashMap: unknown): void
onUserChannelTabStatusChanged(z: unknown): void
onUserOnlineStatusChanged(z: unknown): void
onUserTabStatusChanged(arrayList: unknown): void
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]): void
onMsgWithRichLinkInfoUpdate(...args: unknown[]): void
onRedTouchChanged(...args: unknown[]): void
// 第一次发现于Win 9.9.9 23159
onBroadcastHelperProgerssUpdate(...args: unknown[]): void
}

View File

@@ -1,15 +0,0 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
export interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void
onProfileDetailInfoChanged(profile: User): void
onStatusUpdate(...args: unknown[]): void
onSelfStatusChanged(...args: unknown[]): void
onStrangerRemarkChanged(...args: unknown[]): void
}

View File

@@ -1,3 +0,0 @@
export * from './NodeIKernelProfileListener'
export * from './NodeIKernelGroupListener'
export * from './NodeIKernelMsgListener'

View File

@@ -123,11 +123,15 @@ export function invoke<
return new Promise<R>((resolve, reject) => { return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args] const apiArgs = [method, ...args]
const callbackId = randomUUID() const callbackId = randomUUID()
let success = false const timeoutId = setTimeout(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}, timeout)
if (!options.cbCmd) { if (!options.cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别 // QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[callbackId] = res => { hookApiCallbacks[callbackId] = res => {
success = true clearTimeout(timeoutId)
resolve(res) resolve(res)
} }
} }
@@ -136,17 +140,16 @@ export function invoke<
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => { const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (options.cmdCB) { if (options.cmdCB) {
if (options.cmdCB(payload, result)) { if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true clearTimeout(timeoutId)
resolve(payload) resolve(payload)
} }
} }
else { else {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true clearTimeout(timeoutId)
resolve(payload) resolve(payload)
} }
}) })
@@ -159,16 +162,11 @@ export function invoke<
} }
else { else {
log('ntqq api call failed,', method, res) log('ntqq api call failed,', method, res)
clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`) reject(`ntqq api call failed, ${method}, ${res.errMsg}`)
} }
} }
} }
setTimeout(() => {
if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}
}, timeout)
ipcMain.emit( ipcMain.emit(
channel, channel,
@@ -182,4 +180,4 @@ export function invoke<
apiArgs, apiArgs,
) )
}) })
} }

469
src/ntqqapi/proto/compiled.d.ts vendored Normal file
View File

@@ -0,0 +1,469 @@
import * as $protobuf from "protobufjs";
import Long = require("long");
/** Namespace SysMsg. */
export namespace SysMsg {
/** Properties of a SystemMessage. */
interface ISystemMessage {
/** SystemMessage header */
header?: (SysMsg.ISystemMessageHeader[]|null);
/** SystemMessage msgSpec */
msgSpec?: (SysMsg.ISystemMessageMsgSpec[]|null);
/** SystemMessage bodyWrapper */
bodyWrapper?: (SysMsg.ISystemMessageBodyWrapper|null);
}
/** Represents a SystemMessage. */
class SystemMessage implements ISystemMessage {
/**
* Constructs a new SystemMessage.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessage);
/** SystemMessage header. */
public header: SysMsg.ISystemMessageHeader[];
/** SystemMessage msgSpec. */
public msgSpec: SysMsg.ISystemMessageMsgSpec[];
/** SystemMessage bodyWrapper. */
public bodyWrapper?: (SysMsg.ISystemMessageBodyWrapper|null);
/**
* Decodes a SystemMessage message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessage
* @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.SystemMessage;
/**
* Decodes a SystemMessage message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessage
* @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.SystemMessage;
/**
* Gets the default type url for SystemMessage
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a SystemMessageHeader. */
interface ISystemMessageHeader {
/** SystemMessageHeader peerNumber */
peerNumber?: (number|null);
/** SystemMessageHeader peerString */
peerString?: (string|null);
/** SystemMessageHeader uin */
uin?: (number|null);
/** SystemMessageHeader uid */
uid?: (string|null);
}
/** Represents a SystemMessageHeader. */
class SystemMessageHeader implements ISystemMessageHeader {
/**
* Constructs a new SystemMessageHeader.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessageHeader);
/** SystemMessageHeader peerNumber. */
public peerNumber: number;
/** SystemMessageHeader peerString. */
public peerString: string;
/** SystemMessageHeader uin. */
public uin: number;
/** SystemMessageHeader uid. */
public uid?: (string|null);
/**
* Decodes a SystemMessageHeader message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessageHeader
* @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.SystemMessageHeader;
/**
* Decodes a SystemMessageHeader message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessageHeader
* @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.SystemMessageHeader;
/**
* Gets the default type url for SystemMessageHeader
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a SystemMessageMsgSpec. */
interface ISystemMessageMsgSpec {
/** SystemMessageMsgSpec msgType */
msgType?: (number|null);
/** SystemMessageMsgSpec subType */
subType?: (number|null);
/** SystemMessageMsgSpec subSubType */
subSubType?: (number|null);
/** SystemMessageMsgSpec msgSeq */
msgSeq?: (number|null);
/** SystemMessageMsgSpec time */
time?: (number|null);
/** SystemMessageMsgSpec other */
other?: (number|null);
}
/** Represents a SystemMessageMsgSpec. */
class SystemMessageMsgSpec implements ISystemMessageMsgSpec {
/**
* Constructs a new SystemMessageMsgSpec.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessageMsgSpec);
/** SystemMessageMsgSpec msgType. */
public msgType: number;
/** SystemMessageMsgSpec subType. */
public subType: number;
/** SystemMessageMsgSpec subSubType. */
public subSubType: number;
/** SystemMessageMsgSpec msgSeq. */
public msgSeq: number;
/** SystemMessageMsgSpec time. */
public time: number;
/** SystemMessageMsgSpec other. */
public other: number;
/**
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessageMsgSpec
* @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.SystemMessageMsgSpec;
/**
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessageMsgSpec
* @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.SystemMessageMsgSpec;
/**
* Gets the default type url for SystemMessageMsgSpec
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a SystemMessageBodyWrapper. */
interface ISystemMessageBodyWrapper {
/** SystemMessageBodyWrapper body */
body?: (Uint8Array|null);
}
/** Represents a SystemMessageBodyWrapper. */
class SystemMessageBodyWrapper implements ISystemMessageBodyWrapper {
/**
* Constructs a new SystemMessageBodyWrapper.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessageBodyWrapper);
/** SystemMessageBodyWrapper body. */
public body: Uint8Array;
/**
* Decodes a SystemMessageBodyWrapper message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessageBodyWrapper
* @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.SystemMessageBodyWrapper;
/**
* Decodes a SystemMessageBodyWrapper message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessageBodyWrapper
* @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.SystemMessageBodyWrapper;
/**
* Gets the default type url for SystemMessageBodyWrapper
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a LikeDetail. */
interface ILikeDetail {
/** LikeDetail txt */
txt?: (string|null);
/** LikeDetail uin */
uin?: (number|null);
/** LikeDetail nickname */
nickname?: (string|null);
}
/** Represents a LikeDetail. */
class LikeDetail implements ILikeDetail {
/**
* Constructs a new LikeDetail.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ILikeDetail);
/** LikeDetail txt. */
public txt: string;
/** LikeDetail uin. */
public uin: number;
/** LikeDetail nickname. */
public nickname: string;
/**
* Decodes a LikeDetail message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns LikeDetail
* @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.LikeDetail;
/**
* Decodes a LikeDetail message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns LikeDetail
* @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.LikeDetail;
/**
* Gets the default type url for LikeDetail
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a LikeMsg. */
interface ILikeMsg {
/** LikeMsg count */
count?: (number|null);
/** LikeMsg time */
time?: (number|null);
/** LikeMsg detail */
detail?: (SysMsg.ILikeDetail|null);
}
/** Represents a LikeMsg. */
class LikeMsg implements ILikeMsg {
/**
* Constructs a new LikeMsg.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ILikeMsg);
/** LikeMsg count. */
public count: number;
/** LikeMsg time. */
public time: number;
/** LikeMsg detail. */
public detail?: (SysMsg.ILikeDetail|null);
/**
* Decodes a LikeMsg message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns LikeMsg
* @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.LikeMsg;
/**
* Decodes a LikeMsg message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns LikeMsg
* @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.LikeMsg;
/**
* Gets the default type url for LikeMsg
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a ProfileLikeSubTip. */
interface IProfileLikeSubTip {
/** ProfileLikeSubTip msg */
msg?: (SysMsg.ILikeMsg|null);
}
/** Represents a ProfileLikeSubTip. */
class ProfileLikeSubTip implements IProfileLikeSubTip {
/**
* Constructs a new ProfileLikeSubTip.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.IProfileLikeSubTip);
/** ProfileLikeSubTip msg. */
public msg?: (SysMsg.ILikeMsg|null);
/**
* Decodes a ProfileLikeSubTip message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns ProfileLikeSubTip
* @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.ProfileLikeSubTip;
/**
* Decodes a ProfileLikeSubTip message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns ProfileLikeSubTip
* @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.ProfileLikeSubTip;
/**
* Gets the default type url for ProfileLikeSubTip
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a ProfileLikeTip. */
interface IProfileLikeTip {
/** ProfileLikeTip msgType */
msgType?: (number|null);
/** ProfileLikeTip subType */
subType?: (number|null);
/** ProfileLikeTip content */
content?: (SysMsg.IProfileLikeSubTip|null);
}
/** Represents a ProfileLikeTip. */
class ProfileLikeTip implements IProfileLikeTip {
/**
* Constructs a new ProfileLikeTip.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.IProfileLikeTip);
/** ProfileLikeTip msgType. */
public msgType: number;
/** ProfileLikeTip subType. */
public subType: number;
/** ProfileLikeTip content. */
public content?: (SysMsg.IProfileLikeSubTip|null);
/**
* Decodes a ProfileLikeTip message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns ProfileLikeTip
* @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.ProfileLikeTip;
/**
* Decodes a ProfileLikeTip message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns ProfileLikeTip
* @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.ProfileLikeTip;
/**
* Gets the default type url for ProfileLikeTip
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
syntax = "proto3";
package SysMsg;
message LikeDetail {
string txt = 1;
uint32 uin = 3;
string nickname = 5;
}
message LikeMsg {
uint32 count = 1;
uint32 time = 2;
LikeDetail detail = 3;
}
message ProfileLikeSubTip {
LikeMsg msg = 14;
}
message ProfileLikeTip {
uint32 msgType = 1;
uint32 subType = 2;
ProfileLikeSubTip content = 203;
}

View File

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

View File

@@ -119,7 +119,7 @@ export interface NodeIKernelBuddyService {
reportDoubtBuddyReqUnread(): void reportDoubtBuddyReqUnread(): void
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<unknown> getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<GeneralCallResult & { arkMsg: string }>
isNull(): boolean isNull(): boolean
} }

View File

@@ -45,14 +45,14 @@ export interface NodeIKernelGroupService {
errMsg: string, errMsg: string,
uids: Map<string, string> uids: Map<string, string>
}> }>
//26702(其实更早 但是我不知道) //26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown> checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
//26702(其实更早 但是我不知道) //26702(其实更早 但是我不知道)
getGroupLatestEssenceList(groupCode: string): Promise<unknown> getGroupLatestEssenceList(groupCode: string): Promise<unknown>
//26702(其实更早 但是我不知道) //26702(其实更早 但是我不知道)
shareDigest(Req: { shareDigest(Req: {
appId: string, appId: string,
appType: number, appType: number,
@@ -104,11 +104,20 @@ export interface NodeIKernelGroupService {
createMemberListScene(groupCode: string, scene: string): string createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(SceneId: string): void destroyMemberListScene(sceneId: string): void
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{ getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string, errCode: number
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean } errMsg: string
result: {
ids: {
uid: string
index: number
}[]
infos: Map<string, GroupMember>
finish: boolean
hasRobot: boolean
}
}> }>
getPrevMemberList(): unknown getPrevMemberList(): unknown
@@ -202,11 +211,12 @@ export interface NodeIKernelGroupService {
publishInstructionForNewcomers(groupCode: string, arg: unknown): void publishInstructionForNewcomers(groupCode: string, arg: unknown): void
uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<GeneralCallResult & { uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<{
errCode: number errCode: number
errMsg: string
picInfo?: { picInfo?: {
id: string, id: string
width: number, width: number
height: number height: number
} }
}> }>
@@ -231,7 +241,7 @@ export interface NodeIKernelGroupService {
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void> setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getGroupRecommendContactArkJson(groupCode: string): unknown getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>
getJoinGroupLink(groupCode: string): unknown getJoinGroupLink(groupCode: string): unknown
@@ -243,6 +253,7 @@ export interface NodeIKernelGroupService {
msgRandom: number, msgRandom: number,
msgSeq: number msgSeq: number
}): Promise<unknown> }): Promise<unknown>
//需要提前判断是否存在 高版本新增 //需要提前判断是否存在 高版本新增
removeGroupEssence(param: { removeGroupEssence(param: {
groupCode: string groupCode: string
@@ -251,4 +262,4 @@ export interface NodeIKernelGroupService {
}): Promise<unknown> }): Promise<unknown>
isNull(): boolean isNull(): boolean
} }

View File

@@ -214,7 +214,7 @@ export interface NodeIKernelMsgService {
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown
@@ -223,31 +223,8 @@ export interface NodeIKernelMsgService {
queryMsgsWithFilter(...args: unknown[]): unknown queryMsgsWithFilter(...args: unknown[]): unknown
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>
// this.chatType = i2 // this.chatType = i2
// this.peerUid = str // this.peerUid = str
// this.chatInfo = new ChatInfo() // this.chatInfo = new ChatInfo()
// this.filterMsgType = new ArrayList<>() // this.filterMsgType = new ArrayList<>()
// this.filterSendersUid = new ArrayList<>() // this.filterSendersUid = new ArrayList<>()
@@ -495,16 +472,15 @@ export interface NodeIKernelMsgService {
setMsgEmojiLikes(...args: unknown[]): unknown setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{ getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number, result: number
errMsg: string, errMsg: string
emojiLikesList: emojiLikesList: {
Array<{ tinyId: string
tinyId: string, nickName: string
nickName: string,
headUrl: string headUrl: string
}>, }[]
cookie: string, cookie: string
isLastPage: boolean, isLastPage: boolean
isFirstPage: boolean isFirstPage: boolean
}> }>
@@ -686,9 +662,8 @@ export interface NodeIKernelMsgService {
dataMigrationStopOperation(...args: unknown[]): unknown dataMigrationStopOperation(...args: unknown[]): unknown
//新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{ dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex" extensionData: string //"Hex"
extraData: string //"" extraData: string //""
chatType: number chatType: number
chatUin: string chatUin: string
@@ -740,4 +715,4 @@ export interface NodeIKernelMsgService {
getGroupMsgStorageTime(): unknown//这是嘛啊 getGroupMsgStorageTime(): unknown//这是嘛啊
} }

View File

@@ -1,5 +1,6 @@
import { BuddyProfileLikeReq } from '../types' import { BuddyProfileLikeReq } from '../types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
import { Dict } from 'cosmokit'
export interface NodeIKernelProfileLikeService { export interface NodeIKernelProfileLikeService {
addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void
@@ -10,8 +11,18 @@ export interface NodeIKernelProfileLikeService {
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & { getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
info: { info: {
userLikeInfos: Array<unknown>, userLikeInfos: {
friendMaxVotes: number, uid: string
time: string
favoriteInfo: {
total_count: number
last_time: number
today_count: number
userInfos: Dict[]
}
voteInfo: Dict
}[]
friendMaxVotes: number
start: number start: number
} }
}> }>

View File

@@ -62,6 +62,19 @@ export interface GroupMember {
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean isChangeRole: boolean
joinTime: string joinTime: number
lastSpeakTime: string lastSpeakTime: number
memberLevel: number
}
export interface PublishGroupBulletinReq {
text: string
picInfo?: {
id: string
width: number
height: number
}
oldFeedsId: ''
pinned: number
confirmRequired: number
} }

View File

@@ -6,6 +6,7 @@ export interface GetFileListParam {
startIndex: number startIndex: number
sortOrder: number sortOrder: number
showOnlinedocFolder: number showOnlinedocFolder: number
folderId?: string
} }
export enum ElementType { export enum ElementType {
@@ -102,7 +103,7 @@ export interface SendPicElement {
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY elementType: ElementType.REPLY
elementId: '' elementId: ''
replyElement: ReplyElement replyElement: Partial<ReplyElement>
} }
export interface SendFaceElement { export interface SendFaceElement {
@@ -129,6 +130,12 @@ export interface ReplyElement {
replayMsgId: string replayMsgId: string
senderUin: string senderUin: string
senderUinStr: string senderUinStr: string
sourceMsgIdInRecords: string
senderUid: string
senderUidStr: string
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replyMsgTime: string
} }
export interface FileElement { export interface FileElement {
@@ -282,14 +289,28 @@ export interface PicElement {
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
RECALL = 1, REVOKE = 1,
INVITE_NEW_MEMBER = 12, PROCLAMATION = 2,
MEMBER_NEW_TITLE = 17, EMOJIREPLY = 3,
GROUP = 4,
BUDDY = 5,
FEED = 6,
ESSENCE = 7,
GROUPNOTIFY = 8,
BUDDYNOTIFY = 9,
FILE = 10,
FEEDCHANNELMSG = 11,
XMLMSG = 12,
LOCALMSG = 13,
BLOCK = 14,
AIOOP = 15,
WALLET = 16,
JSON = 17,
} }
export interface GrayTipElement { export interface GrayTipElement {
subElementType: GrayTipElementSubType subElementType: GrayTipElementSubType
revokeElement: { revokeElement?: {
operatorRole: string operatorRole: string
operatorUid: string operatorUid: string
operatorNick: string operatorNick: string
@@ -299,14 +320,14 @@ export interface GrayTipElement {
isSelfOperate?: boolean isSelfOperate?: boolean
wording: string // 自定义的撤回提示语 wording: string // 自定义的撤回提示语
} }
aioOpGrayTipElement: TipAioOpGrayTipElement aioOpGrayTipElement?: TipAioOpGrayTipElement
groupElement: TipGroupElement groupElement?: TipGroupElement
xmlElement: { xmlElement?: {
templId: string templId: string
content: string content: string
} }
jsonGrayTipElement: { jsonGrayTipElement?: {
busiId: number busiId: string
jsonStr: string jsonStr: string
} }
} }
@@ -471,36 +492,7 @@ export interface RawMessage {
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[] records: RawMessage[]
elements: { elements: MessageElement[]
elementId: string
elementType: ElementType
replyElement: {
sourceMsgIdInRecords: string
senderUid: string // 原消息发送者QQ号
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
senderUidStr: string
replyMsgTime: string
}
textElement: {
atType: AtType
atUid: string // QQ号
content: string
atNtUid: string // uid号
}
picElement: PicElement
pttElement: PttElement
arkElement: ArkElement
grayTipElement: GrayTipElement
faceElement: FaceElement
videoElement: VideoElement
fileElement: FileElement
marketFaceElement: MarketFaceElement
inlineKeyboardElement: InlineKeyboardElement
markdownElement: MarkdownElement
multiForwardMsgElement: MultiForwardMsgElement
}[]
} }
export interface Peer { export interface Peer {
@@ -515,7 +507,7 @@ export interface MessageElement {
extBufForUI: string //"0x" extBufForUI: string //"0x"
textElement?: TextElement textElement?: TextElement
faceElement?: FaceElement faceElement?: FaceElement
marketFaceElement?: MarkdownElement marketFaceElement?: MarketFaceElement
replyElement?: ReplyElement replyElement?: ReplyElement
picElement?: PicElement picElement?: PicElement
pttElement?: PttElement pttElement?: PttElement
@@ -541,4 +533,78 @@ export interface MessageElement {
taskTopMsgElement?: unknown taskTopMsgElement?: unknown
recommendedMsgElement?: unknown recommendedMsgElement?: unknown
actionBarElement?: unknown actionBarElement?: unknown
} }
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string
msgElementId: string
msgId: string
fileId: string
fileProgress: string // '0'
fileSpeed: string // '0'
fileErrCode: string // '0'
fileErrMsg: string
fileDownType: number // 暂时未知
thumbSize: number
filePath: string
totalSize: string
trasferStatus: number
step: number
commonFileInfo: unknown
fileSrvErrCode: string
clientMsg: string
businessId: number
userTotalSpacePerDay: unknown
userUsedSpacePerDay: unknown
}
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
}

View File

@@ -33,30 +33,8 @@ export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
} }
export interface WrapperConstructor {
[key: string]: any
}
const wrapperApi: WrapperApi = {} const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {}
const constructor = [
'NodeIKernelBuddyListener',
'NodeIKernelGroupListener',
'NodeQQNTWrapperUtil',
'NodeIKernelMsgListener',
'NodeIQQNTWrapperEngine',
'NodeIGlobalAdapter',
'NodeIDependsAdapter',
'NodeIDispatcherAdapter',
'NodeIKernelSessionListener',
'NodeIKernelLoginService',
'NodeIKernelLoginListener',
'NodeIKernelProfileService',
'NodeIKernelProfileListener',
]
Process.dlopenOrig = Process.dlopen Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) { Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
@@ -69,13 +47,10 @@ Process.dlopen = function (module: Dict, filename: string, flags = constants.dlo
return ret return ret
} }
}) })
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
} }
return dlopenRet return dlopenRet
} }
export function getSession() { export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession'] return wrapperApi['NodeIQQNTWrapperSession']
} }

View File

@@ -1,44 +1,43 @@
import { ActionName, BaseCheckResult } from './types' import { ActionName } from './types'
import { OB11Response } from './OB11Response' import { OB11Response } from './OB11Response'
import { OB11Return } from '../types' import { OB11Return } from '../types'
import { Context } from 'cordis' import { Context, Schema } from 'cordis'
import type Adapter from '../adapter' import type Adapter from '../adapter'
abstract class BaseAction<PayloadType, ReturnDataType> { abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName abstract actionName: ActionName
protected ctx: Context protected ctx: Context
payloadSchema?: Schema<PayloadType>
constructor(protected adapter: Adapter) { constructor(protected adapter: Adapter) {
this.ctx = adapter.ctx this.ctx = adapter.ctx
} }
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
valid: true,
}
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> { public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload) let params: PayloadType
if (!result.valid) { try {
return OB11Response.error(result.message, 400) params = this.payloadSchema ? new this.payloadSchema(payload) : payload
} catch (e) {
return OB11Response.error((e as Error).message, 400)
} }
try { try {
const resData = await this._handle(payload) const resData = await this._handle(params)
return OB11Response.ok(resData) return OB11Response.ok(resData)
} catch (e) { } catch (e) {
this.ctx.logger.error('发生错误', e) this.ctx.logger.error('发生错误', e)
return OB11Response.error(e?.toString() || (e as Error)?.stack?.toString() || '未知错误,可能操作超时', 200) return OB11Response.error((e as Error)?.toString() || (e as Error)?.stack?.toString() || '未知错误,可能操作超时', 200)
} }
} }
public async websocketHandle(payload: PayloadType, echo: unknown): Promise<OB11Return<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: unknown): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload) let params: PayloadType
if (!result.valid) { try {
return OB11Response.error(result.message, 1400) params = this.payloadSchema ? new this.payloadSchema(payload) : payload
} catch (e) {
return OB11Response.error((e as Error).message, 1400)
} }
try { try {
const resData = await this._handle(payload) const resData = await this._handle(params)
return OB11Response.ok(resData, echo) return OB11Response.ok(resData, echo)
} catch (e) { } catch (e) {
this.ctx.logger.error('发生错误', e) this.ctx.logger.error('发生错误', e)
@@ -46,9 +45,7 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
} }
} }
protected async _handle(payload: PayloadType): Promise<ReturnDataType> { protected abstract _handle(payload: PayloadType): Promise<ReturnDataType>
throw `pleas override ${this.actionName} _handle`
}
} }
export default BaseAction export { BaseAction, Schema }

View File

@@ -1,5 +1,5 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import fsPromise from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer, ElementType } from '@/ntqqapi/types' import { Peer, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
@@ -17,13 +17,16 @@ export interface GetFileResponse {
} }
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44 payloadSchema = Schema.object({
file: Schema.string().required()
})
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = this.adapter.config const { enableLocalFile2Url } = this.adapter.config
let fileCache = await MessageUnique.getFileCacheById(String(payload.file)) let fileCache = await MessageUnique.getFileCacheById(payload.file)
if (!fileCache?.length) { if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(String(payload.file)) fileCache = await MessageUnique.getFileCacheByName(payload.file)
} }
if (fileCache?.length) { if (fileCache?.length) {
@@ -56,13 +59,13 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
if (!findEle) { if (!findEle) {
throw new Error('element not found') throw new Error('element not found')
} }
res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement) res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement!)
} else if (fileCache[0].elementType === ElementType.VIDEO) { } else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId) res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
} }
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) { if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try { try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64') res.base64 = await readFile(downloadPath, 'base64')
} catch (e) { } catch (e) {
throw new Error('文件下载失败. ' + e) throw new Error('文件下载失败. ' + e)
} }
@@ -76,11 +79,12 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
export default class GetFile extends GetFileBase { export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile actionName = ActionName.GetFile
payloadSchema = Schema.object({
file: Schema.string(),
file_id: Schema.string().required()
})
protected async _handle(payload: { file_id: string; file: string }): Promise<GetFileResponse> { protected async _handle(payload: { file_id: string, file: string }): Promise<GetFileResponse> {
if (!payload.file_id) {
throw new Error('file_id 不能为空')
}
payload.file = payload.file_id payload.file = payload.file_id
return super._handle(payload) return super._handle(payload)
} }

View File

@@ -3,11 +3,4 @@ import { ActionName } from '../types'
export default class GetImage extends GetFileBase { export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage actionName = ActionName.GetImage
protected async _handle(payload: { file: string }) {
if (!payload.file) {
throw new Error('参数 file 不能为空')
}
return super._handle(payload)
}
} }

View File

@@ -1,8 +1,9 @@
import path from 'node:path'
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile' import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types' import { ActionName } from '../types'
import { decodeSilk } from '@/common/utils/audio' import { decodeSilk } from '@/common/utils/audio'
import path from 'node:path' import { Schema } from '../BaseAction'
import fs from 'node:fs' import { stat, readFile } from 'node:fs/promises'
interface Payload extends GetFilePayload { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
@@ -10,14 +11,18 @@ interface Payload extends GetFilePayload {
export default class GetRecord extends GetFileBase { export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
payloadSchema = Schema.object({
file: Schema.string().required(),
out_format: Schema.string().default('mp3')
})
protected async _handle(payload: Payload): Promise<GetFileResponse> { protected async _handle(payload: Payload): Promise<GetFileResponse> {
const res = await super._handle(payload) const res = await super._handle(payload)
res.file = await decodeSilk(this.ctx, res.file!, payload.out_format) res.file = await decodeSilk(this.ctx, res.file!, payload.out_format)
res.file_name = path.basename(res.file) res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString() res.file_size = (await stat(res.file)).size.toString()
if (this.adapter.config.enableLocalFile2Url) { if (this.adapter.config.enableLocalFile2Url) {
res.base64 = fs.readFileSync(res.file, 'base64') res.base64 = await readFile(res.file, 'base64')
} }
return res return res
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
@@ -9,9 +9,13 @@ interface Payload {
export class CreateGroupFileFolder extends BaseAction<Payload, null> { export class CreateGroupFileFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder actionName = ActionName.GoCQHTTP_CreateGroupFileFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
name: Schema.string().required(),
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
await this.ctx.ntGroupApi.createGroupFileFolder(payload.group_id.toString(), payload.name) await this.ctx.ntGroupApi.createGroupFileFolder(payload.group_id.toString(), payload.name)
return null return null
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
@@ -8,11 +8,11 @@ interface Payload {
export class DelEssenceMsg extends BaseAction<Payload, unknown> { export class DelEssenceMsg extends BaseAction<Payload, unknown> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg actionName = ActionName.GoCQHTTP_DelEssenceMsg
payloadSchema = Schema.object({
message_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found') throw new Error('msg not found')

View File

@@ -1,17 +1,22 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: string | number group_id: string | number
file_id: string file_id: string
busid?: 102 busid: number
} }
export class DelGroupFile extends BaseAction<Payload, null> { export class DelGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFile actionName = ActionName.GoCQHTTP_DelGroupFile
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_id: Schema.string().required(),
busid: Schema.number().default(102)
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
await this.ctx.ntGroupApi.deleteGroupFile(payload.group_id.toString(), [payload.file_id]) await this.ctx.ntGroupApi.deleteGroupFile(payload.group_id.toString(), [payload.file_id], [payload.busid])
return null return null
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
@@ -8,9 +8,13 @@ interface Payload {
export class DelGroupFolder extends BaseAction<Payload, null> { export class DelGroupFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFolder actionName = ActionName.GoCQHTTP_DelGroupFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
folder_id: Schema.string().required()
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
await this.ctx.ntGroupApi.deleteGroupFileFolder(payload.group_id.toString(), payload.folder_id) await this.ctx.ntGroupApi.deleteGroupFileFolder(payload.group_id.toString(), payload.folder_id)
return null return null
} }
} }

View File

@@ -1,7 +1,7 @@
import BaseAction from '../BaseAction'
import fs from 'fs' import fs from 'fs'
import fsPromise from 'fs/promises' import fsPromise from 'fs/promises'
import path from 'node:path' import path from 'node:path'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { calculateFileMD5, fetchFile } from '@/common/utils' import { calculateFileMD5, fetchFile } from '@/common/utils'
import { TEMP_DIR } from '@/common/globalVars' import { TEMP_DIR } from '@/common/globalVars'
@@ -22,6 +22,11 @@ interface FileResponse {
export class DownloadFile extends BaseAction<Payload, FileResponse> { export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile actionName = ActionName.GoCQHTTP_DownloadFile
payloadSchema = Schema.object({
url: String,
base64: String,
headers: Schema.union([String, Schema.array(String)])
})
protected async _handle(payload: Payload): Promise<FileResponse> { protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name const isRandomName = !payload.name

View File

@@ -1,8 +1,9 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { OB11ForwardMessage } from '../../types' import { OB11ForwardMessage } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
import { filterNullable } from '@/common/utils/misc'
interface Payload { interface Payload {
message_id: string // long msg idgocq message_id: string // long msg idgocq
@@ -15,6 +16,11 @@ interface Response {
export class GetForwardMsg extends BaseAction<Payload, Response> { export class GetForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
payloadSchema = Schema.object({
message_id: String,
id: String
})
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const msgId = payload.id || payload.message_id const msgId = payload.id || payload.message_id
if (!msgId) { if (!msgId) {
@@ -33,19 +39,21 @@ export class GetForwardMsg extends BaseAction<Payload, Response> {
const messages = await Promise.all( const messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
const resMsg = await OB11Entities.message(this.ctx, msg) const resMsg = await OB11Entities.message(this.ctx, msg)
if (!resMsg) return
resMsg.message_id = MessageUnique.createMsg({ resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,
}, msg.msgId) }, msg.msgId)
return resMsg return resMsg
}), })
) )
const forwardMessages = messages.map(v => { const forwardMessages = filterNullable(messages)
const msg = v as Partial<OB11ForwardMessage> .map(v => {
msg.content = msg.message const msg = v as Partial<OB11ForwardMessage>
delete msg.message msg.content = msg.message
return msg as OB11ForwardMessage delete msg.message
}) return msg as OB11ForwardMessage
})
return { messages: forwardMessages } return { messages: forwardMessages }
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
@@ -13,6 +13,9 @@ interface Response {
export class GetGroupAtAllRemain extends BaseAction<Payload, Response> { export class GetGroupAtAllRemain extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupRemainAtTimes(payload.group_id.toString()) const data = await this.ctx.ntGroupApi.getGroupRemainAtTimes(payload.group_id.toString())
@@ -22,4 +25,4 @@ export class GetGroupAtAllRemain extends BaseAction<Payload, Response> {
remain_at_all_count_for_uin: data.atInfo.RemainAtAllCountForUin remain_at_all_count_for_uin: data.atInfo.RemainAtAllCountForUin
} }
} }
} }

View File

@@ -0,0 +1,54 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
interface Payload {
group_id: string | number
folder_id: string
file_count: string | number
}
interface Response {
files: OB11GroupFile[]
folders: OB11GroupFileFolder[]
}
export class GetGroupFilesByFolder extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
folder_id: Schema.string().required(),
file_count: Schema.union([Number, String]).default(50)
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId: payload.folder_id
})
return {
files: data.filter(item => item.fileInfo)
.map(item => {
const file = item.fileInfo!
return {
group_id: +item.peerId,
file_id: file.fileId,
file_name: file.fileName,
busid: file.busId,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,
download_times: file.downloadTimes,
uploader: +file.uploaderUin,
uploader_name: file.uploaderName
}
}),
folders: []
}
}
}

View File

@@ -1,16 +1,17 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { RawMessage } from '@/ntqqapi/types' import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
import { filterNullable } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
message_seq?: number | string message_seq?: number | string
count?: number | string count: number | string
reverseOrder?: boolean reverseOrder: boolean
} }
interface Response { interface Response {
@@ -19,10 +20,15 @@ interface Response {
export class GetGroupMsgHistory extends BaseAction<Payload, Response> { export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false),
})
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const count = payload.count || 20 const { count, reverseOrder } = payload
const isReverseOrder = payload.reverseOrder || true
const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() } const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] | undefined let msgList: RawMessage[] | undefined
// 包含 message_seq 0 // 包含 message_seq 0
@@ -34,13 +40,13 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
} }
if (!msgList?.length) throw new Error('未找到消息') if (!msgList?.length) throw new Error('未找到消息')
if (isReverseOrder) msgList.reverse() if (reverseOrder) msgList.reverse()
await Promise.all( await Promise.all(
msgList.map(async msg => { msgList.map(async msg => {
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId) msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
}) })
) )
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Entities.message(this.ctx, msg))) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Entities.message(this.ctx, msg)))
return { messages: ob11MsgList } return { messages: filterNullable(ob11MsgList) }
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types' import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
@@ -14,18 +14,19 @@ interface Response {
export class GetGroupRootFiles extends BaseAction<Payload, Response> { export class GetGroupRootFiles extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupRootFiles actionName = ActionName.GoCQHTTP_GetGroupRootFiles
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_count: Schema.union([Number, String]).default(50),
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), { const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1, sortType: 1,
fileCount: +(payload.file_count ?? 50), fileCount: +payload.file_count,
startIndex: 0, startIndex: 0,
sortOrder: 2, sortOrder: 2,
showOnlinedocFolder: 0, showOnlinedocFolder: 0,
}) })
this.ctx.logger.info(data)
return { return {
files: data.filter(item => item.fileInfo) files: data.filter(item => item.fileInfo)
.map(item => { .map(item => {
@@ -59,4 +60,4 @@ export class GetGroupRootFiles extends BaseAction<Payload, Response> {
}) })
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { GroupNotifyStatus } from '@/ntqqapi/types' import { GroupNotifyStatus } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -56,4 +56,4 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
} }
return data return data
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -12,6 +12,9 @@ interface Payload {
export class GetStrangerInfo extends BaseAction<Payload, OB11User> { export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
if (!(getBuildVersion() >= 26702)) { if (!(getBuildVersion() >= 26702)) {

View File

@@ -1,14 +1,23 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
} }
export class MarkMsgAsRead extends BaseAction<Payload, null> { export class MarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead actionName = ActionName.GoCQHTTP_MarkMsgAsRead
payloadSchema = Schema.object({
message_id: Schema.union([Number, String]).required()
})
protected async _handle() { protected async _handle(payload: Payload) {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
await this.ctx.ntMsgApi.setMsgRead(msg.Peer)
return null return null
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../../helper/quickOperation' import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../../helper/quickOperation'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -9,8 +9,9 @@ interface Payload {
export class HandleQuickOperation extends BaseAction<Payload, null> { export class HandleQuickOperation extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_HandleQuickOperation actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e)) handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e))
return null return null
} }
} }

View File

@@ -1,21 +1,180 @@
import SendMsg from '../msg/SendMsg' import { unlink } from 'node:fs/promises'
import { OB11PostSendMsg } from '../../types' import { OB11MessageNode } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { convertMessage2List } from '../../helper/createMessage' import { BaseAction, Schema } from '../BaseAction'
import { Peer } from '@/ntqqapi/types/msg'
import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
export class SendForwardMsg extends SendMsg { interface Payload {
user_id?: string | number
group_id?: string | number
messages: OB11MessageNode[]
message_type?: 'group' | 'private'
}
interface Response {
message_id: number
forward_id?: string
}
export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_SendForwardMsg actionName = ActionName.GoCQHTTP_SendForwardMsg
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]),
group_id: Schema.union([Number, String]),
messages: Schema.array(Schema.any()).required(),
message_type: Schema.union(['group', 'private'])
})
protected async check(payload: OB11PostSendMsg) { protected async _handle(payload: Payload) {
if (payload.messages) payload.message = convertMessage2List(payload.messages) let contextMode = CreatePeerMode.Normal
return super.check(payload) if (payload.message_type === 'group') {
contextMode = CreatePeerMode.Group
} else if (payload.message_type === 'private') {
contextMode = CreatePeerMode.Private
}
const peer = await createPeer(this.ctx, payload, contextMode)
const returnMsg = await this.handleForwardNode(peer, payload.messages)
return { message_id: returnMsg.msgShortId! }
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
this.ctx.logger.info('克隆的目标消息', msg)
const sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
}
if (sendElements.length === 0) {
this.ctx.logger.warn('需要clone的消息无法解析将会忽略掉', msg)
}
this.ctx.logger.info('克隆消息', sendElements)
try {
const peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements)
await this.ctx.sleep(400)
return nodeMsg
} catch (e) {
this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
}
let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
const nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!nodeMsg) {
this.ctx.logger.warn('转发消息失败,未找到消息', nodeId)
continue
}
nodeMsgIds.push(nodeMsg.MsgId)
}
else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx,
convertMessage2List(messageNode.data.content),
destPeer
)
this.ctx.logger.info('开始生成转发节点', sendElements)
const sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++
}
else {
sendElementsSplit[splitIndex].push(ele)
}
this.ctx.logger.info(sendElementsSplit)
}
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [])
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push(nodeMsg.msgId)
await this.ctx.sleep(400)
}
deleteAfterSentFiles.map(path => unlink(path))
} catch (e) {
this.ctx.logger.error('生成转发消息节点失败', e)
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer | null = null
let needSendSelf = false
for (const msgId of nodeMsgIds) {
const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsgPeer) {
const nodeMsg = (await this.ctx.ntMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
}
nodeMsgArray.push(nodeMsg)
}
}
nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId)
if (needSendSelf) {
for (const msg of nodeMsgArray) {
if (msg.peerUid === selfPeer.peerUid) continue
await this.cloneMsg(msg)
}
}
if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return returnMsg
} }
} }
export class SendPrivateForwardMsg extends SendForwardMsg { export class SendPrivateForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
protected _handle(payload: Payload) {
payload.message_type = 'private'
return super._handle(payload)
}
} }
export class SendGroupForwardMsg extends SendForwardMsg { export class SendGroupForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg actionName = ActionName.GoCQHTTP_SendGroupForwardMsg
protected _handle(payload: Payload) {
payload.message_type = 'group'
return super._handle(payload)
}
} }

View File

@@ -1,37 +1,58 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { unlink } from 'fs/promises'
import { checkFileReceived, uri2local } from '@/common/utils/file'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
content: string content: string
image?: string image?: string
pinned?: number | string //扩展 pinned: number | string //扩展
confirm_required?: number | string //扩展 confirm_required: number | string //扩展
} }
export class SendGroupNotice extends BaseAction<Payload, null> { export class SendGroupNotice extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupNotice actionName = ActionName.GoCQHTTP_SendGroupNotice
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
content: Schema.string().required(),
image: Schema.string(),
pinned: Schema.union([Number, String]).default(0),
confirm_required: Schema.union([Number, String]).default(1)
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
const type = 1 const groupCode = payload.group_id.toString()
const isShowEditCard = 0 const pinned = +payload.pinned
const tipWindowType = 0 const confirmRequired = +payload.confirm_required
const pinned = Number(payload.pinned ?? 0)
const confirmRequired = Number(payload.confirm_required ?? 1)
const result = await this.ctx.ntWebApi.setGroupNotice({ let picInfo: { id: string, width: number, height: number } | undefined
groupCode: payload.group_id.toString(), if (payload.image) {
content: payload.content, const { path, isLocal, success, errMsg } = await uri2local(payload.image, undefined, true)
if (!success) {
throw new Error(`设置群公告失败, 错误信息: uri2local: ${errMsg}`)
}
await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃需要提前判断
const result = await this.ctx.ntGroupApi.uploadGroupBulletinPic(groupCode, path)
if (result.errCode !== 0) {
throw new Error(`设置群公告失败, 错误信息: uploadGroupBulletinPic: ${result.errMsg}`)
}
if (!isLocal) {
unlink(path)
}
picInfo = result.picInfo
}
const res = await this.ctx.ntGroupApi.publishGroupBulletin(groupCode, {
text: encodeURIComponent(payload.content),
oldFeedsId: '',
pinned, pinned,
type,
isShowEditCard,
tipWindowType,
confirmRequired, confirmRequired,
picId: '' picInfo
}) })
if (result.ec !== 0) { if (res.result !== 0) {
throw new Error(`设置群公告失败, 错误信息: ${result.em}`) throw new Error(`设置群公告失败, 错误信息: ${res.errMsg}`)
} }
return null return null
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'

View File

@@ -1,8 +1,6 @@
import fs from 'node:fs' import { BaseAction } from '../BaseAction'
import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElementEntities } from '@/ntqqapi/entities' import { SendElementEntities } from '@/ntqqapi/entities'
import { SendFileElement } from '@/ntqqapi/types'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -18,17 +16,13 @@ export class UploadGroupFile extends BaseAction<UploadGroupFilePayload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: UploadGroupFilePayload): Promise<null> { protected async _handle(payload: UploadGroupFilePayload): Promise<null> {
let file = payload.file const { success, errMsg, path, fileName } = await uri2local(payload.file)
if (fs.existsSync(file)) { if (!success) {
file = `file://${file}` throw new Error(errMsg)
} }
const downloadResult = await uri2local(file) const file = await SendElementEntities.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name, payload.folder_id)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [sendFileEle], [], true) await sendMsg(this.ctx, peer, [file], [])
return null return null
} }
} }
@@ -43,17 +37,13 @@ export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null
actionName = ActionName.GoCQHTTP_UploadPrivateFile actionName = ActionName.GoCQHTTP_UploadPrivateFile
protected async _handle(payload: UploadPrivateFilePayload): Promise<null> { protected async _handle(payload: UploadPrivateFilePayload): Promise<null> {
const { success, errMsg, path, fileName } = await uri2local(payload.file)
if (!success) {
throw new Error(errMsg)
}
const sendFileEle = await SendElementEntities.file(this.ctx, path, payload.name || fileName)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
let file = payload.file await sendMsg(this.ctx, peer, [sendFileEle], [])
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name)
await sendMsg(this.ctx, peer, [sendFileEle], [], true)
return null return null
} }
} }

View File

@@ -1,5 +1,5 @@
import { GroupEssenceMsgRet } from '@/ntqqapi/api' import { GroupEssenceMsgRet } from '@/ntqqapi/api'
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface PayloadType { interface PayloadType {

View File

@@ -1,6 +1,6 @@
import { WebHonorType } from '@/ntqqapi/api' import { WebHonorType } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
interface Payload { interface Payload {
group_id: number group_id: number

View File

@@ -1,6 +1,6 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -1,6 +1,6 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
@@ -10,8 +10,8 @@ interface Payload {
class GetGroupList extends BaseAction<Payload, OB11Group[]> { class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { protected async _handle() {
const groupList = await this.ctx.ntGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true') const groupList = await this.ctx.ntGroupApi.getGroups()
return OB11Entities.groups(groupList) return OB11Entities.groups(groupList)
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -18,7 +18,7 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
if (member) { if (member) {
if (isNullable(member.sex)) { if (isNullable(member.sex)) {
//log('获取群成员详细信息') //log('获取群成员详细信息')
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid, true) const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
//log('群成员详细信息结果', info) //log('群成员详细信息结果', info)
Object.assign(member, info) Object.assign(member, info)
} }

View File

@@ -1,6 +1,6 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -13,11 +13,16 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList actionName = ActionName.GetGroupMemberList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const groupMembers = await this.ctx.ntGroupApi.getGroupMembers(payload.group_id.toString()) const groupCode = payload.group_id.toString()
let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
if (groupMembers.size === 0) {
await this.ctx.sleep(100)
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
}
const groupMembersArr = Array.from(groupMembers.values()) const groupMembersArr = Array.from(groupMembers.values())
let _groupMembers = groupMembersArr.map(item => { const _groupMembers = groupMembersArr.map(item => {
return OB11Entities.groupMember(payload.group_id.toString(), item) return OB11Entities.groupMember(groupCode, item)
}) })
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>() const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
@@ -25,8 +30,8 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
for (let i = 0, len = _groupMembers.length; i < len; i++) { for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了 // 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date _groupMembers[i].join_time ||= date
_groupMembers[i].last_sent_time = date _groupMembers[i].last_sent_time ||= date
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]) MemberMap.set(_groupMembers[i].user_id, _groupMembers[i])
} }
@@ -34,24 +39,24 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
const isPrivilege = selfRole === 3 || selfRole === 4 const isPrivilege = selfRole === 3 || selfRole === 4
if (isPrivilege) { if (isPrivilege) {
const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString()) const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(groupCode)
for (let i = 0, len = webGroupMembers.length; i < len; i++) { for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) { if (!webGroupMembers[i]?.uin) {
continue continue
} }
const MemberData = MemberMap.get(webGroupMembers[i]?.uin) const MemberData = MemberMap.get(webGroupMembers[i]?.uin)
if (MemberData) { if (MemberData) {
MemberData.join_time = webGroupMembers[i]?.join_time if (MemberData.join_time === date) {
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time MemberData.join_time = webGroupMembers[i]?.join_time
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time
}
MemberData.qage = webGroupMembers[i]?.qage MemberData.qage = webGroupMembers[i]?.qage
MemberData.level = webGroupMembers[i]?.lv.level.toString()
MemberMap.set(webGroupMembers[i]?.uin, MemberData) MemberMap.set(webGroupMembers[i]?.uin, MemberData)
} }
} }
} }
_groupMembers = Array.from(MemberMap.values()) return Array.from(MemberMap.values())
return _groupMembers
} }
} }

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
export default class GetGuildList extends BaseAction<null, null> { export default class GetGuildList extends BaseAction<null, null> {

View File

@@ -1,14 +1,13 @@
import SendMsg from '../msg/SendMsg' import SendMsg from '../msg/SendMsg'
import { ActionName, BaseCheckResult } from '../types' import { ActionName } from '../types'
import { OB11PostSendMsg } from '../../types' import { OB11PostSendMsg } from '../../types'
class SendGroupMsg extends SendMsg { class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg actionName = ActionName.SendGroupMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected _handle(payload: OB11PostSendMsg) {
delete (payload as Partial<OB11PostSendMsg>).user_id
payload.message_type = 'group' payload.message_type = 'group'
return super.check(payload) return super._handle(payload)
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { GroupMemberRole } from '@/ntqqapi/types' import { GroupMemberRole } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -10,7 +10,7 @@ import SendGroupMsg from './group/SendGroupMsg'
import SendPrivateMsg from './msg/SendPrivateMsg' import SendPrivateMsg from './msg/SendPrivateMsg'
import SendMsg from './msg/SendMsg' import SendMsg from './msg/SendMsg'
import DeleteMsg from './msg/DeleteMsg' import DeleteMsg from './msg/DeleteMsg'
import BaseAction from './BaseAction' import { BaseAction } from './BaseAction'
import GetVersionInfo from './system/GetVersionInfo' import GetVersionInfo from './system/GetVersionInfo'
import CanSendRecord from './system/CanSendRecord' import CanSendRecord from './system/CanSendRecord'
import CanSendImage from './system/CanSendImage' import CanSendImage from './system/CanSendImage'
@@ -53,7 +53,7 @@ import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { HandleQuickOperation } from './go-cqhttp/QuickOperation' import { HandleQuickOperation } from './go-cqhttp/QuickOperation'
import { SetEssenceMsg } from './go-cqhttp/SetEssenceMsg' import { SetEssenceMsg } from './go-cqhttp/SetEssenceMsg'
import { DelEssenceMsg } from './go-cqhttp/DelEssenceMsg' import { DelEssenceMsg } from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent' import { GetEvent } from './llonebot/GetEvent'
import { DelGroupFile } from './go-cqhttp/DelGroupFile' import { DelGroupFile } from './go-cqhttp/DelGroupFile'
import { GetGroupSystemMsg } from './go-cqhttp/GetGroupSystemMsg' import { GetGroupSystemMsg } from './go-cqhttp/GetGroupSystemMsg'
import { CreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder' import { CreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder'
@@ -62,6 +62,11 @@ import { GetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'
import { GetGroupRootFiles } from './go-cqhttp/GetGroupRootFiles' import { GetGroupRootFiles } from './go-cqhttp/GetGroupRootFiles'
import { SetOnlineStatus } from './llonebot/SetOnlineStatus' import { SetOnlineStatus } from './llonebot/SetOnlineStatus'
import { SendGroupNotice } from './go-cqhttp/SendGroupNotice' import { SendGroupNotice } from './go-cqhttp/SendGroupNotice'
import { GetProfileLike } from './llonebot/GetProfileLike'
import { FetchEmojiLike } from './llonebot/FetchEmojiLike'
import { FetchCustomFace } from './llonebot/FetchCustomFace'
import { GetFriendMsgHistory } from './llonebot/GetFriendMsgHistory'
import { GetGroupFilesByFolder } from './go-cqhttp/GetGroupFilesByFolder'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -74,6 +79,10 @@ export function initActionMap(adapter: Adapter) {
new GetFriendWithCategory(adapter), new GetFriendWithCategory(adapter),
new GetEvent(adapter), new GetEvent(adapter),
new SetOnlineStatus(adapter), new SetOnlineStatus(adapter),
new GetProfileLike(adapter),
new GetFriendMsgHistory(adapter),
new FetchEmojiLike(adapter),
new FetchCustomFace(adapter),
// onebot11 // onebot11
new SendLike(adapter), new SendLike(adapter),
new GetMsg(adapter), new GetMsg(adapter),
@@ -107,7 +116,7 @@ export function initActionMap(adapter: Adapter) {
new SetMsgEmojiLike(adapter), new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(adapter), new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(adapter), new ForwardGroupSingleMsg(adapter),
//以下为go-cqhttp api // go-cqhttp
new GetGroupEssence(adapter), new GetGroupEssence(adapter),
new GetGroupHonorInfo(adapter), new GetGroupHonorInfo(adapter),
new SendForwardMsg(adapter), new SendForwardMsg(adapter),
@@ -130,9 +139,10 @@ export function initActionMap(adapter: Adapter) {
new DelGroupFolder(adapter), new DelGroupFolder(adapter),
new GetGroupAtAllRemain(adapter), new GetGroupAtAllRemain(adapter),
new GetGroupRootFiles(adapter), new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter) new SendGroupNotice(adapter),
new GetGroupFilesByFolder(adapter),
] ]
const actionMap = new Map<string, BaseAction<unknown, unknown>>() const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {
actionMap.set(action.actionName, action) actionMap.set(action.actionName, action)
actionMap.set(action.actionName + '_async', action) actionMap.set(action.actionName + '_async', action)

View File

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

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -0,0 +1,18 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
count: number | string
}
export class FetchCustomFace extends BaseAction<Payload, string[]> {
actionName = ActionName.FetchCustomFace
payloadSchema = Schema.object({
count: Schema.union([Number, String]).default(48)
})
async _handle(payload: Payload) {
const ret = await this.ctx.ntMsgApi.fetchFavEmojiList(+payload.count)
return ret.emojiInfoList.map(e => e.url)
}
}

View File

@@ -0,0 +1,28 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
import { Dict } from 'cosmokit'
interface Payload {
emojiId: string
emojiType: string
message_id: string | number
count: string | number
}
export class FetchEmojiLike extends BaseAction<Payload, Dict> {
actionName = ActionName.FetchEmojiLike
payloadSchema = Schema.object({
emojiId: Schema.string().required(),
emojiType: Schema.string().required(),
message_id: Schema.union([Number, String]).required(),
count: Schema.union([Number, String]).default(20)
})
async _handle(payload: Payload) {
const msgInfo = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msgInfo) throw new Error('消息不存在')
const { msgSeq } = (await this.ctx.ntMsgApi.getMsgsByMsgId(msgInfo.Peer, [msgInfo.MsgId])).msgList[0]
return await this.ctx.ntMsgApi.getMsgEmojiLikesList(msgInfo.Peer, msgSeq, payload.emojiId, payload.emojiType, +payload.count)
}
}

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getHttpEvent } from '../../helper/eventForHttp' import { getHttpEvent } from '../../helper/eventForHttp'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
@@ -11,7 +11,7 @@ interface Payload {
timeout: number timeout: number
} }
export default class GetEvent extends BaseAction<Payload, PostEventType[]> { export class GetEvent extends BaseAction<Payload, PostEventType[]> {
actionName = ActionName.GetEvent actionName = ActionName.GetEvent
protected async _handle(payload: Payload): Promise<PostEventType[]> { protected async _handle(payload: Payload): Promise<PostEventType[]> {

View File

@@ -0,0 +1,53 @@
import { BaseAction, Schema } from '../BaseAction'
import { OB11Message } from '@/onebot11/types'
import { ActionName } from '../types'
import { ChatType, RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
import { OB11Entities } from '@/onebot11/entities'
import { filterNullable } from '@/common/utils/misc'
interface Payload {
user_id: number | string
message_seq?: number | string
message_id?: number | string
count: number | string
reverseOrder: boolean
}
interface Response {
messages: OB11Message[]
}
export class GetFriendMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]),
message_id: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false)
})
async _handle(payload: Payload): Promise<Response> {
const startMsgId = payload.message_seq ?? payload.message_id
let msgList: RawMessage[]
if (startMsgId) {
const msgInfo = await MessageUnique.getMsgIdAndPeerByShortId(+startMsgId)
if (!msgInfo) throw new Error(`消息${startMsgId}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(msgInfo.Peer, msgInfo.MsgId, +payload.count)).msgList
} else {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error(`记录${payload.user_id}不存在`)
const isBuddy = await this.ctx.ntFriendApi.isBuddy(uid)
const peer = { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid: uid }
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList
}
if (msgList.length === 0) throw new Error('未找到消息')
if (payload.reverseOrder) msgList.reverse()
msgList.map(msg => {
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
})
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Entities.message(this.ctx, msg)))
return { messages: filterNullable(ob11MsgList) }
}
}

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { GroupNotify, GroupNotifyStatus } from '@/ntqqapi/types' import { GroupNotify, GroupNotifyStatus } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'

View File

@@ -0,0 +1,17 @@
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars'
import { Dict } from 'cosmokit'
export class GetProfileLike extends BaseAction<void, Dict[]> {
actionName = ActionName.GetProfileLike
async _handle() {
const ret = await this.ctx.ntUserApi.getProfileLike(selfInfo.uid)
const listdata = ret.info.userLikeInfos[0].favoriteInfo.userInfos
for (const item of listdata) {
item.uin = Number(await this.ctx.ntUserApi.getUinByUid(item.uid)) || 0
}
return listdata
}
}

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
@@ -22,4 +22,4 @@ export class SetOnlineStatus extends BaseAction<Payload, null> {
} }
return null return null
} }
} }

View File

@@ -1,6 +1,6 @@
import BaseAction from '../BaseAction' import fs from 'node:fs'
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import * as fs from 'node:fs'
import { checkFileReceived, uri2local } from '../../../common/utils/file' import { checkFileReceived, uri2local } from '../../../common/utils/file'
interface Payload { interface Payload {

View File

@@ -1,5 +1,5 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { MessageUnique } from '@/common/utils/messageUnique' import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload { interface Payload {

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction' import { BaseAction } from '../BaseAction'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -15,12 +15,12 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
protected async _handle(payload: PayloadType) { protected async _handle(payload: PayloadType) {
if (!payload.message_id) { if (!payload.message_id) {
throw '参数message_id不能为空' throw new Error('参数message_id不能为空')
} }
const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString()) const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString())
const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id) const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id)
if (!msgIdWithPeer) { if (!msgIdWithPeer) {
throw ('消息不存在') throw new Error('消息不存在')
} }
const peer = { const peer = {
guildId: '', guildId: '',
@@ -29,6 +29,9 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
} }
const msg = this.adapter.getMsgCache(msgIdWithPeer.MsgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0] const msg = this.adapter.getMsgCache(msgIdWithPeer.MsgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const retMsg = await OB11Entities.message(this.ctx, msg) const retMsg = await OB11Entities.message(this.ctx, msg)
if (!retMsg) {
throw new Error('消息为空')
}
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)! retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_seq = retMsg.message_id retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id retMsg.real_id = retMsg.message_id

View File

@@ -1,25 +1,14 @@
import {
ChatType,
ElementType,
RawMessage,
SendMessageElement,
} from '@/ntqqapi/types'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
OB11MessageDataType, OB11MessageDataType,
OB11MessageJson, OB11MessageJson,
OB11MessageMusic, OB11MessageMusic,
OB11MessageNode,
OB11PostSendMsg, OB11PostSendMsg,
} from '../../types' } from '../../types'
import fs from 'node:fs' import { BaseAction } from '../BaseAction'
import BaseAction from '../BaseAction' import { ActionName } from '../types'
import { ActionName, BaseCheckResult } from '../types'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
interface ReturnData { interface ReturnData {
@@ -29,27 +18,6 @@ interface ReturnData {
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message)
const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node)
if (fmNum && fmNum != messages.length) {
return {
valid: false,
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素',
}
}
const musicNum = this.getSpecialMsgNum(messages, OB11MessageDataType.music)
if (musicNum && messages.length > 1) {
return {
valid: false,
message: '音乐消息不可以和其他消息混在一起发送',
}
}
return {
valid: true,
}
}
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
let contextMode = CreatePeerMode.Normal let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') { if (payload.message_type === 'group') {
@@ -63,12 +31,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',
) )
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) { if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try { throw new Error('请使用 /send_group_forward_msg 或 /send_private_forward_msg 进行合并转发')
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[])
return { message_id: returnMsg.msgShortId! }
} catch (e) {
throw '发送转发消息失败 ' + e
}
} }
else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) { else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) {
const music = messages[0] as OB11MessageMusic const music = messages[0] as OB11MessageMusic
@@ -135,140 +98,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number { private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {
if (Array.isArray(message)) { if (Array.isArray(message)) {
return message.filter((msg) => msg.type == msgType).length return message.filter((msg) => msg.type === msgType).length
} }
return 0 return 0
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
this.ctx.logger.info('克隆的目标消息', msg)
const sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
}
if (sendElements.length === 0) {
this.ctx.logger.warn('需要clone的消息无法解析将会忽略掉', msg)
}
this.ctx.logger.info('克隆消息', sendElements)
try {
const peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements)
await this.ctx.sleep(400)
return nodeMsg
} catch (e) {
this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
}
let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
const nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!nodeMsg) {
this.ctx.logger.warn('转发消息失败,未找到消息', nodeId)
continue
}
nodeMsgIds.push(nodeMsg.MsgId)
}
else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx,
convertMessage2List(messageNode.data.content),
destPeer
)
this.ctx.logger.info('开始生成转发节点', sendElements)
const sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++
}
else {
sendElementsSplit[splitIndex].push(ele)
}
this.ctx.logger.info(sendElementsSplit)
}
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true)
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push(nodeMsg.msgId)
await this.ctx.sleep(400)
}
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
} catch (e) {
this.ctx.logger.error('生成转发消息节点失败', e)
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer | null = null
let needSendSelf = false
for (const msgId of nodeMsgIds) {
const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsgPeer) {
const nodeMsg = (await this.ctx.ntMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
}
nodeMsgArray.push(nodeMsg)
}
}
nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId)
if (needSendSelf) {
for (const msg of nodeMsgArray) {
if (msg.peerUid === selfPeer.peerUid) continue
await this.cloneMsg(msg)
}
}
// elements之间用换行符分隔
// let _sendForwardElements: SendMessageElement[] = []
// for(let i = 0; i < sendForwardElements.length; i++){
// _sendForwardElements.push(sendForwardElements[i])
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
// }
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
// nodeIds.push(nodeMsg.msgId)
// await sleep(500);
// 开发转发
if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return returnMsg
}
} }
export default SendMsg export default SendMsg

View File

@@ -1,13 +1,13 @@
import SendMsg from './SendMsg' import SendMsg from './SendMsg'
import { ActionName, BaseCheckResult } from '../types' import { ActionName } from '../types'
import { OB11PostSendMsg } from '../../types' import { OB11PostSendMsg } from '../../types'
class SendPrivateMsg extends SendMsg { class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg actionName = ActionName.SendPrivateMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected _handle(payload: OB11PostSendMsg) {
payload.message_type = 'private' payload.message_type = 'private'
return super.check(payload) return super._handle(payload)
} }
} }

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