mirror of
https://github.com/chrononeko/chronocat.git
synced 2024-11-25 09:37:35 +00:00
feat(event): add parser
This commit is contained in:
parent
54bcc15da6
commit
8478cf4437
546
packages/engine-chronocat-event/src/parser/index.ts
Normal file
546
packages/engine-chronocat-event/src/parser/index.ts
Normal file
@ -0,0 +1,546 @@
|
||||
import type {
|
||||
Channel,
|
||||
ChronocatContext,
|
||||
ChronocatLogCurrentConfig,
|
||||
ChronocatSatoriEventsConfig,
|
||||
Event,
|
||||
Guild,
|
||||
GuildMember,
|
||||
RedMessage,
|
||||
} from '@chronocat/shell'
|
||||
import {
|
||||
ChannelType,
|
||||
ChatType,
|
||||
FaceType,
|
||||
MsgType,
|
||||
SendType,
|
||||
} from '@chronocat/shell'
|
||||
import { l } from '@chronocat/shell/lib/services/logger'
|
||||
import { PLATFORM } from '@chronocat/shell/lib/utils/consts'
|
||||
import h from '@satorijs/element'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import type { O } from 'ts-toolbelt'
|
||||
import { parseMsgTypes } from './msgt'
|
||||
|
||||
export const buildParser =
|
||||
(
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
) =>
|
||||
(message: RedMessage) =>
|
||||
parseMessageRecv(ctx, config, message)
|
||||
|
||||
export const parseMessageRecv = async (
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
message: RedMessage,
|
||||
) => {
|
||||
const parsed = await parseMessage(ctx, config, message)
|
||||
|
||||
if (!parsed) return undefined
|
||||
|
||||
const result: Event[] = []
|
||||
|
||||
for (const event of parsed) {
|
||||
if (!event.message?.id && !event.user?.id) {
|
||||
l.warn('satori: parser: 丢弃了一条消息', { code: 2127 })
|
||||
continue
|
||||
} else if (!event.message?.id)
|
||||
l.warn(
|
||||
`satori: parser: 来自 ${event.user?.name} (${event.user?.id}) 的消息不带有 messageId,请注意。`,
|
||||
{ code: 2128 },
|
||||
)
|
||||
else if (!event.user?.id)
|
||||
l.warn(
|
||||
`satori: parser: 消息 ${event.message.id} 不带有 userId,请注意。`,
|
||||
{ code: 2129 },
|
||||
)
|
||||
else if (!event.user?.name && !event.member?.nick)
|
||||
l.warn(
|
||||
`satori: parser: 消息 ${event.message.id} 不带有 userName,请注意。`,
|
||||
{
|
||||
code: 2130,
|
||||
},
|
||||
)
|
||||
|
||||
result.push(event)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const parseMessage = async (
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
message: RedMessage,
|
||||
) => {
|
||||
const event: Event = {
|
||||
id: undefined as unknown as number,
|
||||
type: undefined as unknown as string,
|
||||
|
||||
platform: undefined as unknown as string,
|
||||
self_id: undefined as unknown as string,
|
||||
timestamp: Number(message.msgTime) * 1000,
|
||||
}
|
||||
|
||||
event.user = {
|
||||
id: message.senderUin,
|
||||
name: (message.sendNickName || undefined) as unknown as string,
|
||||
avatar: `http://thirdqq.qlogo.cn/headimg_dl?dst_uin=${message.senderUin}&spec=640`,
|
||||
}
|
||||
|
||||
const ntMsgTypes = parseMsgTypes(message)
|
||||
|
||||
// 无论哪种消息都有 Channel 和 User
|
||||
event.channel = {} as Channel
|
||||
|
||||
// 判断消息来源
|
||||
switch (ntMsgTypes.chatType) {
|
||||
case ChatType.Private:
|
||||
event.channel.type = ChannelType.DIRECT
|
||||
event.channel.id = `private:${event.user.id}`
|
||||
event.channel.name = event.user.name
|
||||
break
|
||||
|
||||
case ChatType.Group:
|
||||
// Guild 和 Member 只有群聊有
|
||||
event.guild = {} as Guild
|
||||
event.member = {} as GuildMember
|
||||
|
||||
if (message.sendMemberName) event.member.nick = message.sendMemberName
|
||||
|
||||
event.channel.type = ChannelType.TEXT
|
||||
event.channel.id = event.guild.id = message.peerUid
|
||||
event.channel.name = event.guild.name = message.peerName
|
||||
event.guild.avatar = `https://p.qlogo.cn/gh/${message.peerUid}/${message.peerUid}/640`
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
ntMsgTypes.msgType === MsgType.Ark &&
|
||||
message.subMsgType === 0 &&
|
||||
ntMsgTypes.sendType === SendType.Normal
|
||||
)
|
||||
// ARK 卡片消息,elementType = 10
|
||||
// 不处理
|
||||
return undefined
|
||||
else if (
|
||||
ntMsgTypes.msgType === MsgType.Normal ||
|
||||
ntMsgTypes.msgType === MsgType.Value3 ||
|
||||
ntMsgTypes.msgType === MsgType.Ptt ||
|
||||
ntMsgTypes.msgType === MsgType.Video ||
|
||||
ntMsgTypes.msgType === MsgType.WithRecords ||
|
||||
ntMsgTypes.msgType === MsgType.Vaule17
|
||||
)
|
||||
return parseChatMessage(ctx, config, event, message).then((x) => [
|
||||
x[0],
|
||||
...x[1],
|
||||
])
|
||||
// else if (event.__CHRONO_UNSAFE_NTMSGTYPES__.subMsgType.multiForward)
|
||||
// // 合并转发消息
|
||||
// // multiForwardMsgElement(elementType = 16)内并不带有合并转发的全部内容,
|
||||
// // 需要后续通过 API 再请求
|
||||
// // 考虑到合并转发消息解析需求较少,不调度此 session
|
||||
// return undefined
|
||||
else if (
|
||||
ntMsgTypes.msgType === MsgType.System && // 5
|
||||
message.subMsgType === 8 && // 8
|
||||
ntMsgTypes.sendType === SendType.System && // 3
|
||||
message.elements[0]!.elementType === 8 && // 8
|
||||
message.elements[0]!.grayTipElement!.subElementType === 4 && // 4
|
||||
message.elements[0]!.grayTipElement!.groupElement!.type === 1 // 1
|
||||
)
|
||||
// 新人自行入群
|
||||
return await parseGuildMemberAddedMessage(ctx, config, event, message)
|
||||
else if (
|
||||
ntMsgTypes.msgType === MsgType.System && // 5
|
||||
message.subMsgType === 8 && // 8
|
||||
ntMsgTypes.sendType === SendType.System && // 3
|
||||
message.elements[0]!.elementType === 8 && // 8
|
||||
message.elements[0]!.grayTipElement!.subElementType === 4 && // 4
|
||||
message.elements[0]!.grayTipElement!.groupElement!.type === 8 // 8
|
||||
)
|
||||
// 他人被禁言
|
||||
return await parseGuildMemberMuteMessage(ctx, config, event, message)
|
||||
else if (
|
||||
ntMsgTypes.msgType === MsgType.System && // 5
|
||||
message.subMsgType === 8 && // 8
|
||||
ntMsgTypes.sendType === SendType.System && // 3
|
||||
message.elements[0]!.elementType === 8 && // 8
|
||||
message.elements[0]!.grayTipElement!.subElementType === 4 && // 4
|
||||
message.elements[0]!.grayTipElement!.groupElement!.type === 5 // 5
|
||||
)
|
||||
// 群名称变更
|
||||
return undefined
|
||||
else if (
|
||||
ntMsgTypes.msgType === MsgType.System && // 5
|
||||
message.subMsgType === 12 && // 12
|
||||
ntMsgTypes.sendType === SendType.System && // 3
|
||||
message.elements[0]!.elementType === 8 && // 8
|
||||
message.elements[0]!.grayTipElement!.subElementType === 12 && // 12
|
||||
message.elements[0]!.grayTipElement!.xmlElement!.busiType === '1' && // 1
|
||||
message.elements[0]!.grayTipElement!.xmlElement!.busiId === '10145' // 10145
|
||||
)
|
||||
// 旧版群成员邀请新人入群
|
||||
return await parseGuildMemberAddedLegacyInviteMessage(
|
||||
ctx,
|
||||
config,
|
||||
event,
|
||||
message,
|
||||
)
|
||||
else if (
|
||||
ntMsgTypes.msgType === MsgType.System &&
|
||||
message.subMsgType === 17 &&
|
||||
ntMsgTypes.sendType === SendType.System
|
||||
)
|
||||
// 群主禁止群内临时通话
|
||||
// 群主禁止群内发起新的群聊
|
||||
return undefined
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天消息。
|
||||
*
|
||||
* @remarks
|
||||
* 在消息没有除了消息元素以外的其他属性需要处理的情况下,直接使用此方法。
|
||||
*/
|
||||
async function parseChatMessage(
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
event: Event,
|
||||
message: RedMessage,
|
||||
) {
|
||||
const [elements, extraEvents] = await parseElements(ctx, config, message)
|
||||
event.type = 'message-created'
|
||||
event.message = {
|
||||
id: message.msgId,
|
||||
content: elements.join(''),
|
||||
}
|
||||
return [event, extraEvents] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析新人自行入群消息。
|
||||
*
|
||||
* @remarks
|
||||
* 通过解析 `grayTipElement`(elementType = 8)中的
|
||||
* `groupElement`(subElementType = 4)即可直接提取 QQ 号。
|
||||
*/
|
||||
async function parseGuildMemberAddedMessage(
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
event: Event,
|
||||
message: RedMessage,
|
||||
) {
|
||||
const [event2, extraEvents] = await parseChatMessage(
|
||||
ctx,
|
||||
config,
|
||||
event,
|
||||
message,
|
||||
)
|
||||
event2.type = 'guild-member-added'
|
||||
|
||||
event2.operator = {
|
||||
id: message.elements[0]!.grayTipElement!.groupElement!.adminUin!,
|
||||
name: undefined as unknown as string,
|
||||
}
|
||||
|
||||
event2.user = {
|
||||
id: message.elements[0]!.grayTipElement!.groupElement!.memberUin!,
|
||||
name: message.elements[0]!.grayTipElement!.groupElement!.memberNick!,
|
||||
avatar: `http://thirdqq.qlogo.cn/headimg_dl?dst_uin=${
|
||||
message.elements[0]!.grayTipElement!.groupElement!.memberUin
|
||||
}&spec=640`,
|
||||
}
|
||||
|
||||
if (event2.member) delete event2.member
|
||||
|
||||
return [event2, ...extraEvents]
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析他人被禁言消息。
|
||||
*/
|
||||
async function parseGuildMemberMuteMessage(
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
event: Event,
|
||||
message: RedMessage,
|
||||
) {
|
||||
const [event2, extraEvents] = await parseChatMessage(
|
||||
ctx,
|
||||
config,
|
||||
event,
|
||||
message,
|
||||
)
|
||||
if (
|
||||
Number(message.elements[0]!.grayTipElement!.groupElement!.shutUp!.duration)
|
||||
)
|
||||
event2.type = 'unsafe-guild-mute'
|
||||
else event2.type = 'unsafe-guild-unmute'
|
||||
|
||||
event2.operator = {
|
||||
id: message.elements[0]!.grayTipElement!.groupElement!.shutUp!.admin.uin,
|
||||
name: undefined as unknown as string,
|
||||
}
|
||||
|
||||
event2.user = {
|
||||
id: message.elements[0]!.grayTipElement!.groupElement!.shutUp!.member.uin,
|
||||
name: message.elements[0]!.grayTipElement!.groupElement!.shutUp!.member
|
||||
.name,
|
||||
avatar: `http://thirdqq.qlogo.cn/headimg_dl?dst_uin=${
|
||||
message.elements[0]!.grayTipElement!.groupElement!.shutUp!.member.uin
|
||||
}&spec=640`,
|
||||
}
|
||||
|
||||
if (event2.member) delete event2.member
|
||||
|
||||
return [event2, ...extraEvents]
|
||||
}
|
||||
|
||||
const regexGuildMemberAddedLegacyInviteMessage = /jp="(\d+)".*jp="(\d+)"/gim
|
||||
|
||||
/**
|
||||
* 解析旧版群成员邀请新人入群消息。使用 NT
|
||||
* 以前的客户端邀请他人加群会收到此消息。
|
||||
*
|
||||
* @remarks
|
||||
* 遗憾地,旧版群成员邀请新人入群消息不存在能够直接获取成员 QQ
|
||||
* 号的方法。需要通过解析 `grayTipElement`(elementType = 8)中的
|
||||
* `xmlElement`(subElementType = 12)中的 HTML 来提取 QQ 号。
|
||||
*
|
||||
* 一个 HTML 的示例:
|
||||
*
|
||||
* ```html
|
||||
* <gtip align="center">
|
||||
* <qq uin="u_0gvBEjIEEOk5-EypJRjwxw" col="3" jp="1302744182" />
|
||||
* <nor txt="邀请"/>
|
||||
* <qq uin="u_ENIKFfFS74WSiKNoA6ERWg" col="3" jp="2953529126" />
|
||||
* <nor txt="加入了群聊。"/>
|
||||
* </gtip>
|
||||
* ```
|
||||
*
|
||||
* 目前使用正则进行 QQ 号的提取。如果未来正则失效,则应考虑使用 cheerio
|
||||
* 进行提取。
|
||||
*/
|
||||
async function parseGuildMemberAddedLegacyInviteMessage(
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
event: Event,
|
||||
message: RedMessage,
|
||||
) {
|
||||
const [event2, extraEvents] = await parseChatMessage(
|
||||
ctx,
|
||||
config,
|
||||
event,
|
||||
message,
|
||||
)
|
||||
event2.type = 'guild-member-added'
|
||||
|
||||
const execArr = regexGuildMemberAddedLegacyInviteMessage.exec(
|
||||
message.elements[0]!.grayTipElement!.xmlElement!.content!,
|
||||
)
|
||||
if (!Array.isArray(execArr) || execArr.length < 3) return undefined
|
||||
const [_, operatorId, userId] = execArr
|
||||
|
||||
event2.operator = {
|
||||
id: operatorId!,
|
||||
name: undefined as unknown as string,
|
||||
}
|
||||
|
||||
event2.user = {
|
||||
id: userId!,
|
||||
name: undefined as unknown as string,
|
||||
avatar: `http://thirdqq.qlogo.cn/headimg_dl?dst_uin=${userId}&spec=640`,
|
||||
}
|
||||
|
||||
if (event2.member) delete event2.member
|
||||
|
||||
return [event2, ...extraEvents]
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析消息元素。
|
||||
*/
|
||||
async function parseElements(
|
||||
ctx: ChronocatContext,
|
||||
config: O.Intersect<ChronocatLogCurrentConfig, ChronocatSatoriEventsConfig>,
|
||||
message: RedMessage,
|
||||
) {
|
||||
const elements: h[] = []
|
||||
const extraEvents: Event[] = []
|
||||
|
||||
for (const m of message.elements) {
|
||||
switch (m.elementType) {
|
||||
case 1: {
|
||||
// 文本消息
|
||||
switch (m.textElement!.atType) {
|
||||
case 0: {
|
||||
// 纯文本消息
|
||||
elements.push(h.text(m.textElement?.content))
|
||||
break
|
||||
}
|
||||
|
||||
case 2: {
|
||||
// at 消息
|
||||
const id = m.textElement!.atNtUin
|
||||
const name = m.textElement!.content.slice(1)
|
||||
if (!id) {
|
||||
l.warn(
|
||||
`satori: parser: at 目标 ${name} 不带有 id,将跳过该元素。`,
|
||||
{ code: 2131 },
|
||||
)
|
||||
break
|
||||
}
|
||||
elements.push(
|
||||
h('at', {
|
||||
id,
|
||||
name,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 2: {
|
||||
// 图片消息
|
||||
elements.push(
|
||||
h('img', {
|
||||
src: `${config.self_url}/v1/assets/${Buffer.from(
|
||||
JSON.stringify({
|
||||
msgId: message.msgId,
|
||||
chatType: message.chatType,
|
||||
peerUid: message.peerUid,
|
||||
elementId: m.elementId,
|
||||
thumbSize: m.picElement!.thumbFileSize,
|
||||
}),
|
||||
).toString('base64url')}`,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 3: {
|
||||
// 文件消息
|
||||
elements.push(
|
||||
h('file', {
|
||||
src: `${config.self_url}/v1/assets/${Buffer.from(
|
||||
JSON.stringify({
|
||||
msgId: message.msgId,
|
||||
chatType: message.chatType,
|
||||
peerUid: message.peerUid,
|
||||
elementId: m.elementId,
|
||||
thumbSize: m.fileElement!.thumbFileSize,
|
||||
}),
|
||||
).toString('base64url')}`,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 4: {
|
||||
// 语音消息
|
||||
elements.push(
|
||||
h('audio', {
|
||||
src: `${config.self_url}/v1/assets/${Buffer.from(
|
||||
JSON.stringify({
|
||||
msgId: message.msgId,
|
||||
chatType: message.chatType,
|
||||
peerUid: message.peerUid,
|
||||
elementId: m.elementId,
|
||||
thumbSize: 0,
|
||||
}),
|
||||
).toString('base64url')}`,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 5: {
|
||||
// 视频消息
|
||||
elements.push(
|
||||
h('video', {
|
||||
src: `${config.self_url}/v1/assets/${Buffer.from(
|
||||
JSON.stringify({
|
||||
msgId: message.msgId,
|
||||
chatType: message.chatType,
|
||||
peerUid: message.peerUid,
|
||||
elementId: m.elementId,
|
||||
thumbSize: m.videoElement!.thumbSize,
|
||||
}),
|
||||
).toString('base64url')}`,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 6: {
|
||||
// 表情
|
||||
switch (m.faceElement!.faceType) {
|
||||
case FaceType.PCPoke: {
|
||||
elements.push(
|
||||
h(`${PLATFORM}:pcpoke`, {
|
||||
id: m.faceElement!.pokeType,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case FaceType.Normal:
|
||||
case FaceType.Super: {
|
||||
elements.push(
|
||||
h(`${PLATFORM}:face`, {
|
||||
id: m.faceElement!.faceIndex,
|
||||
name: `[${(await ctx.chronocat.api['chronocat.internal.qface.get'](`${m.faceElement!.faceIndex}`))!.QDes.slice(1)}]`,
|
||||
platform: PLATFORM,
|
||||
'unsafe-super': m.faceElement!.faceType === FaceType.Super,
|
||||
'unsafe-result-id': m.faceElement!.resultId,
|
||||
'unsafe-chain-count': m.faceElement!.chainCount,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 7: {
|
||||
// 引用消息
|
||||
const source = message.records.find(
|
||||
(x) => x.msgId === m.replyElement!.sourceMsgIdInRecords,
|
||||
)!
|
||||
|
||||
elements.push(
|
||||
h(
|
||||
'quote',
|
||||
{
|
||||
'chronocat:seq': m.replyElement!.replayMsgSeq,
|
||||
},
|
||||
[
|
||||
await parseAuthor(source),
|
||||
...(await parseElements(ctx, config, source))[0],
|
||||
],
|
||||
),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [elements, extraEvents] as const
|
||||
}
|
||||
|
||||
async function parseAuthor(message: RedMessage) {
|
||||
return h('author', {
|
||||
id: message.senderUin,
|
||||
name: message.sendMemberName || message.sendNickName,
|
||||
avatar: `http://thirdqq.qlogo.cn/headimg_dl?dst_uin=${message.senderUin}&spec=640`,
|
||||
})
|
||||
}
|
141
packages/engine-chronocat-event/src/parser/msgt.ts
Normal file
141
packages/engine-chronocat-event/src/parser/msgt.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import type { ChatType, RedMessage, SendType } from '@chronocat/shell'
|
||||
import { MsgType } from '@chronocat/shell'
|
||||
|
||||
/**
|
||||
* QQNT 消息类型。
|
||||
*
|
||||
* @remarks
|
||||
* # 消息类型
|
||||
*
|
||||
* QQNT 的消息类型共有四种,它们分别是 {@link RedMessage.chatType}、{@link RedMessage.msgType}、{@link RedMessage.subMsgType}
|
||||
* 和 {@link RedMessage.sendType}。下面分别介绍这四种类型。
|
||||
*
|
||||
* ## {@link RedMessage.chatType}
|
||||
*
|
||||
* 消息的归属。目前只有 {@link ChatType.Private} 和 {@link ChatType.Group}。
|
||||
*
|
||||
* ## {@link RedMessage.msgType}
|
||||
*
|
||||
* 消息的主要类型。
|
||||
*
|
||||
* ## {@link RedMessage.subMsgType}
|
||||
*
|
||||
* 消息内容中包含的元素类型。普通消息的情况下以 Bitset 提供,可使用 {@link adaptMsgTypes}
|
||||
* 解析为一组 {@link Boolean} 字段。
|
||||
*
|
||||
* ## {@link RedMessage.sendType}
|
||||
*
|
||||
* 消息的呈现类型。
|
||||
*/
|
||||
export interface MsgTypes {
|
||||
chatType: ChatType
|
||||
|
||||
msgType: MsgType
|
||||
|
||||
subMsgType: {
|
||||
/**
|
||||
* 文本消息,如纯文本消息、at 消息等。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 0010 | 0000 0000 0000 0001` = `2-1`
|
||||
* - Bit: `0000 1001 | 0000 0000 0000 0001` = `9-1`
|
||||
* - `elementType`: `1`
|
||||
*/
|
||||
text: boolean
|
||||
|
||||
/**
|
||||
* 图片。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 0010 | 0000 0000 0000 0010` = `2-2`
|
||||
* - `elementType`: `2`
|
||||
*/
|
||||
pic: boolean
|
||||
|
||||
/**
|
||||
* 普通表情。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 0010 | 0000 0000 0001 0000` = `2-16`
|
||||
* - Bit: `0000 1001 | 0000 0000 0001 0000` = `9-16`
|
||||
* - `elementType`: `6`
|
||||
*/
|
||||
face: boolean
|
||||
|
||||
/**
|
||||
* 链接。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 0010 | 0000 0000 1000 0000` = `2-128`
|
||||
* - `elementType`: `1`
|
||||
*/
|
||||
link: boolean
|
||||
|
||||
/**
|
||||
* 合并转发。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 0010 | 0000 0000 0000 1000` = `2-8` (NT 合并转发)
|
||||
* - Bit: `0000 1000 | 0000 0000 0000 0000` = `8-0` (手 Q 合并转发)
|
||||
* - `elementType`: `16`
|
||||
*/
|
||||
multiForward: boolean
|
||||
|
||||
/**
|
||||
* 回复(Quote)。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 1001 | 0000 0000 0010 0000` = `9-32`
|
||||
* - `elementType`: `7`
|
||||
*/
|
||||
reply: boolean
|
||||
|
||||
/**
|
||||
* 原创表情。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0001 0001 | 0000 0000 0000 1000` = `17-8`
|
||||
* - `elementType`: `11`
|
||||
*/
|
||||
marketFace: boolean
|
||||
|
||||
/**
|
||||
* 文件。
|
||||
*
|
||||
* @remarks
|
||||
* - Bit: `0000 0011 | 0000 0010 0000 0000` = `3-512`
|
||||
* - `elementType`: `3`
|
||||
*/
|
||||
file: boolean
|
||||
}
|
||||
|
||||
sendType: SendType
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 QQNT 消息类型。
|
||||
*/
|
||||
export const parseMsgTypes = (message: RedMessage): MsgTypes => ({
|
||||
chatType: message.chatType,
|
||||
|
||||
msgType: message.msgType,
|
||||
|
||||
subMsgType: {
|
||||
text: Boolean(message.subMsgType & (1 << 0)),
|
||||
pic: Boolean(message.subMsgType & (1 << 1)),
|
||||
face: Boolean(message.subMsgType & (1 << 4)),
|
||||
link: Boolean(message.subMsgType & (1 << 7)),
|
||||
multiForward: Boolean(message.subMsgType & (1 << 3)),
|
||||
reply:
|
||||
message.msgType === MsgType.WithRecords &&
|
||||
Boolean(message.subMsgType & (1 << 5)),
|
||||
marketFace:
|
||||
message.msgType == MsgType.Vaule17 &&
|
||||
Boolean(message.subMsgType & (1 << 3)),
|
||||
file:
|
||||
message.msgType === MsgType.Value3 &&
|
||||
Boolean(message.subMsgType & (1 << 9)),
|
||||
},
|
||||
|
||||
sendType: message.sendType,
|
||||
})
|
Loading…
Reference in New Issue
Block a user