Merge pull request #406 from LLOneBot/dev

3.32.0
This commit is contained in:
idranme
2024-09-08 21:11:52 +08:00
committed by GitHub
17 changed files with 132 additions and 65 deletions

View File

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

View File

@@ -23,7 +23,7 @@
"cosmokit": "^1.6.2", "cosmokit": "^1.6.2",
"express": "^4.19.2", "express": "^4.19.2",
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"file-type": "^19.4.1", "file-type": "^19.5.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.1", "minato": "^3.5.1",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",

View File

@@ -4,6 +4,7 @@ import path from 'node:path'
import { TEMP_DIR } from '../globalVars' import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { fileTypeFromFile } from 'file-type'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4) const buffer = Buffer.alloc(4)
@@ -116,7 +117,7 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, filename?: string): Promise<Uri2LocalRes> { export async function uri2local(uri: string, filename?: string, needExt?: boolean): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri) const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) { if (type === FileUriType.FileURL) {
@@ -139,8 +140,14 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
} else { } else {
filename ??= randomUUID() filename ??= randomUUID()
} }
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data) await fsPromise.writeFile(filePath, res.data)
if (needExt && !path.extname(filePath)) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e) { } catch (e) {
const errMsg = `${uri} 下载失败, ${(e as Error).message}` const errMsg = `${uri} 下载失败, ${(e as Error).message}`
@@ -150,9 +157,15 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
if (type === FileUriType.OneBotBase64) { if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID() filename ??= randomUUID()
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '') const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
@@ -162,8 +175,14 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
if (capture) { if (capture) {
filename ??= randomUUID() filename ??= randomUUID()
const [, _type, base64] = capture const [, _type, base64] = capture
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
} }

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GetFileListParam } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GetFileListParam, PublishGroupBulletinReq } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window' import { NTQQWindows } from './window'
@@ -288,4 +288,16 @@ export class NTQQGroupApi extends Service {
) )
return data.fileInfo.item return data.fileInfo.item
} }
async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }, null])
}
async uploadGroupBulletinPic(groupCode: string, path: string) {
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }, null])
}
} }

View File

@@ -252,4 +252,8 @@ export class NTQQMsgApi extends Service {
} }
}, null]) }, null])
} }
async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null])
}
} }

View File

@@ -109,6 +109,10 @@ export class NTQQUserApi extends Service {
return cookies return cookies
} }
async getPSkey(domains: string[]) {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null])
}
genBkn(sKey: string) { genBkn(sKey: string) {
sKey = sKey || '' sKey = sKey || ''
let hash = 5381 let hash = 5381

View File

@@ -30,7 +30,7 @@ declare module 'cordis' {
'nt/message-sent': (input: RawMessage[]) => void 'nt/message-sent': (input: RawMessage[]) => void
'nt/group-notify': (input: GroupNotify[]) => void 'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest[]) => void 'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string; members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
} }
} }

View File

@@ -99,19 +99,20 @@ export namespace SendElementEntities {
} }
} }
export async function file(ctx: Context, filePath: string, fileName = '', folderId = ''): Promise<SendFileElement> { export async function file(ctx: Context, filePath: string, fileName: string, folderId = ''): Promise<SendFileElement> {
const { fileName: _fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(filePath, ElementType.FILE) const fileSize = (await stat(filePath)).size.toString()
if (fileSize === 0) { if (fileSize === '0') {
throw '文件异常,大小为 0' ctx.logger.warn(`文件${fileName}异常,大小为 0`)
throw new Error('文件异常,大小为 0')
} }
const element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName: fileName || _fileName, fileName,
folderId: folderId, folderId,
filePath: path!, filePath,
fileSize: fileSize.toString(), fileSize,
}, },
} }
return element return element

View File

@@ -45,7 +45,7 @@ export interface NodeIKernelGroupService {
errMsg: string, errMsg: string,
uids: Map<string, string> uids: Map<string, string>
}> }>
//26702(其实更早 但是我不知道) //26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown> checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
@@ -202,11 +202,12 @@ export interface NodeIKernelGroupService {
publishInstructionForNewcomers(groupCode: string, arg: unknown): void publishInstructionForNewcomers(groupCode: string, arg: unknown): void
uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<GeneralCallResult & { uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<{
errCode: number errCode: number
errMsg: string
picInfo?: { picInfo?: {
id: string, id: string
width: number, width: number
height: number height: number
} }
}> }>

View File

@@ -65,3 +65,15 @@ export interface GroupMember {
joinTime: string joinTime: string
lastSpeakTime: string lastSpeakTime: string
} }
export interface PublishGroupBulletinReq {
text: string
picInfo?: {
id: string
width: number
height: number
}
oldFeedsId: ''
pinned: number
confirmRequired: number
}

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me'
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent { export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease' notice_type = 'group_decrease'
sub_type: GroupDecreaseSubType = 'leave' // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me") sub_type: GroupDecreaseSubType = 'leave'
operator_id: number operator_id: number
group_id: number group_id: number
user_id: number user_id: number

View File

@@ -1 +1 @@
export const version = '3.31.10' export const version = '3.32.0'