chore: OneBotApi

This commit is contained in:
手瓜一十雪
2024-08-09 15:44:45 +08:00
parent 7587e1b8f5
commit 086a6ad2cd
111 changed files with 4493 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { NTQQMsgApi } from '@/core/apis';
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
import { sleep } from '@/common/utils/helper';
import { NTEventDispatch } from '@/common/utils/EventTask';
import { NodeIKernelMsgListener } from '@/core';
const SchemaData = {
type: 'object',
properties: {
message_id: {
oneOf: [
{ type: 'number' },
{ type: 'string' }
]
}
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(Number(payload.message_id));
if (msg) {
let ret = NTEventDispatch.RegisterListen<NodeIKernelMsgListener['onMsgInfoListUpdate']>
(
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgs) => {
if (msgs.find(m => m.msgId === msg.MsgId && m.recallTime !== '0')) {
return true;
}
return false;
}
).catch(e => new Promise<undefined>((resolve, reject) => { resolve(undefined) }));
await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]);
let data = await ret;
if (!data) {
throw new Error('Recall failed');
}
//await sleep(100);
//await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId]);
}
}
}
export default DeleteMsg;

View File

@@ -0,0 +1,57 @@
import BaseAction from '../BaseAction';
import { NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { ChatType, Peer } from '@/core/entities';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'number' },
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class ForwardSingleMsg extends BaseAction<Payload, null> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`);
}
return { chatType: ChatType.friend, peerUid };
}
return { chatType: ChatType.group, peerUid: payload.group_id!.toString() };
}
protected async _handle(payload: Payload): Promise<null> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(payload.message_id);
if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`);
}
const peer = await this.getTargetPeer(payload);
const ret = await NTQQMsgApi.forwardMsg(msg.Peer,
peer,
[msg.MsgId],
);
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`);
}
return null;
}
}
export class ForwardFriendSingleMsg extends ForwardSingleMsg {
PayloadSchema = SchemaData;
actionName = ActionName.ForwardFriendSingleMsg;
}
export class ForwardGroupSingleMsg extends ForwardSingleMsg {
PayloadSchema = SchemaData;
actionName = ActionName.ForwardGroupSingleMsg;
}

View File

@@ -0,0 +1,50 @@
import { OB11Message } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
import { NTQQMsgApi } from '@/core';
export type ReturnDataType = OB11Message
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] },
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetMsg extends BaseAction<Payload, OB11Message> {
actionName = ActionName.GetMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
// log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
const MsgShortId = await MessageUnique.getShortIdByMsgId(payload.message_id.toString());
const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(MsgShortId || parseInt(payload.message_id.toString()));
if (!msgIdWithPeer) {
throw ('消息不存在');
}
const peer = { guildId: '', 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]);
try {
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgList[0].msgId)!;
retMsg.message_seq = retMsg.message_id;
retMsg.real_id = retMsg.message_id;
} catch (e) {
}
return retMsg;
}
}
export default GetMsg;

View File

@@ -0,0 +1,71 @@
import { ChatType, Peer } from '@/core/entities';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQFriendApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
group_id: { type: ['number', 'string'] }
}
} as const satisfies JSONSchema;
type PlayloadType = FromSchema<typeof SchemaData>;
class MarkMsgAsRead extends BaseAction<PlayloadType, null> {
async getPeer(payload: PlayloadType): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!peerUid) {
throw `私聊${payload.user_id}不存在`;
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid };
}
if (!payload.group_id) {
throw '缺少参数 group_id 或 user_id';
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() };
}
protected async _handle(payload: PlayloadType): Promise<null> {
// 调用API
const ret = await NTQQMsgApi.setMsgRead(await this.getPeer(payload));
if (ret.result != 0) {
throw ('设置已读失败,' + ret.errMsg);
}
return null;
}
}
// 以下为非标准实现
export class MarkPrivateMsgAsRead extends MarkMsgAsRead {
PayloadSchema = SchemaData;
actionName = ActionName.MarkPrivateMsgAsRead;
}
export class MarkGroupMsgAsRead extends MarkMsgAsRead {
PayloadSchema = SchemaData;
actionName = ActionName.MarkGroupMsgAsRead;
}
interface Payload {
message_id: number
}
export class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead;
protected async _handle(payload: Payload): Promise<null> {
return null;
}
}
export class MarkAllMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName._MarkAllMsgAsRead;
protected async _handle(payload: Payload): Promise<null> {
await NTQQMsgApi.markallMsgAsRead();
return null;
}
}

View File

@@ -0,0 +1,36 @@
import { OB11MessageData } from '@/onebot11/types';
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (const msg of sendMsgList) {
if (msg['type'] && msg['data']) {
const type = msg['type'];
const data = msg['data'];
if (type === 'text' && !data['text']) {
return 400;
} else if (['image', 'voice', 'record'].includes(type)) {
if (!data['file']) {
return 400;
} else {
if (checkUri(data['file'])) {
return 200;
} else {
return 400;
}
}
} else if (type === 'at' && !data['qq']) {
return 400;
} else if (type === 'reply' && !data['id']) {
return 400;
}
} else {
return 400;
}
}
return 200;
}

View File

@@ -0,0 +1,248 @@
import { OB11MessageData, OB11MessageDataType, OB11MessageFileBase } from '@/onebot11/types';
import {
AtType,
CustomMusicSignPostData,
Group,
IdMusicSignPostData,
NTQQFileApi,
NTQQMsgApi,
Peer,
SendArkElement,
SendMessageElement,
SendMsgElementConstructor,
SignMusicWrapper
} from '@/core';
import { getGroupMember } from '@/core/data';
import { logError, logWarn } from '@/common/utils/log';
import { uri2local } from '@/common/utils/file';
import { ob11Config } from '@/onebot11/config';
import { RequestUtil } from '@/common/utils/request';
import { MessageUnique } from '@/common/utils/MessageUnique';
console.log(process.pid)
export type MessageContext = {
deleteAfterSentFiles: string[],
peer:Peer
}
async function handleOb11FileLikeMessage(
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: MessageContext
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const { path, isLocal, fileName, errMsg,success } = (await uri2local(inputdata?.url || inputdata.file));
if (!success) {
logError('文件下载失败', errMsg);
throw Error('文件下载失败' + errMsg);
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path);
}
return { path, fileName: inputdata.name || fileName };
}
const _handlers: {
[Key in OB11MessageDataType]: (
sendMsg: Extract<OB11MessageData, { type: Key }>,
// This picks the correct message type out
// How great the type system of TypeScript is!
context: MessageContext
) => Promise<SendMessageElement | undefined>
} = {
[OB11MessageDataType.text]: async ({ data: { text } }) => SendMsgElementConstructor.text(text),
[OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => {
if (!context.peer) return undefined;
if (atQQ === 'all') return SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员');
// then the qq is a group member
const atMember = await getGroupMember(context.peer.peerUid, atQQ);
return atMember ?
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick) :
undefined;
},
[OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) {
logWarn('回复消息不存在', id);
return undefined;
}
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(replyMsgM?.Peer!, [replyMsgM?.MsgId!])).msgList[0];
return replyMsg ?
SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) :
undefined;
},
[OB11MessageDataType.face]: async ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)),
[OB11MessageDataType.mface]: async ({
data: {
emoji_package_id,
emoji_id,
key,
summary
}
}) => SendMsgElementConstructor.mface(emoji_package_id, emoji_id, key, summary),
// File service
[OB11MessageDataType.image]: async (sendMsg, context) => {
const PicEle = await SendMsgElementConstructor.pic(
(await handleOb11FileLikeMessage(sendMsg, context)).path,
sendMsg.data.summary || '',
sendMsg.data.subType || 0
);
context.deleteAfterSentFiles.push(PicEle.picElement.sourcePath);
return PicEle;
}
, // currently not supported
[OB11MessageDataType.file]: async (sendMsg, context) => {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context);
//logDebug('发送文件', path, fileName);
const FileEle = await SendMsgElementConstructor.file(path, fileName);
// 清除Upload的应该
// context.deleteAfterSentFiles.push(fileName || FileEle.fileElement.filePath);
return FileEle;
},
[OB11MessageDataType.video]: async (sendMsg, context) => {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context);
//logDebug('发送视频', path, fileName);
let thumb = sendMsg.data.thumb;
if (thumb) {
const uri2LocalRes = await uri2local(thumb);
if (uri2LocalRes.success) thumb = uri2LocalRes.path;
}
const videoEle = await SendMsgElementConstructor.video(path, fileName, thumb);
//未测试
context.deleteAfterSentFiles.push(videoEle.videoElement.filePath);
return videoEle;
},
[OB11MessageDataType.miniapp]: async ({ data: any }) => SendMsgElementConstructor.miniapp(),
[OB11MessageDataType.voice]: async (sendMsg, context) =>
SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: async ({ data: { data } }) => SendMsgElementConstructor.ark(data),
[OB11MessageDataType.dice]: async ({ data: { result } }) => SendMsgElementConstructor.dice(result),
[OB11MessageDataType.RPS]: async ({ data: { result } }) => SendMsgElementConstructor.rps(result),
[OB11MessageDataType.markdown]: async ({ data: { content } }) => SendMsgElementConstructor.markdown(content),
[OB11MessageDataType.music]: async ({ data }) => {
// 保留, 直到...找到更好的解决方案
if (data.type === 'custom') {
if (!data.url) {
logError('自定义音卡缺少参数url');
return undefined;
}
if (!data.audio) {
logError('自定义音卡缺少参数audio');
return undefined;
}
if (!data.title) {
logError('自定义音卡缺少参数title');
return undefined;
}
} else {
if (!['qq', '163'].includes(data.type)) {
logError('音乐卡片type错误, 只支持qq、163、custom当前type:', data.type);
return undefined;
}
if (!data.id) {
logError('音乐卡片缺少参数id');
return undefined;
}
}
let postData: IdMusicSignPostData | CustomMusicSignPostData;
if (data.type === 'custom' && data.content) {
const { content, ...others } = data;
postData = { singer: content, ...others };
} else {
postData = data;
}
const signUrl = ob11Config.musicSignUrl;
if (!signUrl) {
if (data.type === 'qq') {
const musicJson = (await SignMusicWrapper(data.id.toString())).data.arkResult.slice(0, -1);
return SendMsgElementConstructor.ark(musicJson);
}
throw Error('音乐消息签名地址未配置');
}
try {
const musicJson = await RequestUtil.HttpGetJson<any>(signUrl, 'POST', postData);
return SendMsgElementConstructor.ark(musicJson);
} catch (e) {
logError('生成音乐消息失败', e);
}
},
[OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: async () => undefined,
[OB11MessageDataType.xml]: async () => undefined,
[OB11MessageDataType.poke]: async () => undefined,
[OB11MessageDataType.Location]: async () => {
return SendMsgElementConstructor.location();
}
};
const handlers = <{
[Key in OB11MessageDataType]: (
sendMsg: OB11MessageData,
context: MessageContext
) => Promise<SendMessageElement | undefined>
}>_handlers;
export default async function createSendElements(
messageData: OB11MessageData[],
peer: Peer,
ignoreTypes: OB11MessageDataType[] = []
) {
const deleteAfterSentFiles: string[] = [];
const callResultList: Array<Promise<SendMessageElement | undefined>> = [];
for (const sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue;
}
const callResult = handlers[sendMsg.type](
sendMsg,
{ peer, deleteAfterSentFiles }
)?.catch(undefined);
callResultList.push(callResult);
}
const ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => ele) as SendMessageElement[];
return { sendElements, deleteAfterSentFiles };
}
export async function createSendElementsParallel(
messageData: OB11MessageData[],
peer: Peer,
ignoreTypes: OB11MessageDataType[] = []
) {
const deleteAfterSentFiles: string[] = [];
const sendElements = <SendMessageElement[]>(
await Promise.all(
messageData.map(async sendMsg => ignoreTypes.includes(sendMsg.type) ?
undefined :
handlers[sendMsg.type](sendMsg, { peer, deleteAfterSentFiles }))
).then(
results => results.filter(
element => element !== undefined
)
)
);
return { sendElements, deleteAfterSentFiles };
}

View File

@@ -0,0 +1,120 @@
import { ChatType, ElementType, Group, NTQQMsgApi, Peer, RawMessage, SendMessageElement } from '@/core';
import { OB11MessageDataType, OB11MessageNode } from '@/onebot11/types';
import { selfInfo } from '@/core/data';
import createSendElements from '@/onebot11/action/msg/SendMsg/create-send-elements';
import { logDebug, logError } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import { normalize, sendMsg } from '@/onebot11/action/msg/SendMsg/index';
import { MessageUnique } from '@/common/utils/MessageUnique';
async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
};
//logDebug('克隆的目标消息', msg);
const sendElements: SendMessageElement[] = [];
for (const element of msg.elements) {
sendElements.push(element as SendMessageElement);
}
if (sendElements.length === 0) {
logDebug('需要clone的消息无法解析将会忽略掉', msg);
}
try {
const nodeMsg = await NTQQMsgApi.sendMsg(selfPeer, sendElements, true);
return nodeMsg;
} catch (e) {
logError(e, '克隆转发消息失败,将忽略本条消息', msg);
}
}
export async function handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<RawMessage | null> {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
};
let nodeMsgIds: string[] = [];
for (const messageNode of messageNodes) {
const nodeId = messageNode.data.id;
if (nodeId) {
//对Mgsid和OB11ID混用情况兜底
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId);
if (!nodeMsg) {
logError('转发消息失败,未找到消息', nodeId);
continue;
}
nodeMsgIds.push(nodeMsg.MsgId);
} else {
// 自定义的消息
try {
let OB11Data = normalize(messageNode.data.content);
//筛选node消息
let isNodeMsg = OB11Data.filter(e => e.type === OB11MessageDataType.node).length;//找到子转发消息
if (isNodeMsg !== 0) {
if (isNodeMsg !== OB11Data.length) { logError('子消息中包含非node消息 跳过不合法部分'); continue; }
const nodeMsg = await handleForwardNode(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node));
if (nodeMsg) { nodeMsgIds.push(nodeMsg.msgId); MessageUnique.createMsg(selfPeer, nodeMsg.msgId) };
//完成子卡片生成跳过后续
continue;
}
const { sendElements } = await createSendElements(OB11Data, destPeer);
//拆分消息
let MixElement = sendElements.filter(element => element.elementType !== ElementType.FILE && element.elementType !== ElementType.VIDEO);
let SingleElement = sendElements.filter(element => element.elementType === ElementType.FILE || element.elementType === ElementType.VIDEO).map(e => [e]);
let AllElement: SendMessageElement[][] = [MixElement, ...SingleElement].filter(e => e !== undefined && e.length !== 0);
const MsgNodeList: Promise<RawMessage | undefined>[] = [];
for (const sendElementsSplitElement of AllElement) {
MsgNodeList.push(sendMsg(selfPeer, sendElementsSplitElement, [], true).catch(e => new Promise((resolve, reject) => { resolve(undefined) })));
}
(await Promise.allSettled(MsgNodeList)).map((result) => {
if (result.status === 'fulfilled' && result.value) {
nodeMsgIds.push(result.value.msgId);
MessageUnique.createMsg(selfPeer, result.value.msgId);
}
});
} catch (e) {
logDebug('生成转发消息节点失败', e);
}
}
}
const nodeMsgArray: Array<RawMessage> = [];
let srcPeer: Peer | undefined = undefined;
let needSendSelf = false;
//检测是否处于同一个Peer 不在同一个peer则全部消息由自身发送
for (let msgId of nodeMsgIds) {
const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId);
if (!nodeMsgPeer) {
logError('转发消息失败,未找到消息', msgId);
continue;
}
const nodeMsg = (await NTQQMsgApi.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 [index, msg] of nodeMsgArray.entries()) {
if (msg.peerUid === selfInfo.uid) continue;
const ClonedMsg = await cloneMsg(msg);
if (ClonedMsg) retMsgIds.push(ClonedMsg.msgId);
}
} else {
retMsgIds = nodeMsgIds;
}
if (nodeMsgIds.length === 0) throw Error('转发消息失败,生成节点为空');
try {
logDebug('开发转发', srcPeer, destPeer, nodeMsgIds);
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds);
} catch (e) {
logError('forward failed', e);
return null;
}
}

View File

@@ -0,0 +1,166 @@
import BaseAction from '@/onebot11/action/BaseAction';
import {
OB11MessageData,
OB11MessageDataType,
OB11MessageMixType,
OB11MessageNode,
OB11PostSendMsg
} from '@/onebot11/types';
import { ActionName, BaseCheckResult } from '@/onebot11/action/types';
import { getGroup } from '@/core/data';
import { ChatType, ElementType, Group, NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi, Peer, SendMessageElement, } from '@/core';
import fs from 'node:fs';
import fsPromise from 'node:fs/promises';
import { logDebug, logError } from '@/common/utils/log';
import { decodeCQCode } from '@/onebot11/cqcode';
import createSendElements from './create-send-elements';
import { handleForwardNode } from '@/onebot11/action/msg/SendMsg/handle-forward-node';
import { MessageUnique } from '@/common/utils/MessageUnique';
export interface ReturnDataType {
message_id: number;
}
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 { createSendElements };
export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) {
throw ('消息体无法解析, 请检查是否发送了不支持的消息类型');
}
let totalSize = 0;
let timeout = 10000;
try {
for (const fileElement of sendElements) {
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;
}
}
//且 PredictTime ((totalSize / 1024 / 512) * 1000)不等于Nan
const PredictTime = totalSize / 1024 / 256 * 1000;
if (!Number.isNaN(PredictTime)) {
timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s )
}
} catch (e) {
logError('发送消息计算预计时间异常', e);
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
try {
returnMsg!.id = await MessageUnique.createMsg({ chatType: peer.chatType, guildId: '', peerUid: peer.peerUid }, returnMsg!.msgId);
} catch (e: any) {
logDebug('发送消息id获取失败', e);
returnMsg!.id = 0;
}
deleteAfterSentFiles.map((f) => { fsPromise.unlink(f).then().catch(e => logError('发送消息删除文件失败', e)); });
return returnMsg;
}
async function createContext(payload: OB11PostSendMsg, 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) {
const group = (await getGroup(payload.group_id))!; // checked before
return {
chatType: ChatType.group,
peerUid: group.groupCode
};
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const isBuddy = await NTQQFriendApi.isBuddy(Uid!);
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy);
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!
};
}
throw '请指定 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的元素' };
}
if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) {
return { valid: false, message: `${payload.group_id}不存在` };
}
if (payload.user_id && payload.message_type !== 'group') {
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const isBuddy = await NTQQFriendApi.isBuddy(uid!);
// 此处有问题
if (!isBuddy) {
//return { valid: false, message: '异常消息' };
}
}
return { valid: true };
}
protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> {
const peer = await createContext(payload, this.contextMode);
const messages = normalize(
payload.message,
payload.auto_escape === true || payload.auto_escape === 'true'
);
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const returnMsg = await handleForwardNode(peer, messages as OB11MessageNode[]);
if (returnMsg) {
const msgShortId = MessageUnique.createMsg({ guildId: '', peerUid: peer.peerUid, chatType: peer.chatType }, returnMsg!.msgId);
return { message_id: msgShortId! };
} else {
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 createSendElements(messages, peer);
//console.log(peer, JSON.stringify(sendElements,null,2));
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles);
return { message_id: returnMsg!.id! };
}
}
export default SendMsg;

View File

@@ -0,0 +1,15 @@
import SendMsg, { ContextMode } from './SendMsg';
import { ActionName, BaseCheckResult } from '../types';
import { OB11PostSendMsg } from '../../types';
// 未检测参数
class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg;
contextMode: ContextMode = ContextMode.Private;
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
payload.message_type = 'private';
return super.check(payload);
}
}
export default SendPrivateMsg;

View File

@@ -0,0 +1,35 @@
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { NTQQMsgApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['string', 'number'] },
emoji_id: { type: ['string', 'number'] }
},
required: ['message_id', 'emoji_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
if (!payload.emoji_id) {
throw new Error('emojiId not found');
}
const msgData = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList;
if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) {
throw new Error('find msg by msgid error');
}
return await NTQQMsgApi.setEmojiLike(msg.Peer, msgData[0].msgSeq, payload.emoji_id.toString(), true);
}
}