Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
Alen 2024-08-07 15:06:11 +08:00
commit e8d83d2958
82 changed files with 1577 additions and 721 deletions

@ -19,15 +19,14 @@ jobs:
- name: install dependenies
run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
npm install
yarn install
- name: build
run: npm run build
run: yarn build
- name: zip
run: |
sudo apt install zip -y
cp manifest.json ./dist/manifest.json
cd ./dist/
zip -r ../LLOneBot.zip ./*

@ -23,20 +23,6 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<https://llonebot.github.io/zh-CN/develop/api>
## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 框架对接文档
## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
@ -44,8 +30,7 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 鸣谢
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
- [chronocat](https://github.com/chrononeko/chronocat/)
- [chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm)

@ -3,8 +3,8 @@
"type": "extension",
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "实现 OneBot 11 协议,帮助进行 QQ 机器人开发",
"version": "3.27.4",
"description": "实现 OneBot 11 协议,用以 QQ 机器人开发",
"version": "3.28.0",
"icon": "./icon.webp",
"authors": [
{

@ -18,17 +18,18 @@
"dependencies": {
"compressing": "^1.10.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"express": "^4.19.2",
"fast-xml-parser": "^4.4.1",
"file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"file-type": "^19.4.0",
"fluent-ffmpeg": "^2.1.3",
"level": "^8.0.1",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/express": "^4.17.20",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.11.24",
"@types/ws": "^8.5.12",
"electron": "^29.0.1",

@ -1,7 +1,5 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import fs from 'node:fs'
import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper'
import path from 'node:path'
import { selfInfo } from './data'

@ -45,7 +45,7 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
if (friend) {
friends.push(friend)
}
} catch (e) {
} catch (e: any) {
log('刷新好友列表失败', e.stack.toString())
}
}

@ -14,9 +14,9 @@ class DBUtil {
public readonly DB_KEY_PREFIX_FILE = 'file_'
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_'
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map'
public db: Level
public db: Level | undefined
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number
private currentShortId: number | undefined
/*
*
@ -44,7 +44,7 @@ class DBUtil {
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
console.log('llonebot init db success')
resolve(null)
} catch (e) {
} catch (e: any) {
console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300)
}
@ -72,13 +72,13 @@ class DBUtil {
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP))
} catch (e) {}
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!)
} catch (e) { }
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
@ -91,30 +91,30 @@ class DBUtil {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
async getMsgByShortId(shortMsgId: number): Promise<RawMessage | undefined> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
const longId = await this.db?.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e) {
} catch (e: any) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage> {
async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db.get(longIdKey)
const msg = JSON.parse(data)
const data = await this.db?.get(longIdKey)
const msg = JSON.parse(data!)
this.addCache(msg)
return msg
} catch (e) {
@ -122,17 +122,17 @@ class DBUtil {
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage> {
async getMsgBySeqId(seqId: string): Promise<RawMessage | undefined> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db.get(seqIdKey)
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
const longId = await this.db?.get(seqIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e) {
} catch (e: any) {
log('getMsgBySeqId db error', e.stack.toString())
}
}
@ -141,7 +141,7 @@ class DBUtil {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
@ -161,13 +161,13 @@ class DBUtil {
msg.msgShortId = shortMsgId
this.addCache(msg)
// log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch()
this.db.put(longIdKey, JSON.stringify(msg)).then().catch()
this.db?.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(longIdKey, JSON.stringify(msg)).then().catch()
try {
await this.db.get(seqIdKey)
await this.db?.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then().catch()
this.db?.put(seqIdKey, msg.msgId).then().catch()
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg
@ -178,7 +178,7 @@ class DBUtil {
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
@ -187,18 +187,18 @@ class DBUtil {
}
}
Object.assign(existMsg, msg)
this.db.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg.msgShortId
Object.assign(existMsg!, msg)
this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg
this.cache[seqIdKey] = existMsg!
}
this.db.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(shortIdKey, msg.msgId).then().catch()
try {
await this.db.get(seqIdKey)
await this.db?.get(seqIdKey)
} catch (e) {
this.db.put(seqIdKey, msg.msgId).then().catch()
this.db?.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
@ -208,15 +208,15 @@ class DBUtil {
const key = 'msg_current_short_id'
if (this.currentShortId === undefined) {
try {
let id: string = await this.db.get(key)
this.currentShortId = parseInt(id)
const id = await this.db?.get(key)
this.currentShortId = parseInt(id!)
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++
this.db.put(key, this.currentShortId.toString()).then().catch()
this.db?.put(key, this.currentShortId.toString()).then().catch()
return this.currentShortId
}
@ -229,8 +229,8 @@ class DBUtil {
delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data
try {
await this.db.put(key, JSON.stringify(cacheDBData))
} catch (e) {
await this.db?.put(key, JSON.stringify(cacheDBData))
} catch (e: any) {
log('addFileCache db error', e.stack.toString())
}
}
@ -241,8 +241,8 @@ class DBUtil {
return this.cache[key] as FileCache
}
try {
let data = await this.db.get(key)
return JSON.parse(data)
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
@ -255,7 +255,7 @@ class DBUtil {
return
}
this.cache[key] = notify
this.db.put(key, JSON.stringify(notify)).then().catch()
this.db?.put(key, JSON.stringify(notify)).then().catch()
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
@ -264,8 +264,8 @@ class DBUtil {
return this.cache[key] as GroupNotify
}
try {
let data = await this.db.get(key)
return JSON.parse(data)
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}

@ -1,5 +1,5 @@
import express, { Express, Request, Response } from 'express'
import http from 'http'
import http from 'node:http'
import cors from 'cors'
import { log } from '../utils/log'
import { getConfigUtil } from '../config'
@ -10,7 +10,7 @@ type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase {
name: string = 'LLOneBot'
private readonly expressAPP: Express
private server: http.Server = null
private server: http.Server | null = null
constructor() {
this.expressAPP = express()
@ -38,7 +38,7 @@ export abstract class HttpServerBase {
let clientToken = ''
const authHeader = req.get('authorization')
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
clientToken = authHeader.split('Bearer ').pop()!
log('receive http header token', clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
@ -62,7 +62,7 @@ export abstract class HttpServerBase {
})
this.listen(port)
llonebotError.httpServerError = ''
} catch (e) {
} catch (e: any) {
log('HTTP服务启动失败', e.toString())
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString()
}
@ -103,7 +103,7 @@ export abstract class HttpServerBase {
log('收到http请求', url, payload)
try {
res.send(await handler(res, payload))
} catch (e) {
} catch (e: any) {
this.handleFailed(res, payload, e.stack.toString())
}
})

@ -6,9 +6,9 @@ import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
class WebsocketClientBase {
private wsClient: WebSocket
private wsClient: WebSocket | undefined
constructor() {}
constructor() { }
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
@ -16,11 +16,11 @@ class WebsocketClientBase {
}
}
onMessage(msg: string) {}
onMessage(msg: string) { }
}
export class WebsocketServerBase {
private ws: WebSocketServer = null
private ws: WebSocketServer | null = null
constructor() {
console.log(`llonebot websocket service started`)
@ -30,22 +30,22 @@ export class WebsocketServerBase {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e) {
} catch (e: any) {
llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString()
}
this.ws.on('connection', (wsClient, req) => {
const url = req.url.split('?').shift()
this.ws?.on('connection', (wsClient, req) => {
const url = req.url?.split('?').shift()
this.authorize(wsClient, req)
this.onConnect(wsClient, url, req)
this.onConnect(wsClient, url!, req)
wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url, msg.toString())
this.onMessage(wsClient, url!, msg.toString())
})
})
}
stop() {
llonebotError.wsServerError = ''
this.ws.close((err) => {
this.ws?.close((err) => {
log('ws server close failed!', err)
})
this.ws = null
@ -83,11 +83,11 @@ export class WebsocketServerBase {
}
}
authorizeFailed(wsClient: WebSocket) {}
authorizeFailed(wsClient: WebSocket) { }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { }
onMessage(wsClient: WebSocket, url: string, msg: string) {}
onMessage(wsClient: WebSocket, url: string, msg: string) { }
sendHeart() {}
sendHeart() { }
}

@ -0,0 +1,231 @@
import { NodeIQQNTWrapperSession } from '@/ntqqapi/wrapper'
import { randomUUID } from 'node:crypto'
interface Internal_MapKey {
timeout: number
createtime: number
func: (...arg: any[]) => any
checker: ((...args: any[]) => boolean) | undefined
}
export class ListenerClassBase {
[key: string]: string
}
export interface ListenerIBase {
new(listener: any): ListenerClassBase
}
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}
constructor() {
}
createProxyDispatch(ListenerMainName: string) {
const current = this
return new Proxy({}, {
get(target: any, prop: any, receiver: any) {
// console.log('get', prop, typeof target[prop])
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
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
}
CreatEventFunction<T extends (...args: any) => any>(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
}
}
CreatListenerFunction<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.CreatEventFunction<(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: any[]) {
//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> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<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.CreatListenerFunction(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: any = {}
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: any[]) => {
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.CreatListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[]))
})
}
}
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) })

@ -1,170 +1,116 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log'
import path from 'node:path'
import ffmpeg from 'fluent-ffmpeg'
import fsPromise from 'node:fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import { log } from './log'
import { TEMP_DIR } from './index'
import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream'
interface FFmpegOptions {
input?: string[]
output?: string[]
}
type Input = string | Readable
function convert(input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer> | Promise<string> {
return new Promise<any>((resolve, reject) => {
const chunks: Buffer[] = []
let command = ffmpeg(input)
.on('error', err => {
log(`FFmpeg处理转换出错: `, err.message)
reject(err)
})
.on('end', () => {
if (!outputPath) {
resolve(Buffer.concat(chunks))
} else {
resolve(outputPath)
}
})
if (options.input) {
command = command.inputOptions(options.input)
}
if (options.output) {
command = command.outputOptions(options.output)
}
const ffmpegPath = getConfigUtil().getConfig().ffmpeg
if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath)
}
if (!outputPath) {
const stream = command.pipe()
stream.on('data', chunk => {
chunks.push(chunk)
})
} else {
command.save(outputPath)
}
})
}
export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: 'r',
})
const fileHeader = buffer.toString('hex', 0, bytesToRead)
return fileHeader
} catch (err) {
console.error('读取文件错误:', err)
return
}
}
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath))
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try {
const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, randomUUID())
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm'
let sampleRate = 0
const convert = () => {
return new Promise<Buffer>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {
})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
reject(Error(`FFmpeg处理转换失败`))
})
})
}
let input: Buffer
if (!_isWav) {
input = await convert()
let result: EncodeResult
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
result = await encode(file, 0)
} else {
input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
input = await convert()
}
const input = await convert(filePath, {
output: [
'-ar 24000',
'-ac 1',
'-f s16le'
]
})
result = await encode(input, 24000)
}
const silk = await encode(input, sampleRate)
fs.writeFileSync(pttPath, silk.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
duration: result.duration / 1000,
}
} else {
const silk = file
let duration = 0
let duration = 1
try {
duration = getDuration(silk) / 1000
} catch (e) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack)
duration = await guessDuration(filePath)
} catch (e: any) {
log('获取语音文件时长失败, 默认为1秒', filePath, e.stack)
}
return {
converted: false,
path: filePath,
duration,
}
}
} catch (error) {
} catch (error: any) {
log('convert silk failed', error.stack)
return {}
}
}
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') {
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath))
const outPCMPath = fileName + '.pcm'
const outFilePath = fileName + '.' + outFormat
await fsPromise.writeFile(outPCMPath, data)
const convert = () => {
return new Promise<string>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, [
'-y',
'-f', 's16le', // PCM format
'-ar', '24000', // Sample rate
'-ac', '1', // Number of audio channels
'-i', outPCMPath,
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') {
const silk = await fsPromise.readFile(inputFilePath)
const { data } = await decode(silk, 24000)
const outFilePath = path.join(TEMP_DIR, path.basename(inputFilePath)) + `.${outFormat}`
return convert(Readable.from(data), {
input: [
'-f s16le',
'-ar 24000',
'-ac 1'
]
}, outFilePath)
}

@ -52,7 +52,7 @@ export async function file2base64(path: string) {
const data = await fsPromise.readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err) {
} catch (err: any) {
result.err = err.toString()
}
return result
@ -119,7 +119,7 @@ type Uri2LocalRes = {
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> {
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> {
let res = {
success: false,
errMsg: '',
@ -132,10 +132,10 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
fileName = randomUUID()
}
let filePath = path.join(TEMP_DIR, fileName)
let url = null
let url: URL | null = null
try {
url = new URL(uri)
} catch (e) {
} catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
@ -153,10 +153,10 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer = null
let buffer: Buffer | null = null
try {
buffer = await httpDownload(uri)
} catch (e) {
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
@ -208,7 +208,7 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
const ext = (await fileType.fileTypeFromFile(filePath))?.ext
if (ext) {
log('获取文件类型', ext, filePath)
await fsPromise.rename(filePath, filePath + `.${ext}`)

@ -41,7 +41,7 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
})
}
export function isNull(value: any) {
export function isNull(value: any): value is null | undefined | void {
return value === undefined || value === null
}
@ -73,14 +73,14 @@ export function wrapText(str: string, maxLength: number): string {
* @param customKey
* @returns
*/
export function cacheFunc(ttl: number, customKey: string='') {
export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;
const className = target.constructor.name; // 获取类名
const methodName = propertyKey; // 获取方法名
descriptor.value = async function (...args: any[]){
descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {

@ -91,7 +91,7 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) {
releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch {}
return ''
}

@ -31,10 +31,10 @@ export async function getVideoInfo(filePath: string) {
console.log('未找到视频流信息。')
}
resolve({
width: videoStream.width,
height: videoStream.height,
time: parseInt(videoStream.duration),
format: metadata.format.format_name,
width: videoStream?.width!,
height: videoStream?.height!,
time: parseInt(videoStream?.duration!),
format: metadata.format.format_name!,
size,
filePath,
})
@ -67,7 +67,7 @@ export async function encodeMp4(filePath: string) {
return videoInfo
}
export function checkFfmpeg(newPath: string = null): Promise<boolean> {
export function checkFfmpeg(newPath: string | null = null): Promise<boolean> {
return new Promise((resolve, reject) => {
log('开始检查ffmpeg', newPath)
if (newPath) {

@ -36,11 +36,8 @@ import {
RawMessage,
} from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { OB11FriendRecallNoticeEvent } from '../onebot11/event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from '../onebot11/event/notice/OB11GroupRecallNoticeEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import * as path from 'node:path'
@ -48,16 +45,15 @@ import { dbUtil } from '../common/db'
import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from '../onebot11/event/notice/OB11PokeEvent'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/native/wrapper'
import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api'
let running = false
import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperApi, wrapperConstructor } from '../ntqqapi/wrapper'
let mainWindow: BrowserWindow | null = null
@ -127,7 +123,7 @@ function onLoad() {
return
}
dialog
.showMessageBox(mainWindow, {
.showMessageBox(mainWindow!, {
type: 'question',
buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
@ -214,7 +210,7 @@ function onLoad() {
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList)
} catch (e) {
} catch (e: any) {
log('report message error: ', e.stack.toString())
}
})
@ -258,7 +254,7 @@ function onLoad() {
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord])
} catch (e) {
} catch (e: any) {
log('report self message error: ', e.stack.toString())
}
})
@ -336,7 +332,7 @@ function onLoad() {
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid)
operatorId = member2.uin
operatorId = member2?.uin!
subType = 'kick'
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(
@ -346,14 +342,12 @@ function onLoad() {
subType,
)
postOb11Event(groupDecreaseEvent, true)
} catch (e) {
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
}
}
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求')
let groupRequestEvent = new OB11GroupRequestEvent()
groupRequestEvent.group_id = parseInt(notify.group.groupCode)
let requestQQ = uidMaps[notify.user1.uid]
if (!requestQQ) {
try {
@ -362,40 +356,47 @@ function onLoad() {
log('获取加群人QQ号失败', e)
}
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0
groupRequestEvent.sub_type = 'add'
groupRequestEvent.comment = notify.postscript
groupRequestEvent.flag = notify.seq
let invitorId: number
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid]
if (!invitorQQ) {
try {
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))
groupRequestEvent.invitor_id = parseInt(invitor.uin)
invitorId = parseInt(invitor.uin)
} catch (e) {
groupRequestEvent.invitor_id = 0
invitorId = 0
log('获取邀请人QQ号失败', e)
}
}
}
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0,
notify.seq,
notify.postscript,
invitorId!,
'add'
)
postOb11Event(groupRequestEvent)
}
else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
let groupInviteEvent = new OB11GroupRequestEvent()
groupInviteEvent.group_id = parseInt(notify.group.groupCode)
let user_id = uidMaps[notify.user2.uid]
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
let userId = uidMaps[notify.user2.uid]
if (!userId) {
userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id)
groupInviteEvent.sub_type = 'invite'
// groupInviteEvent.invitor_id = parseInt(user_id)
groupInviteEvent.flag = notify.seq
const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId),
notify.seq,
undefined,
undefined,
'invite'
)
postOb11Event(groupInviteEvent)
}
} catch (e) {
} catch (e: any) {
log('解析群通知失败', e.stack.toString())
}
}
@ -407,19 +408,18 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime
const flag = req.friendUid + req.reqTime
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) {
friendRequests[flag] = req
log('有新的好友请求', req)
let friendRequestEvent = new OB11FriendRequestEvent()
let userId: number
try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin)
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
userId = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
friendRequestEvent.flag = flag
friendRequestEvent.comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
postOb11Event(friendRequestEvent)
}
}
@ -442,6 +442,7 @@ function onLoad() {
uidMaps[value] = key
}
})
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: wrapperApi.NodeIQQNTWrapperSession! })
try {
log('start get groups')
const _groups = await NTQQGroupApi.getGroups()
@ -512,7 +513,7 @@ function onLoad() {
selfInfo.nick = userInfo.nick
return
}
} catch (e) {
} catch (e: any) {
log('get self nickname failed', e.stack)
}
if (getSelfNickCount < 10) {
@ -540,7 +541,7 @@ function onBrowserWindowCreated(window: BrowserWindow) {
try {
hookNTQQApiCall(window)
hookNTQQApiReceive(window)
} catch (e) {
} catch (e: any) {
log('LLOneBot hook error: ', e.toString())
}
}

@ -11,22 +11,24 @@ import {
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement,
} from '../types'
import path from 'path'
import fs from 'fs'
import path from 'node:path'
import fs from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import { Peer } from '@/ntqqapi/api/msg'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
return (await wrapperApi.NodeIQQNTWrapperSession.getRichMediaService().getVideoPlayUrlV2(peer,
const session = wrapperApi.NodeIQQNTWrapperSession
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url;
}
static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API,

@ -1,8 +1,10 @@
import { Friend, FriendRequest } from '../types'
import { Friend, FriendRequest, FriendV2 } from '../types'
import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { friendRequests } from '../../common/data'
import { log } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '../../common/utils/EventTask'
export class NTQQFriendApi {
static async getFriends(forced = false) {
@ -26,6 +28,7 @@ export class NTQQFriendApi {
}
return _friends
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
@ -42,6 +45,7 @@ export class NTQQFriendApi {
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
@ -62,4 +66,16 @@ export class NTQQFriendApi {
delete friendRequests[flag]
return result
}
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
const session = wrapperApi.NodeIQQNTWrapperSession
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values())
}
}

@ -5,25 +5,26 @@ import { deleteGroup, uidMaps } from '../../common/data'
import { dbUtil } from '../../common/db'
import { log } from '../../common/utils/log'
import { NTQQWindowApi, NTQQWindows } from './window'
import { wrapperApi } from '../native/wrapper'
import { wrapperApi } from '../wrapper'
export class NTQQGroupApi {
static async activateMemberListChange(){
static async activateMemberListChange() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async activateMemberInfoChange(){
static async activateMemberInfoChange() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async getGroupAllInfo(groupCode: string, source: number=4){
static async getGroupAllInfo(groupCode: string, source: number = 4) {
return await callNTQQApi<GeneralCallResult & Group>({
methodName: NTQQApiMethod.GET_GROUP_ALL_INFO,
args: [
@ -35,6 +36,7 @@ export class NTQQGroupApi {
],
})
}
static async getGroups(forced = false) {
// let cbCmd = ReceiveCmdS.GROUPS
// if (process.platform != 'win32') {
@ -52,6 +54,7 @@ export class NTQQGroupApi {
log('get groups result', result)
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
@ -62,7 +65,7 @@ export class NTQQGroupApi {
},
],
})
// log("get group member sceneId", sceneId);
// log("get group member sceneId", sceneId)
try {
const result = await callNTQQApi<{
result: { infos: any }
@ -83,8 +86,8 @@ export class NTQQGroupApi {
for (const member of members) {
uidMaps[member.uid] = member.uin
}
// log(uidMaps);
// log("members info", values);
// log(uidMaps)
// log("members info", values)
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
@ -92,7 +95,8 @@ export class NTQQGroupApi {
return []
}
}
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean=false) {
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO,
args: [
@ -105,6 +109,7 @@ export class NTQQGroupApi {
],
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
@ -119,6 +124,7 @@ export class NTQQGroupApi {
args: [{ doubt: false, startSeq: '', number: 14 }, null],
})
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies()
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
@ -127,12 +133,13 @@ export class NTQQGroupApi {
ReceiveCmdS.GROUP_NOTIFY,
)
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
const notify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
// delete groupNotifies[seq]
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
@ -152,6 +159,7 @@ export class NTQQGroupApi {
],
})
}
static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
@ -162,6 +170,7 @@ export class NTQQGroupApi {
}
return result
}
static async kickMember(
groupQQ: string,
kickUids: string[],
@ -180,7 +189,8 @@ export class NTQQGroupApi {
],
})
}
static async banMember(groupQQ: string, memList: Array<{ uid: string; timeStamp: number }>) {
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_MEMBER,
@ -192,6 +202,7 @@ export class NTQQGroupApi {
],
})
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
@ -204,6 +215,7 @@ export class NTQQGroupApi {
],
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log)
const res = await callNTQQApi<GeneralCallResult>({
@ -218,8 +230,9 @@ export class NTQQGroupApi {
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res;
return res
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
@ -233,6 +246,7 @@ export class NTQQGroupApi {
],
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
@ -282,29 +296,34 @@ export class NTQQGroupApi {
],
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {}
static publishGroupBulletin(groupQQ: string, title: string, content: string) { }
static async removeGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await wrapperApi.NodeIQQNTWrapperSession.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return wrapperApi.NodeIQQNTWrapperSession.getGroupService().removeGroupEssence(param);
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param)
}
static async addGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await wrapperApi.NodeIQQNTWrapperSession.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return wrapperApi.NodeIQQNTWrapperSession.getGroupService().addGroupEssence(param);
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
}
}

@ -6,7 +6,7 @@ import { ReceiveCmdS, registerReceiveHook } from '../hook'
import { log } from '../../common/utils/log'
import { sleep } from '../../common/utils/helper'
import { isQQ998 } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import { wrapperApi } from '@/ntqqapi/wrapper'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
@ -33,7 +33,7 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
}
await waitLastSend()
let sentMessage: RawMessage = null
let sentMessage: RawMessage | null = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
@ -289,6 +289,7 @@ export class NTQQMsgApi {
})
}
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
return await wrapperApi.NodeIQQNTWrapperSession.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
const session = wrapperApi.NodeIQQNTWrapperSession
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
}
}

@ -2,58 +2,63 @@
import { log } from '@/common/utils'
interface ServerRkeyData{
group_rkey: string;
private_rkey: string;
expired_time: number;
interface ServerRkeyData {
group_rkey: string
private_rkey: string
expired_time: number
}
class RkeyManager {
serverUrl: string = '';
serverUrl: string = ''
private rkeyData: ServerRkeyData = {
group_rkey: '',
private_rkey: '',
expired_time: 0
};
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}
async getRkey(){
constructor(serverUrl: string) {
this.serverUrl = serverUrl
}
async getRkey() {
if (this.isExpired()) {
try {
await this.refreshRkey();
await this.refreshRkey()
} catch (e) {
log('获取rkey失败', e);
log('获取rkey失败', e)
}
}
return this.rkeyData;
return this.rkeyData
}
isExpired(): boolean {
const now = new Date().getTime() / 1000;
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`);
return now > this.rkeyData.expired_time;
const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time
}
async refreshRkey(): Promise<any> {
//刷新rkey
this.rkeyData = await this.fetchServerRkey();
this.rkeyData = await this.fetchServerRkey()
}
async fetchServerRkey(){
async fetchServerRkey() {
return new Promise<ServerRkeyData>((resolve, reject) => {
fetch(this.serverUrl)
.then(response => {
if (!response.ok) {
return reject(response.statusText); // 请求失败,返回错误信息
return reject(response.statusText) // 请求失败,返回错误信息
}
return response.json(); // 解析 JSON 格式的响应体
return response.json() // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data);
resolve(data)
})
.catch(error => {
reject(error);
});
});
reject(error)
})
})
}
}
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey');
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey')

@ -2,19 +2,21 @@ import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../
import { Group, SelfInfo, User } from '../types'
import { ReceiveCmdS } from '../hook'
import { selfInfo, uidMaps } from '../../common/data'
import { NTQQWindowApi, NTQQWindows } from './window'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import * as https from 'https'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
let userInfoCache: Record<string, User> = {} // uid: User
const userInfoCache: Record<string, User> = {} // uid: User
export interface ClientKeyData extends GeneralCallResult {
url: string;
keyIndex: string;
clientKey: string;
expireTime: string;
url: string
keyIndex: string
clientKey: string
expireTime: string
}
export class NTQQUserApi {
@ -48,8 +50,49 @@ export class NTQQUserApi {
return result.profiles.get(uid)
}
// 26702
static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
1,
5000,
(profile) => {
if (profile.uid === uid) {
return true;
}
return false;
},
'BuddyProfileStore',
[
uid
],
UserDetailSource.KSERVER,
[
ProfileBizType.KALL
]
)
const RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: ''
}
return RetUser
}
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
// this.getUserInfo(uid);
if (+qqPkgInfo.buildVersion >= 26702) {
return this.fetchUserDetailInfo(uid)
}
// this.getUserInfo(uid)
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
if (!withBizInfo) {
methodName = NTQQApiMethod.USER_DETAIL_INFO
@ -82,7 +125,7 @@ export class NTQQUserApi {
await fetchInfo()
await sleep(1000)
}
let userInfo = await fetchInfo()
const userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
@ -99,16 +142,17 @@ export class NTQQUserApi {
],
})
}
static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string; } = {};
let cookies: { [key: string]: string } = {}
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl);
cookies = await RequestUtil.HttpsGetCookies(requestUrl)
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies;
return cookies
}
static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey()
@ -117,24 +161,24 @@ export class NTQQUserApi {
}
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;
+ '&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
}
@cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) {
let data = (await NTQQUserApi.getQzoneCookies());
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin;
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue };
let data = (await NTQQUserApi.getQzoneCookies())
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue }
}
const skey = await this.getSkey();
const pskey = (await this.getPSkey([domain])).get(domain);
const skey = await this.getSkey()
const pskey = (await this.getPSkey([domain])).get(domain)
if (!pskey || !skey) {
throw new Error('获取Cookies失败')
}
const bkn = NTQQUserApi.genBkn(skey)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`;
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`
return { cookies, bkn }
}
@ -151,7 +195,8 @@ export class NTQQUserApi {
}
static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const res = await wrapperApi.NodeIQQNTWrapperSession.getTipOffService().getPskey(domains, true)
const session = wrapperApi.NodeIQQNTWrapperSession
const res = await session?.getTipOffService().getPskey(domains, true)
if (res.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`)
}
@ -159,7 +204,7 @@ export class NTQQUserApi {
}
static async getClientKey(): Promise<ClientKeyData> {
return await wrapperApi.NodeIQQNTWrapperSession.getTicketService().forceFetchClientKey('')
const session = wrapperApi.NodeIQQNTWrapperSession
return await session?.getTicketService().forceFetchClientKey('')
}
}

@ -1,7 +1,8 @@
import { WebGroupData, groups, selfInfo } from '@/common/data';
import { log } from '@/common/utils/log';
import { NTQQUserApi } from './user';
import { RequestUtil } from '@/common/utils/request';
import { WebGroupData, groups, selfInfo } from '@/common/data'
import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request'
export enum WebHonorType {
ALL = 'all',
TALKACTIVE = 'talkative',
@ -10,6 +11,7 @@ export enum WebHonorType {
STORONGE_NEWBI = 'strong_newbie',
EMOTION = 'emotion'
}
export interface WebApiGroupMember {
uin: number
role: number
@ -27,6 +29,7 @@ export interface WebApiGroupMember {
qage: number
rm: number
}
interface WebApiGroupMemberRet {
ec: number
errcode: number
@ -41,6 +44,7 @@ interface WebApiGroupMemberRet {
search_count: number
extmode: number
}
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
@ -69,6 +73,7 @@ export interface WebApiGroupNoticeFeed {
is_read: number
is_all_confirm: number
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
@ -89,6 +94,7 @@ export interface WebApiGroupNoticeRet {
svrt: number
ad: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
@ -102,6 +108,7 @@ interface GroupEssenceMsg {
msg_content: any[]
can_be_removed: true
}
export interface GroupEssenceMsgRet {
retcode: number
retmsg: string
@ -112,22 +119,24 @@ export interface GroupEssenceMsgRet {
config_page_url: string
}
}
export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet> {
const {cookies: CookieValue, bkn: Bkn} = (await NTQQUserApi.getCookies('qun.qq.com'))
const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20';
let ret;
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> {
const { cookies: CookieValue, bkn: Bkn } = (await NTQQUserApi.getCookies('qun.qq.com'))
const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'
let ret: GroupEssenceMsgRet
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue });
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue })
} catch {
return undefined;
return undefined
}
//console.log(url, CookieValue);
//console.log(url, CookieValue)
if (ret.retcode !== 0) {
return undefined;
return undefined
}
return ret;
return ret
}
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode);
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
@ -190,6 +199,7 @@ export class WebApi {
// const res = await this.request(url);
// return await res.json();
// }
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
@ -213,6 +223,7 @@ export class WebApi {
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
@ -236,6 +247,7 @@ export class WebApi {
}
return undefined;
}
static genBkn(sKey: string) {
sKey = sKey || '';
let hash = 5381;
@ -247,6 +259,7 @@ export class WebApi {
return (hash & 0x7FFFFFFF).toString();
}
//实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) {

@ -27,7 +27,7 @@ export class NTQQWindowApi {
static async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow,
args: any[],
cbCmd: ReceiveCmd = null,
cbCmd: ReceiveCmd | null = null,
autoCloseSeconds: number = 2,
) {
const result = await callNTQQApi<R>({

@ -283,7 +283,7 @@ export class SendMsgElementConstructor {
if (faceId >= 222){
faceType = 2
}
if (face.AniStickerType){
if (face?.AniStickerType){
faceType = 3;
}
return {
@ -292,10 +292,10 @@ export class SendMsgElementConstructor {
faceElement: {
faceIndex: faceId,
faceType,
faceText: face.QDes,
stickerId: face.AniStickerId,
stickerType: face.AniStickerType,
packId: face.AniStickerPackId,
faceText: face?.QDes,
stickerId: face?.AniStickerId,
stickerType: face?.AniStickerType,
packId: face?.AniStickerPackId,
sourceType: 1,
},
}
@ -329,7 +329,7 @@ export class SendMsgElementConstructor {
stickerId: '33',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
resultId: resultId?.toString(),
surpriseId: '',
// "randomType": 1,
},
@ -351,7 +351,7 @@ export class SendMsgElementConstructor {
stickerId: '34',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
resultId: resultId?.toString(),
surpriseId: '',
// "randomType": 1,
},

@ -83,7 +83,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
let isLogger = false
try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) {}
} catch (e) { }
if (!isLogger) {
try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
@ -102,7 +102,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
; (_ as Promise<void>).then()
}
} catch (e) {
log('hook error', e, receiveData.payload)
@ -123,7 +123,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
delete hookApiCallbacks[callbackId]
}
}
} catch (e) {
} catch (e: any) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args)
@ -142,11 +142,11 @@ export function hookNTQQApiCall(window: BrowserWindow) {
let isLogger = false
try {
isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) {}
} catch (e) { }
if (!isLogger) {
try {
HOOK_LOG && log('call NTQQ api', thisArg, args)
} catch (e) {}
} catch (e) { }
try {
const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
@ -157,7 +157,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
try {
let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
(_ as Promise<void>).then()
}
} catch (e) {
log('hook call error', e, _args)
@ -165,7 +165,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
}).then()
}
})
} catch (e) {}
} catch (e) { }
}
return target.apply(thisArg, args)
},
@ -189,7 +189,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
let ret = target.apply(thisArg, args)
try {
HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) {}
} catch (e) { }
return ret
},
})
@ -296,7 +296,7 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
@ -320,7 +320,7 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
}
updateGroups(newGroupList, false).then()
} catch (e) {
} catch (e: any) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
@ -328,7 +328,7 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
export async function startHook() {
// 群列表变动
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
@ -372,10 +372,11 @@ export async function startHook() {
)
} else if (member.role != existMember.role) {
log('有管理员变动通知')
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(groupCode)
groupAdminNoticeEvent.user_id = parseInt(member.uin)
groupAdminNoticeEvent.sub_type = member.role == GroupMemberRole.admin ? 'set' : 'unset'
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent(
member.role == GroupMemberRole.admin ? 'set' : 'unset',
parseInt(groupCode),
parseInt(member.uin)
)
postOb11Event(groupAdminNoticeEvent, true)
}
Object.assign(existMember, member)
@ -397,7 +398,7 @@ export async function startHook() {
// }
})
// 好友列表变动
// 好友列表变动
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
@ -453,7 +454,7 @@ export async function startHook() {
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
@ -471,7 +472,7 @@ export async function startHook() {
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
}, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
}
}
})
@ -486,7 +487,7 @@ export async function startHook() {
if (sendCallback) {
try {
sendCallback(message)
} catch (e) {
} catch (e: any) {
log('receive self msg error', e.stack)
}
}
@ -518,8 +519,8 @@ export async function startHook() {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg.msgTime) < 5) {
OB11Constructor.message(lastTempMsg).then((r) => postOb11Event(r))
if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) {
OB11Constructor.message(lastTempMsg!).then((r) => postOb11Event(r))
}
})
})
@ -550,5 +551,4 @@ export async function startHook() {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}

@ -0,0 +1,44 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
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
}
export interface NodeIKernelProfileListener extends IProfileListener {
new(listener: IProfileListener): NodeIKernelProfileListener
}
export class ProfileListener implements IProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]) {
}
onProfileDetailInfoChanged(profile: User) {
}
onStatusUpdate(...args: unknown[]) {
}
onSelfStatusChanged(...args: unknown[]) {
}
onStrangerRemarkChanged(...args: unknown[]) {
}
}

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

@ -1,19 +0,0 @@
let Process = require('process')
let os = require('os')
Process.dlopenOrig = Process.dlopen
export const wrapperApi: any = {}
Process.dlopen = function(module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
let dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
let ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
},
})
}
return dlopenRet
}

@ -1,7 +1,6 @@
import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log'
import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window'
import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto'

@ -0,0 +1,125 @@
import { GeneralCallResult } from './common'
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService {
// 26702 以上
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: Array<{
categoryId: number,
categorySortId: number,
categroyName: string,
categroyMbCount: number,
onlineCount: number,
buddyUids: Array<string>
}>
}>
//26702 以上
getBuddyListFromCache(callFrom: string): Promise<Array<
{
categoryId: number,//9999应该跳过 那是兜底数据吧
categorySortId: number,//排序方式
categroyName: string,//分类名
categroyMbCount: number,//不懂
onlineCount: number,//在线数目
buddyUids: Array<string>//Uids
}>>
addKernelBuddyListener(listener: any): number
getAllBuddyCount(): number
removeKernelBuddyListener(listener: unknown): void
getBuddyList(nocache: boolean): Promise<GeneralCallResult>
getBuddyNick(uid: number): string
getBuddyRemark(uid: number): string
setBuddyRemark(uid: number, remark: string): void
getAvatarUrl(uid: number): string
isBuddy(uid: string): boolean
getCategoryNameWithUid(uid: number): string
getTargetBuddySetting(uid: number): unknown
getTargetBuddySettingByType(uid: number, type: number): unknown
getBuddyReqUnreadCnt(): number
getBuddyReq(): unknown
delBuddyReq(uid: number): void
clearBuddyReqUnreadCnt(): void
reqToAddFriends(uid: number, msg: string): void
setSpacePermission(uid: number, permission: number): void
approvalFriendRequest(arg: {
friendUid: string
reqTime: string
accept: boolean
}): Promise<void>
delBuddy(uid: number): void
delBatchBuddy(uids: number[]): void
getSmartInfos(uid: number): unknown
setBuddyCategory(uid: number, category: number): void
setBatchBuddyCategory(uids: number[], category: number): void
addCategory(category: string): void
delCategory(category: string): void
renameCategory(oldCategory: string, newCategory: string): void
resortCategory(categorys: string[]): void
pullCategory(uid: number, category: string): void
setTop(uid: number, isTop: boolean): void
SetSpecialCare(uid: number, isSpecialCare: boolean): void
setMsgNotify(uid: number, isNotify: boolean): void
hasBuddyList(): boolean
setBlock(uid: number, isBlock: boolean): void
isBlocked(uid: number): boolean
modifyAddMeSetting(setting: unknown): void
getAddMeSetting(): unknown
getDoubtBuddyReq(): unknown
getDoubtBuddyUnreadNum(): number
approvalDoubtBuddyReq(uid: number, isAgree: boolean): void
delDoubtBuddyReq(uid: number): void
delAllDoubtBuddyReq(): void
reportDoubtBuddyReqUnread(): void
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<unknown>
isNull(): boolean
}

@ -0,0 +1,106 @@
import { AnyCnameRecord } from 'node:dns'
import { SimpleInfo } from '../types'
import { GeneralCallResult } from './common'
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string,string>>//uin->uid
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string,string>>
// {
// coreInfo: CoreInfo,
// baseInfo: BaseInfo,
// status: null,
// vasInfo: null,
// relationFlags: null,
// otherFlags: null,
// intimate: null
// }
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
addKernelProfileListener(listener: any): number
removeKernelProfileListener(listenerId: number): void
prepareRegionConfig(...args: unknown[]): unknown
getLocalStrangerRemark(): Promise<AnyCnameRecord>
enumCountryOptions(): Array<string>
enumProvinceOptions(Country: string): Array<string>
enumCityOptions(Country: string, Province: string): unknown
enumAreaOptions(...args: unknown[]): unknown
//SimpleInfo
// this.uid = ""
// this.uid = str
// this.uin = j2
// this.isBuddy = z
// this.coreInfo = coreInfo
// this.baseInfo = baseInfo
// this.status = statusInfo
// this.vasInfo = vasInfo
// this.relationFlags = relationFlag
// this.otherFlags = otherFlag
// this.intimate = intimate
modifySelfProfile(...args: unknown[]): Promise<unknown>
modifyDesktopMiniProfile(param: any): Promise<GeneralCallResult>
setNickName(NickName: string): Promise<unknown>
setLongNick(longNick: string): Promise<unknown>
setBirthday(...args: unknown[]): Promise<unknown>
setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown>
setRecommendImgFlag(...args: unknown[]): Promise<unknown>
getUserSimpleInfo(force: boolean, uids: string[],): Promise<unknown>
getUserDetailInfo(uid: string): Promise<unknown>
getUserDetailInfoWithBizInfo(uid: string, Biz: any[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<any>
getZplanAvatarInfos(args: string[]): Promise<unknown>
getStatus(uid: string): Promise<unknown>
startStatusPolling(isForceReset: boolean): Promise<unknown>
getSelfStatus(): Promise<unknown>
setdisableEmojiShortCuts(...args: unknown[]): unknown
getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown>
//profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList)
getCoreInfo(name: string, arg: any[]): unknown
//m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>())
isNull(): boolean
}

@ -0,0 +1,16 @@
export enum GeneralCallResultStatus {
OK = 0
// ERROR = 1
}
export interface GeneralCallResult {
result: GeneralCallResultStatus
errMsg: string
}
export interface forceFetchClientKeyRetType extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}

@ -0,0 +1,2 @@
export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService'

@ -1,5 +1,4 @@
import { GroupMemberRole } from './group'
import exp from 'constants'
export enum ElementType {
TEXT = 1,
@ -417,7 +416,7 @@ export interface RawMessage {
}
export interface Peer {
chatType: ChatType;
peerUid: string; // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string;
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string
}

@ -77,8 +77,185 @@ export interface Friend extends User {
}
export interface CategoryFriend {
categoryId: number;
categroyName: string;
categroyMbCount: number;
categoryId: number
categroyName: string
categroyMbCount: number
buddyList: User[]
}
export interface CoreInfo {
uid: string
uin: string
nick: string
remark: string
}
export interface BaseInfo {
qid: string
longNick: string
birthday_year: number
birthday_month: number
birthday_day: number
age: number
sex: number
eMail: string
phoneNum: string
categoryId: number
richTime: number
richBuffer: string
}
interface MusicInfo {
buf: string
}
interface VideoBizInfo {
cid: string
tvUrl: string
synchType: string
}
interface VideoInfo {
name: string
}
interface ExtOnlineBusinessInfo {
buf: string
customStatus: any
videoBizInfo: VideoBizInfo
videoInfo: VideoInfo
}
interface ExtBuffer {
buf: string
}
interface UserStatus {
uid: string
uin: string
status: number
extStatus: number
batteryStatus: number
termType: number
netType: number
iconType: number
customStatus: any
setTime: string
specialFlag: number
abiFlag: number
eNetworkType: number
showName: string
termDesc: string
musicInfo: MusicInfo
extOnlineBusinessInfo: ExtOnlineBusinessInfo
extBuffer: ExtBuffer
}
interface PrivilegeIcon {
jumpUrl: string
openIconList: any[]
closeIconList: any[]
}
interface VasInfo {
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
bigClub: boolean
bigClubLevel: number
nameplateVipType: number
grayNameplateFlag: number
superVipTemplateId: number
diyFontId: number
pendantId: number
pendantDiyId: number
faceId: number
vipFont: number
vipFontType: number
magicFont: number
fontEffect: number
newLoverDiamondFlag: number
extendNameplateId: number
diyNameplateIDs: any[]
vipStartFlag: number
vipDataFlag: number
gameNameplateId: string
gameLastLoginTime: string
gameRank: number
gameIconShowFlag: boolean
gameCardId: string
vipNameColorId: string
privilegeIcon: PrivilegeIcon
}
export interface SimpleInfo {
uid?: string
uin?: string
coreInfo: CoreInfo
baseInfo: BaseInfo
status: UserStatus | null
vasInfo: VasInfo | null
relationFlags: RelationFlags | null
otherFlags: any | null
intimate: any | null
}
interface RelationFlags {
topTime: string
isBlock: boolean
isMsgDisturb: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
isBlocked: boolean
recommendImgFlag: number
disableEmojiShortCuts: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
isHideQQLevel: number
isHidePrivilegeIcon: number
}
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt {
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
college: string
country: string
province: string
city: string
postCode: string
address: string
regTime: number
interest: string
labels: any[]
qqLevel: QQLevel
}
interface Pic {
picId: string
picTime: number
picUrlMap: Record<string, string>
}
interface PhotoWall {
picList: Pic[]
}
export interface UserDetailInfoListenerArg {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: PhotoWall
}

68
src/ntqqapi/wrapper.ts Normal file

@ -0,0 +1,68 @@
import { NodeIKernelBuddyService } from './services/NodeIKernelBuddyService'
import os from 'node:os'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
[key: string]: any
getBuddyService(): NodeIKernelBuddyService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
export interface WrapperConstructor {
[key: string]: any
NodeIKernelBuddyListener?: any
NodeIKernelGroupListener?: any
NodeQQNTWrapperUtil?: any
NodeIKernelMsgListener?: any
NodeIQQNTWrapperEngine?: any
NodeIGlobalAdapter?: any
NodeIDependsAdapter?: any
NodeIDispatcherAdapter?: any
NodeIKernelSessionListener?: any
NodeIKernelLoginService?: any
NodeIKernelLoginListener?: any
NodeIKernelProfileService?: any
NodeIKernelProfileListener?: any
}
export 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.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
}
return dlopenRet
}

@ -4,8 +4,8 @@ import { OB11Return } from '../types'
import { log } from '../../common/utils/log'
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName
abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
@ -21,7 +21,7 @@ class BaseAction<PayloadType, ReturnDataType> {
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData)
} catch (e) {
} catch (e: any) {
log('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
}
@ -35,7 +35,7 @@ class BaseAction<PayloadType, ReturnDataType> {
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo)
} catch (e) {
} catch (e: any) {
log('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}

@ -20,7 +20,7 @@ export interface GetFileResponse {
base64?: string
}
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement {
let element = msg.elements.find((e) => e.elementId === elementId)
if (!element) {
@ -41,7 +41,7 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg, cache.elementId).filePath
cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}

@ -14,7 +14,7 @@ export default class GetRecord extends GetFileBase {
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = await super._handle(payload)
res.file = await decodeSilk(res.file, payload.out_format)
res.file = await decodeSilk(res.file!, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){

@ -1,6 +1,6 @@
import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { dbUtil } from '../../../common/db'
import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types'
@ -37,12 +37,13 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
let messages = await Promise.all(
msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg)
resMsg.message_id = (await dbUtil.addMsg(msg))!
return resMsg
}),
)
messages.map((msg) => {
;(<OB11ForwardMessage>msg).content = msg.message
messages.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
})
return { messages }

@ -6,7 +6,6 @@ import { ChatType } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import { OB11Constructor } from '../../constructor'
import { log } from '../../../common/utils'
interface Payload {
group_id: number

@ -1,11 +1,12 @@
import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } from '../../../common/data'
import { getGroup, getUidByUin } from '@/common/data'
import { ActionName } from '../types'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import { ChatType, SendFileElement } from '../../../ntqqapi/types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types'
import fs from 'fs'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg'
import { uri2local } from '../../../common/utils'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
interface Payload {
user_id: number
@ -20,9 +21,9 @@ class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> {
getPeer(payload: Payload): Peer {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) }
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! }
}
protected async _handle(payload: Payload): Promise<null> {

@ -1,24 +1,24 @@
import { GroupEssenceMsgRet, WebApi } from "@/ntqqapi/api";
import BaseAction from "../BaseAction";
import { ActionName } from "../types";
import { GroupEssenceMsgRet, WebApi } from '@/ntqqapi/api'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface PayloadType {
group_id: number;
pages?: number;
group_id: number
pages?: number
}
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg;
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet | void> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg
protected async _handle(payload: PayloadType) {
throw '此 api 暂不支持'
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0');
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0')
if (!ret) {
throw new Error('获取失败');
throw new Error('获取失败')
}
// ret.map((item) => {
//
// })
return ret;
return ret
}
}

@ -1,22 +1,23 @@
import { WebApi, WebHonorType } from "@/ntqqapi/api";
import { ActionName } from "../types";
import BaseAction from "../BaseAction";
import { WebApi, WebHonorType } from '@/ntqqapi/api'
import { ActionName } from '../types'
import BaseAction from '../BaseAction'
interface Payload {
group_id: number,
group_id: number
type?: WebHonorType
}
export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo;
actionName = ActionName.GetGroupHonorInfo
protected async _handle(payload: Payload) {
// console.log(await NTQQUserApi.getRobotUinRange());
// console.log(await NTQQUserApi.getRobotUinRange())
if (!payload.group_id) {
throw '缺少参数group_id';
throw '缺少参数group_id'
}
if (!payload.type) {
payload.type = WebHonorType.ALL;
payload.type = WebHonorType.ALL
}
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type);
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type)
}
}

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

@ -1,9 +1,10 @@
import BaseAction from '../BaseAction'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api'
import { ChatType, RawMessage } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { getUidByUin } from '../../../common/data'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { ChatType, RawMessage } from '@/ntqqapi/types'
import { dbUtil } from '@/common/db'
import { getUidByUin } from '@/common/data'
import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types'
interface Payload {
message_id: number
@ -15,16 +16,16 @@ interface Response {
message_id: number
}
class ForwardSingleMsg extends BaseAction<Payload, Response> {
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) }
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
}
protected async _handle(payload: Payload): Promise<Response> {
const msg = await dbUtil.getMsgByShortId(payload.message_id)
const msg = (await dbUtil.getMsgByShortId(payload.message_id))!
const peer = await this.getTargetPeer(payload)
const sentMsg = await NTQQMsgApi.forwardMsg(
{
@ -35,7 +36,7 @@ class ForwardSingleMsg extends BaseAction<Payload, Response> {
[msg.msgId],
)
const ob11MsgId = await dbUtil.addMsg(sentMsg)
return {message_id: ob11MsgId}
return { message_id: ob11MsgId! }
}
}

@ -34,7 +34,6 @@ import { ALLOW_SEND_TEMP_MSG, getConfigUtil } from '../../../common/config'
import { log } from '../../../common/utils/log'
import { sleep } from '../../../common/utils/helper'
import { uri2local } from '../../../common/utils'
import { crychic } from '../../../ntqqapi/native/crychic'
import { NTQQGroupApi } from '../../../ntqqapi/api'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign'
import { Peer } from '../../../ntqqapi/types/msg'
@ -142,7 +141,7 @@ export async function createSendElements(
.RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin)
isAdmin = self.role === GroupMemberRole.admin || self.role === GroupMemberRole.owner
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) {
}
}
@ -171,8 +170,8 @@ export async function createSendElements(
SendMsgElementConstructor.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin,
replyMsg.senderUin,
replyMsg.senderUin!,
replyMsg.senderUin!,
),
)
}
@ -251,7 +250,7 @@ export async function createSendElements(
await SendMsgElementConstructor.pic(
path,
sendMsg.data.summary || '',
<PicSubType>parseInt(sendMsg.data?.subType?.toString()) || 0,
<PicSubType>parseInt(sendMsg.data?.subType?.toString()!) || 0,
),
)
}
@ -265,18 +264,6 @@ export async function createSendElements(
break
case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id
if (qq) {
if ('groupCode' in target) {
crychic.sendGroupPoke(target.groupCode, qq.toString())
}
else {
if (!qq) {
qq = parseInt(target.uin)
}
crychic.sendFriendPoke(qq.toString())
}
sendElements.push(SendMsgElementConstructor.poke('', ''))
}
}
break
case OB11MessageDataType.dice: {
@ -387,10 +374,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let group: Group | undefined = undefined
let friend: Friend | undefined = undefined
const genGroupPeer = async () => {
group = await getGroup(payload.group_id.toString())
group = await getGroup(payload.group_id?.toString()!)
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group.groupCode
peer.peerUid = group?.groupCode!
}
const genFriendPeer = () => {
@ -429,8 +416,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return { message_id: returnMsg.msgShortId }
} catch (e) {
return { message_id: returnMsg?.msgShortId! }
} catch (e: any) {
throw '发送转发消息失败 ' + e.toString()
}
}
@ -447,8 +434,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
const postData: MusicSignPostData = { ...music.data }
if (type === 'custom' && music.data.content) {
;(postData as CustomMusicSignPostData).singer = music.data.content
delete (postData as OB11MessageCustomMusic['data']).content
const data = postData as CustomMusicSignPostData
data.singer = music.data.content
delete (data as OB11MessageCustomMusic['data']).content
}
if (type === 'custom') {
const customMusicData = music.data as CustomMusicSignPostData
@ -493,7 +481,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
return { message_id: returnMsg.msgShortId }
return { message_id: returnMsg.msgShortId! }
}
private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {
@ -503,7 +491,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
return 0
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage> {
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
log('克隆的目标消息', msg)
let sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
@ -549,11 +537,11 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId))
if (!needClone) {
nodeMsgIds.push(nodeMsg.msgId)
nodeMsgIds.push(nodeMsg?.msgId!)
}
else {
if (nodeMsg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg)
if (nodeMsg?.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg!)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
@ -605,7 +593,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = []
let srcPeer: Peer = null
let srcPeer: Peer | null = null
let needSendSelf = false
for (const [index, msgId] of nodeMsgIds.entries()) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId)
@ -648,7 +636,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
try {
log('开发转发', nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
} catch (e) {
log('forward failed', e)
return null

@ -5,8 +5,8 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, Peer } from '@/ntqqapi/api'
import { ChatType, Group, GroupRequestOperateTypes } from '@/ntqqapi/types'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils'
@ -71,17 +71,17 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peerUid: msg.user_id.toString(),
}
if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString())
peer.peerUid = getUidByUin(msg.user_id.toString())!
if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp
}
}
else {
peer.chatType = ChatType.group
peer.peerUid = msg.group_id.toString()
peer.peerUid = msg.group_id?.toString()!
}
if (reply) {
let group: Group = null
let group: Group | null = null
let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) {
replyMessage.push({
@ -93,7 +93,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
if (msg.message_type == 'group') {
group = await getGroup(msg.group_id.toString())
group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({
type: 'at',
@ -104,7 +104,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
}
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group)
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!)
log(`发送消息给`, peer, sendElements)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
}
@ -112,15 +112,15 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
// handle group msg
if (groupMsgQuickAction.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage.msgId]).then().catch(log)
NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log)
}
if (groupMsgQuickAction.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage.senderUid]).then().catch(log)
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage?.senderUid!]).then().catch(log)
}
if (groupMsgQuickAction.ban) {
NTQQGroupApi.banMember(peer.peerUid, [
{
uid: rawMessage.senderUid,
uid: rawMessage?.senderUid!,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
},
]).then().catch(log)
@ -129,7 +129,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
async function handleFriendRequest(request: OB11FriendRequestEvent,
quickAction: QuickOperationFriendRequest) {
quickAction: QuickOperationFriendRequest) {
if (!isNull(quickAction.approve)) {
// todo: set remark
NTQQFriendApi.handleFriendRequest(request.flag, quickAction.approve).then().catch(log)
@ -138,7 +138,7 @@ async function handleFriendRequest(request: OB11FriendRequestEvent,
async function handleGroupRequest(request: OB11GroupRequestEvent,
quickAction: QuickOperationGroupRequest) {
quickAction: QuickOperationGroupRequest) {
if (!isNull(quickAction.approve)) {
NTQQGroupApi.handleGroupRequest(
request.flag,

@ -1,9 +1,8 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import fs from 'fs'
import Path from 'path'
import fs from 'node:fs'
import Path from 'node:path'
import { ChatType, ChatCacheListItemBasic, CacheFileType } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { NTQQFileApi, NTQQFileCacheApi } from '../../../ntqqapi/api/file'
export default class CleanCache extends BaseAction<void, void> {
@ -12,14 +11,16 @@ export default class CleanCache extends BaseAction<void, void> {
protected _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => {
try {
// dbUtil.clearCache();
// dbUtil.clearCache()
const cacheFilePaths: string[] = []
await NTQQFileCacheApi.setCacheSilentScan(false)
cacheFilePaths.push(await NTQQFileCacheApi.getHotUpdateCachePath())
cacheFilePaths.push(await NTQQFileCacheApi.getDesktopTmpPath())
;(await NTQQFileCacheApi.getCacheSessionPathList()).forEach((e) => cacheFilePaths.push(e.value))
const list = await NTQQFileCacheApi.getCacheSessionPathList()
list.forEach((e) => cacheFilePaths.push(e.value))
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQFileCacheApi.scanCache()

@ -8,7 +8,7 @@ export default class GetStatus extends BaseAction<any, OB11Status> {
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: selfInfo.online,
online: selfInfo.online!,
good: true,
}
}

@ -5,6 +5,7 @@ import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQFriendApi } from '@/ntqqapi/api'
import { CategoryFriend } from '@/ntqqapi/types'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface Payload {
no_cache: boolean | string
@ -14,6 +15,9 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList
protected async _handle(payload: Payload) {
if (+qqPkgInfo.buildVersion >= 26702) {
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true'))
}
if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
const _friends = await NTQQFriendApi.getFriends(true)
// log('强制刷新好友列表,结果: ', _friends)

@ -19,7 +19,7 @@ export default class SendLike extends BaseAction<Payload, null> {
const friend = await getFriend(qq)
let uid: string
if (!friend) {
uid = getUidByUin(qq)
uid = getUidByUin(qq)!
} else {
uid = friend.uid
}

@ -24,6 +24,7 @@ import {
TipGroupElementType,
User,
VideoElement,
FriendV2
} from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data'
import { EventType } from './event/OB11BaseEvent'
@ -38,7 +39,7 @@ import { NTQQFileApi } from '../ntqqapi/api/file'
import { NTQQMsgApi } from '../ntqqapi/api/msg'
import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log'
import { sleep } from '../common/utils/helper'
import { isNull, sleep } from '../common/utils/helper'
import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
@ -50,8 +51,8 @@ import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEven
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent';
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent';
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
let lastRKeyUpdateTime = 0
@ -66,14 +67,14 @@ export class OB11Constructor {
const message_type = msg.chatType == ChatType.group ? 'group' : 'private'
const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin),
user_id: parseInt(msg.senderUin),
user_id: parseInt(msg.senderUin!),
time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId,
real_id: msg.msgShortId,
message_seq: msg.msgShortId,
message_id: msg.msgShortId!,
real_id: msg.msgShortId!,
message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private',
sender: {
user_id: parseInt(msg.senderUin),
user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName,
card: msg.sendMemberName || '',
},
@ -90,7 +91,7 @@ export class OB11Constructor {
if (msg.chatType == ChatType.group) {
resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin!)
if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
@ -98,7 +99,7 @@ export class OB11Constructor {
}
else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin)
const friend = await getFriend(msg.senderUin!)
if (friend) {
resMsg.sender.nickname = friend.nick
}
@ -139,7 +140,7 @@ export class OB11Constructor {
message_data = {
type: OB11MessageDataType.at,
data: {
qq,
qq: qq!,
name
}
}
@ -159,12 +160,12 @@ export class OB11Constructor {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) {
message_data['data']['id'] = replyMsg.msgShortId.toString()
message_data['data']['id'] = replyMsg.msgShortId?.toString()
}
else {
continue
}
} catch (e) {
} catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
}
}
@ -220,12 +221,12 @@ export class OB11Constructor {
)
}
dbUtil
.addFileCache(videoOrFileElement.fileUuid, {
.addFileCache(videoOrFileElement.fileUuid!, {
msgId: msg.msgId,
elementId: element.elementId,
fileName: videoOrFileElement.fileName,
filePath: videoOrFileElement.filePath,
fileSize: videoOrFileElement.fileSize,
fileSize: videoOrFileElement.fileSize!,
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(
msg.msgId,
@ -233,7 +234,7 @@ export class OB11Constructor {
msg.peerUid,
element.elementId,
ob11MessageDataType == OB11MessageDataType.video
? (videoOrFileElement as VideoElement).thumbPath.get(0)
? (videoOrFileElement as VideoElement).thumbPath?.get(0)
: null,
videoOrFileElement.filePath,
)
@ -259,9 +260,9 @@ export class OB11Constructor {
// log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text);
// console.log("语音转文字结果", text)
// }).catch(err => {
// console.log("语音转文字失败", err);
// console.log("语音转文字失败", err)
// })
}
else if (element.arkElement) {
@ -296,7 +297,7 @@ export class OB11Constructor {
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId)
message_data['data']['key'] = element.marketFaceElement.key
mFaceCache.set(md5, element.marketFaceElement.faceName)
mFaceCache.set(md5, element.marketFaceElement.faceName!)
}
else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown
@ -320,22 +321,22 @@ export class OB11Constructor {
return resMsg
}
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent> {
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent | void> {
if (msg.chatType !== ChatType.friend) {
return;
return
}
for (const element of msg.elements) {
if (element.grayTipElement) {
if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr);
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr)
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items;
const pokedetail: any[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid);
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail);
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
@ -349,7 +350,7 @@ export class OB11Constructor {
}
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> {
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | void> {
if (msg.chatType !== ChatType.group) {
return
}
@ -359,14 +360,14 @@ export class OB11Constructor {
const event = new OB11GroupCardEvent(
parseInt(msg.peerUid),
parseInt(msg.senderUin),
msg.sendMemberName,
msg.sendMemberName!,
member.cardName,
)
member.cardName = msg.sendMemberName
member.cardName = msg.sendMemberName!
return event
}
}
// log("group msg", msg);
// log("group msg", msg)
for (let element of msg.elements) {
const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement
@ -392,10 +393,10 @@ export class OB11Constructor {
}
else if (groupElement.type === TipGroupElementType.ban) {
log('收到群群员禁言提示', groupElement)
const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid
const memberUid = groupElement.shutUp?.member.uid
const adminUid = groupElement.shutUp?.admin.uid
let memberUin: string = ''
let duration = parseInt(groupElement.shutUp.duration)
let duration = parseInt(groupElement.shutUp?.duration!)
let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban'
if (memberUid) {
memberUin =
@ -409,7 +410,7 @@ export class OB11Constructor {
}
}
const adminUin =
(await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid))?.uin
(await getGroupMember(msg.peerUid, adminUid!))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid!))?.uin
if (memberUin && adminUin) {
return new OB11GroupBanEvent(
parseInt(msg.peerUid),
@ -442,8 +443,8 @@ export class OB11Constructor {
}
}
else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {
id: element.fileElement.fileUuid,
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin!), {
id: element.fileElement.fileUuid!,
name: element.fileElement.fileName,
size: parseInt(element.fileElement.fileSize),
busid: element.fileElement.fileBizId || 0,
@ -475,13 +476,13 @@ export class OB11Constructor {
if (!msg) {
return
}
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId, [
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [
{
emoji_id: emojiId,
count: 1,
},
])
} catch (e) {
} catch (e: any) {
log('解析表情回应消息失败', e.stack)
}
}
@ -494,8 +495,8 @@ export class OB11Constructor {
if (xmlElement?.content) {
const regex = /jp="(\d+)"/g
let matches = []
let match = null
const matches: string[] = []
let match: RegExpExecArray | null = null
while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1])
@ -536,40 +537,42 @@ export class OB11Constructor {
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items;
const pokedetail: any[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid);
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail);
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
}
}
if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
log('收到群精华消息', json)
const searchParams = new URL(json.items[0].jp).searchParams;
const msgSeq = searchParams.get('msgSeq')!;
const Group = searchParams.get('groupCode');
const Businessid = searchParams.get('businessid');
const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode')
const Businessid = searchParams.get('businessid')
const Peer: Peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
};
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList;
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId);
const postMsg = await dbUtil.getMsgBySeqId(origMsg.msgSeq) ?? origMsg;
}
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin;
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin
}
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg.msgShortId, parseInt(msgList[0].senderUin));
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin))
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
log('收到群成员新头衔消息', json)
getGroupMember(msg.peerUid, memberUin).then((member) => {
member.memberSpecialTitle = title
getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNull(member)) {
member.memberSpecialTitle = title
}
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
}
@ -591,16 +594,16 @@ export class OB11Constructor {
const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid),
parseInt(sender.uin),
parseInt(operator.uin),
msg.msgShortId,
parseInt(sender?.uin!),
parseInt(operator?.uin!),
msg.msgShortId!,
)
}
else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin), msg.msgShortId)
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!)
}
}
@ -609,7 +612,7 @@ export class OB11Constructor {
user_id: parseInt(friend.uin),
nickname: friend.nick,
remark: friend.remark,
sex: OB11Constructor.sex(friend.sex),
sex: OB11Constructor.sex(friend.sex!),
level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0,
}
}
@ -625,6 +628,25 @@ export class OB11Constructor {
return friends.map(OB11Constructor.friend)
}
static friendsV2(friends: FriendV2[]): OB11User[] {
const data: OB11User[] = []
for (const friend of friends) {
const sexValue = this.sex(friend.baseInfo.sex!)
data.push({
...friend.baseInfo,
...friend.coreInfo,
user_id: parseInt(friend.coreInfo.uin),
nickname: friend.coreInfo.nick,
remark: friend.coreInfo.nick,
sex: sexValue,
level: 0,
categroyName: friend.categroyName,
categoryId: friend.categoryId
})
}
return data
}
static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return {
4: OB11GroupMemberRole.owner,
@ -648,7 +670,7 @@ export class OB11Constructor {
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName,
sex: OB11Constructor.sex(member.sex),
sex: OB11Constructor.sex(member.sex!),
age: 0,
area: '',
level: 0,
@ -670,7 +692,7 @@ export class OB11Constructor {
...user,
user_id: parseInt(user.uin),
nickname: user.nick,
sex: OB11Constructor.sex(user.sex),
sex: OB11Constructor.sex(user.sex!),
age: 0,
qid: user.qid,
login_days: 0,

@ -11,5 +11,5 @@ export enum EventType {
export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin)
post_type: EventType
abstract post_type: EventType
}

@ -2,5 +2,5 @@ import { EventType, OB11BaseEvent } from '../OB11BaseEvent'
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META
meta_event_type: string
abstract meta_event_type: string
}

@ -2,5 +2,14 @@ import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin'
sub_type: 'set' | 'unset' // "set" | "unset"
sub_type: 'set' | 'unset'
group_id: number
user_id: number
constructor(subType: 'set' | 'unset', groupId: number, userId: number) {
super()
this.sub_type = subType
this.group_id = groupId
this.user_id = userId
}
}

@ -5,6 +5,8 @@ export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
operator_id: number
duration: number
sub_type: 'ban' | 'lift_ban'
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: 'ban' | 'lift_ban') {
super()

@ -4,6 +4,8 @@ export class OB11GroupCardEvent extends OB11GroupNoticeEvent {
notice_type = 'group_card'
card_new: string
card_old: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, cardNew: string, cardOld: string) {
super()

@ -6,6 +6,8 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease'
sub_type: GroupDecreaseSubType = 'leave' // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operator_id: number
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super()

@ -1,14 +1,16 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
notice_type = 'essence';
message_id: number;
sender_id: number;
sub_type: 'add' | 'delete' = 'add';
notice_type = 'essence'
message_id: number
sender_id: number
sub_type: 'add' | 'delete' = 'add'
group_id: number
user_id: number = 0
constructor(groupId: number, message_id: number, sender_id: number) {
super();
this.group_id = groupId;
this.message_id = message_id;
this.sender_id = sender_id;
super()
this.group_id = groupId
this.message_id = message_id
this.sender_id = sender_id
}
}

@ -1,10 +1,14 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
type GroupIncreaseSubType = 'approve' | 'invite'
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_increase'
operator_id: number
sub_type: GroupIncreaseSubType
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = 'approve') {
super()
this.group_id = groupId

@ -1,6 +1,7 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number
user_id: number
abstract group_id: number
abstract user_id: number
abstract notice_type: string
}

@ -4,6 +4,8 @@ export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_recall'
operator_id: number
message_id: number
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, messageId: number) {
super()

@ -4,6 +4,8 @@ export class OB11GroupTitleEvent extends OB11GroupNoticeEvent {
notice_type = 'notify'
sub_type = 'title'
title: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, title: string) {
super()

@ -10,6 +10,8 @@ export interface GroupUploadFile {
export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_upload'
file: GroupUploadFile
group_id: number
user_id: number
constructor(groupId: number, userId: number, file: GroupUploadFile) {
super()

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

@ -1,16 +1,15 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
import { selfInfo } from '../../../common/data'
import { OB11BaseEvent } from '../OB11BaseEvent'
class OB11PokeEvent extends OB11BaseNoticeEvent {
abstract class OB11PokeEvent extends OB11BaseNoticeEvent {
notice_type = 'notify'
sub_type = 'poke'
target_id = 0
user_id: number
abstract user_id: number
raw_message: any
}
export class OB11FriendPokeEvent extends OB11PokeEvent {
user_id: number
constructor(user_id: number, target_id: number, raw_message: any) {
super();
@ -21,6 +20,7 @@ export class OB11FriendPokeEvent extends OB11PokeEvent {
}
export class OB11GroupPokeEvent extends OB11PokeEvent {
user_id: number
group_id: number
constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) {
super()

@ -4,7 +4,15 @@ import { EventType } from '../OB11BaseEvent'
export class OB11FriendRequestEvent extends OB11BaseNoticeEvent {
post_type = EventType.REQUEST
user_id: number
request_type: 'friend' = 'friend'
request_type: 'friend'
comment: string
flag: string
constructor(userId: number, comment: string, flag: string, requestType: 'friend' = 'friend') {
super()
this.user_id = userId
this.comment = comment
this.flag = flag
this.request_type = requestType
}
}

@ -1,11 +1,24 @@
import { OB11GroupNoticeEvent } from '../notice/OB11GroupNoticeEvent'
import { OB11BaseNoticeEvent } from '../notice/OB11BaseNoticeEvent'
import { EventType } from '../OB11BaseEvent'
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent {
export class OB11GroupRequestEvent extends OB11BaseNoticeEvent {
post_type = EventType.REQUEST
request_type: 'group' = 'group'
sub_type: 'add' | 'invite' = 'add'
invitor_id: number | undefined = undefined
comment: string
request_type: 'group'
sub_type: 'add' | 'invite'
invitor_id: number | undefined
comment?: string
flag: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, flag: string, comment?: string, invitorId?: number, subType: 'add' | 'invite' = 'add', requestType: 'group' = 'group') {
super()
this.group_id = groupId
this.user_id = userId
this.comment = comment
this.flag = flag
this.request_type = requestType
this.sub_type = subType
this.invitor_id = invitorId
}
}

@ -1,5 +1,4 @@
import { PostEventType } from "./post-ob11-event"
import { PostEventType } from './post-ob11-event'
interface HttpEventType {
seq: number
@ -11,58 +10,56 @@ interface HttpUserType {
userSeq: number
}
let curentSeq:number = 0;
let eventList:HttpEventType[] = [];
let httpUser:Record<string,HttpUserType> = {};
let curentSeq: number = 0
const eventList: HttpEventType[] = []
const httpUser: Record<string, HttpUserType> = {}
export function postHttpEvent(event: PostEventType) {
curentSeq += 1;
curentSeq += 1
eventList.push({
seq: curentSeq,
event: event
});
while(eventList.length > 100) {
eventList.shift();
while (eventList.length > 100) {
eventList.shift()
}
}
export async function getHttpEvent(userKey:string,timeout = 0) {
let toRetEvent = [];
export async function getHttpEvent(userKey: string, timeout = 0) {
const toRetEvent: PostEventType[] = []
// 清除过时的user5分钟没访问过的user将被删除
let now = Date.now();
for(let key in httpUser) {
let user = httpUser[key];
if(now - user.lastAccessTime > 1000 * 60 * 5) {
delete httpUser[key];
const now = Date.now();
for (let key in httpUser) {
let user = httpUser[key]
if (now - user.lastAccessTime > 1000 * 60 * 5) {
delete httpUser[key]
}
}
// 增加新的user
if(!httpUser[userKey] ) {
if (!httpUser[userKey]) {
httpUser[userKey] = {
lastAccessTime: now,
userSeq: curentSeq
}
}
let user = httpUser[userKey];
const user = httpUser[userKey]
// 等待数据到来,暂时先这么写吧......
while(curentSeq == user.userSeq && Date.now() - now < timeout) {
await new Promise( resolve => setTimeout(resolve, 10) );
while (curentSeq == user.userSeq && Date.now() - now < timeout) {
await new Promise(resolve => setTimeout(resolve, 10))
}
// 取数据
for(let i = 0; i < eventList.length; i++) {
let evt = eventList[i];
if(evt.seq > user.userSeq) {
toRetEvent.push(evt.event);
for (let i = 0; i < eventList.length; i++) {
let evt = eventList[i]
if (evt.seq > user.userSeq) {
toRetEvent.push(evt.event)
}
}
// 更新user数据
user.lastAccessTime = Date.now();
user.userSeq = curentSeq;
return toRetEvent;
user.lastAccessTime = Date.now()
user.userSeq = curentSeq
return toRetEvent
}

@ -40,7 +40,7 @@ class HTTPHeart {
}
this.intervalId = setInterval(() => {
// ws的心跳是ws自己维护的
postOb11Event(new OB11HeartbeatEvent(selfInfo.online, true, heartInterval), false, false)
postOb11Event(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!), false, false)
}, heartInterval)
}

@ -43,7 +43,7 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
}
if (config.ob11.enableHttpPost) {
const msgStr = JSON.stringify(msg)
const hmac = crypto.createHmac('sha1', config.ob11.httpSecret)
const hmac = crypto.createHmac('sha1', config.ob11.httpSecret!)
hmac.update(msgStr)
const sig = hmac.digest('hex')
let headers = {
@ -79,9 +79,8 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
if (postWs) {
postWsEvent(msg)
}
if(!(msg.post_type == 'meta_event' && (msg as OB11BaseMetaEvent).meta_event_type == 'heartbeat')) {
if (!(msg.post_type == 'meta_event' && (msg as OB11BaseMetaEvent).meta_event_type == 'heartbeat')) {
// 不上报心跳
postHttpEvent(msg)
}
}

@ -15,7 +15,7 @@ import { version } from '../../../version'
export let rwsList: ReverseWebsocket[] = []
export class ReverseWebsocket {
public websocket: WebSocketClass
public websocket?: WebSocketClass
public url: string
private running: boolean = false
@ -27,38 +27,38 @@ export class ReverseWebsocket {
public stop() {
this.running = false
this.websocket.close()
this.websocket?.close()
}
public onopen() {
wsReply(this.websocket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
wsReply(this.websocket!, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
}
public async onmessage(msg: string) {
let receiveData: { action: ActionName; params: any; echo?: any } = { action: null, params: {} }
let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} }
let echo = null
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
log('收到反向Websocket消息', receiveData)
} catch (e) {
return wsReply(this.websocket, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
return wsReply(this.websocket!, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
}
const action: BaseAction<any, any> = actionMap.get(receiveData.action)
const action: BaseAction<any, any> = actionMap.get(receiveData.action!)!
if (!action) {
return wsReply(this.websocket, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo))
return wsReply(this.websocket!, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(receiveData.params, echo)
wsReply(this.websocket, handleResult)
wsReply(this.websocket!, handleResult)
} catch (e) {
wsReply(this.websocket, OB11Response.error(`api处理出错:${e}`, 1200, echo))
wsReply(this.websocket!, OB11Response.error(`api处理出错:${e}`, 1200, echo))
}
}
public onclose = function () {
public onclose = () => {
log('反向ws断开', this.url)
unregisterWsEventSender(this.websocket)
unregisterWsEventSender(this.websocket!)
if (this.running) {
this.reconnect()
}
@ -104,7 +104,7 @@ export class ReverseWebsocket {
this.websocket.on('error', log)
const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online, true, heartInterval))
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
}, heartInterval) // 心跳包
this.websocket.on('close', () => {
clearInterval(wsClientInterval)
@ -121,7 +121,7 @@ class OB11ReverseWebsockets {
new Promise(() => {
try {
rwsList.push(new ReverseWebsocket(url))
} catch (e) {
} catch (e: any) {
log(e.stack)
}
}).then()
@ -132,7 +132,7 @@ class OB11ReverseWebsockets {
for (let rws of rwsList) {
try {
rws.stop()
} catch (e) {
} catch (e: any) {
log('反向ws关闭:', e.stack)
}
}

@ -13,15 +13,13 @@ import { selfInfo } from '../../../common/data'
import { log } from '../../../common/utils/log'
import { getConfigUtil } from '../../../common/config'
let heartbeatRunning = false
class OB11WebsocketServer extends WebsocketServerBase {
authorizeFailed(wsClient: WebSocket) {
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')))
}
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> = actionMap.get(actionName)
const action: BaseAction<any, any> = actionMap.get(actionName)!
if (!action) {
return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo))
}
@ -29,7 +27,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
let handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo
wsReply(wsClient, handleResult)
} catch (e) {
} catch (e: any) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))
}
}
@ -37,7 +35,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
if (url == '/api' || url == '/api/' || url == '/') {
wsClient.on('message', async (msg) => {
let receiveData: { action: ActionName; params: any; echo?: any } = { action: null, params: {} }
let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} }
let echo = null
try {
receiveData = JSON.parse(msg.toString())
@ -46,7 +44,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} catch (e) {
return wsReply(wsClient, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
}
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then()
this.handleAction(wsClient, receiveData.action!, receiveData.params, receiveData.echo).then()
})
}
if (url == '/event' || url == '/event/' || url == '/') {
@ -61,7 +59,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
}
const { heartInterval } = getConfigUtil().getConfig()
const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online, true, heartInterval))
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
}, heartInterval) // 心跳包
wsClient.on('close', () => {
log('event上报ws客户端已断开')

@ -12,7 +12,7 @@ export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEvent
}
wsClient.send(JSON.stringify(packet))
log('ws 消息上报', wsClient.url || '', data)
} catch (e) {
} catch (e: any) {
log('websocket 回复失败', e.stack, data)
}
}

@ -11,6 +11,8 @@ export interface OB11User {
age?: number
qid?: string
login_days?: number
categroyName?: string
categoryId?: number
}
export enum OB11UserSex {

@ -1,6 +1,6 @@
export const SettingItem = (
title: string,
subtitle?: string,
subtitle?: string | null,
action?: string,
id?: string,
visible: boolean = true,

@ -30,11 +30,11 @@ window.customElements.define(
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.append(SelectTemplate.content.cloneNode(true))
this.shadowRoot?.append(SelectTemplate.content.cloneNode(true))
this._button = this.shadowRoot.querySelector('div[part="button"]')
this._text = this.shadowRoot.querySelector('input[part="current-text"]')
this._context = this.shadowRoot.querySelector('ul[part="option-list"]')
this._button = this.shadowRoot?.querySelector('div[part="button"]')!
this._text = this.shadowRoot?.querySelector('input[part="current-text"]')!
this._context = this.shadowRoot?.querySelector('ul[part="option-list"]')!
const buttonClick = () => {
const isHidden = this._context.classList.toggle('hidden')
@ -46,7 +46,8 @@ window.customElements.define(
}
this._button.addEventListener('click', buttonClick)
this._context.addEventListener('click', ({ target }: MouseEventExtend) => {
this._context.addEventListener('click', e => {
const { target } = e as MouseEventExtend
if (target.tagName !== 'SETTING-OPTION') return
buttonClick()
@ -55,7 +56,7 @@ window.customElements.define(
this.querySelectorAll('setting-option[is-selected]').forEach((dom) => dom.toggleAttribute('is-selected'))
target.toggleAttribute('is-selected')
this._text.value = target.textContent
this._text.value = target.textContent!
this.dispatchEvent(
new CustomEvent('selected', {
bubbles: true,
@ -68,7 +69,7 @@ window.customElements.define(
)
})
this._text.value = this.querySelector('setting-option[is-selected]').textContent
this._text.value = this.querySelector('setting-option[is-selected]')?.textContent!
}
},
)

@ -12,16 +12,16 @@ function aprilFoolsEgg(node: Element) {
let today = new Date()
if (today.getDate() === 1) {
console.log('超时空猫猫!!!')
node.querySelector('.name').innerHTML = 'ChronoCat'
node.querySelector('.name')!.innerHTML = 'ChronoCat'
}
}
function initSideBar() {
document.querySelectorAll('.nav-item.liteloader').forEach((node) => {
if (node.textContent.startsWith('LLOneBot')) {
if (node.textContent?.startsWith('LLOneBot')) {
aprilFoolsEgg(node)
let iconEle = node.querySelector('.q-icon')
iconEle.innerHTML = iconSvg
iconEle!.innerHTML = iconSvg
}
})
}
@ -64,11 +64,11 @@ async function onSettingWindowCreated(view: Element) {
),
]),
SettingList([
SettingItem(
'是否启用 LLOneBot, 重启QQ后生效',
null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)]
SettingItem(
'是否启用 LLOneBot, 重启QQ后生效',
null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)]
),
SettingList([
SettingItem(
@ -234,29 +234,29 @@ async function onSettingWindowCreated(view: Element) {
await new Promise((res) => setTimeout(() => res(true), 1000))
const errDom = document.querySelector('#llonebot-error') || doc.querySelector('#llonebot-error')
const errCodeDom = errDom.querySelector('code')
const errCodeDom = errDom?.querySelector('code')
const errMsg = await window.llonebot.getError()
if (!errMsg) {
errDom.classList.remove('show')
errDom?.classList.remove('show')
} else {
errDom.classList.add('show')
errDom?.classList.add('show')
}
errCodeDom.innerHTML = errMsg
errCodeDom!.innerHTML = errMsg
}
showError().then()
// 外链按钮
doc.querySelector('#open-github').addEventListener('click', () => {
doc.querySelector('#open-github')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://github.com/LLOneBot/LLOneBot')
})
doc.querySelector('#open-telegram').addEventListener('click', () => {
doc.querySelector('#open-telegram')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://t.me/+nLZEnpne-pQ1OWFl')
})
doc.querySelector('#open-qq-group').addEventListener('click', () => {
doc.querySelector('#open-qq-group')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://qm.qq.com/q/bDnHRG38aI')
})
doc.querySelector('#open-docs').addEventListener('click', () => {
doc.querySelector('#open-docs')?.addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://llonebot.github.io/')
})
// 生成反向地址列表
@ -303,14 +303,14 @@ async function onSettingWindowCreated(view: Element) {
}
const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr))
hostContainerDom?.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr))
ob11Config[type].push('')
}
const initReverseHost = (type: string, doc: Document = document) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`)
;[...hostContainerDom.childNodes].forEach((dom) => dom.remove())
;[...hostContainerDom?.childNodes!].forEach((dom) => dom.remove())
buildHostList(ob11Config[type], type).forEach((dom) => {
hostContainerDom.appendChild(dom)
hostContainerDom?.appendChild(dom)
})
}
initReverseHost('httpHosts', doc)
@ -318,42 +318,43 @@ async function onSettingWindowCreated(view: Element) {
doc
.querySelector('#config-ob11-httpHosts-add')
.addEventListener('click', () =>
?.addEventListener('click', () =>
addReverseHost('httpHosts', document, { placeholder: '如http://127.0.0.1:5140/onebot' }),
)
doc
.querySelector('#config-ob11-wsHosts-add')
.addEventListener('click', () =>
?.addEventListener('click', () =>
addReverseHost('wsHosts', document, { placeholder: '如ws://127.0.0.1:5140/onebot' }),
)
doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => {
doc.querySelector('#config-ffmpeg-select')?.addEventListener('click', () => {
window.llonebot.selectFile().then((path) => {
if (!isEmpty(path)) {
setConfig('ffmpeg', path)
document.querySelector('#config-ffmpeg-path-text').innerHTML = path
document.querySelector('#config-ffmpeg-path-text')!.innerHTML = path
}
})
})
doc.querySelector('#config-open-log-path').addEventListener('click', () => {
doc.querySelector('#config-open-log-path')?.addEventListener('click', () => {
window.LiteLoader.api.openPath(window.LiteLoader.plugins['LLOneBot'].path.data)
})
// 开关
doc.querySelectorAll('setting-switch[data-config-key]').forEach((dom: HTMLElement) => {
doc.querySelectorAll('setting-switch[data-config-key]').forEach(element => {
const dom = element as HTMLElement
dom.addEventListener('click', () => {
const active = dom.getAttribute('is-active') === null
setConfig(dom.dataset.configKey, active)
setConfig(dom.dataset.configKey!, active)
if (active) dom.setAttribute('is-active', '')
else dom.removeAttribute('is-active')
if (!isEmpty(dom.dataset.controlDisplayId)) {
const displayDom = document.querySelector(`#${dom.dataset.controlDisplayId}`)
if (active) displayDom.removeAttribute('is-hidden')
else displayDom.setAttribute('is-hidden', '')
if (active) displayDom?.removeAttribute('is-hidden')
else displayDom?.setAttribute('is-hidden', '')
}
})
})
@ -361,28 +362,31 @@ async function onSettingWindowCreated(view: Element) {
// 输入框
doc
.querySelectorAll('setting-item .q-input input.q-input__inner[data-config-key]')
.forEach((dom: HTMLInputElement) => {
.forEach(element => {
const dom = element as HTMLInputElement
dom.addEventListener('input', () => {
const Type = dom.getAttribute('type')
const configKey = dom.dataset.configKey
const configValue = Type === 'number' ? (parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1) : dom.value
setConfig(configKey, configValue)
setConfig(configKey!, configValue)
})
})
// 下拉框
doc.querySelectorAll('ob-setting-select[data-config-key]').forEach((dom: HTMLElement) => {
dom.addEventListener('selected', (e: CustomEvent) => {
doc?.querySelectorAll('ob-setting-select[data-config-key]').forEach(element => {
const dom = element as HTMLElement
dom?.addEventListener('selected', e => {
const { detail } = e as CustomEvent
const configKey = dom.dataset.configKey
const configValue = e.detail.value
const configValue = detail.value
setConfig(configKey, configValue)
setConfig(configKey!, configValue)
})
})
// 保存按钮
doc.querySelector('#config-ob11-save').addEventListener('click', () => {
doc.querySelector('#config-ob11-save')?.addEventListener('click', () => {
config.ob11 = ob11Config
window.llonebot.setConfig(false, config)
@ -446,7 +450,7 @@ function init() {
}
if (location.hash === '#/blank') {
;(window as any).navigation.addEventListener('navigatesuccess', init, { once: true })
globalThis.navigation.addEventListener('navigatesuccess', init, { once: true })
} else {
init()
}

@ -1 +1 @@
export const version = '3.27.4'
export const version = '3.28.0'

@ -3,7 +3,8 @@
"target": "ESNext",
"module": "commonjs",
"outDir": "./dist",
"strict": false,
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
@ -23,6 +24,12 @@
},
"noEmit": true
},
"include": ["src/*", "src/**/*", "scripts/*"],
"exclude": ["node_modules"]
}
"include": [
"src/*",
"src/**/*",
"scripts/*"
],
"exclude": [
"node_modules"
]
}