mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2024-11-21 09:36:35 +00:00
331 lines
15 KiB
TypeScript
331 lines
15 KiB
TypeScript
import {
|
||
OB11MessageData,
|
||
OB11MessageDataType,
|
||
OB11MessageMixType,
|
||
OB11MessageNode,
|
||
OB11PostContext,
|
||
OB11PostSendMsg,
|
||
} from '@/onebot/types';
|
||
import { ActionName, BaseCheckResult } from '@/onebot/action/types';
|
||
import { decodeCQCode } from '@/onebot/cqcode';
|
||
import { MessageUnique } from '@/common/message-unique';
|
||
import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendArkElement, SendMessageElement } from '@/core';
|
||
import BaseAction from '../BaseAction';
|
||
import { rawMsgWithSendMsg } from "@/core/packet/msg/converter";
|
||
import { PacketMsg } from "@/core/packet/msg/message";
|
||
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
|
||
|
||
export interface ReturnDataType {
|
||
message_id: number;
|
||
res_id?: string;
|
||
}
|
||
|
||
export enum ContextMode {
|
||
Normal = 0,
|
||
Private = 1,
|
||
Group = 2
|
||
}
|
||
|
||
// Normalizes a mixed type (CQCode/a single segment/segment array) into a segment array.
|
||
export function normalize(message: OB11MessageMixType, autoEscape = false): OB11MessageData[] {
|
||
return typeof message === 'string' ? (
|
||
autoEscape ?
|
||
[{ type: OB11MessageDataType.text, data: { text: message } }] :
|
||
decodeCQCode(message)
|
||
) : Array.isArray(message) ? message : [message];
|
||
}
|
||
|
||
export async function createContext(core: NapCatCore, payload: OB11PostContext, contextMode: ContextMode): Promise<Peer> {
|
||
// This function determines the type of message by the existence of user_id / group_id,
|
||
// not message_type.
|
||
// This redundant design of Ob11 here should be blamed.
|
||
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
|
||
return {
|
||
chatType: ChatType.KCHATTYPEGROUP,
|
||
peerUid: payload.group_id.toString(),
|
||
};
|
||
}
|
||
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
|
||
const Uid = await core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||
if (!Uid) throw new Error('无法获取用户信息');
|
||
const isBuddy = await core.apis.FriendApi.isBuddy(Uid);
|
||
if (!isBuddy) {
|
||
const ret = await core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, Uid);
|
||
if (ret.tmpChatInfo?.groupCode) {
|
||
return {
|
||
chatType: ChatType.KCHATTYPETEMPC2CFROMGROUP,
|
||
peerUid: Uid,
|
||
guildId: '',
|
||
};
|
||
}
|
||
if (payload.group_id) {
|
||
return {
|
||
chatType: ChatType.KCHATTYPETEMPC2CFROMGROUP,
|
||
peerUid: Uid,
|
||
guildId: payload.group_id.toString(),
|
||
};
|
||
}
|
||
return {
|
||
chatType: ChatType.KCHATTYPEC2C,
|
||
peerUid: Uid!,
|
||
guildId: '',
|
||
};
|
||
}
|
||
return {
|
||
chatType: ChatType.KCHATTYPEC2C,
|
||
peerUid: Uid,
|
||
guildId: '',
|
||
};
|
||
}
|
||
throw new Error('请指定 group_id 或 user_id');
|
||
}
|
||
|
||
function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
|
||
if (Array.isArray(payload.message)) {
|
||
return payload.message.filter(msg => msg.type == msgType).length;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
|
||
actionName = ActionName.SendMsg;
|
||
contextMode = ContextMode.Normal;
|
||
|
||
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||
const messages = normalize(payload.message);
|
||
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
|
||
if (nodeElementLength > 0 && nodeElementLength != messages.length) {
|
||
return {
|
||
valid: false,
|
||
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素',
|
||
};
|
||
}
|
||
return { valid: true };
|
||
}
|
||
|
||
async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||
this.contextMode = ContextMode.Normal;
|
||
if (payload.message_type === 'group') this.contextMode = ContextMode.Group;
|
||
if (payload.message_type === 'private') this.contextMode = ContextMode.Private;
|
||
const peer = await createContext(this.core, payload, this.contextMode);
|
||
|
||
const messages = normalize(
|
||
payload.message,
|
||
typeof payload.auto_escape === 'string' ? payload.auto_escape === 'true' : !!payload.auto_escape,
|
||
);
|
||
|
||
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
|
||
const packetMode = this.core.apis.PacketApi.available;
|
||
const returnMsgAndResId = packetMode
|
||
? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt)
|
||
: await this.handleForwardedNodes(peer, messages as OB11MessageNode[]);
|
||
if (returnMsgAndResId.message) {
|
||
const msgShortId = MessageUnique.createUniqueMsgId({
|
||
guildId: '',
|
||
peerUid: peer.peerUid,
|
||
chatType: peer.chatType,
|
||
}, (returnMsgAndResId.message)!.msgId);
|
||
return { message_id: msgShortId!, res_id: returnMsgAndResId.res_id };
|
||
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
|
||
throw Error(`发送转发消息(res_id:${returnMsgAndResId.res_id} 失败`);
|
||
}
|
||
throw Error('发送转发消息失败');
|
||
} else {
|
||
// if (getSpecialMsgNum(payload, OB11MessageDataType.music)) {
|
||
// const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
|
||
// if (music) {
|
||
// }
|
||
// }
|
||
}
|
||
// log("send msg:", peer, sendElements)
|
||
|
||
const { sendElements, deleteAfterSentFiles } = await this.obContext.apis.MsgApi
|
||
.createSendElements(messages, peer);
|
||
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles);
|
||
return { message_id: returnMsg!.id! };
|
||
}
|
||
|
||
// TODO: recursively handle forwarded nodes
|
||
private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: { text: string }[], summary?: string, prompt?: string): Promise<{
|
||
message: RawMessage | null,
|
||
res_id?: string
|
||
}> {
|
||
const logger = this.core.context.logger;
|
||
const packetMsg: PacketMsg[] = [];
|
||
for (const node of messageNodes) {
|
||
if ((node.data.id && typeof node.data.content !== "string") || !node.data.id) {
|
||
const OB11Data = normalize(node.data.content);
|
||
const { sendElements } = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
|
||
const packetMsgElements: rawMsgWithSendMsg = {
|
||
senderUin: node.data.user_id ?? +this.core.selfInfo.uin,
|
||
senderName: node.data.nickname,
|
||
groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined,
|
||
time: Date.now(),
|
||
msg: sendElements,
|
||
};
|
||
logger.logDebug(`handleForwardedNodesPacket 开始转换 ${JSON.stringify(packetMsgElements)}`);
|
||
const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements);
|
||
logger.logDebug(`handleForwardedNodesPacket 转换为 ${JSON.stringify(transformedMsg)}`);
|
||
packetMsg.push(transformedMsg!);
|
||
} else {
|
||
logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${JSON.stringify(node)}`);
|
||
}
|
||
}
|
||
const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
|
||
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
|
||
const finallySendElements = {
|
||
elementType: ElementType.ARK,
|
||
elementId: "",
|
||
arkElement: {
|
||
bytesData: JSON.stringify(forwardJson),
|
||
},
|
||
} as SendArkElement;
|
||
let returnMsg: RawMessage | undefined;
|
||
try {
|
||
returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], [], true).catch(_ => undefined);
|
||
} catch (e) {
|
||
logger.logWarn("发送伪造合并转发消息失败!", e);
|
||
}
|
||
return { message: returnMsg ?? null, res_id: resid };
|
||
}
|
||
|
||
private async handleForwardedNodes(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{
|
||
message: RawMessage | null,
|
||
res_id?: string
|
||
}> {
|
||
const selfPeer = {
|
||
chatType: ChatType.KCHATTYPEC2C,
|
||
peerUid: this.core.selfInfo.uid,
|
||
};
|
||
let nodeMsgIds: string[] = [];
|
||
const logger = this.core.context.logger;
|
||
for (const messageNode of messageNodes) {
|
||
const nodeId = messageNode.data.id;
|
||
if (nodeId) {
|
||
// 对Msgid和OB11ID混用情况兜底
|
||
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId);
|
||
if (!nodeMsg) {
|
||
logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', nodeId);
|
||
continue;
|
||
}
|
||
nodeMsgIds.push(nodeMsg.MsgId);
|
||
} else {
|
||
// 自定义的消息
|
||
try {
|
||
const OB11Data = normalize(messageNode.data.content);
|
||
//筛选node消息
|
||
const isNodeMsg = OB11Data.filter(e => e.type === OB11MessageDataType.node).length;//找到子转发消息
|
||
if (isNodeMsg !== 0) {
|
||
if (isNodeMsg !== OB11Data.length) {
|
||
logger.logError.bind(this.core.context.logger)('子消息中包含非node消息 跳过不合法部分');
|
||
continue;
|
||
}
|
||
const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node));
|
||
if (nodeMsg) {
|
||
nodeMsgIds.push(nodeMsg.message!.msgId);
|
||
MessageUnique.createUniqueMsgId(selfPeer, nodeMsg.message!.msgId);
|
||
}
|
||
//完成子卡片生成跳过后续
|
||
continue;
|
||
}
|
||
const { sendElements } = await this.obContext.apis.MsgApi
|
||
.createSendElements(OB11Data, destPeer);
|
||
|
||
//拆分消息
|
||
|
||
const MixElement = sendElements.filter(
|
||
element =>
|
||
element.elementType !== ElementType.FILE && element.elementType !== ElementType.VIDEO && element.elementType !== ElementType.ARK
|
||
);
|
||
const SingleElement = sendElements.filter(
|
||
element =>
|
||
element.elementType === ElementType.FILE || element.elementType === ElementType.VIDEO || element.elementType === ElementType.ARK
|
||
).map(e => [e]);
|
||
|
||
const AllElement: SendMessageElement[][] = [MixElement, ...SingleElement].filter(e => e !== undefined && e.length !== 0);
|
||
const MsgNodeList: Promise<RawMessage | undefined>[] = [];
|
||
for (const sendElementsSplitElement of AllElement) {
|
||
MsgNodeList.push(this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(selfPeer, sendElementsSplitElement, [], true).catch(_ => undefined));
|
||
}
|
||
(await Promise.allSettled(MsgNodeList)).map((result) => {
|
||
if (result.status === 'fulfilled' && result.value) {
|
||
nodeMsgIds.push(result.value.msgId);
|
||
MessageUnique.createUniqueMsgId(selfPeer, result.value.msgId);
|
||
}
|
||
});
|
||
} catch (e) {
|
||
logger.logDebug('生成转发消息节点失败', e);
|
||
}
|
||
}
|
||
}
|
||
const nodeMsgArray: Array<RawMessage> = [];
|
||
let srcPeer: Peer | undefined = undefined;
|
||
let needSendSelf = false;
|
||
//检测是否处于同一个Peer 不在同一个peer则全部消息由自身发送
|
||
for (const msgId of nodeMsgIds) {
|
||
const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId);
|
||
if (!nodeMsgPeer) {
|
||
logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', msgId);
|
||
continue;
|
||
}
|
||
const nodeMsg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0];
|
||
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid };
|
||
if (srcPeer.peerUid !== nodeMsg.peerUid) {
|
||
needSendSelf = true;
|
||
}
|
||
nodeMsgArray.push(nodeMsg);
|
||
}
|
||
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
|
||
let retMsgIds: string[] = [];
|
||
if (needSendSelf) {
|
||
for (const [, msg] of nodeMsgArray.entries()) {
|
||
if (msg.peerUid === this.core.selfInfo.uid) {
|
||
retMsgIds.push(msg.msgId);
|
||
continue;
|
||
}
|
||
const ClonedMsg = await this.cloneMsg(msg);
|
||
if (ClonedMsg) retMsgIds.push(ClonedMsg.msgId);
|
||
}
|
||
} else {
|
||
retMsgIds = nodeMsgIds;
|
||
}
|
||
if (retMsgIds.length === 0) throw Error('转发消息失败,生成节点为空');
|
||
try {
|
||
logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds);
|
||
return {
|
||
message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
|
||
};
|
||
} catch (e) {
|
||
logger.logError.bind(this.core.context.logger)('forward failed', e);
|
||
return {
|
||
message: null
|
||
};
|
||
}
|
||
}
|
||
|
||
async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
|
||
const selfPeer = {
|
||
chatType: ChatType.KCHATTYPEC2C,
|
||
peerUid: this.core.selfInfo.uid,
|
||
};
|
||
const logger = this.core.context.logger;
|
||
//msg 为待克隆消息
|
||
const sendElements: SendMessageElement[] = [];
|
||
|
||
for (const element of msg.elements) {
|
||
sendElements.push(element as SendMessageElement);
|
||
}
|
||
|
||
if (sendElements.length === 0) {
|
||
logger.logDebug('需要clone的消息无法解析,将会忽略掉', msg);
|
||
}
|
||
try {
|
||
return await this.core.apis.MsgApi.sendMsg(selfPeer, sendElements, true);
|
||
} catch (e) {
|
||
logger.logError.bind(this.core.context.logger)(e, '克隆转发消息失败,将忽略本条消息', msg);
|
||
}
|
||
}
|
||
}
|
||
|
||
export default SendMsg;
|