Compare commits

..

7 Commits

Author SHA1 Message Date
idranme
0182803ae1 Merge pull request #339 from LLOneBot/dev
3.29.2
2024-08-15 11:14:35 +08:00
idranme
94c1aea6df chore: v3.29.2 2024-08-15 10:57:15 +08:00
idranme
d143dc043c fix 2024-08-15 10:31:51 +08:00
idranme
3f4b0b44cf feat: cache recalled message content 2024-08-14 23:04:15 +08:00
idranme
26fc0c68b2 Merge pull request #337 from LLOneBot/dev
3.29.1
2024-08-14 19:00:42 +08:00
idranme
c1d7aa7aed chore: v3.29.1 2024-08-14 18:59:27 +08:00
idranme
6aa44bdd79 fix: /get_image 2024-08-14 18:20:39 +08:00
15 changed files with 164 additions and 102 deletions

View File

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

View File

@@ -52,6 +52,7 @@ export class ConfigUtil {
autoDeleteFile: false,
autoDeleteFileSecond: 60,
musicSignUrl: '',
msgCacheExpire: 120
}
if (!fs.existsSync(this.configPath)) {

View File

@@ -9,6 +9,8 @@ import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
export let groups: Group[] = []
export let friends: Friend[] = []
@@ -128,3 +130,24 @@ export function getSelfUid() {
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
let expire: number
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
expire ??= getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
}
const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
export function getMsgCache(msgId: string) {
return messages.get(msgId)
}

View File

@@ -30,6 +30,8 @@ export interface Config {
ffmpeg?: string // ffmpeg路径
musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
}
export interface LLOneBotError {
@@ -41,11 +43,10 @@ export interface LLOneBotError {
export interface FileCache {
fileName: string
filePath: string
fileSize: string
fileUuid?: string
url?: string
msgId?: string
msgId: string
peerUid: string
chatType: number
elementId: string
downloadFunc?: () => Promise<void>
elementType: number
}

View File

@@ -34,7 +34,7 @@ export class NTEventWrapper {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
@@ -48,7 +48,7 @@ export class NTEventWrapper {
this.WrapperSession = WrapperSession
}
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
createEventFunction<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>> }
@@ -69,16 +69,14 @@ export class NTEventWrapper {
}
}
createEventFunction = this.CreatEventFunction
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.CreatEventFunction<(listener: T) => number>(Service)
const addfunc = this.createEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
@@ -87,7 +85,7 @@ export class NTEventWrapper {
}
//统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
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)
@@ -103,7 +101,7 @@ export class NTEventWrapper {
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)
const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
@@ -152,7 +150,7 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
this.createListenerFunction(ListenerMainName)
})
}
@@ -198,8 +196,8 @@ export class NTEventWrapper {
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)
this.createListenerFunction(ListenerMainName)
const EventFunc = this.createEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[]))
})
}

View File

@@ -7,6 +7,7 @@ import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import { FileCache } from '../types'
interface SQLiteTables extends Tables {
message: {
@@ -15,6 +16,7 @@ interface SQLiteTables extends Tables {
chatType: number
peerUid: string
}
file: FileCache
}
interface MsgIdAndPeerByShortId {
@@ -50,6 +52,17 @@ class MessageUniqueWrapper {
}, {
primary: 'shortId'
})
database.extend('file', {
fileName: 'string',
fileSize: 'string',
msgId: 'string(24)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileName'
})
this.db = database
}
@@ -128,6 +141,14 @@ class MessageUniqueWrapper {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCache) {
return this.db?.upsert('file', [data], 'fileName')
}
getFileCache(fileName: string) {
return this.db?.get('file', { fileName })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -21,7 +21,8 @@ import {
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin
getSelfUin,
addMsgCache
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
@@ -95,7 +96,7 @@ function onLoad() {
}
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常'
llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常'
let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace('\n\n', '\n')
@@ -159,6 +160,7 @@ function onLoad() {
peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
addMsgCache(message)
OB11Constructor.message(message)
.then((msg) => {
@@ -183,7 +185,7 @@ function onLoad() {
}
})
OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message)
//log(message)
if (privateEvent) {
// log("post private event", privateEvent);
postOb11Event(privateEvent)

View File

@@ -179,7 +179,6 @@ export class NTQQFileApi {
const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
const fileUuid = element.fileUuid
if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接

View File

@@ -3,9 +3,9 @@ import fsPromise from 'node:fs/promises'
import { getConfigUtil } from '@/common/config'
import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
import { RawMessage } from '@/ntqqapi/types'
import { UUIDConverter } from '@/common/utils/helper'
import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
export interface GetFilePayload {
file: string // 文件名或者fileUuid
@@ -51,10 +51,10 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
throw new Error('chattype not support')
}
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId])
if (msgList.msgList.length == 0) {
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
}
const msg = msgList.msgList[0];
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT)
if (!findEle) {
throw new Error('element not found')
@@ -68,7 +68,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
file_size: fileSize,
file_name: fileName,
}
if (enableLocalFile2Url) {
if (enableLocalFile2Url && downloadPath) {
try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) {
@@ -82,33 +82,42 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
}
const NTSearchNameResult = (await NTQQFileApi.searchfile([payload.file])).resultItems
if (NTSearchNameResult.length !== 0) {
const MsgId = NTSearchNameResult[0].msgId
let peer: Peer | undefined = undefined
if (NTSearchNameResult[0].chatType == ChatType.group) {
peer = { chatType: ChatType.group, peerUid: NTSearchNameResult[0].groupChatInfo[0].groupCode }
}
if (!peer) {
throw new Error('chattype not support')
}
const msgList: RawMessage[] = (await NTQQMsgApi.getMsgsByMsgId(peer, [MsgId]))?.msgList
if (!msgList || msgList.length == 0) {
throw new Error('msg not found')
}
const msg = msgList[0]
const file = msg.elements.filter(e => e.elementType == NTSearchNameResult[0].elemType)
if (file.length == 0) {
throw new Error('file not found')
}
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, file[0].elementId, '', '')
const fileCache = await MessageUnique.getFileCache(String(payload.file))
if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia(
fileCache[0].msgId,
fileCache[0].chatType,
fileCache[0].peerUid,
fileCache[0].elementId,
'',
''
)
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: NTSearchNameResult[0].fileSize.toString(),
file_name: NTSearchNameResult[0].fileName,
file_size: fileCache[0].fileSize,
file_name: fileCache[0].fileName,
}
if (enableLocalFile2Url) {
const peer: Peer = {
chatType: fileCache[0].chatType,
peerUid: fileCache[0].peerUid,
guildId: ''
}
if (fileCache[0].elementType === ElementType.PIC) {
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
}
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementId === fileCache[0].elementId)
if (!findEle) {
throw new Error('element not found')
}
res.url = await NTQQFileApi.getImageUrl(findEle.picElement)
} else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
}
if (enableLocalFile2Url && downloadPath && res.file === res.url) {
try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) {

View File

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

View File

@@ -4,6 +4,7 @@ import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { getMsgCache } from '@/common/data'
export interface PayloadType {
message_id: number | string
@@ -29,12 +30,9 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
peerUid: msgIdWithPeer.Peer.peerUid,
chatType: msgIdWithPeer.Peer.chatType
}
const msg = await NTQQMsgApi.getMsgsByMsgId(
peer,
[msgIdWithPeer?.MsgId || payload.message_id.toString()]
)
const retMsg = await OB11Constructor.message(msg.msgList[0])
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgList[0].msgId)!
const msg = getMsgCache(msgIdWithPeer.MsgId) ?? (await NTQQMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const retMsg = await OB11Constructor.message(msg)
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg

View File

@@ -119,9 +119,8 @@ export async function createSendElements(
if (!peer) {
continue
}
let atQQ = sendMsg.data?.qq
if (atQQ) {
atQQ = atQQ.toString()
if (sendMsg.data?.qq) {
const atQQ = String(sendMsg.data.qq)
if (atQQ === 'all') {
// todo查询剩余的at全体次数
const groupCode = peer.peerUid
@@ -161,7 +160,7 @@ export async function createSendElements(
}
break
case OB11MessageDataType.reply: {
if (sendMsg.data.id) {
if (sendMsg.data?.id) {
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
if (!replyMsgId) {
log('回复消息不存在', replyMsgId)

View File

@@ -184,21 +184,30 @@ export class OB11Constructor {
}
else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image
let fileName = element.picElement.fileName
const isGif = element.picElement.picType === PicType.gif
const { picElement } = element
/*let fileName = picElement.fileName
const isGif = picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif'
}
message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
}*/
message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = picElement.picSubType
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId)
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement)
message_data['data']['file_size'] = element.picElement.fileSize
message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: picElement.fileName,
fileSize: String(picElement.fileSize || '0'),
})
}
else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data['type'] = ob11MessageDataType
message_data['type'] = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data['data']['file'] = videoOrFileElement.fileName
message_data['data']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId)
@@ -210,40 +219,32 @@ export class OB11Constructor {
}, msg.msgId, element.elementId,
)
}
NTQQFileApi.addFileCache(
{
peerUid: msg.peerUid,
chatType: msg.chatType,
guildId: '',
},
msg.msgId,
msg.msgSeq,
msg.senderUid,
element.elementId,
element.elementType.toString(),
videoOrFileElement.fileSize || '0',
videoOrFileElement.fileName,
)
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: videoOrFileElement.fileName,
fileSize: String(videoOrFileElement.fileSize || '0')
})
}
else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName
message_data['data']['path'] = element.pttElement.filePath
const { pttElement } = element
message_data['data']['file'] = pttElement.fileName
message_data['data']['path'] = pttElement.filePath
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId)
message_data['data']['file_size'] = element.pttElement.fileSize
NTQQFileApi.addFileCache({
message_data['data']['file_size'] = pttElement.fileSize
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
chatType: msg.chatType,
guildId: '',
},
msg.msgId,
msg.msgSeq,
msg.senderUid,
element.elementId,
element.elementType.toString(),
element.pttElement.fileSize || '0',
element.pttElement.fileUuid || '',
)
elementId: element.elementId,
elementType: element.elementType,
fileName: pttElement.fileName,
fileSize: String(pttElement.fileSize || '0')
})
}
else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json
@@ -589,21 +590,19 @@ export class OB11Constructor {
msg: RawMessage,
shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
let msgElement = msg.elements.find(
const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL,
)
if (!msgElement) {
return
}
const isGroup = msg.chatType === ChatType.group
const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) {
if (msg.chatType === ChatType.group) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid),
parseInt(sender?.uin!),
parseInt(operator?.uin!),
parseInt(msg.senderUin!),
parseInt(operator?.uin || msg.senderUin!),
shortId,
)
}

View File

@@ -219,6 +219,11 @@ async function onSettingWindowCreated(view: Element) {
`${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`,
SettingButton('打开', 'config-open-log-path'),
),
SettingItem(
'消息内容缓存时长',
'单位为秒,可用于获取撤回的消息',
`<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`,
),
]),
SettingList([
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),

View File

@@ -1 +1 @@
export const version = '3.29.0'
export const version = '3.29.2'