mirror of
https://github.com/LLOneBot/LLOneBot.git
synced 2024-11-22 01:56:33 +00:00
304 lines
9.7 KiB
TypeScript
304 lines
9.7 KiB
TypeScript
import fs from 'node:fs'
|
||
import fsPromise from 'node:fs/promises'
|
||
import {
|
||
AtType,
|
||
ChatType,
|
||
GroupMemberRole,
|
||
SendMessageElement,
|
||
ElementType
|
||
} from '@/ntqqapi/types'
|
||
import {
|
||
OB11MessageData,
|
||
OB11MessageDataType,
|
||
OB11MessageFileBase,
|
||
OB11MessageMixType
|
||
} from '../types'
|
||
import { decodeCQCode } from '../cqcode'
|
||
import { Peer } from '@/ntqqapi/types/msg'
|
||
import { SendElementEntities } from '@/ntqqapi/entities'
|
||
import { MessageUnique } from '@/common/utils/messageUnique'
|
||
import { selfInfo } from '@/common/globalVars'
|
||
import { uri2local } from '@/common/utils'
|
||
import { Context } from 'cordis'
|
||
|
||
export async function createSendElements(
|
||
ctx: Context,
|
||
messageData: OB11MessageData[],
|
||
peer: Peer,
|
||
ignoreTypes: OB11MessageDataType[] = [],
|
||
) {
|
||
const sendElements: SendMessageElement[] = []
|
||
const deleteAfterSentFiles: string[] = []
|
||
for (const sendMsg of messageData) {
|
||
if (ignoreTypes.includes(sendMsg.type)) {
|
||
continue
|
||
}
|
||
switch (sendMsg.type) {
|
||
case OB11MessageDataType.text: {
|
||
const text = sendMsg.data?.text
|
||
if (text) {
|
||
sendElements.push(SendElementEntities.text(sendMsg.data!.text))
|
||
}
|
||
}
|
||
break
|
||
case OB11MessageDataType.at: {
|
||
if (!peer) {
|
||
continue
|
||
}
|
||
if (sendMsg.data?.qq) {
|
||
const atQQ = String(sendMsg.data.qq)
|
||
if (atQQ === 'all') {
|
||
// todo:查询剩余的at全体次数
|
||
const groupCode = peer.peerUid
|
||
let remainAtAllCount = 1
|
||
let isAdmin: boolean = true
|
||
if (groupCode) {
|
||
try {
|
||
remainAtAllCount = (await ctx.ntGroupApi.getGroupRemainAtTimes(groupCode)).atInfo
|
||
.RemainAtAllCountForUin
|
||
ctx.logger.info(`群${groupCode}剩余at全体次数`, remainAtAllCount)
|
||
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uin)
|
||
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
|
||
} catch (e) {
|
||
}
|
||
}
|
||
if (isAdmin && remainAtAllCount > 0) {
|
||
sendElements.push(SendElementEntities.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
|
||
}
|
||
}
|
||
else if (peer.chatType === ChatType.group) {
|
||
const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ)
|
||
if (atMember) {
|
||
const display = `@${atMember.cardName || atMember.nick}`
|
||
sendElements.push(
|
||
SendElementEntities.at(atQQ, atMember.uid, AtType.atUser, display),
|
||
)
|
||
} else {
|
||
const atNmae = sendMsg.data?.name
|
||
const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
|
||
const display = atNmae ? `@${atNmae}` : ''
|
||
sendElements.push(
|
||
SendElementEntities.at(atQQ, uid, AtType.atUser, display),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break
|
||
case OB11MessageDataType.reply: {
|
||
if (sendMsg.data?.id) {
|
||
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
|
||
if (!replyMsgId) {
|
||
ctx.logger.warn('回复消息不存在', replyMsgId)
|
||
continue
|
||
}
|
||
const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId(
|
||
replyMsgId.Peer,
|
||
[replyMsgId.MsgId!]
|
||
)).msgList[0]
|
||
if (replyMsg) {
|
||
sendElements.push(
|
||
SendElementEntities.reply(
|
||
replyMsg.msgSeq,
|
||
replyMsg.msgId,
|
||
replyMsg.senderUin!,
|
||
replyMsg.senderUin!,
|
||
),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
break
|
||
case OB11MessageDataType.face: {
|
||
const faceId = sendMsg.data?.id
|
||
if (faceId) {
|
||
sendElements.push(SendElementEntities.face(parseInt(faceId)))
|
||
}
|
||
}
|
||
break
|
||
case OB11MessageDataType.mface: {
|
||
sendElements.push(
|
||
SendElementEntities.mface(
|
||
+sendMsg.data.emoji_package_id,
|
||
sendMsg.data.emoji_id,
|
||
sendMsg.data.key,
|
||
sendMsg.data.summary,
|
||
),
|
||
)
|
||
}
|
||
break
|
||
case OB11MessageDataType.image: {
|
||
const res = await SendElementEntities.pic(
|
||
ctx,
|
||
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
|
||
sendMsg.data.summary || '',
|
||
sendMsg.data.subType || 0
|
||
)
|
||
deleteAfterSentFiles.push(res.picElement.sourcePath)
|
||
sendElements.push(res)
|
||
}
|
||
break
|
||
case OB11MessageDataType.file: {
|
||
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
|
||
sendElements.push(await SendElementEntities.file(ctx, path, fileName))
|
||
}
|
||
break
|
||
case OB11MessageDataType.video: {
|
||
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
|
||
let thumb = sendMsg.data.thumb
|
||
if (thumb) {
|
||
const uri2LocalRes = await uri2local(thumb)
|
||
if (uri2LocalRes.success) thumb = uri2LocalRes.path
|
||
}
|
||
const res = await SendElementEntities.video(ctx, path, fileName, thumb)
|
||
deleteAfterSentFiles.push(res.videoElement.filePath)
|
||
sendElements.push(res)
|
||
}
|
||
break
|
||
case OB11MessageDataType.voice: {
|
||
const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
|
||
sendElements.push(await SendElementEntities.ptt(ctx, path))
|
||
}
|
||
break
|
||
case OB11MessageDataType.json: {
|
||
sendElements.push(SendElementEntities.ark(sendMsg.data.data))
|
||
}
|
||
break
|
||
case OB11MessageDataType.dice: {
|
||
const resultId = sendMsg.data?.result
|
||
sendElements.push(SendElementEntities.dice(resultId))
|
||
}
|
||
break
|
||
case OB11MessageDataType.RPS: {
|
||
const resultId = sendMsg.data?.result
|
||
sendElements.push(SendElementEntities.rps(resultId))
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
return {
|
||
sendElements,
|
||
deleteAfterSentFiles,
|
||
}
|
||
}
|
||
|
||
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
|
||
async function handleOb11FileLikeMessage(
|
||
ctx: Context,
|
||
{ data: inputdata }: OB11MessageFileBase,
|
||
{ deleteAfterSentFiles }: { deleteAfterSentFiles: string[] },
|
||
) {
|
||
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
|
||
const {
|
||
path,
|
||
isLocal,
|
||
fileName,
|
||
errMsg,
|
||
success,
|
||
} = (await uri2local(inputdata?.url || inputdata.file))
|
||
|
||
if (!success) {
|
||
ctx.logger.error(errMsg)
|
||
throw Error(errMsg)
|
||
}
|
||
|
||
if (!isLocal) { // 只删除http和base64转过来的文件
|
||
deleteAfterSentFiles.push(path)
|
||
}
|
||
|
||
return { path, fileName: inputdata.name || fileName }
|
||
}
|
||
|
||
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
|
||
if (typeof message === 'string') {
|
||
if (autoEscape === true) {
|
||
message = [
|
||
{
|
||
type: OB11MessageDataType.text,
|
||
data: {
|
||
text: message,
|
||
},
|
||
},
|
||
]
|
||
}
|
||
else {
|
||
message = decodeCQCode(message.toString())
|
||
}
|
||
}
|
||
else if (!Array.isArray(message)) {
|
||
message = [message]
|
||
}
|
||
return message
|
||
}
|
||
|
||
export async function sendMsg(
|
||
ctx: Context,
|
||
peer: Peer,
|
||
sendElements: SendMessageElement[],
|
||
deleteAfterSentFiles: string[]
|
||
) {
|
||
if (!sendElements.length) {
|
||
throw '消息体无法解析,请检查是否发送了不支持的消息类型'
|
||
}
|
||
// 计算发送的文件大小
|
||
let totalSize = 0
|
||
for (const fileElement of sendElements) {
|
||
try {
|
||
if (fileElement.elementType === ElementType.PTT) {
|
||
totalSize += fs.statSync(fileElement.pttElement.filePath).size
|
||
}
|
||
if (fileElement.elementType === ElementType.FILE) {
|
||
totalSize += fs.statSync(fileElement.fileElement.filePath).size
|
||
}
|
||
if (fileElement.elementType === ElementType.VIDEO) {
|
||
totalSize += fs.statSync(fileElement.videoElement.filePath).size
|
||
}
|
||
if (fileElement.elementType === ElementType.PIC) {
|
||
totalSize += fs.statSync(fileElement.picElement.sourcePath).size
|
||
}
|
||
} catch (e) {
|
||
ctx.logger.warn('文件大小计算失败', e, fileElement)
|
||
}
|
||
}
|
||
//log('发送消息总大小', totalSize, 'bytes')
|
||
const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
|
||
//log('设置消息超时时间', timeout)
|
||
const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, timeout)
|
||
if (returnMsg) {
|
||
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
|
||
ctx.logger.info('消息发送', returnMsg.msgShortId)
|
||
deleteAfterSentFiles.map(path => fsPromise.unlink(path))
|
||
return returnMsg
|
||
}
|
||
}
|
||
|
||
export interface CreatePeerPayload {
|
||
group_id?: string | number
|
||
user_id?: string | number
|
||
}
|
||
|
||
export enum CreatePeerMode {
|
||
Normal = 0,
|
||
Private = 1,
|
||
Group = 2
|
||
}
|
||
|
||
export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode: CreatePeerMode): Promise<Peer> {
|
||
if ((mode === CreatePeerMode.Group || mode === CreatePeerMode.Normal) && payload.group_id) {
|
||
return {
|
||
chatType: ChatType.group,
|
||
peerUid: payload.group_id.toString(),
|
||
}
|
||
}
|
||
if ((mode === CreatePeerMode.Private || mode === CreatePeerMode.Normal) && payload.user_id) {
|
||
const uid = await ctx.ntUserApi.getUidByUin(payload.user_id.toString())
|
||
if (!uid) throw new Error('无法获取用户信息')
|
||
const isBuddy = await ctx.ntFriendApi.isBuddy(uid)
|
||
return {
|
||
chatType: isBuddy ? ChatType.friend : ChatType.temp,
|
||
peerUid: uid,
|
||
}
|
||
}
|
||
throw new Error('请指定 group_id 或 user_id')
|
||
} |