mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
74ca2e2e16 | ||
![]() |
8ab550f2f5 | ||
![]() |
018aca4db2 | ||
![]() |
d4327166c1 | ||
![]() |
fa25d2e779 | ||
![]() |
3ce1c3f0ec | ||
![]() |
96dff5141e | ||
![]() |
78d85d9965 | ||
![]() |
37ec455b02 | ||
![]() |
6ab82739a6 | ||
![]() |
a36917e7c0 | ||
![]() |
21f3428b36 | ||
![]() |
f8a487db25 | ||
![]() |
73a859be04 | ||
![]() |
63bcee01a1 | ||
![]() |
85b4966ba8 | ||
![]() |
36c2c567b7 | ||
![]() |
7b1ac224f6 | ||
![]() |
34d9f04f15 | ||
![]() |
be5da7cc6f | ||
![]() |
8d32ccb5d4 | ||
![]() |
6acceb884c | ||
![]() |
4c834fd640 | ||
![]() |
301278c7a9 | ||
![]() |
42ee83c54f | ||
![]() |
e631f69621 | ||
![]() |
ce8760a39a | ||
![]() |
ff952956de | ||
![]() |
28f3ff4971 | ||
![]() |
19e728c3cb | ||
![]() |
269773ed6b |
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "3.3.25",
|
"version": "3.4.6",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.3.25",
|
"version": "3.4.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:framework": "vite build --mode framework",
|
"build:framework": "vite build --mode framework",
|
||||||
"build:shell": "vite build --mode shell",
|
"build:shell": "vite build --mode shell",
|
||||||
|
@@ -245,3 +245,36 @@ export function stringifyWithBigInt(obj: any) {
|
|||||||
typeof value === 'bigint' ? value.toString() : value
|
typeof value === 'bigint' ? value.toString() : value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||||
|
const hexSequence = "A4 09 00 00 00 35";
|
||||||
|
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
|
||||||
|
const filePath = path.resolve(nodeMajor);
|
||||||
|
const fileContent = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
let searchPosition = 0;
|
||||||
|
while (true) {
|
||||||
|
const index = fileContent.indexOf(sequenceBytes, searchPosition);
|
||||||
|
if (index === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = index + sequenceBytes.length - 1;
|
||||||
|
const end = fileContent.indexOf(0x00, start);
|
||||||
|
if (end === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const content = fileContent.subarray(start, end);
|
||||||
|
if (!content.every(byte => byte === 0x00)) {
|
||||||
|
try {
|
||||||
|
return content.toString("utf-8");
|
||||||
|
} catch (error) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPosition = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
@@ -1,8 +1,9 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { systemPlatform } from '@/common/system';
|
import { systemPlatform } from '@/common/system';
|
||||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath } from './helper';
|
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
|
||||||
import AppidTable from '@/core/external/appid.json';
|
import AppidTable from '@/core/external/appid.json';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
|
import { getMajorPath } from '@/core';
|
||||||
|
|
||||||
export class QQBasicInfoWrapper {
|
export class QQBasicInfoWrapper {
|
||||||
QQMainPath: string | undefined;
|
QQMainPath: string | undefined;
|
||||||
@@ -72,6 +73,7 @@ export class QQBasicInfoWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAppidV2(): { appid: string; qua: string } {
|
getAppidV2(): { appid: string; qua: string } {
|
||||||
|
// 通过已有表 性能好
|
||||||
const appidTbale = AppidTable as unknown as QQAppidTableType;
|
const appidTbale = AppidTable as unknown as QQAppidTableType;
|
||||||
const fullVersion = this.getFullQQVesion();
|
const fullVersion = this.getFullQQVesion();
|
||||||
if (fullVersion) {
|
if (fullVersion) {
|
||||||
@@ -80,10 +82,25 @@ export class QQBasicInfoWrapper {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 通过Major拉取 性能差
|
||||||
// else
|
try {
|
||||||
|
let majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||||
|
if (majorAppid) {
|
||||||
|
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
|
||||||
|
return { appid: majorAppid, qua: this.getQUAFallback() };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.context.logger.log(`[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||||
|
}
|
||||||
|
// 最终兜底为老版本
|
||||||
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||||
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
||||||
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
||||||
}
|
}
|
||||||
|
getAppidV2ByMajor(QQVersion: string) {
|
||||||
|
let majorPath = getMajorPath(QQVersion);
|
||||||
|
let appid = parseAppidFromMajor(majorPath);
|
||||||
|
return appid;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '3.3.25';
|
export const napCatVersion = '3.4.6';
|
||||||
|
@@ -54,7 +54,9 @@ export class NTQQGroupApi {
|
|||||||
}, pskey);
|
}, pskey);
|
||||||
}
|
}
|
||||||
async getGroupShutUpMemberList(groupCode: string) {
|
async getGroupShutUpMemberList(groupCode: string) {
|
||||||
return this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
let data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', 1, 1000, (group_id) => group_id === groupCode);
|
||||||
|
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||||
|
return (await data)[1];
|
||||||
}
|
}
|
||||||
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
||||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
||||||
|
@@ -3,6 +3,9 @@ import { InstanceContext, NapCatCore } from '@/core';
|
|||||||
import { GeneralCallResult } from '@/core/services/common';
|
import { GeneralCallResult } from '@/core/services/common';
|
||||||
|
|
||||||
export class NTQQMsgApi {
|
export class NTQQMsgApi {
|
||||||
|
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
||||||
|
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
||||||
|
}
|
||||||
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
||||||
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
||||||
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
||||||
@@ -22,7 +25,9 @@ export class NTQQMsgApi {
|
|||||||
async sendShowInputStatusReq(peer: Peer, eventType: number) {
|
async sendShowInputStatusReq(peer: Peer, eventType: number) {
|
||||||
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
|
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
|
||||||
}
|
}
|
||||||
|
async getSourceOfReplyMsgV2(peer: Peer, clientSeq: string, time: string) {
|
||||||
|
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
|
||||||
|
}
|
||||||
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
||||||
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
||||||
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
||||||
@@ -106,9 +111,9 @@ export class NTQQMsgApi {
|
|||||||
pageLimit: 1,
|
pageLimit: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//@deprecated
|
// 客户端还在用别慌
|
||||||
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
|
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
|
||||||
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
|
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
|
||||||
}
|
}
|
||||||
async getMsgExBySeq(peer: Peer, msgSeq: string) {
|
async getMsgExBySeq(peer: Peer, msgSeq: string) {
|
||||||
const DateNow = Math.floor(Date.now() / 1000);
|
const DateNow = Math.floor(Date.now() / 1000);
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { ChatType, InstanceContext, NapCatCore } from '..';
|
import { ChatType, InstanceContext, NapCatCore } from '..';
|
||||||
import offset from '@/core/external/offset.json';
|
import offset from '@/core/external/offset.json';
|
||||||
import { PacketClient, RecvPacketData } from '@/core/packet/client';
|
import { PacketClient, RecvPacketData } from '@/core/packet/client';
|
||||||
import { PacketSession } from "@/core/packet/session";
|
import { PacketSession } from "@/core/packet/session";
|
||||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||||
import { NapProtoMsg } from '@/core/packet/proto/NapProto';
|
import { NapProtoEncodeStructType, NapProtoDecodeStructType, NapProtoMsg } from '@/core/packet/proto/NapProto';
|
||||||
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
|
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
|
||||||
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
|
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
|
||||||
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
|
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
|
||||||
@@ -20,6 +21,10 @@ import {
|
|||||||
} from "@/core/packet/message/element";
|
} from "@/core/packet/message/element";
|
||||||
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
|
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
|
||||||
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||||
|
import { AIVoiceChatType, AIVoiceItemList } from "@/core/packet/entities/aiChat";
|
||||||
|
import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929";
|
||||||
|
import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||||
|
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||||
|
|
||||||
|
|
||||||
interface OffsetType {
|
interface OffsetType {
|
||||||
@@ -188,10 +193,54 @@ export class NTQQPacketApi {
|
|||||||
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
|
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
|
const data = this.packetSession?.packer.packGroupPttFileDownloadReq(groupUin, node);
|
||||||
|
const ret = await this.sendOidbPacket(data!, true);
|
||||||
|
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||||
|
const resp = new NapProtoMsg(NTV2RichMediaResp).decode(body);
|
||||||
|
const info = resp.download.info;
|
||||||
|
return `https://${info.domain}${info.urlPath}${resp.download.rKeyParam}`;
|
||||||
|
}
|
||||||
|
|
||||||
async sendMiniAppShareInfoReq(param: MiniAppReqParams) {
|
async sendMiniAppShareInfoReq(param: MiniAppReqParams) {
|
||||||
const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param);
|
const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param);
|
||||||
const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true);
|
const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true);
|
||||||
const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex'));
|
const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex'));
|
||||||
return JSON.parse(body.content.jsonContent) as MiniAppRawData;
|
return JSON.parse(body.content.jsonContent) as MiniAppRawData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType) : Promise<AIVoiceItemList[] | null> {
|
||||||
|
const data = this.packetSession?.packer.packFetchAiVoiceListReq(groupUin, chatType);
|
||||||
|
const ret = await this.sendOidbPacket(data!, true);
|
||||||
|
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||||
|
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929D_0Resp).decode(body);
|
||||||
|
if (!resp.content) return null;
|
||||||
|
return resp.content.map((item) => {
|
||||||
|
return {
|
||||||
|
category: item.category,
|
||||||
|
voices: item.voices
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
|
||||||
|
let reqTime = 0;
|
||||||
|
const reqMaxTime = 30;
|
||||||
|
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
|
||||||
|
while (true) {
|
||||||
|
if (reqTime >= reqMaxTime) {
|
||||||
|
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
|
||||||
|
}
|
||||||
|
reqTime++;
|
||||||
|
const data = this.packetSession?.packer.packAiVoiceChatReq(groupUin, voiceId, text, chatType, sessionId);
|
||||||
|
const ret = await this.sendOidbPacket(data!, true);
|
||||||
|
const body = new NapProtoMsg(OidbSvcTrpcTcpBase).decode(Buffer.from(ret.hex_data, 'hex'));
|
||||||
|
if (body.errorCode) {
|
||||||
|
throw new Error(`sendAiVoiceChatReq retCode: ${body.errorCode} error: ${body.errorMsg}`);
|
||||||
|
}
|
||||||
|
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929B_0Resp).decode(body.body);
|
||||||
|
if (!resp.msgInfo) continue;
|
||||||
|
return resp.msgInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -822,6 +822,8 @@ export interface RawMessage {
|
|||||||
elements: MessageElement[];
|
elements: MessageElement[];
|
||||||
|
|
||||||
sourceType: MsgSourceType;
|
sourceType: MsgSourceType;
|
||||||
|
|
||||||
|
isOnlineMsg: boolean;
|
||||||
}
|
}
|
||||||
export interface QueryMsgsParams {
|
export interface QueryMsgsParams {
|
||||||
chatInfo: Peer;
|
chatInfo: Peer;
|
||||||
|
@@ -43,6 +43,50 @@ export enum GroupInviteType {
|
|||||||
BYGROUPMEMBER,
|
BYGROUPMEMBER,
|
||||||
BYDISCUSSMEMBER
|
BYDISCUSSMEMBER
|
||||||
}
|
}
|
||||||
|
export interface ShutUpGroupHonor {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShutUpGroupMember {
|
||||||
|
uid: string;
|
||||||
|
qid: string;
|
||||||
|
uin: string;
|
||||||
|
nick: string;
|
||||||
|
remark: string;
|
||||||
|
cardType: number;
|
||||||
|
cardName: string;
|
||||||
|
role: number;
|
||||||
|
avatarPath: string;
|
||||||
|
shutUpTime: number;
|
||||||
|
isDelete: boolean;
|
||||||
|
isSpecialConcerned: boolean;
|
||||||
|
isSpecialShield: boolean;
|
||||||
|
isRobot: boolean;
|
||||||
|
groupHonor: ShutUpGroupHonor;
|
||||||
|
memberRealLevel: number;
|
||||||
|
memberLevel: number;
|
||||||
|
globalGroupLevel: number;
|
||||||
|
globalGroupPoint: number;
|
||||||
|
memberTitleId: number;
|
||||||
|
memberSpecialTitle: string;
|
||||||
|
specialTitleExpireTime: string;
|
||||||
|
userShowFlag: number;
|
||||||
|
userShowFlagNew: number;
|
||||||
|
richFlag: number;
|
||||||
|
mssVipType: number;
|
||||||
|
bigClubLevel: number;
|
||||||
|
bigClubFlag: number;
|
||||||
|
autoRemark: string;
|
||||||
|
creditLevel: number;
|
||||||
|
joinTime: number;
|
||||||
|
lastSpeakTime: number;
|
||||||
|
memberFlag: number;
|
||||||
|
memberFlagExt: number;
|
||||||
|
memberMobileFlag: number;
|
||||||
|
memberFlagExt2: number;
|
||||||
|
isSpecialShielded: boolean;
|
||||||
|
cardNameId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GroupNotify {
|
export interface GroupNotify {
|
||||||
seq: string; // 通知序列号
|
seq: string; // 通知序列号
|
||||||
|
14
src/core/external/appid.json
vendored
14
src/core/external/appid.json
vendored
@@ -51,7 +51,7 @@
|
|||||||
"appid": 537249739,
|
"appid": 537249739,
|
||||||
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
||||||
},
|
},
|
||||||
"9.9.16-28971":{
|
"9.9.16-28971": {
|
||||||
"appid": 537249775,
|
"appid": 537249775,
|
||||||
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
||||||
},
|
},
|
||||||
@@ -62,5 +62,17 @@
|
|||||||
"6.9.58-28971": {
|
"6.9.58-28971": {
|
||||||
"appid": 537249826,
|
"appid": 537249826,
|
||||||
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
|
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.16-29271": {
|
||||||
|
"appid": 537249813,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.16_29271_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.13-29271": {
|
||||||
|
"appid": 537249913,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.13_29271_GW_B"
|
||||||
|
},
|
||||||
|
"6.9.59-29271": {
|
||||||
|
"appid": 537249863,
|
||||||
|
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
|
||||||
}
|
}
|
||||||
}
|
}
|
22
src/core/external/offset.json
vendored
22
src/core/external/offset.json
vendored
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"6.9.56-28418-arm64": {
|
||||||
|
"send": "4471360",
|
||||||
|
"recv": "4473BCC"
|
||||||
|
},
|
||||||
"3.2.12-28418-x64": {
|
"3.2.12-28418-x64": {
|
||||||
"recv": "A0723E0",
|
"recv": "A0723E0",
|
||||||
"send": "A06EAE0"
|
"send": "A06EAE0"
|
||||||
@@ -35,8 +39,20 @@
|
|||||||
"send": "6E91318",
|
"send": "6E91318",
|
||||||
"recv": "6E94B50"
|
"recv": "6E94B50"
|
||||||
},
|
},
|
||||||
"6.9.56-28418-arm64": {
|
"6.9.58-28971-arm64": {
|
||||||
"send": "4471360",
|
"send": "449ACA0",
|
||||||
"recv": "4473BCC"
|
"recv": "449D50C"
|
||||||
|
},
|
||||||
|
"9.9.16-29271-x64": {
|
||||||
|
"send": "3833510",
|
||||||
|
"recv": "3837944"
|
||||||
|
},
|
||||||
|
"3.2.13-29271-x64": {
|
||||||
|
"send": "A11E680",
|
||||||
|
"recv": "A121F80"
|
||||||
|
},
|
||||||
|
"3.2.13-29271-arm64": {
|
||||||
|
"send": "6ECA098",
|
||||||
|
"recv": "6ECD8D0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -62,7 +62,26 @@ export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
|
|||||||
process.dlopen(nativemodule, wrapperNodePath);
|
process.dlopen(nativemodule, wrapperNodePath);
|
||||||
return nativemodule.exports;
|
return nativemodule.exports;
|
||||||
}
|
}
|
||||||
|
export function getMajorPath(QQVersion: string): string {
|
||||||
|
// major.node
|
||||||
|
let appPath;
|
||||||
|
if (os.platform() === 'darwin') {
|
||||||
|
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
||||||
|
} else if (os.platform() === 'linux') {
|
||||||
|
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
||||||
|
} else {
|
||||||
|
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
||||||
|
}
|
||||||
|
let majorPath = path.resolve(appPath, 'major.node');
|
||||||
|
if (!fs.existsSync(majorPath)) {
|
||||||
|
majorPath = path.join(appPath, `./resources/app/major.node`);
|
||||||
|
}
|
||||||
|
//老版本兼容 未来去掉
|
||||||
|
if (!fs.existsSync(majorPath)) {
|
||||||
|
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
|
||||||
|
}
|
||||||
|
return majorPath;
|
||||||
|
}
|
||||||
export class NapCatCore {
|
export class NapCatCore {
|
||||||
readonly context: InstanceContext;
|
readonly context: InstanceContext;
|
||||||
readonly apis: StableNTApiWrapper;
|
readonly apis: StableNTApiWrapper;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/core/entities';
|
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
|
||||||
|
|
||||||
export class NodeIKernelGroupListener {
|
export class NodeIKernelGroupListener {
|
||||||
onGroupListInited(listEmpty: boolean): void { }
|
onGroupListInited(listEmpty: boolean): void { }
|
||||||
@@ -80,6 +80,6 @@ export class NodeIKernelGroupListener {
|
|||||||
onSearchMemberChange(...args: unknown[]) {
|
onSearchMemberChange(...args: unknown[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShutUpMemberListChanged(...args: unknown[]) {
|
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
16
src/core/packet/entities/aiChat.ts
Normal file
16
src/core/packet/entities/aiChat.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export enum AIVoiceChatType {
|
||||||
|
Unknown = 0,
|
||||||
|
Sound = 1,
|
||||||
|
Sing = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIVoiceItem {
|
||||||
|
voiceId: string;
|
||||||
|
voiceDisplayName: string;
|
||||||
|
voiceExampleUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIVoiceItemList {
|
||||||
|
category: string;
|
||||||
|
voices: AIVoiceItem[];
|
||||||
|
}
|
@@ -1,13 +1,13 @@
|
|||||||
import * as zlib from "node:zlib";
|
import * as zlib from "node:zlib";
|
||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||||
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
|
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
|
||||||
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
|
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
|
||||||
import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2";
|
import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2";
|
||||||
import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2";
|
import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2";
|
||||||
import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
|
import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
|
||||||
import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
import { IndexNode, NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||||
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
||||||
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
||||||
import { PacketMsgBuilder } from "@/core/packet/message/builder";
|
import { PacketMsgBuilder } from "@/core/packet/message/builder";
|
||||||
@@ -28,6 +28,8 @@ import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800
|
|||||||
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
||||||
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
||||||
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||||
|
import {AIVoiceChatType} from "@/core/packet/entities/aiChat";
|
||||||
|
import {OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0} from "@/core/packet/proto/oidb/Oidb.0x929";
|
||||||
|
|
||||||
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
||||||
|
|
||||||
@@ -696,6 +698,37 @@ export class PacketPacker {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||||
|
return this.packOidbPacket(0x126E, 200, new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||||
|
reqHead: {
|
||||||
|
common: {
|
||||||
|
requestId: 4,
|
||||||
|
command: 200
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
requestType: 1,
|
||||||
|
businessType: 3,
|
||||||
|
sceneType: 2,
|
||||||
|
group: {
|
||||||
|
groupUin: groupUin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
agentType: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
node: node,
|
||||||
|
download: {
|
||||||
|
video: {
|
||||||
|
busiType: 0,
|
||||||
|
sceneType: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), true, false);
|
||||||
|
}
|
||||||
|
|
||||||
packGroupSignReq(uin: string, groupCode: string): OidbPacket {
|
packGroupSignReq(uin: string, groupCode: string): OidbPacket {
|
||||||
return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode(
|
return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode(
|
||||||
{
|
{
|
||||||
@@ -744,4 +777,27 @@ export class PacketPacker {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType): OidbPacket {
|
||||||
|
return this.packOidbPacket(0x929D, 0,
|
||||||
|
new NapProtoMsg(OidbSvcTrpcTcp0X929D_0).encode({
|
||||||
|
groupUin: groupUin,
|
||||||
|
chatType: chatType
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
packAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType, sessionId: number): OidbPacket {
|
||||||
|
return this.packOidbPacket(0x929B, 0,
|
||||||
|
new NapProtoMsg(OidbSvcTrpcTcp0X929B_0).encode({
|
||||||
|
groupUin: groupUin,
|
||||||
|
voiceId: voiceId,
|
||||||
|
text: text,
|
||||||
|
chatType: chatType,
|
||||||
|
session: {
|
||||||
|
sessionId: sessionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
42
src/core/packet/proto/oidb/Oidb.0x929.ts
Normal file
42
src/core/packet/proto/oidb/Oidb.0x929.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { ScalarType } from "@protobuf-ts/runtime";
|
||||||
|
import { ProtoField } from "../NapProto";
|
||||||
|
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929D_0 = {
|
||||||
|
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||||
|
chatType: ProtoField(2, ScalarType.UINT32),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929D_0Resp = {
|
||||||
|
content: ProtoField(1, () => OidbSvcTrpcTcp0X929D_0RespContent, false, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929D_0RespContent = {
|
||||||
|
category: ProtoField(1, ScalarType.STRING),
|
||||||
|
voices: ProtoField(2, () => OidbSvcTrpcTcp0X929D_0RespContentVoice, false, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929D_0RespContentVoice = {
|
||||||
|
voiceId: ProtoField(1, ScalarType.STRING),
|
||||||
|
voiceDisplayName: ProtoField(2, ScalarType.STRING),
|
||||||
|
voiceExampleUrl: ProtoField(3, ScalarType.STRING),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929B_0 = {
|
||||||
|
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||||
|
voiceId: ProtoField(2, ScalarType.STRING),
|
||||||
|
text: ProtoField(3, ScalarType.STRING),
|
||||||
|
chatType: ProtoField(4, ScalarType.UINT32),
|
||||||
|
session: ProtoField(5, () => OidbSvcTrpcTcp0X929B_0_Session),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929B_0_Session = {
|
||||||
|
sessionId: ProtoField(1, ScalarType.UINT32),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OidbSvcTrpcTcp0X929B_0Resp = {
|
||||||
|
statusCode: ProtoField(1, ScalarType.UINT32),
|
||||||
|
field2: ProtoField(2, ScalarType.UINT32, true),
|
||||||
|
field3: ProtoField(3, ScalarType.UINT32),
|
||||||
|
msgInfo: ProtoField(4, () => MsgInfo, true),
|
||||||
|
};
|
@@ -4,10 +4,11 @@ import { ProtoField } from "../NapProto";
|
|||||||
export const OidbSvcTrpcTcpBase = {
|
export const OidbSvcTrpcTcpBase = {
|
||||||
command: ProtoField(1, ScalarType.UINT32),
|
command: ProtoField(1, ScalarType.UINT32),
|
||||||
subCommand: ProtoField(2, ScalarType.UINT32),
|
subCommand: ProtoField(2, ScalarType.UINT32),
|
||||||
|
errorCode: ProtoField(3, ScalarType.UINT32),
|
||||||
body: ProtoField(4, ScalarType.BYTES),
|
body: ProtoField(4, ScalarType.BYTES),
|
||||||
errorMsg: ProtoField(5, ScalarType.STRING, true),
|
errorMsg: ProtoField(5, ScalarType.STRING, true),
|
||||||
isReserved: ProtoField(12, ScalarType.UINT32)
|
isReserved: ProtoField(12, ScalarType.UINT32)
|
||||||
};
|
};
|
||||||
export const OidbSvcTrpcTcpBaseRsp = {
|
export const OidbSvcTrpcTcpBaseRsp = {
|
||||||
body: ProtoField(4, ScalarType.BYTES)
|
body: ProtoField(4, ScalarType.BYTES)
|
||||||
};
|
};
|
||||||
|
@@ -172,7 +172,7 @@ export interface NodeIKernelMsgService {
|
|||||||
msgList: RawMessage[]
|
msgList: RawMessage[]
|
||||||
}>;
|
}>;
|
||||||
//@deprecated
|
//@deprecated
|
||||||
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & {
|
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean): Promise<GeneralCallResult & {
|
||||||
msgList: RawMessage[]
|
msgList: RawMessage[]
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -186,27 +186,29 @@ export interface NodeIKernelMsgService {
|
|||||||
|
|
||||||
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown;
|
// 下面的msgid全部不真实
|
||||||
|
getSourceOfReplyMsg(peer: Peer, msgId: string, sourceSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown;
|
//用法和聊天记录一样
|
||||||
|
getSourceOfReplyMsgV2(peer: Peer, rootMsgId: string, replyMsgId: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown;
|
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown;
|
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string, replyMsgId: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: {
|
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: {
|
||||||
type: number,
|
type: number,
|
||||||
subtype: Array<number>
|
subtype: Array<number>
|
||||||
}): unknown;
|
}): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{
|
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{
|
||||||
type: number,
|
type: number,
|
||||||
subtype: Array<number>
|
subtype: Array<number>
|
||||||
}>): unknown;
|
}>): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown;
|
getMsgWithAbstractByFilterParam(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
queryMsgsWithFilter(...args: unknown[]): unknown;
|
queryMsgsWithFilter(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
|
||||||
|
|
||||||
//queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>;
|
//queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>;
|
||||||
|
|
||||||
|
41
src/onebot/action/extends/GetAiCharacters.ts
Normal file
41
src/onebot/action/extends/GetAiCharacters.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {ActionName} from '../types';
|
||||||
|
import {FromSchema, JSONSchema} from 'json-schema-to-ts';
|
||||||
|
import {GetPacketStatusDepends} from "@/onebot/action/packet/GetPacketStatus";
|
||||||
|
import {AIVoiceChatType} from "@/core/packet/entities/aiChat";
|
||||||
|
|
||||||
|
const SchemaData = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
group_id: { type: ['number', 'string'] },
|
||||||
|
chat_type: { type: ['number', 'string'] },
|
||||||
|
},
|
||||||
|
required: ['group_id'],
|
||||||
|
} as const satisfies JSONSchema;
|
||||||
|
|
||||||
|
type Payload = FromSchema<typeof SchemaData>;
|
||||||
|
|
||||||
|
interface GetAiCharactersResponse {
|
||||||
|
type: string;
|
||||||
|
characters: {
|
||||||
|
character_id: string;
|
||||||
|
character_name: string;
|
||||||
|
preview_url: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharactersResponse[]> {
|
||||||
|
actionName = ActionName.GetAiCharacters;
|
||||||
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
|
async _handle(payload: Payload) {
|
||||||
|
const rawList = await this.core.apis.PacketApi.sendFetchAiVoiceListReq(+payload.group_id, +(payload.chat_type ?? 1) as AIVoiceChatType);
|
||||||
|
return rawList?.map((item) => ({
|
||||||
|
type: item.category,
|
||||||
|
characters: item.voices.map((voice) => ({
|
||||||
|
character_id: voice.voiceId,
|
||||||
|
character_name: voice.voiceDisplayName,
|
||||||
|
preview_url: voice.voiceExampleUrl,
|
||||||
|
})),
|
||||||
|
})) ?? [];
|
||||||
|
}
|
||||||
|
}
|
@@ -5,9 +5,11 @@ import { promises as fs } from 'fs';
|
|||||||
import { decode } from 'silk-wasm';
|
import { decode } from 'silk-wasm';
|
||||||
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
|
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
|
||||||
|
|
||||||
interface Payload extends GetFilePayload {
|
const out_format = ['mp3' , 'amr' , 'wma' , 'm4a' , 'spx' , 'ogg' , 'wav' , 'flac'];
|
||||||
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac';
|
|
||||||
}
|
type Payload = {
|
||||||
|
out_format : string
|
||||||
|
} & GetFilePayload
|
||||||
|
|
||||||
export default class GetRecord extends GetFileBase {
|
export default class GetRecord extends GetFileBase {
|
||||||
actionName = ActionName.GetRecord;
|
actionName = ActionName.GetRecord;
|
||||||
@@ -17,12 +19,19 @@ export default class GetRecord extends GetFileBase {
|
|||||||
if (payload.out_format && typeof payload.out_format === 'string') {
|
if (payload.out_format && typeof payload.out_format === 'string') {
|
||||||
const inputFile = res.file;
|
const inputFile = res.file;
|
||||||
if (!inputFile) throw new Error('file not found');
|
if (!inputFile) throw new Error('file not found');
|
||||||
|
if (!out_format.includes(payload.out_format)) {
|
||||||
|
throw new Error('转换失败 out_format 字段可能格式不正确');
|
||||||
|
}
|
||||||
const pcmFile = `${inputFile}.pcm`;
|
const pcmFile = `${inputFile}.pcm`;
|
||||||
const outputFile = `${inputFile}.${payload.out_format}`;
|
const outputFile = `${inputFile}.${payload.out_format}`;
|
||||||
try {
|
try {
|
||||||
await fs.access(inputFile);
|
await fs.access(inputFile);
|
||||||
await this.decodeFile(inputFile, pcmFile);
|
try {
|
||||||
await this.convertFile(pcmFile, outputFile, payload.out_format);
|
await fs.access(outputFile);
|
||||||
|
} catch (error) {
|
||||||
|
await this.decodeFile(inputFile, pcmFile);
|
||||||
|
await this.convertFile(pcmFile, outputFile, payload.out_format);
|
||||||
|
}
|
||||||
const base64Data = await fs.readFile(outputFile, { encoding: 'base64' });
|
const base64Data = await fs.readFile(outputFile, { encoding: 'base64' });
|
||||||
res.file = outputFile;
|
res.file = outputFile;
|
||||||
res.url = outputFile;
|
res.url = outputFile;
|
||||||
@@ -48,7 +57,8 @@ export default class GetRecord extends GetFileBase {
|
|||||||
|
|
||||||
private convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
|
private convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ffmpeg = spawn(FFMPEG_PATH, ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, outputFile]);
|
const params = format === 'amr' ? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, '-ar', '8000', '-b:a', '12.2k', outputFile] : ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, outputFile];
|
||||||
|
const ffmpeg = spawn(FFMPEG_PATH, params);
|
||||||
|
|
||||||
ffmpeg.on('close', (code) => {
|
ffmpeg.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
@@ -63,4 +73,4 @@ export default class GetRecord extends GetFileBase {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -73,16 +73,14 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
|
|||||||
|
|
||||||
const singleMsg = data.msgList[0];
|
const singleMsg = data.msgList[0];
|
||||||
const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理
|
const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理
|
||||||
if (!resMsg) {
|
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
|
||||||
throw new Error('找不到相关的聊天记录');
|
throw new Error('找不到相关的聊天记录');
|
||||||
}
|
}
|
||||||
//if (this.obContext.configLoader.configData.messagePostFormat === 'array') {
|
return {
|
||||||
//提取
|
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
|
||||||
const realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message;
|
};
|
||||||
//里面都是offline消息 id都是0 没得说话
|
|
||||||
return { message: realmsg };
|
|
||||||
//}
|
//}
|
||||||
|
|
||||||
// return { message: resMsg };
|
// return { message: resMsg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,11 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
|
|||||||
if (!uid) uid = extendData.detail.uid;
|
if (!uid) uid = extendData.detail.uid;
|
||||||
const info = (await this.core.apis.UserApi.getUserDetailInfo(uid));
|
const info = (await this.core.apis.UserApi.getUserDetailInfo(uid));
|
||||||
return {
|
return {
|
||||||
|
...extendData.detail.simpleInfo.coreInfo,
|
||||||
|
...extendData.detail.commonExt ?? {},
|
||||||
|
...extendData.detail.simpleInfo.baseInfo,
|
||||||
|
...extendData.detail.simpleInfo.relationFlags ?? {},
|
||||||
|
...extendData.detail.simpleInfo.status ?? {},
|
||||||
user_id: parseInt(extendData.detail.uin) ?? 0,
|
user_id: parseInt(extendData.detail.uin) ?? 0,
|
||||||
uid: info.uid ?? uid,
|
uid: info.uid ?? uid,
|
||||||
nickname: extendData.detail.simpleInfo.coreInfo.nick,
|
nickname: extendData.detail.simpleInfo.coreInfo.nick,
|
||||||
@@ -33,7 +38,7 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
|
|||||||
qqLevel: calcQQLevel(extendData.detail.commonExt?.qqLevel ?? info.qqLevel),
|
qqLevel: calcQQLevel(extendData.detail.commonExt?.qqLevel ?? info.qqLevel),
|
||||||
sex: OB11Entities.sex(extendData.detail.simpleInfo.baseInfo.sex) ?? OB11UserSex.unknown,
|
sex: OB11Entities.sex(extendData.detail.simpleInfo.baseInfo.sex) ?? OB11UserSex.unknown,
|
||||||
long_nick: extendData.detail.simpleInfo.baseInfo.longNick ?? info.longNick,
|
long_nick: extendData.detail.simpleInfo.baseInfo.longNick ?? info.longNick,
|
||||||
reg_time: extendData.detail.commonExt.regTime ?? info.regTime,
|
reg_time: extendData.detail.commonExt?.regTime ?? info.regTime,
|
||||||
is_vip: extendData.detail.simpleInfo.vasInfo?.svipFlag,
|
is_vip: extendData.detail.simpleInfo.vasInfo?.svipFlag,
|
||||||
is_years_vip: extendData.detail.simpleInfo.vasInfo?.yearVipFlag,
|
is_years_vip: extendData.detail.simpleInfo.vasInfo?.yearVipFlag,
|
||||||
vip_level: extendData.detail.simpleInfo.vasInfo?.vipLevel,
|
vip_level: extendData.detail.simpleInfo.vasInfo?.vipLevel,
|
||||||
|
28
src/onebot/action/group/GetAiRecord.ts
Normal file
28
src/onebot/action/group/GetAiRecord.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {ActionName} from '../types';
|
||||||
|
import {FromSchema, JSONSchema} from 'json-schema-to-ts';
|
||||||
|
import {GetPacketStatusDepends} from "@/onebot/action/packet/GetPacketStatus";
|
||||||
|
import {AIVoiceChatType} from "@/core/packet/entities/aiChat";
|
||||||
|
import {NapProtoEncodeStructType} from "@/core/packet/proto/NapProto";
|
||||||
|
import {IndexNode} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||||
|
|
||||||
|
const SchemaData = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
character: { type: ['string'] },
|
||||||
|
group_id: { type: ['number', 'string'] },
|
||||||
|
text: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['character', 'group_id', 'text'],
|
||||||
|
} as const satisfies JSONSchema;
|
||||||
|
|
||||||
|
type Payload = FromSchema<typeof SchemaData>;
|
||||||
|
|
||||||
|
export class GetAiRecord extends GetPacketStatusDepends<Payload, string> {
|
||||||
|
actionName = ActionName.GetAiRecord;
|
||||||
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
|
async _handle(payload: Payload) {
|
||||||
|
const rawRsp = await this.core.apis.PacketApi.sendAiVoiceChatReq(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
|
||||||
|
return await this.core.apis.PacketApi.sendGroupPttFileDownloadReq(+payload.group_id, rawRsp.msgInfoBody[0].index);
|
||||||
|
}
|
||||||
|
}
|
@@ -13,7 +13,7 @@ const SchemaData = {
|
|||||||
|
|
||||||
type Payload = FromSchema<typeof SchemaData>;
|
type Payload = FromSchema<typeof SchemaData>;
|
||||||
|
|
||||||
export class GetGroupShutList extends BaseAction<Payload, OB11Group> {
|
export class GetGroupShutList extends BaseAction<Payload, any> {
|
||||||
actionName = ActionName.GetGroupShutList;
|
actionName = ActionName.GetGroupShutList;
|
||||||
payloadSchema = SchemaData;
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
|
40
src/onebot/action/group/SendGroupAiRecord.ts
Normal file
40
src/onebot/action/group/SendGroupAiRecord.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {ActionName} from '../types';
|
||||||
|
import {FromSchema, JSONSchema} from 'json-schema-to-ts';
|
||||||
|
import {GetPacketStatusDepends} from "@/onebot/action/packet/GetPacketStatus";
|
||||||
|
import {AIVoiceChatType} from "@/core/packet/entities/aiChat";
|
||||||
|
import {uri2local} from "@/common/file";
|
||||||
|
import {ChatType, Peer} from "@/core";
|
||||||
|
import {NapProtoEncodeStructType} from "@/core/packet/proto/NapProto";
|
||||||
|
import {IndexNode} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||||
|
|
||||||
|
const SchemaData = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
character: { type: ['string'] },
|
||||||
|
group_id: { type: ['number', 'string'] },
|
||||||
|
text: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['character', 'group_id', 'text'],
|
||||||
|
} as const satisfies JSONSchema;
|
||||||
|
|
||||||
|
type Payload = FromSchema<typeof SchemaData>;
|
||||||
|
|
||||||
|
export class SendGroupAiRecord extends GetPacketStatusDepends<Payload, {
|
||||||
|
message_id: string
|
||||||
|
}> {
|
||||||
|
actionName = ActionName.SendGroupAiRecord;
|
||||||
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
|
async _handle(payload: Payload) {
|
||||||
|
const rawRsp = await this.core.apis.PacketApi.sendAiVoiceChatReq(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
|
||||||
|
const url = await this.core.apis.PacketApi.sendGroupPttFileDownloadReq(+payload.group_id, rawRsp.msgInfoBody[0].index);
|
||||||
|
const { path, fileName, errMsg, success} = (await uri2local(this.core.NapCatTempPath, url));
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
const peer = {chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString()} as Peer;
|
||||||
|
const element = await this.core.apis.FileApi.createValidSendPttElement(path);
|
||||||
|
const sendRes = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [element], [path]);
|
||||||
|
return {message_id: sendRes.msgId};
|
||||||
|
}
|
||||||
|
}
|
@@ -99,6 +99,9 @@ import { GoCQHTTPGetModelShow } from './go-cqhttp/GoCQHTTPGetModelShow';
|
|||||||
import { GoCQHTTPSetModelShow } from './go-cqhttp/GoCQHTTPSetModelShow';
|
import { GoCQHTTPSetModelShow } from './go-cqhttp/GoCQHTTPSetModelShow';
|
||||||
import { GoCQHTTPDeleteFriend } from './go-cqhttp/GoCQHTTPDeleteFriend';
|
import { GoCQHTTPDeleteFriend } from './go-cqhttp/GoCQHTTPDeleteFriend';
|
||||||
import { GetMiniAppArk } from "@/onebot/action/extends/GetMiniAppArk";
|
import { GetMiniAppArk } from "@/onebot/action/extends/GetMiniAppArk";
|
||||||
|
import { GetAiRecord } from "@/onebot/action/group/GetAiRecord";
|
||||||
|
import { SendGroupAiRecord } from "@/onebot/action/group/SendGroupAiRecord";
|
||||||
|
import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters";
|
||||||
|
|
||||||
|
|
||||||
export type ActionMap = Map<string, BaseAction<any, any>>;
|
export type ActionMap = Map<string, BaseAction<any, any>>;
|
||||||
@@ -212,6 +215,9 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
|||||||
new GetGroupShutList(obContext, core),
|
new GetGroupShutList(obContext, core),
|
||||||
new GetGroupFileUrl(obContext, core),
|
new GetGroupFileUrl(obContext, core),
|
||||||
new GetMiniAppArk(obContext, core),
|
new GetMiniAppArk(obContext, core),
|
||||||
|
new GetAiRecord(obContext, core),
|
||||||
|
new SendGroupAiRecord(obContext, core),
|
||||||
|
new GetAiCharacters(obContext, core),
|
||||||
];
|
];
|
||||||
const actionMap = new Map();
|
const actionMap = new Map();
|
||||||
for (const action of actionHandlers) {
|
for (const action of actionHandlers) {
|
||||||
|
@@ -138,4 +138,7 @@ export enum ActionName {
|
|||||||
SetGroupSign = "set_group_sign",
|
SetGroupSign = "set_group_sign",
|
||||||
GetMiniAppArk = "get_mini_app_ark",
|
GetMiniAppArk = "get_mini_app_ark",
|
||||||
// UploadForwardMsg = "upload_forward_msg",
|
// UploadForwardMsg = "upload_forward_msg",
|
||||||
|
GetAiRecord = "get_ai_record",
|
||||||
|
GetAiCharacters = "get_ai_characters",
|
||||||
|
SendGroupAiRecord = "send_group_ai_record",
|
||||||
}
|
}
|
||||||
|
@@ -217,20 +217,34 @@ export class OneBotMsgApi {
|
|||||||
if (records.peerUin === '284840486' || records.peerUin === '1094950020') {
|
if (records.peerUin === '284840486' || records.peerUin === '1094950020') {
|
||||||
return createReplyData(records.msgId);
|
return createReplyData(records.msgId);
|
||||||
}
|
}
|
||||||
let replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, element.replyMsgTime, [element.senderUidStr])).msgList;
|
let replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, records.msgTime, [element.senderUidStr])).msgList;
|
||||||
let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
||||||
|
|
||||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
||||||
// 我猜测可能是时间参数未对上 导致找不到引用消息 或者msgList 存在问题
|
this.core.context.logger.logError.bind(this.core.context.logger)(
|
||||||
this.core.context.logger.logWarn.bind(this.core.context.logger)(
|
'筛选结果,筛选消息失败,将使用Fallback-1 Seq: ',
|
||||||
'初次筛选消息失败,获取不到引用的消息 Seq:',
|
element.replayMsgSeq,
|
||||||
|
',消息长度:',
|
||||||
|
replyMsgList.length
|
||||||
|
);
|
||||||
|
replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(peer, element.replayMsgSeq, 1, true, true)).msgList;
|
||||||
|
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
||||||
|
this.core.context.logger.logWarn.bind(this.core.context.logger)(
|
||||||
|
'筛选消息失败,将使用Fallback-2 Seq:',
|
||||||
element.replayMsgSeq,
|
element.replayMsgSeq,
|
||||||
',消息长度:',
|
',消息长度:',
|
||||||
replyMsgList.length
|
replyMsgList.length
|
||||||
);
|
);
|
||||||
// 再次筛选
|
|
||||||
replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(peer, element.replayMsgSeq, [element.senderUidStr])).msgList;
|
replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(peer, element.replayMsgSeq, [element.senderUidStr])).msgList;
|
||||||
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 丢弃该消息段
|
||||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
||||||
this.core.context.logger.logError.bind(this.core.context.logger)(
|
this.core.context.logger.logError.bind(this.core.context.logger)(
|
||||||
'最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ',
|
'最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ',
|
||||||
|
@@ -311,15 +311,19 @@ export class NapCatOneBot11Adapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const msgIdSend = new LRUCache<string, boolean>(100);
|
const msgIdSend = new LRUCache<string, number>(100);
|
||||||
const recallMsgs = new LRUCache<string, boolean>(100);
|
const recallMsgs = new LRUCache<string, boolean>(100);
|
||||||
|
msgListener.onAddSendMsg = async msg => {
|
||||||
|
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
|
||||||
|
msgIdSend.put(msg.msgId, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
msgListener.onMsgInfoListUpdate = async msgList => {
|
msgListener.onMsgInfoListUpdate = async msgList => {
|
||||||
this.emitRecallMsg(msgList, recallMsgs)
|
this.emitRecallMsg(msgList, recallMsgs)
|
||||||
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e));
|
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e));
|
||||||
|
|
||||||
for (const msg of msgList.filter(e => e.senderUin == this.core.selfInfo.uin)) {
|
for (const msg of msgList.filter(e => e.senderUin == this.core.selfInfo.uin)) {
|
||||||
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS && !msgIdSend.get(msg.msgId)) {
|
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS && msgIdSend.get(msg.msgId) == 0) {
|
||||||
msgIdSend.put(msg.msgId, true);
|
msgIdSend.put(msg.msgId, 1);
|
||||||
// 完成后再post
|
// 完成后再post
|
||||||
this.apis.MsgApi.parseMessage(msg)
|
this.apis.MsgApi.parseMessage(msg)
|
||||||
.then((ob11Msg) => {
|
.then((ob11Msg) => {
|
||||||
@@ -523,7 +527,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async emitMsg(message: RawMessage) {
|
private async emitMsg(message: RawMessage, parseEvent: boolean = true) {
|
||||||
const { debug, reportSelfMessage, messagePostFormat } = this.configLoader.configData;
|
const { debug, reportSelfMessage, messagePostFormat } = this.configLoader.configData;
|
||||||
this.context.logger.logDebug('收到新消息 RawMessage', message);
|
this.context.logger.logDebug('收到新消息 RawMessage', message);
|
||||||
this.apis.MsgApi.parseMessage(message, messagePostFormat).then((ob11Msg) => {
|
this.apis.MsgApi.parseMessage(message, messagePostFormat).then((ob11Msg) => {
|
||||||
|
@@ -143,7 +143,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
|
|||||||
private registerHeartBeat() {
|
private registerHeartBeat() {
|
||||||
this.heartbeatIntervalId = setInterval(() => {
|
this.heartbeatIntervalId = setInterval(() => {
|
||||||
this.wsClientsMutex.runExclusive(async () => {
|
this.wsClientsMutex.runExclusive(async () => {
|
||||||
this.wsClients.forEach((wsClient) => {
|
this.wsClientWithEvent.forEach((wsClient) => {
|
||||||
if (wsClient.readyState === WebSocket.OPEN) {
|
if (wsClient.readyState === WebSocket.OPEN) {
|
||||||
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatInterval, this.core.selfInfo.online, true)));
|
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatInterval, this.core.selfInfo.online, true)));
|
||||||
}
|
}
|
||||||
|
@@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
|
|||||||
SettingItem(
|
SettingItem(
|
||||||
'<span id="napcat-update-title">Napcat</span>',
|
'<span id="napcat-update-title">Napcat</span>',
|
||||||
void 0,
|
void 0,
|
||||||
SettingButton("V3.3.25", "napcat-update-button", "secondary")
|
SettingButton("V3.4.6", "napcat-update-button", "secondary")
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
SettingList([
|
SettingList([
|
||||||
|
Reference in New Issue
Block a user