Source

src/objects/OutboundMessageChain.ts

import {
	Emoticon,
	IdBasedOutboundImage,
	Mention,
	MentionAll,
	Message,
	MessageContent,
	MessageContentType,
	PlainText,
	UrlBasedOutboundImage,
	Voice,
} from './Message';
import { XmlCard } from './XmlCard';

/**
 * 出方向消息链
 */
export class OutboundMessageChain {
	/**
	 * 在头部加入内容。
	 *
	 * @param {MessageContent} x 消息内容块
	 * @since 0.0.1
	 */
	public prepend(x: MessageContent): OutboundMessageChain {
		this.content.unshift(x);
		return this;
	}

	/**
	 * 在尾部加入内容。
	 *
	 * @param {MessageContent} x 消息内容块
	 * @since 0.0.1
	 */
	public append(x: MessageContent): OutboundMessageChain {
		this.content.push(x);
		return this;
	}

	/**
	 * 追加文字消息。
	 *
	 * @param {string} msg 要追加的消息
	 * @since 0.1.3
	 */
	public appendText(msg: string): OutboundMessageChain {
		this.append({
			type: MessageContentType.TEXT,
			text: msg,
		} as PlainText);
		return this;
	}

	/**
	 * 合并两条消息链。这个合并会返回新的消息链,原有的两条消息链不做更改。
	 *
	 * @param {OutboundMessageChain} xs 要被合并的消息链,追加在本消息链之后
	 * @since 0.0.1
	 */
	public concat(xs: OutboundMessageChain): OutboundMessageChain {
		this.content = this.content.concat(xs.content);
		return this;
	}

	/**
	 * 构建一个纯文字出站消息链。
	 *
	 * @param {string} msg 要发送的文字
	 * @since 0.0.1
	 */
	public static ofText(msg: string): OutboundMessageChain {
		const out = new OutboundMessageChain();
		const obj: PlainText = {
			type: MessageContentType.TEXT,
			text: msg,
		};
		out.append(obj);
		return out;
	}

	/**
	 * 构建一个图片出站消息链。从网络获取图片。
	 *
	 * @param {string} imageUrl 图片 URL
	 * @since 0.0.1
	 */
	public static ofInternetImage(imageUrl: string): OutboundMessageChain {
		const out = new OutboundMessageChain();
		const obj: UrlBasedOutboundImage = {
			type: MessageContentType.IMAGE,
			url: imageUrl,
		};
		out.append(obj);
		return out;
	}

	/**
	 * 构建一个提到他人的消息链。
	 *
	 * @param {number} id 被提到的人的 QQ 号
	 * @since 0.1.3
	 */
	public static ofMention(id: number): OutboundMessageChain {
		const out = new OutboundMessageChain();
		const obj: Mention = {
			type: MessageContentType.MENTION,
			target: id,
			display: '', // 出站时忽略
		};
		out.append(obj);
		return out;
	}

	/**
	 * 从入站消息构建消息链。
	 *
	 * @param {Message} msg 入站消息
	 * @since 0.1.6
	 */
	public static ofMessage(msg: Message): OutboundMessageChain {
		const out = new OutboundMessageChain();
		out.content = msg.messageChain;
		return out;
	}

	/**
	 * 构建一个 XML 消息链。
	 *
	 * @param {XmlCard} card XML 卡片实例
	 * @since 0.1.8
	 */
	public static ofXmlCard(card: XmlCard): OutboundMessageChain {
		const out = new OutboundMessageChain();
		out.append(card.xmlChain);
		return out;
	}

	/**
	 * 从多个消息内容构建消息链。
	 *
	 * @param {MessageContent} content 消息内容,不定参数
	 * @since 0.1.8
	 */
	public static ofMultiple(...content: MessageContent[]): OutboundMessageChain {
		const out = new OutboundMessageChain();
		out.content = content;
		return out;
	}

	/**
	 * 从消息链数组构建消息链。
	 *
	 * @param {MessageContent[]} content 消息内容
	 * @since 0.1.8
	 */
	public static ofChain(content: MessageContent[]): OutboundMessageChain {
		const out = new OutboundMessageChain();
		out.content = content;
		return out;
	}

	/**
	 * 解析 Mirai 码。如果明文中包含方括号,则需要用反斜杠转义。
	 * 可以使用 {@link escapeMirai} 方法。
	 * <br>
	 * 由于上游文档的不一致性,部分在文档中的内容也不会进行转义。
	 * 这些内容包括 `PokeMessage` ({@link InteractMessage}) 和 `VipFace` (未在 `mirai-api-http` 中实现).
	 * <br>
	 * 解析器会忽略 `source` 标签。
	 * <br>
	 * 解析器将不抛出异常,除非遇到数字转换失败,其他任何的错误都将会被安静地忽略。
	 * 如果解析遇到不能理解或尚未实现的标签,则会保留原标签内容。
	 * 解析器会最大限度地从不合法的输入中恢复,但可能会出现不可预料的后果。
	 * <br>
	 * 注意:参数将不会 `trim()`. 任何白字符将会被保留。
	 *
	 * @param {string} str Mirai 码
	 * @since 0.1.7
	 * @see {@link https://github.com/mamoe/mirai/blob/dev/docs/mirai-code-specification.md 规范文档}
	 */
	public static ofMiraiCode(str: string): OutboundMessageChain {
		const out = new OutboundMessageChain();
		const iter = str[Symbol.iterator]();
		let escapeNext = false, inTag = false;
		let item = iter.next();
		let buf = '', paramBuf = '';
		const params: Array<string> = [];
		while (!item.done) {
			if (item.value === '\\') {
				if (escapeNext) {
					buf += '\\';
				} else {
					escapeNext = true;
				}
			} else if (item.value === '[') {
				if (escapeNext) {
					buf += '[';
				} else {
					if (inTag) {
						// malformed tag
						buf += paramBuf + '[';
						paramBuf = '';
						inTag = false;
					} else {
						// flush previous block
						if (buf.length > 0) {
							out.appendText(buf);
							buf = '';
						}
						paramBuf = '';
						inTag = true;
					}
				}
			} else if (item.value === ']') {
				if (escapeNext) {
					if (inTag) {
						paramBuf += ']';
					} else {
						buf += ']';
					}
				} else {
					// flush the last part
					params.push(paramBuf);
					// assemble tags
					// the first part must be mirai:*
					const identifier = params[0];
					const identifierParts = identifier.split(':');
					if (identifierParts[0] !== 'mirai' || identifierParts.length < 2) {
						// bad tag
						buf += '[' + params.join(',') + ']';
					} else {
						if (identifierParts.length > 2) {
							const firstParam = identifierParts[2];
							switch(identifierParts[1]) {
								case 'at':
									out.append({
										type: MessageContentType.MENTION,
										target: Number(firstParam),
										display: ''
									} as Mention);
									break;
								case 'face':
									out.append({
										type: MessageContentType.EMOTICON,
										faceId: Number(firstParam)
									} as Emoticon);
									break;
								case 'flash':
									out.append({
										type: MessageContentType.TRANSIENT_IMAGE,
										imageId: firstParam,
									} as IdBasedOutboundImage);
									break;
								case 'image':
									out.append({
										type: MessageContentType.IMAGE,
										imageId: firstParam,
									} as IdBasedOutboundImage);
									break;
								case 'voice':
									out.append({
										type: MessageContentType.VOICE,
										voiceId: firstParam
									} as Voice);
									break;
								case 'source':
									// this tag is ignored
									break;
								default:
									// unidentified tag
									buf += '[' + params.join(',') + ']';
									break;
							}
						} else {
							switch(identifierParts[1]) {
								case 'atall':
									out.append({
										type: MessageContentType.MENTION_ALL
									} as MentionAll);
									break;
								default:
									// unidentified tag
									buf += '[' + params.join(',') + ']';
									break;
							}
						}
					}
					inTag = false;
				}
			} else {
				if (inTag) {
					if (item.value === ',') {
						if (escapeNext) {
							paramBuf += ',';
						} else {
							params.push(paramBuf);
							paramBuf = '';
						}
					}
				} else {
					buf += item.value;
				}
			}
			escapeNext = false;
			item = iter.next();
		}
		return out;
	}

	private content: MessageContent[] = [];

	/**
	 * 获取消息链
	 */
	public get chain(): MessageContent[] {
		return this.content;
	}
}