LLOneBot/src/onebot11/constructor.ts
2024-04-30 11:28:24 +08:00

553 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fastXmlParser, { XMLParser } from 'fast-xml-parser'
import {
OB11Group,
OB11GroupMember,
OB11GroupMemberRole,
OB11Message,
OB11MessageData,
OB11MessageDataType,
OB11User,
OB11UserSex,
} from './types'
import {
AtType,
ChatType,
FaceIndex,
GrayTipElementSubType,
Group,
GroupMember,
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT,
RawMessage,
SelfInfo,
Sex,
TipGroupElementType,
User,
VideoElement,
} from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, groups, selfInfo, tempGroupCodeMap } from '../common/data'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQFileApi } from '../ntqqapi/api/file'
import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log'
import { sleep } from '../common/utils/helper'
import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { NTQQGroupApi } from '../ntqqapi/api'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
let lastRKeyUpdateTime = 0
export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> {
let config = getConfigUtil().getConfig()
const {
enableLocalFile2Url,
ob11: { messagePostFormat },
} = config
const message_type = msg.chatType == ChatType.group ? 'group' : 'private'
const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin),
user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId,
real_id: msg.msgShortId,
message_type: msg.chatType == ChatType.group ? 'group' : 'private',
sender: {
user_id: parseInt(msg.senderUin),
nickname: msg.sendNickName,
card: msg.sendMemberName || '',
},
raw_message: '',
font: 14,
sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
}
if (msg.chatType == ChatType.group) {
resMsg.sub_type = 'normal' // 这里go-cqhttp是group而onebot11标准是normal, 蛋疼
resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin)
if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
}
} else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin)
if (friend) {
resMsg.sender.nickname = friend.nick
}
} else if (msg.chatType == ChatType.temp) {
resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin]
if (tempGroupCode) {
resMsg.group_id = parseInt(tempGroupCode)
}
}
for (let element of msg.elements) {
let message_data: OB11MessageData | any = {
data: {},
type: 'unknown',
}
if (element.textElement && element.textElement?.atType !== AtType.notAt) {
message_data['type'] = OB11MessageDataType.at
if (element.textElement.atType == AtType.atAll) {
// message_data["data"]["mention"] = "all"
message_data['data']['qq'] = 'all'
} else {
let atUid = element.textElement.atNtUid
let atQQ = element.textElement.atUid
if (!atQQ || atQQ === '0') {
const atMember = await getGroupMember(msg.peerUin, atUid)
if (atMember) {
atQQ = atMember.uin
}
}
if (atQQ) {
// message_data["data"]["mention"] = atQQ
message_data['data']['qq'] = atQQ
}
}
} else if (element.textElement) {
message_data['type'] = 'text'
let text = element.textElement.content
if (!text.trim()) {
continue
}
message_data['data']['text'] = text
} else if (element.replyElement) {
message_data['type'] = 'reply'
// log("收到回复消息", element.replyElement.replayMsgSeq)
try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) {
message_data['data']['id'] = replyMsg.msgShortId.toString()
} else {
continue
}
} catch (e) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
}
} else if (element.picElement) {
message_data['type'] = 'image'
// message_data["data"]["file"] = element.picElement.sourcePath
message_data['data']['file'] = element.picElement.fileName
// message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
message_data['data']['url'] = await NTQQFileApi.getImageUrl(msg)
// message_data["data"]["file_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize
dbUtil
.addFileCache(element.picElement.fileName, {
fileName: element.picElement.fileName,
filePath: element.picElement.sourcePath,
fileSize: element.picElement.fileSize.toString(),
url: message_data['data']['url'],
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(
msg.msgId,
msg.chatType,
msg.peerUid,
element.elementId,
element.picElement.thumbPath?.get(0) || '',
element.picElement.sourcePath,
)
},
})
.then()
// 不在自动下载图片
} else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data['type'] = ob11MessageDataType
message_data['data']['file'] = videoOrFileElement.fileName
message_data['data']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = videoOrFileElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize
dbUtil
.addFileCache(videoOrFileElement.fileUuid, {
msgId: msg.msgId,
fileName: videoOrFileElement.fileName,
filePath: videoOrFileElement.filePath,
fileSize: videoOrFileElement.fileSize,
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(
msg.msgId,
msg.chatType,
msg.peerUid,
element.elementId,
ob11MessageDataType == OB11MessageDataType.video
? (videoOrFileElement as VideoElement).thumbPath.get(0)
: null,
videoOrFileElement.filePath,
)
},
})
.then()
// 怎么拿到url呢
} else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName
message_data['data']['path'] = element.pttElement.filePath
// message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data['data']['file_size'] = element.pttElement.fileSize
dbUtil
.addFileCache(element.pttElement.fileName, {
fileName: element.pttElement.fileName,
filePath: element.pttElement.filePath,
fileSize: element.pttElement.fileSize,
})
.then()
// log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text);
// }).catch(err => {
// console.log("语音转文字失败", err);
// })
} else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json
message_data['data']['data'] = element.arkElement.bytesData
} else if (element.faceElement) {
const faceId = element.faceElement.faceIndex
if (faceId === FaceIndex.dice) {
message_data['type'] = OB11MessageDataType.dice
message_data['data']['result'] = element.faceElement.resultId
} else if (faceId === FaceIndex.RPS) {
message_data['type'] = OB11MessageDataType.RPS
message_data['data']['result'] = element.faceElement.resultId
} else {
message_data['type'] = OB11MessageDataType.face
message_data['data']['id'] = element.faceElement.faceIndex.toString()
}
} else if (element.marketFaceElement) {
message_data['type'] = OB11MessageDataType.mface
message_data['data']['text'] = element.marketFaceElement.faceName
const md5 = element.marketFaceElement.emojiId
// 取md5的前两位
const dir = md5.substring(0, 2)
// 获取组装url
// const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000`
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${md5}/raw300.gif`
message_data['data']['url'] = url
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId)
message_data['data']['key'] = element.marketFaceElement.key
} else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown
message_data['data']['data'] = element.markdownElement.content
} else if (element.multiForwardMsgElement) {
message_data['type'] = OB11MessageDataType.forward
message_data['data']['id'] = msg.msgId
}
if (message_data.type !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode
} else (resMsg.message as OB11MessageData[]).push(message_data)
resMsg.raw_message += cqCode
}
}
resMsg.raw_message = resMsg.raw_message.trim()
return resMsg
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> {
if (msg.chatType !== ChatType.group) {
return
}
if (msg.senderUin) {
let member = await getGroupMember(msg.peerUid, msg.senderUin)
if (member && member.cardName !== msg.sendMemberName) {
const event = new OB11GroupCardEvent(
parseInt(msg.peerUid),
parseInt(msg.senderUin),
msg.sendMemberName,
member.cardName,
)
member.cardName = msg.sendMemberName
return event
}
}
// log("group msg", msg);
for (let element of msg.elements) {
const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement
if (groupElement) {
// log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) {
log('收到群成员增加消息', groupElement)
await sleep(1000)
const member = await getGroupMember(msg.peerUid, groupElement.memberUid)
let memberUin = member?.uin
if (!memberUin) {
memberUin = (await NTQQUserApi.getUserDetailInfo(groupElement.memberUid)).uin
}
// log("获取新群成员QQ", memberUin)
const adminMember = await getGroupMember(msg.peerUid, groupElement.adminUid)
// log("获取同意新成员入群的管理员", adminMember)
if (memberUin) {
const operatorUin = adminMember?.uin || memberUin
let event = new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin))
// log("构造群增加事件", event)
return event
}
} else if (groupElement.type === TipGroupElementType.ban) {
log('收到群群员禁言提示', groupElement)
const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid
let memberUin: string = ''
let duration = parseInt(groupElement.shutUp.duration)
let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban'
if (memberUid) {
memberUin =
(await getGroupMember(msg.peerUid, memberUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(memberUid))?.uin
} else {
memberUin = '0' // 0表示全员禁言
if (duration > 0) {
duration = -1
}
}
const adminUin =
(await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid))?.uin
if (memberUin && adminUin) {
return new OB11GroupBanEvent(
parseInt(msg.peerUid),
parseInt(memberUin),
parseInt(adminUin),
duration,
sub_type,
)
}
} else if (groupElement.type == TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then()
try {
const adminUin =
(await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
if (adminUin) {
return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(selfInfo.uin),
parseInt(adminUin),
'kick_me',
)
}
} catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave')
}
}
} else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {
id: element.fileElement.fileUuid,
name: element.fileElement.fileName,
size: parseInt(element.fileElement.fileSize),
busid: element.fileElement.fileBizId || 0,
})
}
if (grayTipElement) {
const xmlElement = grayTipElement.xmlElement
if (xmlElement?.templId === '10382') {
// 表情回应消息
// "content":
// "<gtip align=\"center\">
// <qq uin=\"u_snYxnEfja-Po_\" col=\"3\" jp=\"3794\"/>
// <nor txt=\"回应了你的\"/>
// <url jp= \"\" msgseq=\"74711\" col=\"3\" txt=\"消息:\"/>
// <face type=\"1\" id=\"76\"> </face>
// </gtip>",
const emojiLikeData = new fastXmlParser.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
}).parse(xmlElement.content)
log('收到表情回应我的消息', emojiLikeData)
try {
const senderUin = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id
const msg = await dbUtil.getMsgBySeqId(msgSeq)
if (!msg) {
return
}
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId, [
{
emoji_id: emojiId,
count: 1,
},
])
} catch (e) {
log('解析表情回应消息失败', e.stack)
}
}
if (
grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER &&
xmlElement?.templId == '10179'
) {
log('收到新人被邀请进群消息', grayTipElement)
if (xmlElement?.content) {
const regex = /jp="(\d+)"/g
let matches = []
let match = null
while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1])
}
// log("新人进群匹配到的QQ号", matches)
if (matches.length === 2) {
const [inviter, invitee] = matches
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite')
}
}
} else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
/*
{
align: 'center',
items: [
{ txt: '恭喜', type: 'nor' },
{
col: '3',
jp: '5',
param: ["QQ号"],
txt: '林雨辰',
type: 'url'
},
{ txt: '获得群主授予的', type: 'nor' },
{
col: '3',
jp: '',
txt: '好好好',
type: 'url'
},
{ txt: '头衔', type: 'nor' }
]
}
* */
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
log('收到群成员新头衔消息', json)
getGroupMember(msg.peerUid, memberUin).then((member) => {
member.memberSpecialTitle = title
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
}
}
}
}
static friend(friend: User): OB11User {
return {
user_id: parseInt(friend.uin),
nickname: friend.nick,
remark: friend.remark,
sex: OB11Constructor.sex(friend.sex),
level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0,
}
}
static selfInfo(selfInfo: SelfInfo): OB11User {
return {
user_id: parseInt(selfInfo.uin),
nickname: selfInfo.nick,
}
}
static friends(friends: User[]): OB11User[] {
return friends.map(OB11Constructor.friend)
}
static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return {
4: OB11GroupMemberRole.owner,
3: OB11GroupMemberRole.admin,
2: OB11GroupMemberRole.member,
}[role]
}
static sex(sex: Sex): OB11UserSex {
const sexMap = {
[Sex.male]: OB11UserSex.male,
[Sex.female]: OB11UserSex.female,
[Sex.unknown]: OB11UserSex.unknown,
}
return sexMap[sex] || OB11UserSex.unknown
}
static groupMember(group_id: string, member: GroupMember): OB11GroupMember {
return {
group_id: parseInt(group_id),
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName,
sex: OB11Constructor.sex(member.sex),
age: 0,
area: '',
level: 0,
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取
title_expire_time: 0,
unfriendly: false,
card_changeable: true,
is_robot: member.isRobot,
shut_up_timestamp: member.shutUpTime,
role: OB11Constructor.groupMemberRole(member.role),
title: member.memberSpecialTitle || '',
}
}
static stranger(user: User): OB11User {
return {
...user,
user_id: parseInt(user.uin),
nickname: user.nick,
sex: OB11Constructor.sex(user.sex),
age: 0,
qid: user.qid,
login_days: 0,
level: (user.qqLevel && calcQQLevel(user.qqLevel)) || 0,
}
}
static groupMembers(group: Group): OB11GroupMember[] {
log('construct ob11 group members', group)
return group.members.map((m) => OB11Constructor.groupMember(group.groupCode, m))
}
static group(group: Group): OB11Group {
return {
group_id: parseInt(group.groupCode),
group_name: group.groupName,
member_count: group.memberCount,
max_member_count: group.maxMember,
}
}
static groups(groups: Group[]): OB11Group[] {
return groups.map(OB11Constructor.group)
}
}