This commit is contained in:
linyuchen
2024-04-15 00:09:08 +08:00
parent 0ef3e38d70
commit 356aba762c
218 changed files with 8465 additions and 5 deletions

View File

@@ -0,0 +1,49 @@
import { ActionName, BaseCheckResult } from './types';
import { OB11Response } from './OB11Response';
import { OB11Return } from '../types';
import { log } from '../../common/utils/log';
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName;
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
valid: true,
};
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 400);
}
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
log('发生错误', e);
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200);
}
}
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 1400);
}
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData, echo);
} catch (e) {
log('发生错误', e);
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo);
}
}
protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
throw `pleas override ${this.actionName} _handle`;
}
}
export default BaseAction;

View File

@@ -0,0 +1,32 @@
import { OB11Return } from '../types';
import { isNull } from '../../common/utils/helper';
export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ''): OB11Return<T> {
return {
status: status,
retcode: retcode,
data: data,
message: message,
wording: message,
echo: null
};
}
static ok<T>(data: T, echo: any = null) {
const res = OB11Response.res<T>(data, 'ok', 0);
if (!isNull(echo)) {
res.echo = echo;
}
return res;
}
static error(err: string, retcode: number, echo: any = null) {
const res = OB11Response.res(null, 'failed', retcode, err);
if (!isNull(echo)) {
res.echo = echo;
}
return res;
}
}

View File

@@ -0,0 +1,116 @@
import BaseAction from '../BaseAction';
import fs from 'fs/promises';
import { dbUtil } from '@/common/utils/db';
import { ob11Config } from '@/onebot11/config';
import { log } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import { uri2local } from '@/common/utils/file';
import { ActionName } from '../types';
import { FileElement, RawMessage, VideoElement } from '@/core/qqnt/entities';
import { NTQQFileApi } from '@/core/qqnt/apis';
export interface GetFilePayload {
file: string; // 文件名或者fileUuid
}
export interface GetFileResponse {
file?: string; // path
url?: string;
file_size?: string;
file_name?: string;
base64?: string;
}
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage): { id: string, element: VideoElement | FileElement } {
let element = msg.elements.find(e => e.fileElement);
if (!element) {
element = msg.elements.find(e => e.videoElement);
if (element) {
return { id: element.elementId, element: element.videoElement };
} else {
throw new Error('找不到文件');
}
}
return { id: element.elementId, element: element.fileElement };
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCacheByName(payload.file);
if (!cache) {
cache = await dbUtil.getFileCacheByUuid(payload.file);
}
if (!cache) {
throw new Error('file not found');
}
const { enableLocalFile2Url } = ob11Config;
try {
await fs.access(cache.path, fs.constants.F_OK);
} catch (e) {
log('local file not found, start download...');
// if (cache.url) {
// const downloadResult = await uri2local(cache.url);
// if (downloadResult.success) {
// cache.path = downloadResult.path;
// dbUtil.updateFileCache(cache).then();
// } else {
// throw new Error('file download failed. ' + downloadResult.errMsg);
// }
// } else {
// // 没有url的可能是私聊文件或者群文件需要自己下载
// log('需要调用 NTQQ 下载文件api');
let msg = await dbUtil.getMsgByLongId(cache.msgId);
// log('文件 msg', msg);
if (msg) {
// 构建下载函数
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
cache.elementId, '', '');
// await sleep(1000);
// log('download result', downloadPath);
msg = await dbUtil.getMsgByLongId(cache.msgId);
// log('下载完成后的msg', msg);
cache.path = downloadPath!;
dbUtil.updateFileCache(cache).then();
// log('下载完成后的msg', msg);
// }
}
}
// log('file found', cache);
const res: GetFileResponse = {
file: cache.path,
url: cache.url,
file_size: cache.size.toString(),
file_name: cache.name
};
if (enableLocalFile2Url) {
if (!cache.url) {
try {
res.base64 = await fs.readFile(cache.path, 'base64');
} catch (e) {
throw new Error('文件下载失败. ' + e);
}
}
}
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res;
}
}
export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile;
protected async _handle(payload: { file_id: string, file: string }): Promise<GetFileResponse> {
if (!payload.file_id) {
throw new Error('file_id 不能为空');
}
payload.file = payload.file_id;
return super._handle(payload);
}
}

View File

@@ -0,0 +1,7 @@
import { GetFileBase } from './GetFile';
import { ActionName } from '../types';
export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage;
}

View File

@@ -0,0 +1,15 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile';
import { ActionName } from '../types';
interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
}
export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord;
protected async _handle(payload: Payload): Promise<GetFileResponse> {
const res = super._handle(payload);
return res;
}
}

View File

@@ -0,0 +1,73 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import fs from 'fs';
import { join as joinPath } from 'node:path';
import { calculateFileMD5, getTempDir, httpDownload } from '@/common/utils/file';
import { v4 as uuid4 } from 'uuid';
interface Payload {
thread_count?: number;
url?: string;
base64?: string;
name?: string;
headers?: string | string[];
}
interface FileResponse {
file: string;
}
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile;
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name;
const name = payload.name || uuid4();
const filePath = joinPath(getTempDir(), name);
if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64');
} else if (payload.url) {
const headers = this.getHeaders(payload.headers);
const buffer = await httpDownload({ url: payload.url, headers: headers });
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
} else {
throw new Error('不存在任何文件, 无法下载');
}
if (fs.existsSync(filePath)) {
if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath);
const newPath = joinPath(getTempDir(), md5);
fs.renameSync(filePath, newPath);
return { file: newPath };
}
return { file: filePath };
} else {
throw new Error('文件写入失败, 检查权限');
}
}
getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers: Record<string, string> = {};
if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]');
}
if (Array.isArray(headersIn)) {
for (const headerItem of headersIn) {
const spilt = headerItem.indexOf('=');
if (spilt < 0) {
headers[headerItem] = '';
} else {
const key = headerItem.substring(0, spilt);
headers[key] = headerItem.substring(0, spilt + 1);
}
}
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/octet-stream';
}
return headers;
}
}

View File

@@ -0,0 +1,43 @@
import BaseAction from '../BaseAction';
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types';
import { NTQQMsgApi } from '@/core/qqnt/apis';
import { dbUtil } from '@/common/utils/db';
import { OB11Constructor } from '../../constructor';
import { ActionName } from '../types';
interface Payload {
message_id: string; // long msg id
}
interface Response {
messages: (OB11Message & { content: OB11MessageData })[];
}
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetForwardMsg;
protected async _handle(payload: Payload): Promise<any> {
const rootMsg = await dbUtil.getMsgByLongId(payload.message_id);
if (!rootMsg) {
throw Error('msg not found');
}
const data = await NTQQMsgApi.getMultiMsg({
chatType: rootMsg.chatType,
peerUid: rootMsg.peerUid
}, rootMsg.msgId, rootMsg.msgId);
if (!data || data.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg);
}
const msgList = data.msgList;
const messages = await Promise.all(msgList.map(async msg => {
const resMsg = await OB11Constructor.message(msg);
resMsg.message_id = await dbUtil.addMsg(msg);
return resMsg;
}));
messages.map(msg => {
(<OB11ForwardMessage>msg).content = msg.message;
delete (<any>msg).message;
});
return {messages};
}
}

View File

@@ -0,0 +1,43 @@
import BaseAction from '../BaseAction';
import { OB11Message, OB11User } from '../../types';
import { getGroup, groups } from '@/common/data';
import { ActionName } from '../types';
import { ChatType } from '@/core/qqnt/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/qqnt/apis/msg';
import { OB11Constructor } from '../../constructor';
interface Payload {
group_id: number
message_seq: number,
count: number
}
interface Response {
messages: OB11Message[];
}
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
protected async _handle(payload: Payload): Promise<Response> {
const group = await getGroup(payload.group_id.toString());
if (!group) {
throw `${payload.group_id}不存在`;
}
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0';
// log("startMsgId", startMsgId)
const historyResult = (await NTQQMsgApi.getMsgHistory({
chatType: ChatType.group,
peerUid: group.groupCode
}, startMsgId, parseInt(payload.count?.toString()) || 20));
console.log(historyResult);
const msgList = historyResult.msgList;
await Promise.all(msgList.map(async msg => {
msg.id = await dbUtil.addMsg(msg);
}));
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList };
}
}

View File

@@ -0,0 +1,22 @@
import BaseAction from '../BaseAction';
import { OB11User } from '../../types';
import { getUidByUin, uid2UinMap } from '@/common/data';
import { OB11Constructor } from '../../constructor';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/qqnt/apis/user';
import { log } from '@/common/utils/log';
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo;
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString();
log('uidMaps', uid2UinMap);
const uid = getUidByUin(user_id);
if (!uid) {
throw new Error('查无此人');
}
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid));
}
}

View File

@@ -0,0 +1,20 @@
import SendMsg, { convertMessage2List } from '../msg/SendMsg';
import { OB11PostSendMsg } from '../../types';
import { ActionName } from '../types';
export class GoCQHTTPSendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg;
protected async check(payload: OB11PostSendMsg) {
if (payload.messages) payload.message = convertMessage2List(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
}

View File

@@ -0,0 +1,37 @@
import BaseAction from '../BaseAction';
import { getGroup } from '@/common/data';
import { ActionName } from '../types';
import { SendMsgElementConstructor } from '@/core/qqnt/entities/constructor';
import { ChatType, SendFileElement } from '@/core/qqnt/entities';
import fs from 'fs';
import { NTQQMsgApi } from '@/core/qqnt/apis/msg';
import { uri2local } from '@/common/utils/file';
interface Payload {
group_id: number;
file: string;
name: string;
folder: string;
}
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile;
protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id.toString());
if (!group) {
throw new Error(`群组${payload.group_id}不存在`);
}
let file = payload.file;
if (fs.existsSync(file)) {
file = `file://${file}`;
}
const downloadResult = await uri2local(file);
if (downloadResult.errMsg) {
throw new Error(downloadResult.errMsg);
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await NTQQMsgApi.sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle]);
return null;
}
}

View File

@@ -0,0 +1,24 @@
import { getGroup } from '@/common/data';
import { OB11Group } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
interface PayloadType {
group_id: number
}
class GetGroupInfo extends BaseAction<PayloadType, OB11Group> {
actionName = ActionName.GetGroupInfo;
protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString());
if (group) {
return OB11Constructor.group(group);
} else {
throw `${payload.group_id}不存在`;
}
}
}
export default GetGroupInfo;

View File

@@ -0,0 +1,20 @@
import { OB11Group } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { groups } from '@/common/data';
class GetGroupList extends BaseAction<null, OB11Group[]> {
actionName = ActionName.GetGroupList;
protected async _handle(payload: null) {
// if (groups.length === 0) {
// const groups = await NTQQGroupApi.getGroups(true)
// log("get groups", groups)
// }
return OB11Constructor.groups(Array.from(groups.values()));
}
}
export default GetGroupList;

View File

@@ -0,0 +1,38 @@
import { OB11GroupMember } from '../../types';
import { getGroupMember } from '../../../common/data';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/qqnt/apis/user';
import { log } from '../../../common/utils/log';
import { isNull } from '../../../common/utils/helper';
export interface PayloadType {
group_id: number;
user_id: number;
}
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo;
protected async _handle(payload: PayloadType) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString());
// log(member);
if (member) {
log('获取群成员详细信息');
try {
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
log('群成员详细信息结果', info);
Object.assign(member, info);
} catch (e) {
log('获取群成员详细信息失败, 只能返回基础信息', e);
}
return OB11Constructor.groupMember(payload.group_id.toString(), member);
} else {
throw (`群成员${payload.user_id}不存在`);
}
}
}
export default GetGroupMemberInfo;

View File

@@ -0,0 +1,26 @@
import { getGroup } from '@/common/data';
import { OB11GroupMember } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { napCatCore } from '@/core';
export interface PayloadType {
group_id: number
}
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList;
protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString());
if (group) {
return OB11Constructor.groupMembers(group);
} else {
throw (`${payload.group_id}不存在`);
}
}
}
export default GetGroupMemberList;

View File

@@ -0,0 +1,10 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
export default class GetGuildList extends BaseAction<null, null> {
actionName = ActionName.GetGuildList;
protected async _handle(payload: null): Promise<null> {
return null;
}
}

View File

@@ -0,0 +1,16 @@
import SendMsg from '../msg/SendMsg';
import { ActionName, BaseCheckResult } from '../types';
import { OB11PostSendMsg } from '../../types';
class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg;
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
delete payload.user_id;
payload.message_type = 'group';
return super.check(payload);
}
}
export default SendGroupMsg;

View File

@@ -0,0 +1,31 @@
import BaseAction from '../BaseAction';
import { GroupRequestOperateTypes } from '@/core/qqnt/entities';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
import { groupNotifies } from '@/common/data';
interface Payload {
flag: string,
// sub_type: "add" | "invite",
// type: "add" | "invite"
approve: boolean,
reason: string
}
export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest;
protected async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString();
const approve = payload.approve.toString() === 'true';
const notify = groupNotifies[flag];
if (!notify) {
throw `${flag}对应的加群通知不存在`;
}
await NTQQGroupApi.handleGroupRequest(notify,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason
);
return null;
}
}

View File

@@ -0,0 +1,25 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '@/common/data';
import { GroupMemberRole } from '@/core/qqnt/entities';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
interface Payload {
group_id: number,
user_id: number,
enable: boolean
}
export default class SetGroupAdmin extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAdmin;
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id);
const enable = payload.enable.toString() === 'true';
if (!member) {
throw `群成员${payload.user_id}不存在`;
}
await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, enable ? GroupMemberRole.admin : GroupMemberRole.normal);
return null;
}
}

View File

@@ -0,0 +1,24 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '../../../common/data';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
interface Payload {
group_id: number,
user_id: number,
duration: number
}
export default class SetGroupBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupBan;
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id);
if (!member) {
throw `群成员${payload.user_id}不存在`;
}
await NTQQGroupApi.banMember(payload.group_id.toString(),
[{ uid: member.uid, timeStamp: parseInt(payload.duration.toString()) }]);
return null;
}
}

View File

@@ -0,0 +1,23 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '../../../common/data';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
interface Payload {
group_id: number,
user_id: number,
card: string
}
export default class SetGroupCard extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupCard;
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id);
if (!member) {
throw `群成员${payload.user_id}不存在`;
}
await NTQQGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || '');
return null;
}
}

View File

@@ -0,0 +1,23 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '../../../common/data';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
interface Payload {
group_id: number,
user_id: number,
reject_add_request: boolean
}
export default class SetGroupKick extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupKick;
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id);
if (!member) {
throw `群成员${payload.user_id}不存在`;
}
await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request);
return null;
}
}

View File

@@ -0,0 +1,22 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
import { log } from '../../../common/utils/log';
interface Payload {
group_id: number,
is_dismiss: boolean
}
export default class SetGroupLeave extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupLeave;
protected async _handle(payload: Payload): Promise<any> {
try {
await NTQQGroupApi.quitGroup(payload.group_id.toString());
} catch (e) {
log('退群失败', e);
throw e;
}
}
}

View File

@@ -0,0 +1,18 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
interface Payload {
group_id: number,
group_name: string
}
export default class SetGroupName extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupName;
protected async _handle(payload: Payload): Promise<null> {
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name);
return null;
}
}

View File

@@ -0,0 +1,18 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
interface Payload {
group_id: number,
enable: boolean
}
export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan;
protected async _handle(payload: Payload): Promise<null> {
const enable = payload.enable.toString() === 'true';
await NTQQGroupApi.banGroup(payload.group_id.toString(), enable);
return null;
}
}

View File

@@ -0,0 +1,104 @@
import GetMsg from './msg/GetMsg';
import GetLoginInfo from './system/GetLoginInfo';
import GetFriendList from './user/GetFriendList';
import GetGroupList from './group/GetGroupList';
import GetGroupInfo from './group/GetGroupInfo';
import GetGroupMemberList from './group/GetGroupMemberList';
import GetGroupMemberInfo from './group/GetGroupMemberInfo';
import SendGroupMsg from './group/SendGroupMsg';
import SendPrivateMsg from './msg/SendPrivateMsg';
import SendMsg from './msg/SendMsg';
import DeleteMsg from './msg/DeleteMsg';
import BaseAction from './BaseAction';
import GetVersionInfo from './system/GetVersionInfo';
import CanSendRecord from './system/CanSendRecord';
import CanSendImage from './system/CanSendImage';
import GetStatus from './system/GetStatus';
import {
GoCQHTTPSendForwardMsg,
GoCQHTTPSendGroupForwardMsg,
GoCQHTTPSendPrivateForwardMsg
} from './go-cqhttp/SendForwardMsg';
import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo';
import SendLike from './user/SendLike';
import SetGroupAddRequest from './group/SetGroupAddRequest';
import SetGroupLeave from './group/SetGroupLeave';
import GetGuildList from './group/GetGuildList';
import Debug from './llonebot/Debug';
import SetFriendAddRequest from './user/SetFriendAddRequest';
import SetGroupWholeBan from './group/SetGroupWholeBan';
import SetGroupName from './group/SetGroupName';
import SetGroupBan from './group/SetGroupBan';
import SetGroupKick from './group/SetGroupKick';
import SetGroupAdmin from './group/SetGroupAdmin';
import SetGroupCard from './group/SetGroupCard';
import GetImage from './file/GetImage';
import GetRecord from './file/GetRecord';
import GoCQHTTPMarkMsgAsRead from './msg/MarkMsgAsRead';
import CleanCache from './system/CleanCache';
import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile';
import { GetConfigAction, SetConfigAction } from './llonebot/Config';
import GetGroupAddRequest from './llonebot/GetGroupAddRequest';
import SetQQAvatar from './llonebot/SetQQAvatar';
import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile';
import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory';
import GetFile from './file/GetFile';
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg';
export const actionHandlers = [
new GetFile(),
new Debug(),
// new GetConfigAction(),
// new SetConfigAction(),
// new GetGroupAddRequest(),
new SetQQAvatar(),
// onebot11
new SendLike(),
new GetMsg(),
new GetLoginInfo(),
new GetFriendList(),
new GetGroupList(), new GetGroupInfo(),
new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg(),
new SetGroupAddRequest(),
new SetFriendAddRequest(),
new SetGroupLeave(),
new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage(),
new GetStatus(),
new SetGroupWholeBan(),
new SetGroupBan(),
new SetGroupKick(),
new SetGroupAdmin(),
new SetGroupName(),
new SetGroupCard(),
new GetImage(),
new GetRecord(),
// new CleanCache(),
//以下为go-cqhttp api
new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GoCQHTTPDownloadFile(),
new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(),
];
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>();
for (const action of actionHandlers) {
actionMap.set(action.actionName, action);
}
return actionMap;
}
export const actionMap = initActionMap();

View File

@@ -0,0 +1,20 @@
import BaseAction from '../BaseAction';
import { OB11Config, ob11Config } from '@/onebot11/config';
import { ActionName } from '../types';
export class GetConfigAction extends BaseAction<null, OB11Config> {
actionName = ActionName.GetConfig;
protected async _handle(payload: null): Promise<OB11Config> {
return ob11Config;
}
}
export class SetConfigAction extends BaseAction<OB11Config, void> {
actionName = ActionName.SetConfig;
protected async _handle(payload: OB11Config): Promise<void> {
ob11Config.save(payload);
}
}

View File

@@ -0,0 +1,44 @@
import BaseAction from '../BaseAction';
// import * as ntqqApi from "../../../ntqqapi/api";
import {
NTQQMsgApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQUserApi,
NTQQFileApi,
// NTQQFileCacheApi,
NTQQWindowApi,
} from '@/core/qqnt/apis';
import { ActionName } from '../types';
import { log } from '../../../common/utils/log';
interface Payload {
method: string,
args: any[],
}
export default class Debug extends BaseAction<Payload, any> {
actionName = ActionName.Debug;
protected async _handle(payload: Payload): Promise<any> {
log('debug call ntqq api', payload);
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi,
// NTQQFileCacheApi,
NTQQWindowApi];
for (const ntqqApiClass of ntqqApi) {
log('ntqqApiClass', ntqqApiClass);
const method = (<any>ntqqApiClass)[payload.method];
if (method) {
const result = method(...payload.args);
if (method.constructor.name === 'AsyncFunction') {
return await result;
}
return result;
}
}
throw `${payload.method}方法 不存在`;
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

@@ -0,0 +1,33 @@
import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { uid2UinMap } from '../../../common/data';
import { NTQQUserApi } from '@/core/qqnt/apis/user';
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
import { log } from '../../../common/utils/log';
interface OB11GroupRequestNotify {
group_id: number,
user_id: number,
flag: string
}
export default class GetGroupAddRequest extends BaseAction<null, OB11GroupRequestNotify[]> {
actionName = ActionName.GetGroupIgnoreAddRequest;
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQGroupApi.getGroupIgnoreNotifies();
// log(data);
const notifies: GroupNotify[] = data.notifies.filter(notify => notify.status === GroupNotifyStatus.WAIT_HANDLE);
const returnData: OB11GroupRequestNotify[] = [];
for (const notify of notifies) {
const uin = uid2UinMap[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin;
returnData.push({
group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin),
flag: notify.seq
});
}
return returnData;
}
}

View File

@@ -0,0 +1,43 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import * as fs from 'node:fs';
import { NTQQUserApi } from '@/core/qqnt/apis/user';
import { checkFileReceived, uri2local } from '../../../common/utils/file';
// import { log } from "../../../common/utils";
interface Payload {
file: string
}
export default class SetAvatar extends BaseAction<Payload, null> {
actionName = ActionName.SetQQAvatar;
protected async _handle(payload: Payload): Promise<null> {
const { path, isLocal, errMsg } = (await uri2local(payload.file));
if (errMsg){
throw `头像${payload.file}设置失败,file字段可能格式不正确`;
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQUserApi.setQQAvatar(path);
if (!isLocal){
fs.unlink(path, () => {});
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`;
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
if (ret['result'] == 1004022) {
throw `头像${payload.file}设置失败,文件可能不是图片格式`;
} else if(ret['result'] != 0) {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`;
}
} else {
if (!isLocal){
fs.unlink(path, () => {});
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
import { NTQQMsgApi } from '@/core/qqnt/apis';
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { dbUtil } from '@/common/utils/db';
import { napCatCore } from '@/core';
interface Payload {
message_id: number
}
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg;
protected async _handle(payload: Payload) {
const msg = await dbUtil.getMsgByShortId(payload.message_id);
if (msg) {
await NTQQMsgApi.recallMsg({ peerUid: msg.peerUid, chatType: msg.chatType }, [msg.msgId]);
}
}
}
export default DeleteMsg;

View File

@@ -0,0 +1,33 @@
import { OB11Message } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { dbUtil } from '@/common/utils/db';
export interface PayloadType {
message_id: number
}
export type ReturnDataType = OB11Message
class GetMsg extends BaseAction<PayloadType, OB11Message> {
actionName = ActionName.GetMsg;
protected async _handle(payload: PayloadType) {
// log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id) {
throw ('参数message_id不能为空');
}
let msg = await dbUtil.getMsgByShortId(payload.message_id);
if (!msg) {
msg = await dbUtil.getMsgByLongId(payload.message_id.toString());
}
if (!msg) {
throw ('消息不存在');
}
return await OB11Constructor.message(msg);
}
}
export default GetMsg;

View File

@@ -0,0 +1,14 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
interface Payload{
message_id: number
}
export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null>{
actionName = ActionName.GoCQHTTP_MarkMsgAsRead;
protected async _handle(payload: Payload): Promise<null> {
return null;
}
}

View File

@@ -0,0 +1,532 @@
import {
AtType,
ChatType,
ElementType,
Group, PicSubType,
RawMessage,
SendArkElement,
SendMessageElement,
Peer
} from '@/core/qqnt/entities';
import {
OB11MessageCustomMusic,
OB11MessageData,
OB11MessageDataType,
OB11MessageMixType,
OB11MessageNode,
OB11PostSendMsg
} from '../../types';
import { SendMsgElementConstructor } from '@/core/qqnt/entities/constructor';
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import * as fs from 'node:fs';
import { decodeCQCode } from '../../cqcode';
import { dbUtil } from '@/common/utils/db';
import { log } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import { uri2local } from '@/common/utils/file';
import { getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } from '@/common/data';
import { NTQQMsgApi } from '@/core/qqnt/apis/msg';
const ALLOW_SEND_TEMP_MSG = false;
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (const msg of sendMsgList) {
if (msg['type'] && msg['data']) {
const type = msg['type'];
const data = msg['data'];
if (type === 'text' && !data['text']) {
return 400;
} else if (['image', 'voice', 'record'].includes(type)) {
if (!data['file']) {
return 400;
} else {
if (checkUri(data['file'])) {
return 200;
} else {
return 400;
}
}
} else if (type === 'at' && !data['qq']) {
return 400;
} else if (type === 'reply' && !data['id']) {
return 400;
}
} else {
return 400;
}
}
return 200;
}
export interface ReturnDataType {
message_id: number;
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') {
if (!autoEscape) {
message = decodeCQCode(message.toString());
} else {
message = [{
type: OB11MessageDataType.text,
data: {
text: message
}
}];
}
} else if (!Array.isArray(message)) {
message = [message];
}
return message;
}
export async function createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) {
const sendElements: SendMessageElement[] = [];
const deleteAfterSentFiles: string[] = [];
for (const sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue;
}
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text;
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text));
}
}
break;
case OB11MessageDataType.at: {
if (!group) {
continue;
}
let atQQ = sendMsg.data?.qq;
if (atQQ) {
atQQ = atQQ.toString();
if (atQQ === 'all') {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'));
} else {
// const atMember = group?.members.find(m => m.uin == atQQ)
const atMember = await getGroupMember(group?.groupCode, atQQ);
if (atMember) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick));
}
}
}
}
break;
case OB11MessageDataType.reply: {
const replyMsgId = sendMsg.data.id;
if (replyMsgId) {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId));
if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!));
}
}
}
break;
case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id;
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)));
}
}
break;
case OB11MessageDataType.image:
case OB11MessageDataType.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: {
const file = sendMsg.data?.file;
const payloadFileName = sendMsg.data?.name;
if (file) {
// todo: 使用缓存文件发送
// const cache = await dbUtil.getFileCache(file);
// if (cache) {
// if (fs.existsSync(cache.filePath)) {
// file = "file://" + cache.filePath;
// } else if (cache.downloadFunc) {
// await cache.downloadFunc();
// file = cache.filePath;
// } else if (cache.url) {
// file = cache.url;
// }
// log("找到文件缓存", file);
// }
const {path, isLocal, fileName, errMsg} = (await uri2local(file));
if (errMsg) {
throw errMsg;
}
if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path);
}
if (sendMsg.type === OB11MessageDataType.file) {
log('发送文件', path, payloadFileName || fileName);
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName));
} else if (sendMsg.type === OB11MessageDataType.video) {
log('发送视频', path, payloadFileName || fileName);
let thumb = sendMsg.data?.thumb;
if (thumb) {
const uri2LocalRes = await uri2local(thumb);
if (uri2LocalRes.success) {
thumb = uri2LocalRes.path;
}
}
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb));
} else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path));
} else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(await SendMsgElementConstructor.pic(path, sendMsg.data.summary || '', <PicSubType>parseInt(sendMsg.data?.subType?.toString() || '0')));
}
}
}
}
break;
case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data));
}
break;
}
}
return {
sendElements,
deleteAfterSentFiles
};
}
export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) {
throw ('消息体无法解析');
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000);
try {
returnMsg.id = await dbUtil.addMsg(returnMsg, false);
} catch (e: any) {
log('发送消息id获取失败', e);
returnMsg.id = 0;
}
// log('消息发送结果', returnMsg);
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}));
return returnMsg;
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg;
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message);
const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node);
if (fmNum && fmNum != messages.length) {
return {
valid: false,
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素'
};
}
if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) {
return {
valid: false,
message: `${payload.group_id}不存在`
};
}
if (payload.user_id && payload.message_type !== 'group') {
if (!(await getFriend(payload.user_id))) {
if (!ALLOW_SEND_TEMP_MSG
&& !(await dbUtil.getUidByTempUin(payload.user_id.toString()))
) {
return {
valid: false,
message: '不能发送临时消息'
};
}
}
}
return {
valid: true,
};
}
protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> {
const peer: Peer = {
chatType: ChatType.friend,
peerUid: ''
};
let isTempMsg = false;
let group: Group | undefined = undefined;
const genGroupPeer = async () => {
if (payload.group_id) {
group = await getGroup(payload.group_id.toString());
if (group) {
peer.chatType = ChatType.group;
// peer.name = group.name
peer.peerUid = group.groupCode;
}
}
};
const genFriendPeer = async () => {
if (!payload.user_id) {
return;
}
const friend = await getFriend(payload.user_id.toString());
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid;
} else {
peer.chatType = ChatType.temp;
const tempUserUid = getUidByUin(payload.user_id.toString());
if (!tempUserUid) {
throw (`找不到私聊对象${payload.user_id}`);
}
// peer.name = tempUser.nickName
isTempMsg = true;
peer.peerUid = tempUserUid;
}
};
if (payload?.group_id && payload.message_type === 'group') {
await genGroupPeer();
} else if (payload?.user_id) {
await genFriendPeer();
} else if (payload.group_id) {
await genGroupPeer();
} else {
throw ('发送消息参数错误, 请指定group_id或user_id');
}
const messages = convertMessage2List(payload.message);
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group);
return {message_id: returnMsg!.id!};
} catch (e: any) {
throw ('发送转发消息失败 ' + e.toString());
}
} else {
if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) {
const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
if (music) {
const {url, audio, title, content, image} = music.data;
const selfPeer: Peer = {peerUid: selfInfo.uid, chatType: ChatType.friend};
// 搞不定!
// const musicMsg = await this.send(selfPeer, [this.genMusicElement(url, audio, title, content, image)], [], false)
// 转发
// const res = await NTQQApi.forwardMsg(selfPeer, peer, [musicMsg.msgId])
// log("转发音乐消息成功", res);
// return {message_id: musicMsg.msgShortId}
}
}
}
// log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await createSendElements(messages, group);
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles);
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}));
const res = {message_id: returnMsg.id!};
// console.log(res);
return res;
}
private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == msgType).length;
}
return 0;
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
log('克隆的目标消息', msg);
const sendElements: SendMessageElement[] = [];
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement);
// Object.keys(ele).forEach((eleKey) => {
// if (eleKey.endsWith("Element")) {
// }
}
if (sendElements.length === 0) {
log('需要clone的消息无法解析将会忽略掉', msg);
}
log('克隆消息', sendElements);
try {
const nodeMsg = await NTQQMsgApi.sendMsg({
chatType: ChatType.friend,
peerUid: selfInfo.uid
}, sendElements, true);
await sleep(500);
return nodeMsg;
} catch (e) {
log(e, '克隆转发消息失败,将忽略本条消息', msg);
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined): Promise<RawMessage | null> {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
};
let nodeMsgIds: string[] = [];
// 先判断一遍是不是id和自定义混用
const needClone = messageNodes.filter(node => node.data.id).length && messageNodes.filter(node => !node.data.id).length;
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
const nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId));
if (!needClone) {
nodeMsgIds.push(nodeMsg!.msgId);
} else {
if (nodeMsg!.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg!);
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId);
}
}
}
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const {
sendElements,
deleteAfterSentFiles
} = await createSendElements(convertMessage2List(messageNode.data.content), group);
log('开始生成转发节点', sendElements);
const sendElementsSplit: SendMessageElement[][] = [];
let splitIndex = 0;
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = [];
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++;
}
sendElementsSplit[splitIndex] = [ele];
splitIndex++;
} else {
sendElementsSplit[splitIndex].push(ele);
}
log(sendElementsSplit);
}
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(selfPeer, eles, [], true);
nodeMsgIds.push(nodeMsg.msgId);
await sleep(500);
log('转发节点生成成功', nodeMsg.msgId);
}
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}));
} catch (e) {
log('生成转发消息节点失败', e);
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: Array<RawMessage> = [];
let srcPeer: Peer | undefined = undefined;
let needSendSelf = false;
for (const [index, msgId] of nodeMsgIds.entries()) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId);
if (nodeMsg) {
nodeMsgArray.push(nodeMsg);
if (!srcPeer) {
srcPeer = {chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid};
} else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true;
srcPeer = selfPeer;
}
}
}
log('nodeMsgArray', nodeMsgArray);
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
if (needSendSelf) {
log('需要克隆转发消息');
for (const [index, msg] of nodeMsgArray.entries()) {
if (msg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(msg);
if (cloneMsg) {
nodeMsgIds[index] = cloneMsg.msgId;
}
}
}
}
// elements之间用换行符分隔
// let _sendForwardElements: SendMessageElement[] = []
// for(let i = 0; i < sendForwardElements.length; i++){
// _sendForwardElements.push(sendForwardElements[i])
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
// }
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
// nodeIds.push(nodeMsg.msgId)
// await sleep(500);
// 开发转发
try {
log('开发转发', nodeMsgIds);
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds);
} catch (e) {
log('forward failed', e);
return null;
}
}
private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
const musicJson = {
app: 'com.tencent.structmsg',
config: {
ctime: 1709689928,
forward: 1,
token: '5c1e4905f926dd3a64a4bd3841460351',
type: 'normal'
},
extra: {app_type: 1, appid: 100497308, uin: selfInfo.uin},
meta: {
news: {
action: '',
android_pkg_name: '',
app_type: 1,
appid: 100497308,
ctime: 1709689928,
desc: content || title,
jumpUrl: url,
musicUrl: audio,
preview: image,
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
source_url: '',
tag: 'QQ音乐',
title: title,
uin: selfInfo.uin,
}
},
prompt: content || title,
ver: '0.0.0.1',
view: 'news'
};
return SendMsgElementConstructor.ark(musicJson);
}
}
export default SendMsg;

View File

@@ -0,0 +1,14 @@
import SendMsg from './SendMsg';
import { ActionName, BaseCheckResult } from '../types';
import { OB11PostSendMsg } from '../../types';
class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg;
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
payload.message_type = 'private';
return super.check(payload);
}
}
export default SendPrivateMsg;

View File

@@ -0,0 +1,10 @@
import { ActionName } from '../types';
import CanSendRecord from './CanSendRecord';
interface ReturnType {
yes: boolean
}
export default class CanSendImage extends CanSendRecord {
actionName = ActionName.CanSendImage;
}

View File

@@ -0,0 +1,16 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
interface ReturnType {
yes: boolean
}
export default class CanSendRecord extends BaseAction<any, ReturnType> {
actionName = ActionName.CanSendRecord;
protected async _handle(payload): Promise<ReturnType> {
return {
yes: true
};
}
}

View File

@@ -0,0 +1,105 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import fs from 'fs';
import Path from 'path';
import {
ChatType,
ChatCacheListItemBasic,
CacheFileType
} from '../../../ntqqapi/types';
import { dbUtil } from '../../../common/db';
import { NTQQFileApi, NTQQFileCacheApi } from '@/core/qqnt/apis/file';
export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache;
protected _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => {
try {
// dbUtil.clearCache();
const cacheFilePaths: string[] = [];
await NTQQFileCacheApi.setCacheSilentScan(false);
cacheFilePaths.push((await NTQQFileCacheApi.getHotUpdateCachePath()));
cacheFilePaths.push((await NTQQFileCacheApi.getDesktopTmpPath()));
(await NTQQFileCacheApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value));
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQFileCacheApi.scanCache();
const cacheSize = parseInt(cacheScanResult.size[6]);
if (cacheScanResult.result !== 0) {
throw('Something went wrong while scanning cache. Code: ' + cacheScanResult.result);
}
await NTQQFileCacheApi.setCacheSilentScan(true);
if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作
// await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了
deleteCachePath(cacheFilePaths);
}
// 获取聊天记录列表
// NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关
// const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息
// const groupChatCache = await getCacheList(ChatType.group); // 群聊消息
// const chatCacheList = [ ...privateChatCache, ...groupChatCache ];
const chatCacheList: ChatCacheListItemBasic[] = [];
// 获取聊天缓存文件列表
const cacheFileList: string[] = [];
for (const name in CacheFileType) {
if (!isNaN(parseInt(name))) continue;
const fileTypeAny: any = CacheFileType[name];
const fileType: CacheFileType = fileTypeAny;
cacheFileList.push(...(await NTQQFileCacheApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey));
}
// 一并清除
await NTQQFileCacheApi.clearChatCache(chatCacheList, cacheFileList);
res();
} catch(e) {
console.error('清理缓存时发生了错误');
rej(e);
}
});
}
}
function deleteCachePath(pathList: string[]) {
const emptyPath = (path: string) => {
if (!fs.existsSync(path)) return;
const files = fs.readdirSync(path);
files.forEach(file => {
const filePath = Path.resolve(path, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) emptyPath(filePath);
else fs.unlinkSync(filePath);
});
fs.rmdirSync(path);
};
for (const path of pathList) {
emptyPath(path);
}
}
function getCacheList(type: ChatType) { // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理
return new Promise<Array<ChatCacheListItemBasic>>((res, rej) => {
NTQQFileCacheApi.getChatCacheList(type, 1000, 0)
.then(data => {
const list = data.infos.filter(e => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0);
const result = list.map(e => {
const result = { ...e.basicChatCacheInfo };
result.chatType = type;
result.isChecked = true;
return result;
});
res(result);
})
.catch(e => rej(e));
});
}

View File

@@ -0,0 +1,17 @@
import { selfInfo } from '@/common/data';
import { OB11User } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { napCatCore } from '@/core';
class GetLoginInfo extends BaseAction<null, OB11User> {
actionName = ActionName.GetLoginInfo;
protected async _handle(payload: null) {
return OB11Constructor.selfInfo(selfInfo);
}
}
export default GetLoginInfo;

View File

@@ -0,0 +1,16 @@
import BaseAction from '../BaseAction';
import { OB11Status } from '../../types';
import { ActionName } from '../types';
import { selfInfo } from '../../../common/data';
export default class GetStatus extends BaseAction<any, OB11Status> {
actionName = ActionName.GetStatus;
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: !!selfInfo.online,
good: true
};
}
}

View File

@@ -0,0 +1,16 @@
import BaseAction from '../BaseAction';
import { OB11Version } from '../../types';
import { ActionName } from '../types';
import { version } from '@/onebot11/version';
export default class GetVersionInfo extends BaseAction<any, OB11Version> {
actionName = ActionName.GetVersionInfo;
protected async _handle(payload: any): Promise<OB11Version> {
return {
app_name: 'NapCat.Onebot',
protocol_version: 'v11',
app_version: version
};
}
}

View File

@@ -0,0 +1,64 @@
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
export interface ValidCheckResult {
valid: true
[k: string | number]: any
}
export interface InvalidCheckResult {
valid: false
message: string
[k: string | number]: any
}
export enum ActionName {
// llonebot
GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
SetQQAvatar = 'set_qq_avatar',
GetConfig = 'get_config',
SetConfig = 'set_config',
Debug = 'llonebot_debug',
GetFile = 'get_file',
// onebot 11
SendLike = 'send_like',
GetLoginInfo = 'get_login_info',
GetFriendList = 'get_friend_list',
GetGroupInfo = 'get_group_info',
GetGroupList = 'get_group_list',
GetGroupMemberInfo = 'get_group_member_info',
GetGroupMemberList = 'get_group_member_list',
GetMsg = 'get_msg',
SendMsg = 'send_msg',
SendGroupMsg = 'send_group_msg',
SendPrivateMsg = 'send_private_msg',
DeleteMsg = 'delete_msg',
SetGroupAddRequest = 'set_group_add_request',
SetFriendAddRequest = 'set_friend_add_request',
SetGroupLeave = 'set_group_leave',
GetVersionInfo = 'get_version_info',
GetStatus = 'get_status',
CanSendRecord = 'can_send_record',
CanSendImage = 'can_send_image',
SetGroupKick = 'set_group_kick',
SetGroupBan = 'set_group_ban',
SetGroupWholeBan = 'set_group_whole_ban',
SetGroupAdmin = 'set_group_admin',
SetGroupCard = 'set_group_card',
SetGroupName = 'set_group_name',
GetImage = 'get_image',
GetRecord = 'get_record',
CleanCache = 'clean_cache',
// 以下为go-cqhttp api
GoCQHTTP_SendForwardMsg = 'send_forward_msg',
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
GetGuildList = 'get_guild_list',
GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
GoCQHTTP_UploadGroupFile = 'upload_group_file',
GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
}

View File

@@ -0,0 +1,16 @@
import { OB11User } from '../../types';
import { OB11Constructor } from '../../constructor';
import { friends } from '../../../common/data';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
class GetFriendList extends BaseAction<null, OB11User[]> {
actionName = ActionName.GetFriendList;
protected async _handle(payload: null) {
return OB11Constructor.friends(Array.from(friends.values()));
}
}
export default GetFriendList;

View File

@@ -0,0 +1,36 @@
import { NTQQUserApi } from '@/core/qqnt/apis';
import BaseAction from '../BaseAction';
import { getFriend, getUidByUin, uid2UinMap } from '../../../common/data';
import { ActionName } from '../types';
import { log } from '../../../common/utils/log';
interface Payload {
user_id: number,
times: number
}
export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike;
protected async _handle(payload: Payload): Promise<null> {
log('点赞参数', payload);
try {
const qq = payload.user_id.toString();
const friend = await getFriend(qq);
let uid: string;
if (!friend) {
uid = getUidByUin(qq) || '';
} else {
uid = friend.uid;
}
const result = await NTQQUserApi.like(uid, parseInt(payload.times?.toString()) || 1);
console.log('点赞结果', result);
if (result.result !== 0) {
throw Error(result.errMsg);
}
} catch (e) {
throw `点赞失败 ${e}`;
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQFriendApi } from '@/core/qqnt/apis/friend';
import { friendRequests } from '@/common/data';
interface Payload {
flag: string,
approve: boolean,
remark?: string,
}
export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest;
protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve.toString() === 'true';
const request = friendRequests[payload.flag];
await NTQQFriendApi.handleFriendRequest(request, approve);
return null;
}
}