qq-official-bot
Version:
418 lines (417 loc) • 12.9 kB
JavaScript
"use strict";
/**
* 消息构建器 - 专门负责构建消息内容
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageBuilder = void 0;
const utils_1 = require("../utils");
const crypto_1 = require("crypto");
/**
* 消息构建器
* 专门负责将Sendable类型的消息转换为API所需的格式
*/
class MessageBuilder {
constructor(appid, isGuild, source) {
this.appid = appid;
this.isGuild = isGuild;
this.source = source;
this.buttons = [];
this.isFile = false;
this.contentType = 'application/json';
this.brief = '';
this.messagePayload = {
msg_seq: (0, crypto_1.randomInt)(1, 1000000),
content: ''
};
this.filePayload = {
url: ''
};
if (source?.id) {
this.messagePayload.msg_id = source.id;
}
if (source?.event_id) {
this.messagePayload.event_id = source.event_id;
}
}
/**
* 构建消息
*/
async build(message) {
await this.processMessage(message);
await this.processButtons();
return {
messagePayload: this.messagePayload,
filePayload: this.filePayload,
isFile: this.isFile,
contentType: this.contentType,
brief: this.brief
};
}
/**
* 处理消息内容
*/
async processMessage(message) {
if (!Array.isArray(message)) {
message = [message];
}
const messageQueue = [...message];
while (messageQueue.length) {
const elem = messageQueue.shift();
if (typeof elem === 'string') {
const parsedElems = this.parseFromTemplate(elem);
messageQueue.unshift(...parsedElems);
continue;
}
await this.processElement(elem);
}
}
/**
* 处理单个消息元素
*/
async processElement(elem) {
switch (elem.type) {
case 'reply':
this.handleReply(elem);
break;
case 'at':
this.handleAt(elem);
break;
case 'link':
this.handleLink(elem);
break;
case 'text':
this.handleText(elem);
break;
case 'face':
this.handleFace(elem);
break;
case 'image':
case 'audio':
case 'video':
await this.handleMedia(elem);
break;
case 'markdown':
this.handleMarkdown(elem);
break;
case 'keyboard':
this.handleKeyboard(elem);
break;
case 'button':
this.handleButton(elem);
break;
case 'embed':
this.handleEmbed(elem);
break;
case 'ark':
this.handleArk(elem);
break;
default:
console.warn(`未知的消息元素类型: ${elem.type}`);
break;
}
}
/**
* 处理回复元素
*/
handleReply(elem) {
if (elem.data.event_id) {
this.messagePayload.event_id = elem.data.event_id;
this.brief += `<reply,event_id=${elem.data.event_id}>`;
}
else if (elem.data.id) {
this.messagePayload.msg_id = elem.data.id;
this.messagePayload.message_reference = {
message_id: elem.data.id
};
this.brief += `<reply,msg_id=${elem.data.id}>`;
}
}
/**
* 处理@元素
*/
handleAt(elem) {
const userId = elem.data.user_id === 'all' ? 'everyone' : elem.data.user_id;
this.messagePayload.content += `<@${userId}>`;
this.brief += `<at,user=${userId}>`;
}
/**
* 处理链接元素
*/
handleLink(elem) {
this.messagePayload.content += `<#${elem.data.channel_id}>`;
this.brief += `<link,channel=${elem.data.channel_id}>`;
}
/**
* 处理文本元素
*/
handleText(elem) {
this.messagePayload.content += elem.data.text;
this.brief += elem.data.text;
}
/**
* 处理表情元素
*/
handleFace(elem) {
this.messagePayload.content += `<emoji:${elem.data.id}>`;
this.brief += `<face,id=${elem.data.id}>`;
}
/**
* 处理媒体元素(图片、视频、音频)
*/
async handleMedia(elem) {
const mediaType = this.getMediaType(elem.type);
// 如果是回复消息,需要特殊处理
if (this.messagePayload.msg_id || this.messagePayload.event_id) { // 频道消息的文件处理
const { url, blob, base64 } = await this.formatMediaData(elem);
if (this.isGuild) {
this.contentType = 'multipart/form-data';
if (blob) {
this.messagePayload.file_image = blob;
}
else {
this.messagePayload.image = url;
}
}
else {
// 群聊/私聊消息的文件处理
this.messagePayload.msg_type = 7;
// 这里需要调用上传接口,暂时标记为需要文件上传
this.isFile = true;
this.filePayload.file_type = mediaType;
if (base64) {
this.filePayload.file_data = base64;
}
else {
this.filePayload.url = url;
}
}
}
else {
const { url, blob, base64 } = await this.formatMediaData(elem);
// 非回复消息的处理
if (this.isGuild) {
if (blob) {
this.messagePayload.file_image = blob;
}
else {
this.messagePayload.image = url;
}
}
else {
this.messagePayload.msg_type = 7;
this.isFile = true;
this.filePayload.file_type = mediaType;
if (base64) {
this.filePayload.file_data = base64;
}
else {
this.filePayload.url = url;
}
}
}
this.brief += `<${elem.type}:${this.getFileHash(elem.data.file)}>`;
}
/**
* 处理Markdown元素
*/
handleMarkdown(elem) {
this.messagePayload.markdown = elem.data;
this.messagePayload.msg_type = 2;
const content = elem.data.content ? `content=${elem.data.content}` : `template_id=${elem.data.custom_template_id}`;
this.brief += `<markdown,${content}>`;
}
/**
* 处理键盘元素
*/
handleKeyboard(elem) {
this.messagePayload.msg_type = 2;
this.messagePayload.keyboard = elem.data;
this.messagePayload.bot_appid = this.appid;
this.brief += `<keyboard>`;
}
/**
* 处理按钮元素
*/
handleButton(elem) {
this.buttons.push(elem.data);
this.brief += `<button,data=${JSON.stringify(elem.data)}>`;
}
/**
* 处理嵌入元素
*/
handleEmbed(elem) {
if (!this.isGuild)
return;
this.messagePayload.msg_type = 4;
this.messagePayload.embed = elem.data;
this.brief += `<embed>`;
}
/**
* 处理ARK元素
*/
handleArk(elem) {
this.messagePayload.msg_type = 3;
this.messagePayload.ark = elem.data;
this.brief += `<ark>`;
}
/**
* 处理按钮组
*/
async processButtons() {
if (this.buttons.length === 0)
return;
const rows = [];
let row = [];
for (let i = 0; i < this.buttons.length; i++) {
if (row.length >= 5) {
rows.push(row);
row = [];
}
// 支持按钮模板发送
if (this.buttons[i].id) {
this.messagePayload.keyboard = {
id: this.buttons[i].id,
bot_appid: this.appid
};
return;
}
// 如果是按钮组,则直接添加到行中
if (Array.isArray(this.buttons[i].buttons)) {
rows.push(this.buttons[i].buttons);
}
else {
row.push(this.buttons[i]);
}
}
if (row.length > 0) {
rows.push(row);
}
this.messagePayload.keyboard = {
content: {
rows: rows.map(row => ({
buttons: row
}))
},
bot_appid: this.appid
};
}
/**
* 从模板字符串解析消息元素
*/
parseFromTemplate(template) {
const result = [];
let cursor = 0;
while (cursor < template.length) {
const tagStart = template.indexOf('<', cursor);
if (tagStart === -1) {
const text = template.slice(cursor);
if (text) {
result.push({
type: 'text',
data: { text }
});
}
break;
}
if (tagStart > cursor) {
result.push({
type: 'text',
data: { text: template.slice(cursor, tagStart) }
});
}
const tagEnd = template.indexOf('>', tagStart + 1);
if (tagEnd === -1) {
const text = template.slice(tagStart);
if (text) {
result.push({
type: 'text',
data: { text }
});
}
break;
}
const match = template.slice(tagStart, tagEnd + 1);
cursor = tagEnd + 1;
const [type, ...attrArr] = match.slice(1, -1).split(',');
const attrs = Object.fromEntries(attrArr.map((attr) => {
const [key, value] = attr.split('=');
try {
return [key, JSON.parse(value)];
}
catch {
return [key, value];
}
}));
result.push({
type,
data: attrs
});
}
return result;
}
/**
* 准备频道媒体数据
*/
async formatMediaData(elem) {
if ('url' in elem.data && elem.data.url)
return { url: elem.data.url };
if (typeof elem.data.file === "string" && elem.data.file.startsWith('http')) {
return { url: elem.data.file };
}
if (Buffer.isBuffer(elem.data.file)) {
return {
url: '',
blob: new Blob([Buffer.from(elem.data.file)]),
base64: elem.data.file.toString('base64')
};
}
else if (typeof elem.data.file !== "string") {
throw new Error("无效的文件参数: " + elem.data.file);
}
else if (elem.data.file.startsWith("base64://")) {
return {
url: '',
blob: new Blob([Buffer.from(elem.data.file.slice(9), 'base64')]),
base64: elem.data.file.slice(9)
};
}
else if (/^data:[^/]+\/[^;]+;base64,/.test(elem.data.file)) {
return {
url: '',
blob: new Blob([Buffer.from(elem.data.file.replace(/^data:[^/]+\/[^;]+;base64,/, ''), 'base64')]),
base64: elem.data.file.replace(/^data:[^/]+\/[^;]+;base64,/, '')
};
}
else {
try {
const fs = require('node:fs/promises');
return {
url: '',
blob: new Blob([await fs.readFile(elem.data.file.replace("file://", ""))]),
base64: (await fs.readFile(elem.data.file.replace("file://", ""))).toString('base64')
};
}
catch {
throw new Error("无效的文件路径: " + elem.data.file);
}
}
}
/**
* 获取媒体类型编号
*/
getMediaType(type) {
return (['image', 'video', 'audio'].indexOf(type) + 1);
}
/**
* 获取文件哈希值
*/
getFileHash(file) {
try {
return (0, utils_1.md5)(file);
}
catch {
return 'unknown';
}
}
}
exports.MessageBuilder = MessageBuilder;