SnowCat 雪猫底层全源码:NapCat 4.18.28 底座 + SnowLuma 能力移植 + V8 通道适配 9.9.37
26 包 monorepo(TS 全源码): - snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包 - snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线 - snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI) - snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports) - snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时 - 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏) - 工具件: 解包器/启动器/便携制作器/doctor GitHub 关联已全量指向 DandanLLab/SnowCatQQ
This commit is contained in:
commit
b9655a5ab1
1048 files changed
+130851
No files matched your search
@@ -0,0 +1,228 @@
|
||||
import { NapCatOneBot11Adapter } from '@/snowcat-onebot/index';
|
||||
import { encodeSilk } from '@/snowcat-core/helper/audio';
|
||||
import { FFmpegService } from '@/snowcat-core/helper/ffmpeg/ffmpeg';
|
||||
import { calculateFileMD5 } from 'snowcat-common/src/file';
|
||||
import { ElementType, SnowCatCore, NTVideoType, PicElement, PicSubType, SendFileElement, SendPicElement, SendPttElement, SendVideoElement } from 'snowcat-core';
|
||||
import { getFileTypeForSendType } from 'snowcat-core/helper/msg';
|
||||
import { imageSizeFallBack } from 'snowcat-image-size';
|
||||
import { SendMessageContext } from './msg';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import pathLib from 'node:path';
|
||||
import fsPromises from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { defaultVideoThumbB64 } from '@/snowcat-core/helper/ffmpeg/video';
|
||||
|
||||
const videoFormatByExtension: Partial<Record<string, NTVideoType>> = {
|
||||
afs: NTVideoType.VIDEO_FORMAT_AFS,
|
||||
avi: NTVideoType.VIDEO_FORMAT_AVI,
|
||||
mkv: NTVideoType.VIDEO_FORMAT_MKV,
|
||||
mod: NTVideoType.VIDEO_FORMAT_MOD,
|
||||
mov: NTVideoType.VIDEO_FORMAT_MOV,
|
||||
mp4: NTVideoType.VIDEO_FORMAT_MP4,
|
||||
mts: NTVideoType.VIDEO_FORMAT_MTS,
|
||||
rm: NTVideoType.VIDEO_FORMAT_RM,
|
||||
rmvb: NTVideoType.VIDEO_FORMAT_RMVB,
|
||||
ts: NTVideoType.VIDEO_FORMAT_TS,
|
||||
wmv: NTVideoType.VIDEO_FORMAT_WMV,
|
||||
};
|
||||
|
||||
export class OneBotFileApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: SnowCatCore;
|
||||
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async createValidSendFileElement (context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = '', uploadGroupFile: boolean = false, _isLocal: boolean = false): Promise<SendFileElement> {
|
||||
const {
|
||||
fileName: _fileName,
|
||||
path,
|
||||
fileSize,
|
||||
} = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE, 0, uploadGroupFile);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (uploadGroupFile) {
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
fileElement: {
|
||||
fileName: fileName || _fileName,
|
||||
folderId,
|
||||
filePath: path,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPicElement (context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0, _isLocal: boolean = false): Promise<SendPicElement> {
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const imageSize = await imageSizeFallBack(picPath);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
picElement: {
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
picWidth: imageSize.width,
|
||||
picHeight: imageSize.height,
|
||||
fileName,
|
||||
sourcePath: path,
|
||||
original: true,
|
||||
picType: await getFileTypeForSendType(picPath),
|
||||
picSubType: subType,
|
||||
fileUuid: '',
|
||||
fileSubId: '',
|
||||
thumbFileSize: 0,
|
||||
summary,
|
||||
} as PicElement,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement (context: SendMessageContext, filePath: string, fileName: string = '', _diyThumbPath: string = '', isLocal: boolean = false): Promise<SendVideoElement> {
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
time: 15,
|
||||
format: 'mp4',
|
||||
size: 0,
|
||||
filePath,
|
||||
};
|
||||
let fileExt = 'mp4';
|
||||
try {
|
||||
const tempExt = (await fileTypeFromFile(filePath))?.ext;
|
||||
if (tempExt) fileExt = tempExt;
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('获取文件类型失败', e);
|
||||
}
|
||||
const currentExt = pathLib.extname(filePath).slice(1).toLowerCase();
|
||||
const needsExtCopy = currentExt !== fileExt.toLowerCase();
|
||||
if (needsExtCopy) {
|
||||
if (isLocal) {
|
||||
// 本地文件缺少正确扩展名:拷贝到临时目录,避免污染用户目录
|
||||
const tempFilePath = pathLib.join(this.core.SnowCatTempPath, `${randomUUID()}.${fileExt}`);
|
||||
fs.copyFileSync(filePath, tempFilePath);
|
||||
context.deleteAfterSentFiles.push(tempFilePath);
|
||||
filePath = tempFilePath;
|
||||
} else {
|
||||
// 临时文件缺少正确扩展名:在原位置加扩展名
|
||||
const newFilePath = `${filePath}.${fileExt}`;
|
||||
fs.copyFileSync(filePath, newFilePath);
|
||||
context.deleteAfterSentFiles.push(newFilePath);
|
||||
filePath = newFilePath;
|
||||
}
|
||||
}
|
||||
// 扩展名已匹配时:本地文件直接用原始路径(零拷贝),临时文件也无需额外操作
|
||||
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
|
||||
fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true });
|
||||
const usePacketVideoMetadata = context.usePacketVideoMetadata === true;
|
||||
const thumbExtension = usePacketVideoMetadata ? 'jpg' : 'png';
|
||||
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.${thumbExtension}`);
|
||||
try {
|
||||
if (usePacketVideoMetadata) {
|
||||
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
|
||||
} else {
|
||||
videoInfo = await FFmpegService.getVideoInfo(filePath, '');
|
||||
await FFmpegService.extractLegacyVideoThumbnail(filePath, thumbPath);
|
||||
}
|
||||
if (!fs.existsSync(thumbPath)) {
|
||||
this.core.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
|
||||
throw new Error('获取视频缩略图失败');
|
||||
}
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('获取视频信息失败', e);
|
||||
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||
}
|
||||
if (_diyThumbPath) {
|
||||
try {
|
||||
await this.core.apis.FileApi.copyFile(_diyThumbPath, thumbPath);
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('复制自定义缩略图失败', e);
|
||||
}
|
||||
}
|
||||
context.deleteAfterSentFiles.push(thumbPath);
|
||||
const thumbSize = (await fsPromises.stat(thumbPath)).size;
|
||||
const thumbMd5 = await calculateFileMD5(thumbPath);
|
||||
|
||||
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`;
|
||||
const videoMetadata = usePacketVideoMetadata
|
||||
? {
|
||||
fileTime: Math.max(0, Math.round(videoInfo.time)),
|
||||
fileFormat: videoFormatByExtension[fileExt.toLowerCase()] ?? NTVideoType.VIDEO_FORMAT_MP4,
|
||||
fileWidth: videoInfo.width,
|
||||
fileHeight: videoInfo.height,
|
||||
}
|
||||
: {
|
||||
fileTime: videoInfo.time,
|
||||
};
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
videoElement: {
|
||||
fileName: uploadName,
|
||||
filePath: path,
|
||||
videoMd5: md5,
|
||||
thumbMd5,
|
||||
...videoMetadata,
|
||||
thumbPath: new Map([[0, thumbPath]]),
|
||||
thumbSize,
|
||||
thumbWidth: videoInfo.width,
|
||||
thumbHeight: videoInfo.height,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPttElement (_context: SendMessageContext, pttPath: string): Promise<SendPttElement> {
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.SnowCatTempPath, this.core.context.logger);
|
||||
if (!silkPath) {
|
||||
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
||||
}
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(silkPath, ElementType.PTT);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath).then().catch((e) => this.core.context.logger.logError('删除临时文件失败', e));
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
elementId: '',
|
||||
pttElement: {
|
||||
fileName,
|
||||
filePath: path,
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
duration: duration ?? 1,
|
||||
formatType: 1,
|
||||
voiceType: 1,
|
||||
voiceChangeType: 0,
|
||||
canConvert2Text: true,
|
||||
waveAmplitudes: [
|
||||
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
|
||||
],
|
||||
fileSubId: '',
|
||||
playState: 1,
|
||||
autoConvertText: 0,
|
||||
storeID: 0,
|
||||
otherBusinessInfo: {
|
||||
aiVoiceType: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { GrayTipElement, SnowCatCore } from 'snowcat-core';
|
||||
import { NapCatOneBot11Adapter } from '@/snowcat-onebot/index';
|
||||
import { OB11FriendPokeEvent } from '@/snowcat-onebot/event/notice/OB11PokeEvent';
|
||||
|
||||
export class OneBotFriendApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: SnowCatCore;
|
||||
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
// 使用前预先判断 busiId 1061
|
||||
async parsePrivatePokeEvent (grayTipElement: GrayTipElement, uin: number) {
|
||||
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
const pokedetail: Array<{ uid: string; }> = json.items;
|
||||
// 筛选item带有uid的元素
|
||||
const poke_uid = pokedetail.filter(item => item.uid);
|
||||
if (poke_uid.length === 2 && poke_uid[0]?.uid && poke_uid[1]?.uid) {
|
||||
return new OB11FriendPokeEvent(
|
||||
this.core,
|
||||
uin,
|
||||
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid))),
|
||||
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid))),
|
||||
pokedetail
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import {
|
||||
ChatType,
|
||||
FileElement,
|
||||
GrayTipElement,
|
||||
InstanceContext,
|
||||
JsonGrayBusiId,
|
||||
MessageElement,
|
||||
SnowCatCore,
|
||||
NTGrayTipElementSubTypeV2,
|
||||
RawMessage,
|
||||
TipGroupElement,
|
||||
TipGroupElementType,
|
||||
} from 'snowcat-core';
|
||||
import { OB11GroupBanEvent } from '@/snowcat-onebot/event/notice/OB11GroupBanEvent';
|
||||
import fastXmlParser from 'fast-xml-parser';
|
||||
import { OB11GroupMsgEmojiLikeEvent } from '@/snowcat-onebot/event/notice/OB11MsgEmojiLikeEvent';
|
||||
import { MessageUnique } from 'snowcat-common/src/message-unique';
|
||||
import { OB11GroupCardEvent } from '@/snowcat-onebot/event/notice/OB11GroupCardEvent';
|
||||
import { OB11GroupPokeEvent } from '@/snowcat-onebot/event/notice/OB11PokeEvent';
|
||||
import { OB11GroupEssenceEvent } from '@/snowcat-onebot/event/notice/OB11GroupEssenceEvent';
|
||||
import { OB11GroupTitleEvent } from '@/snowcat-onebot/event/notice/OB11GroupTitleEvent';
|
||||
import { OB11GroupGrayTipEvent } from '@/snowcat-onebot/event/notice/OB11GroupGrayTipEvent';
|
||||
import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNoticeEvent';
|
||||
import { OB11GroupNameEvent } from '../event/notice/OB11GroupNameEvent';
|
||||
import { FileNapCatOneBotUUID } from 'snowcat-common/src/file-uuid';
|
||||
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
|
||||
import { NapProtoMsg } from 'snowcat-protobuf';
|
||||
import { GroupReactNotify, PushMsg } from 'snowcat-core/packet/transformer/proto';
|
||||
import { NapCatOneBot11Adapter } from '..';
|
||||
|
||||
export class OneBotGroupApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: SnowCatCore;
|
||||
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async parseGroupBanEvent (GroupCode: string, grayTipElement: GrayTipElement) {
|
||||
const groupElement = grayTipElement?.groupElement;
|
||||
if (!groupElement?.shutUp) return undefined;
|
||||
const memberUid = groupElement.shutUp.member.uid;
|
||||
const adminUid = groupElement.shutUp.admin.uid;
|
||||
let memberUin: string;
|
||||
let duration = parseInt(groupElement.shutUp.duration);
|
||||
const subType: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban';
|
||||
if (memberUid) {
|
||||
memberUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, memberUid))?.uin ?? '';
|
||||
} else {
|
||||
memberUin = '0'; // 0表示全员禁言
|
||||
if (duration > 0) {
|
||||
duration = -1;
|
||||
}
|
||||
}
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(GroupCode, memberUid);
|
||||
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin;
|
||||
if (memberUin && adminUin) {
|
||||
return new OB11GroupBanEvent(
|
||||
this.core,
|
||||
parseInt(GroupCode),
|
||||
parseInt(memberUin),
|
||||
parseInt(adminUin),
|
||||
duration,
|
||||
subType
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async parseGroupEmojiLikeEventByGrayTip (
|
||||
groupCode: string,
|
||||
grayTipElement: GrayTipElement
|
||||
) {
|
||||
if (this.core.apis.PacketApi.packetStatus === true) {
|
||||
return;
|
||||
// Raw包解析支持时禁用NT解析
|
||||
}
|
||||
const emojiLikeData = new fastXmlParser.XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '',
|
||||
}).parse(grayTipElement.xmlElement.content);
|
||||
this.core.context.logger.logDebug('收到表情回应我的消息', emojiLikeData);
|
||||
const senderUin = emojiLikeData.gtip.qq.jp;
|
||||
const msgSeq = emojiLikeData.gtip.url.msgseq;
|
||||
const emojiId = emojiLikeData.gtip.face.id;
|
||||
return await this.createGroupEmojiLikeEvent(groupCode, senderUin, msgSeq, emojiId, true, 1);
|
||||
}
|
||||
|
||||
async createGroupEmojiLikeEvent (
|
||||
groupCode: string,
|
||||
senderUin: string,
|
||||
msgSeq: string,
|
||||
emojiId: string,
|
||||
isAdd: boolean = true,
|
||||
count: number = 1
|
||||
) {
|
||||
const peer = {
|
||||
chatType: ChatType.KCHATTYPEGROUP,
|
||||
guildId: '',
|
||||
peerUid: groupCode,
|
||||
};
|
||||
const replyMsgList = (await this.core.apis.MsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList;
|
||||
if (replyMsgList.length < 1) {
|
||||
return;
|
||||
}
|
||||
const replyMsg = replyMsgList[0];
|
||||
if (!replyMsg) {
|
||||
this.core.context.logger.logError('解析表情回应消息失败: 未找到回应消息');
|
||||
return undefined;
|
||||
}
|
||||
return new OB11GroupMsgEmojiLikeEvent(
|
||||
this.core,
|
||||
parseInt(groupCode),
|
||||
parseInt(senderUin),
|
||||
MessageUnique.createUniqueMsgId({ chatType: ChatType.KCHATTYPEGROUP, guildId: '', peerUid: groupCode }, replyMsg.msgId),
|
||||
[{
|
||||
emoji_id: emojiId,
|
||||
count,
|
||||
}],
|
||||
isAdd,
|
||||
msgSeq
|
||||
);
|
||||
}
|
||||
|
||||
async parseCardChangedEvent (msg: RawMessage) {
|
||||
if (msg.senderUin && msg.senderUin !== '0') {
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
|
||||
if (member && member.cardName !== msg.sendMemberName) {
|
||||
const newCardName = msg.sendMemberName ?? '';
|
||||
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
|
||||
member.cardName = newCardName;
|
||||
return event;
|
||||
}
|
||||
if (member && member.nick !== msg.sendNickName) {
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async registerParseGroupReactEvent () {
|
||||
this.obContext.core.context.packetHandler.onCmd('trpc.msg.olpush.OlPushService.MsgPush', async (packet) => {
|
||||
const data = new NapProtoMsg(PushMsg).decode(Buffer.from(packet.hex_data, 'hex'));
|
||||
if (data.message.contentHead.type === 732 && data.message.contentHead.subType === 16) {
|
||||
const pbNotify = data.message.body?.msgContent?.slice(7);
|
||||
if (!pbNotify) {
|
||||
return;
|
||||
}
|
||||
// 开始解析Notify
|
||||
const notify = new NapProtoMsg(GroupReactNotify).decode(pbNotify);
|
||||
if ((notify.field13 ?? 0) === 35) {
|
||||
// Group React Notify
|
||||
const groupCode = notify.groupUin?.toString() ?? '';
|
||||
const operatorUid = notify.groupReactionData?.data?.data?.groupReactionDataContent?.operatorUid ?? '';
|
||||
const type = notify.groupReactionData?.data?.data?.groupReactionDataContent?.type ?? 0;
|
||||
const seq = notify.groupReactionData?.data?.data?.groupReactionTarget?.seq?.toString() ?? '';
|
||||
const code = notify.groupReactionData?.data?.data?.groupReactionDataContent?.code ?? '';
|
||||
// const count = notify.groupReactionData?.data?.data?.groupReactionDataContent?.count ?? 0;
|
||||
const senderUin = await this.core.apis.UserApi.getUinByUidV2(operatorUid);
|
||||
const event = await this.createGroupEmojiLikeEvent(
|
||||
groupCode,
|
||||
senderUin,
|
||||
seq,
|
||||
code,
|
||||
type === 1,
|
||||
1
|
||||
);
|
||||
if (event) {
|
||||
this.obContext.networkManager.emitEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async registerParseGroupReactEventByCore () {
|
||||
this.core.event.on('event:emoji_like', async (data) => {
|
||||
const event = await this.createGroupEmojiLikeEvent(
|
||||
data.groupId,
|
||||
data.senderUin,
|
||||
data.msgSeq,
|
||||
data.emojiId,
|
||||
data.isAdd,
|
||||
data.count
|
||||
);
|
||||
if (event) {
|
||||
this.obContext.networkManager.emitEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async parsePaiYiPai (msg: RawMessage, jsonStr: string) {
|
||||
const json = JSON.parse(jsonStr);
|
||||
// 判断业务类型
|
||||
// Poke事件
|
||||
const pokedetail: Array<{ uid: string; }> = json.items;
|
||||
// 筛选item带有uid的元素
|
||||
const poke_uid = pokedetail.filter(item => item.uid);
|
||||
if (poke_uid.length === 2 && poke_uid[0]?.uid && poke_uid[1]?.uid) {
|
||||
return new OB11GroupPokeEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid),
|
||||
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid),
|
||||
pokedetail
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async parseOtherJsonEvent (msg: RawMessage, jsonGrayTipElement: GrayTipElement['jsonGrayTipElement'], context: InstanceContext) {
|
||||
let json: { items?: { txt?: string; param?: string[] }[] };
|
||||
try {
|
||||
json = JSON.parse(jsonGrayTipElement.jsonStr);
|
||||
} catch (e) {
|
||||
context.logger.logWarn('灰条消息JSON解析失败', jsonGrayTipElement.jsonStr, e);
|
||||
return undefined;
|
||||
}
|
||||
const type = json.items?.[json.items.length - 1]?.txt;
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
|
||||
if (type === '头衔') {
|
||||
const memberUin = json.items?.[1]?.param?.[0];
|
||||
const title = json.items?.[3]?.txt;
|
||||
context.logger.logDebug('收到群成员新头衔消息', json);
|
||||
if (memberUin == null || title == null) {
|
||||
context.logger.logWarn('收到格式异常的群成员新头衔灰条消息', json);
|
||||
return undefined;
|
||||
}
|
||||
return new OB11GroupTitleEvent(
|
||||
this.core,
|
||||
+msg.peerUid,
|
||||
+memberUin,
|
||||
title
|
||||
);
|
||||
} else if (type === '移出') {
|
||||
context.logger.logDebug('收到机器人被踢消息', json);
|
||||
} else {
|
||||
context.logger.logWarn('收到未知的灰条消息', json);
|
||||
|
||||
// 如果有真实发送者(非0),生成事件上报,可用于检测和撤回伪造灰条
|
||||
const senderUin = Number(msg.senderUin) || 0;
|
||||
if (senderUin !== 0) {
|
||||
const peer = { chatType: ChatType.KCHATTYPEGROUP, guildId: '', peerUid: msg.peerUid };
|
||||
const messageId = MessageUnique.createUniqueMsgId(peer, msg.msgId);
|
||||
return new OB11GroupGrayTipEvent(
|
||||
this.core,
|
||||
+msg.peerUin,
|
||||
senderUin,
|
||||
messageId,
|
||||
jsonGrayTipElement.busiId,
|
||||
jsonGrayTipElement.jsonStr,
|
||||
{
|
||||
msgSeq: msg.msgSeq,
|
||||
msgTime: msg.msgTime,
|
||||
msgId: msg.msgId,
|
||||
json,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async parseEssenceMsg (msg: RawMessage, jsonStr: string) {
|
||||
const json = JSON.parse(jsonStr);
|
||||
const searchParams = new URL(json.items[0].jp).searchParams;
|
||||
const msgSeq = searchParams.get('msgSeq')!;
|
||||
const Group = searchParams.get('groupCode');
|
||||
if (!Group) return undefined;
|
||||
// const businessId = searchParams.get('businessid');
|
||||
const Peer = {
|
||||
guildId: '',
|
||||
chatType: ChatType.KCHATTYPEGROUP,
|
||||
peerUid: Group,
|
||||
};
|
||||
const msgData = await this.core.apis.MsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
|
||||
const msgList = (await this.core.apis.WebApi.getGroupEssenceMsgAll(Group)).flatMap((e) => e.data.msg_list);
|
||||
const realMsg = msgList.find((e) => e.msg_seq.toString() === msgSeq);
|
||||
if (msgData.msgList[0]) {
|
||||
return new OB11GroupEssenceEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!,
|
||||
parseInt(msgData.msgList[0].senderUin ?? await this.core.apis.UserApi.getUinByUidV2(msgData.msgList[0].senderUid)),
|
||||
parseInt(realMsg?.add_digest_uin ?? '0')
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
// 获取MsgSeq+Peer可获取具体消息
|
||||
}
|
||||
|
||||
async parseGroupUploadFileEvene (msg: RawMessage, element: FileElement, elementWrapper: MessageElement) {
|
||||
return new OB11GroupUploadNoticeEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid), parseInt(msg.senderUin || ''),
|
||||
{
|
||||
id: FileNapCatOneBotUUID.encode({
|
||||
chatType: ChatType.KCHATTYPEGROUP,
|
||||
peerUid: msg.peerUid,
|
||||
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, element.fileMd5 ?? element.fileUuid),
|
||||
name: element.fileName,
|
||||
size: parseInt(element.fileSize),
|
||||
busid: element.fileBizId ?? 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async parseGroupElement (msg: RawMessage, element: TipGroupElement, elementWrapper: GrayTipElement) {
|
||||
if (element.type === TipGroupElementType.KGROUPNAMEMODIFIED) {
|
||||
this.core.context.logger.logDebug('收到群名称变更事件', element);
|
||||
return new OB11GroupNameEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
parseInt(await this.core.apis.UserApi.getUinByUidV2(element.memberUid)),
|
||||
element.groupName
|
||||
);
|
||||
} else if (element.type === TipGroupElementType.KSHUTUP) {
|
||||
const event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
|
||||
return event;
|
||||
} else if (element.type === TipGroupElementType.KMEMBERADD) {
|
||||
// 自己的通知 协议推送为type->85 在这里实现为了避免邀请出现问题
|
||||
if (element.memberUid === this.core.selfInfo.uid) {
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
+this.core.selfInfo.uin,
|
||||
element.adminUid ? +await this.core.apis.UserApi.getUinByUidV2(element.adminUid) : 0,
|
||||
'approve'
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async parseSelfInviteEvent (msg: RawMessage, inviterUin: string, inviteeUin: string) {
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
+inviteeUin,
|
||||
+inviterUin,
|
||||
'invite'
|
||||
);
|
||||
}
|
||||
|
||||
async parse51TypeEvent (msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
// 神经腾讯 没了妈妈想出来的
|
||||
// Warn 下面存在高并发危险
|
||||
if (grayTipElement.jsonGrayTipElement.jsonStr) {
|
||||
const json: {
|
||||
align: string,
|
||||
items: Array<{ txt: string, type: string; }>;
|
||||
} = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
if (json.items.length === 1 && json.items[0]?.txt.endsWith('加入群')) {
|
||||
const old_members = structuredClone(this.core.apis.GroupApi.groupMemberCache.get(msg.peerUid));
|
||||
if (!old_members) return;
|
||||
const new_members_map = await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
|
||||
if (!new_members_map) return;
|
||||
const new_members = Array.from(new_members_map.values());
|
||||
// 对比members查找新成员
|
||||
const new_member = new_members.find((member) => old_members.get(member.uid) === undefined);
|
||||
if (!new_member) return;
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
+msg.peerUid,
|
||||
+new_member.uin,
|
||||
0,
|
||||
'invite'
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async parseGrayTipElement (msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
|
||||
// 解析群组事件 由sysmsg解析
|
||||
return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
|
||||
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
|
||||
// 筛选自身入群情况
|
||||
// if (grayTipElement.xmlElement.busiId === '10145') {
|
||||
// const inviteData = new fastXmlParser.XMLParser({
|
||||
// ignoreAttributes: false,
|
||||
// attributeNamePrefix: '',
|
||||
// }).parse(grayTipElement.xmlElement.content);
|
||||
|
||||
// const inviterUin: string = inviteData.gtip.qq[0].jp;
|
||||
// const inviteeUin: string = inviteData.gtip.qq[1].jp;
|
||||
// //刷新群缓存
|
||||
// if (inviteeUin === this.core.selfInfo.uin) {
|
||||
// this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
|
||||
// return this.parseSelfInviteEvent(msg, inviterUin, inviteeUin);
|
||||
// }
|
||||
// } else
|
||||
if (grayTipElement.xmlElement?.templId === '10382') {
|
||||
return await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, grayTipElement);
|
||||
} else {
|
||||
// return await this.obContext.apis.GroupApi.parseGroupIncreaseEvent(msg.peerUid, grayTipElement);
|
||||
}
|
||||
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
||||
// 解析json事件 busiId好像是string类型的?
|
||||
if (grayTipElement.jsonGrayTipElement.busiId === '1061') {
|
||||
return await this.parsePaiYiPai(msg, grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
} else if (grayTipElement.jsonGrayTipElement.busiId === JsonGrayBusiId.AIO_GROUP_ESSENCE_MSG_TIP.toString()) {
|
||||
return await this.parseEssenceMsg(msg, grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
} else if (grayTipElement.jsonGrayTipElement.busiId === '51') {
|
||||
// 51是什么?{"align":"center","items":[{"txt":"下一秒起床通过王者荣耀加入群","type":"nor"}]
|
||||
return await this.parse51TypeEvent(msg, grayTipElement);
|
||||
} else {
|
||||
console.log('Unknown JSON event:', grayTipElement.jsonGrayTipElement, JSON.stringify(grayTipElement));
|
||||
return await this.parseOtherJsonEvent(msg, grayTipElement.jsonGrayTipElement, this.core.context);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './friend';
|
||||
export * from './group';
|
||||
export * from './user';
|
||||
export * from './msg';
|
||||
export * from './quick-action';
|
||||
export * from './file';
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
NapCatOneBot11Adapter,
|
||||
OB11Message,
|
||||
OB11MessageAt,
|
||||
OB11MessageData,
|
||||
OB11MessageReply,
|
||||
QuickAction,
|
||||
QuickActionEvent,
|
||||
QuickActionFriendRequest,
|
||||
QuickActionGroupMessage,
|
||||
QuickActionGroupRequest,
|
||||
} from '@/snowcat-onebot/index';
|
||||
import { NTGroupRequestOperateTypes, type SnowCatCore, type Peer } from 'snowcat-core';
|
||||
import type { OB11FriendRequestEvent } from '@/snowcat-onebot/event/request/OB11FriendRequest';
|
||||
import type { OB11GroupRequestEvent } from '@/snowcat-onebot/event/request/OB11GroupRequest';
|
||||
|
||||
import { ContextMode, createContext, normalize } from '@/snowcat-onebot/action/msg/SendMsg';
|
||||
import { isNull } from 'snowcat-common/src/helper';
|
||||
|
||||
export class OneBotQuickActionApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: SnowCatCore;
|
||||
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async handleQuickOperation (eventContext: QuickActionEvent, quickAction: QuickAction) {
|
||||
if (eventContext.post_type === 'message') {
|
||||
await this.handleMsg(eventContext as OB11Message, quickAction)
|
||||
.catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
if (eventContext.post_type === 'request') {
|
||||
const friendRequest = eventContext as unknown as OB11FriendRequestEvent;
|
||||
const groupRequest = eventContext as unknown as OB11GroupRequestEvent;
|
||||
if ((friendRequest).request_type === 'friend') {
|
||||
await this.handleFriendRequest(friendRequest, quickAction)
|
||||
.catch(e => this.core.context.logger.logError(e));
|
||||
} else if (groupRequest.request_type === 'group') {
|
||||
await this.handleGroupRequest(groupRequest, quickAction)
|
||||
.catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleMsg (msg: OB11Message, quickAction: QuickAction) {
|
||||
const reply = quickAction.reply;
|
||||
const peerContextMode = msg.message_type === 'private' ? ContextMode.Private : ContextMode.Group;
|
||||
const peer: Peer = await createContext(this.core, {
|
||||
group_id: msg.group_id?.toString(),
|
||||
user_id: msg.user_id?.toString(),
|
||||
}, peerContextMode);
|
||||
|
||||
if (reply) {
|
||||
// let group: Group | undefined;
|
||||
let replyMessage: OB11MessageData[] = [];
|
||||
|
||||
if (msg.message_type === 'group') {
|
||||
// group = await core.apis.GroupApi.getGroup(msg.group_id!.toString());
|
||||
replyMessage.push({
|
||||
type: 'reply',
|
||||
data: {
|
||||
id: msg.message_id.toString(),
|
||||
},
|
||||
} as OB11MessageReply);
|
||||
if ((quickAction as QuickActionGroupMessage).at_sender) {
|
||||
replyMessage.push({
|
||||
type: 'at',
|
||||
data: {
|
||||
qq: msg.user_id.toString(),
|
||||
},
|
||||
} as OB11MessageAt);
|
||||
}
|
||||
}
|
||||
replyMessage = replyMessage.concat(normalize(reply, quickAction.auto_escape));
|
||||
const {
|
||||
sendElements,
|
||||
deleteAfterSentFiles,
|
||||
} = await this.obContext.apis.MsgApi.createSendElements(replyMessage, peer);
|
||||
this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles).then().catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
|
||||
async findNotify (flag: string) {
|
||||
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq === flag);
|
||||
if (!notify) {
|
||||
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq === flag);
|
||||
return { doubt: true, notify };
|
||||
}
|
||||
return { doubt: false, notify };
|
||||
}
|
||||
|
||||
async handleGroupRequest (request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
|
||||
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
|
||||
const { doubt, notify } = invite_notify ? { doubt: false, notify: invite_notify } : await this.findNotify(request.flag);
|
||||
|
||||
if (!isNull(quickAction.approve) && notify) {
|
||||
this.core.apis.GroupApi.handleGroupRequest(
|
||||
doubt,
|
||||
notify,
|
||||
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
|
||||
quickAction.reason
|
||||
).catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
|
||||
async handleFriendRequest (request: OB11FriendRequestEvent, quickAction: QuickActionFriendRequest) {
|
||||
const notify = (await this.core.apis.FriendApi.getBuddyReq()).buddyReqs.find(e => e.reqTime === request.flag.toString());
|
||||
if (!isNull(quickAction.approve) && notify) {
|
||||
this.core.apis.FriendApi.handleFriendRequest(notify, !!quickAction.approve).then().catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SnowCatCore } from 'snowcat-core';
|
||||
import { NapCatOneBot11Adapter } from '@/snowcat-onebot/index';
|
||||
import { OB11ProfileLikeEvent } from '@/snowcat-onebot/event/notice/OB11ProfileLikeEvent';
|
||||
import { decodeProfileLikeTip } from 'snowcat-core/helper/adaptDecoder';
|
||||
|
||||
export class OneBotUserApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: SnowCatCore;
|
||||
|
||||
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async parseLikeEvent (wrappedBody: Uint8Array): Promise<OB11ProfileLikeEvent | undefined> {
|
||||
const likeTip = decodeProfileLikeTip(wrappedBody);
|
||||
if (likeTip?.msgType !== 0 || likeTip?.subType !== 203) return;
|
||||
this.core.context.logger.logDebug('收到点赞通知消息');
|
||||
const likeMsg = likeTip.content.msg;
|
||||
if (!likeMsg) return;
|
||||
const detail = likeMsg.detail;
|
||||
if (!detail) return;
|
||||
const times = detail.txt.match(/\d+/) ?? '0';
|
||||
return new OB11ProfileLikeEvent(
|
||||
this.core,
|
||||
Number(detail.uin),
|
||||
detail.nickname,
|
||||
parseInt(times[0], 10),
|
||||
likeMsg.time
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user