chore: OneBotApi

This commit is contained in:
手瓜一十雪
2024-08-09 15:44:45 +08:00
parent 7842cd0bc0
commit 8a4add257f
111 changed files with 4493 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ import { SelfInfo, LineDevice, SelfStatusInfo } from "./entities";
import { LegacyNTEventWrapper } from "@/common/framework/event-legacy"; import { LegacyNTEventWrapper } from "@/common/framework/event-legacy";
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "./apis"; import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "./apis";
import os from "node:os"; import os from "node:os";
import { NTQQCollectionApi } from "./apis/collection";
export enum NapCatCoreWorkingEnv { export enum NapCatCoreWorkingEnv {
Unknown = 0, Unknown = 0,
Shell = 1, Shell = 1,
@@ -42,6 +43,7 @@ export class NapCatCore {
this.eventWrapper = new LegacyNTEventWrapper(context.wrapper, context.session); this.eventWrapper = new LegacyNTEventWrapper(context.wrapper, context.session);
this.initNapCatCoreListeners().then().catch(console.error); this.initNapCatCoreListeners().then().catch(console.error);
this.ApiContext = { this.ApiContext = {
CollectionApi:new NTQQCollectionApi(this.context, this),
WebApi: new NTQQWebApi(this.context, this), WebApi: new NTQQWebApi(this.context, this),
FriendApi: new NTQQFriendApi(this.context, this), FriendApi: new NTQQFriendApi(this.context, this),
MsgApi: new NTQQMsgApi(this.context, this), MsgApi: new NTQQMsgApi(this.context, this),

View File

@@ -5,6 +5,7 @@ import { SelfInfo } from "../entities";
import { NodeIKernelLoginService } from "../services"; import { NodeIKernelLoginService } from "../services";
import { WrapperNodeApi, NodeIQQNTWrapperSession } from "@/core"; import { WrapperNodeApi, NodeIQQNTWrapperSession } from "@/core";
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "../apis"; import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "../apis";
import { NTQQCollectionApi } from "../apis/collection";
export interface InstanceContext { export interface InstanceContext {
readonly workingEnv: NapCatCoreWorkingEnv; readonly workingEnv: NapCatCoreWorkingEnv;
@@ -15,6 +16,7 @@ export interface InstanceContext {
readonly basicInfoWrapper: QQBasicInfoWrapper; readonly basicInfoWrapper: QQBasicInfoWrapper;
} }
export interface NTApiContext { export interface NTApiContext {
CollectionApi: NTQQCollectionApi,
WebApi: NTQQWebApi, WebApi: NTQQWebApi,
FriendApi: NTQQFriendApi, FriendApi: NTQQFriendApi,
MsgApi: NTQQMsgApi, MsgApi: NTQQMsgApi,

View File

@@ -0,0 +1,64 @@
import { ActionName, BaseCheckResult } from './types';
import { OB11Response } from './OB11Response';
import { OB11Return } from '@/onebot/types';
import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
import { NapCatCore } from '@/core';
class BaseAction<PayloadType, ReturnDataType> {
actionName!: ActionName;
CoreContext!: NapCatCore;
private validate: undefined | ValidateFunction<any> = undefined;
PayloadSchema: any = undefined;
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
if (this.PayloadSchema) {
this.validate = new Ajv({ allowUnionTypes: true }).compile(this.PayloadSchema);
}
if (this.validate && !this.validate(payload)) {
const errors = this.validate.errors as ErrorObject[];
const errorMessages: string[] = errors.map((e) => {
return `Key: ${e.instancePath.split('/').slice(1).join('.')}, Message: ${e.message}`;
});
return {
valid: false,
message: errorMessages.join('\n') as string || '未知错误'
};
}
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: any) {
this.CoreContext.context.logger.logError('发生错误', 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: any) {
this.CoreContext.context.logger.logError('发生错误', 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,27 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
rawData: { type: 'string' },
brief: { type: 'string' }
},
required: ['brief', 'rawData'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class CreateCollection extends BaseAction<Payload, any> {
actionName = ActionName.CreateCollection;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
return await this.CoreContext.getApiContext().CollectionApi.createCollection(
this.CoreContext.selfInfo.uin,
this.CoreContext.selfInfo.uid,
this.CoreContext.selfInfo.nick,
payload.brief, payload.rawData
);
}
}

View File

@@ -0,0 +1,43 @@
import BaseAction from '../BaseAction';
// import * as ntqqApi from "../../../ntqqapi/api";
import {
NTQQMsgApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQUserApi,
NTQQFileApi,
// NTQQFileCacheApi,
} from '@/core';
import { ActionName } from '../types';
import { log, logDebug } 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> {
//logDebug('debug call ntqq api', payload);
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi,
// NTQQFileCacheApi,
];
for (const ntqqApiClass of ntqqApi) {
// logDebug('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,22 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQMsgApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
count: { type: 'number' },
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class FetchCustomFace extends BaseAction<Payload, string[]> {
actionName = ActionName.FetchCustomFace;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
//48 可能正好是QQ需要的一个页面的数量 Tagged Mlikiowa
const ret = await NTQQMsgApi.fetchFavEmojiList(payload.count || 48);
return ret.emojiInfoList.map(e => e.url);
}
}

View File

@@ -0,0 +1,32 @@
//getMsgEmojiLikesList
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQMsgApi } from '@/core/apis';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: 'string' },
group_id: { type: 'string' },
emojiId: { type: 'string' },
emojiType: { type: 'string' },
message_id: { type: ['string', 'number'] },
count: { type: 'number' }
},
required: ['emojiId', 'emojiType', 'message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class FetchEmojioLike extends BaseAction<Payload, any> {
actionName = ActionName.FetchEmojioLike;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if(!msgIdPeer) throw new Error('消息不存在');
const msg = (await NTQQMsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
const ret = await NTQQMsgApi.getMsgEmojiLikesList(msgIdPeer.Peer,msg.msgSeq,payload.emojiId,payload.emojiType,payload.count);
return ret;
}
}

View File

@@ -0,0 +1,26 @@
import { NTQQCollectionApi } from '@/core/apis/collection';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { selfInfo } from '@/core/data';
const SchemaData = {
type: 'object',
properties: {
category: { type: 'number' },
count: { type: 'number' }
},
required: ['category', 'count'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetCollectionList extends BaseAction<Payload, any> {
actionName = ActionName.GetCollectionList;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
return await NTQQCollectionApi.getAllCollection(payload.category, payload.count);
}
}

View File

@@ -0,0 +1,19 @@
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { BuddyCategoryType } from '@/core/entities/';
import { NTQQFriendApi } from '@/core';
import { OB11Constructor } from '@/onebot11/constructor';
export class GetFriendWithCategory extends BaseAction<void, any> {
actionName = ActionName.GetFriendsWithCategory;
protected async _handle(payload: void) {
if (requireMinNTQQBuild('26702')) {
//全新逻辑
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true));
} else {
throw new Error('this ntqq version not support, must be 26702 or later');
}
}
}

View File

@@ -0,0 +1,31 @@
import { GroupNotify, GroupNotifyStatus } from '@/core/entities';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user';
import { NTQQGroupApi } from '@/core/apis/group';
interface OB11GroupRequestNotify {
group_id: number,
user_id: number,
flag: string
}
export default class GetGroupAddRequest extends BaseAction<null, OB11GroupRequestNotify[] | null> {
actionName = ActionName.GetGroupIgnoreAddRequest;
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[] | null> {
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 = || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin;
// returnData.push({
// group_id: parseInt(notify.group.groupCode),
// user_id: parseInt(uin),
// flag: notify.seq
// });
// }
return null;
}
}

View File

@@ -0,0 +1,16 @@
import { selfInfo } from '@/core/data';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
export class GetProfileLike extends BaseAction<void, any> {
actionName = ActionName.GetProfileLike;
protected async _handle(payload: void) {
const ret = await NTQQUserApi.getProfileLike(selfInfo.uid);
const listdata: any[] = ret.info.userLikeInfos[0].favoriteInfo.userInfos;
for (let i = 0; i < listdata.length; i++) {
listdata[i].uin = parseInt((await NTQQUserApi.getUinByUid(listdata[i].uid)) || '');
}
return listdata;
}
}

View File

@@ -0,0 +1,11 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
export class GetRobotUinRange extends BaseAction<void, Array<any>> {
actionName = ActionName.GetRobotUinRange;
protected async _handle(payload: void) {
// console.log(await NTQQUserApi.getRobotUinRange());
return await NTQQUserApi.getRobotUinRange();
}
}

View File

@@ -0,0 +1,46 @@
import { DeviceList } from '@/onebot11/main';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { checkFileReceived, uri2local } from '@/common/utils/file';
import { NTQQSystemApi } from '@/core';
import fs from 'fs';
const SchemaData = {
type: 'object',
properties: {
image: { type: 'string' },
},
required: ['image']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class OCRImage extends BaseAction<Payload, any> {
actionName = ActionName.OCRImage;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const { path, isLocal, errMsg,success } = (await uri2local(payload.image));
if (!success) {
throw `OCR ${payload.image}失败,image字段可能格式不正确`;
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQSystemApi.ORCImage(path);
if (!isLocal) {
fs.unlink(path, () => { });
}
if (!ret) {
throw `OCR ${payload.file}失败`;
}
return ret.result;
}
if (!isLocal) {
fs.unlink(path, () => { });
}
throw `OCR ${payload.file}失败,文件可能不存在`;
}
}
export class IOCRImage extends OCRImage {
actionName = ActionName.IOCRImage;
}

View File

@@ -0,0 +1,57 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import * as fs from 'node:fs';
import { NTQQUserApi } from '@/core/apis/user';
import { checkFileReceived, uri2local } from '@/common/utils/file';
import { NTQQGroupApi } from '@/core';
// import { log } from "../../../common/utils";
interface Payload {
file: string,
groupCode: string
}
export default class SetGroupHeader extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupHeader;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
if (!payload.file || typeof payload.file != 'string' || !payload.groupCode || typeof payload.groupCode != 'string') {
return {
valid: false,
message: 'file和groupCode字段不能为空或者类型错误',
};
}
return {
valid: true,
};
}
protected async _handle(payload: Payload): Promise<any> {
const { path, isLocal, errMsg,success } = (await uri2local(payload.file));
if (!success) {
throw `头像${payload.file}设置失败,file字段可能格式不正确`;
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQGroupApi.setGroupAvatar(payload.groupCode,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']}`;
// }
return ret;
} else {
if (!isLocal) {
fs.unlink(path, () => { });
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
}
return null;
}
}

View File

@@ -0,0 +1,24 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
longNick: { type: 'string' },
},
required: [ 'longNick'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetLongNick extends BaseAction<Payload, any> {
actionName = ActionName.SetLongNick;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQUserApi.setLongNick(payload.longNick);
return ret;
}
}

View File

@@ -0,0 +1,35 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import { NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
// 设置在线状态
const SchemaData = {
type: 'object',
properties: {
status: { type: 'number' },
extStatus: { type: 'number' },
batteryStatus: { type: 'number' }
},
required: ['status', 'extStatus', 'batteryStatus'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetOnlineStatus extends BaseAction<Payload, null> {
actionName = ActionName.SetOnlineStatus;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
// 可设置状态
// { status: 10, extStatus: 1027, batteryStatus: 0 }
// { status: 30, extStatus: 0, batteryStatus: 0 }
// { status: 50, extStatus: 0, batteryStatus: 0 }
// { status: 60, extStatus: 0, batteryStatus: 0 }
// { status: 70, extStatus: 0, batteryStatus: 0 }
const ret = await NTQQUserApi.setSelfOnlineStatus(payload.status, payload.extStatus, payload.batteryStatus);
if (ret.result !== 0) {
throw new Error('设置在线状态失败');
}
return null;
}
}

View File

@@ -0,0 +1,54 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import * as fs from 'node:fs';
import { NTQQUserApi } from '@/core/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 check(payload: Payload): Promise<BaseCheckResult> {
if (!payload.file || typeof payload.file != 'string') {
return {
valid: false,
message: 'file字段不能为空或者类型错误',
};
}
return {
valid: true,
};
}
protected async _handle(payload: Payload): Promise<null> {
const { path, isLocal, errMsg,success } = (await uri2local(payload.file));
if (!success) {
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,32 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
nick: { type: 'string' },
longNick: { type: 'string' },
sex: { type: 'number' }//传Sex值建议传0
},
required: ['nick', 'longNick', 'sex'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetSelfProfile extends BaseAction<Payload, any | null> {
actionName = ActionName.SetSelfProfile;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQUserApi.modifySelfProfile({
nick: payload.nick,
longNick: payload.longNick,
sex: payload.sex,
birthday: { birthday_year: '', birthday_month: '', birthday_day: '' },
location: undefined
});
return ret;
}
}

View File

@@ -0,0 +1,28 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import { napCatCore, NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
cmd: { type: 'string' },
param: { type: 'string' }
},
required: ['cmd', 'param'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class TestApi01 extends BaseAction<Payload, any> {
actionName = ActionName.TestApi01;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
return {
valid: true,
};
}
protected async _handle(payload: Payload): Promise<any> {
return await napCatCore.session.getMsgService().sendSsoCmdReqByContend(payload.cmd, payload.param);
}
}

View File

@@ -0,0 +1,32 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import { NTQQSystemApi, NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv';
// 设置在线状态
const SchemaData = {
type: 'object',
properties: {
words: {
type: 'array',
items: { type: 'string' }
}
},
required: ['words'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class TranslateEnWordToZn extends BaseAction<Payload, Array<any> | null> {
actionName = ActionName.TranslateEnWordToZn;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQSystemApi.translateEnWordToZn(payload.words);
if (ret.result !== 0) {
throw new Error('翻译失败');
}
return ret.words;
}
}

View File

@@ -0,0 +1,45 @@
import { NTQQGroupApi, NTQQUserApi } from '@/core';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { BuddyCategoryType } from '@/core/entities/';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: 'string' },
group_id: { type: 'string' },
phoneNumber: { type: 'string' },
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class sharePeer extends BaseAction<Payload, any> {
actionName = ActionName.SharePeer;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
if (payload.group_id) {
return await NTQQGroupApi.getGroupRecommendContactArkJson(payload.group_id);
} else if (payload.user_id) {
return await NTQQUserApi.getBuddyRecommendContactArkJson(payload.user_id, payload.phoneNumber || '');
}
}
}
const SchemaDataGroupEx = {
type: 'object',
properties: {
group_id: { type: 'string' },
},
required: ['group_id']
} as const satisfies JSONSchema;
type PayloadGroupEx = FromSchema<typeof SchemaDataGroupEx>;
export class shareGroupEx extends BaseAction<PayloadGroupEx, any> {
actionName = ActionName.ShareGroupEx;
PayloadSchema = SchemaDataGroupEx;
protected async _handle(payload: PayloadGroupEx) {
return await NTQQGroupApi.getArkJsonGroupShare(payload.group_id);
}
}

View File

@@ -0,0 +1,23 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
file_id: { type: 'string' },
},
required: ['group_id', 'file_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class DelGroupFile extends BaseAction<Payload, any> {
actionName = ActionName.DelGroupFile;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
return await NTQQGroupApi.DelGroupFile(payload.group_id.toString(), [payload.file_id]);
}
}

View File

@@ -0,0 +1,23 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
folder_id: { type: 'string' },
},
required: ['group_id', 'folder_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class DelGroupFileFolder extends BaseAction<Payload, any> {
actionName = ActionName.DelGroupFileFolder;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
return (await NTQQGroupApi.DelGroupFileFolder(payload.group_id.toString(), payload.folder_id)).groupFileCommonResult;
}
}

View File

@@ -0,0 +1,227 @@
import BaseAction from '../BaseAction';
import fs from 'fs/promises';
import { ob11Config } from '@/onebot11/config';
import { UUIDConverter } from '@/common/utils/helper';
import { ActionName, BaseCheckResult } from '../types';
import { ChatType, ElementType, FileElement, Peer, RawMessage, VideoElement } from '@/core/entities';
import { NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { getGroup } from '@/core/data';
export interface GetFilePayload {
file: string; // 文件名或者fileUuid
}
export interface GetFileResponse {
file?: string; // path
url?: string;
file_size?: string;
file_name?: string;
base64?: string;
}
const GetFileBase_PayloadSchema = {
type: 'object',
properties: {
file: { type: 'string' }
},
required: ['file']
} as const satisfies JSONSchema;
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
PayloadSchema: any = GetFileBase_PayloadSchema;
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> {
const { enableLocalFile2Url } = ob11Config;
let UuidData: {
high: string;
low: string;
} | undefined;
try {
UuidData = UUIDConverter.decode(payload.file);
if (UuidData) {
const peerUin = UuidData.high;
const msgId = UuidData.low;
const isGroup = await getGroup(peerUin);
let peer: Peer | undefined;
//识别Peer
if (isGroup) {
peer = { chatType: ChatType.group, peerUid: peerUin };
}
const PeerUid = await NTQQUserApi.getUidByUin(peerUin);
if (PeerUid) {
const isBuddy = await NTQQFriendApi.isBuddy(PeerUid);
if (isBuddy) {
peer = { chatType: ChatType.friend, peerUid: PeerUid };
} else {
peer = { chatType: ChatType.temp, peerUid: PeerUid };
}
}
if (!peer) {
throw new Error('chattype not support');
}
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId]);
if (msgList.msgList.length == 0) {
throw new Error('msg not found');
}
const msg = msgList.msgList[0];
const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT);
if (!findEle) {
throw new Error('element not found');
}
const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '');
const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0';
const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || '';
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: fileSize,
file_name: fileName
};
if (enableLocalFile2Url) {
try {
res.base64 = await fs.readFile(downloadPath, 'base64');
} catch (e) {
throw new Error('文件下载失败. ' + e);
}
}
//不手动删除?文件持久化了
return res;
}
} catch {
}
const NTSearchNameResult = (await NTQQFileApi.searchfile([payload.file])).resultItems;
if (NTSearchNameResult.length !== 0) {
const MsgId = NTSearchNameResult[0].msgId;
let peer: Peer | undefined = undefined;
if (NTSearchNameResult[0].chatType == ChatType.group) {
peer = { chatType: ChatType.group, peerUid: NTSearchNameResult[0].groupChatInfo[0].groupCode };
}
if (!peer) {
throw new Error('chattype not support');
}
const msgList: RawMessage[] = (await NTQQMsgApi.getMsgsByMsgId(peer, [MsgId]))?.msgList;
if (!msgList || msgList.length == 0) {
throw new Error('msg not found');
}
const msg = msgList[0];
const file = msg.elements.filter(e => e.elementType == NTSearchNameResult[0].elemType);
if (file.length == 0) {
throw new Error('file not found');
}
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, file[0].elementId, '', '');
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: NTSearchNameResult[0].fileSize.toString(),
file_name: NTSearchNameResult[0].fileName
};
if (enableLocalFile2Url) {
try {
res.base64 = await fs.readFile(downloadPath, 'base64');
} catch (e) {
throw new Error('文件下载失败. ' + e);
}
}
//不手动删除?文件持久化了
return res;
}
throw new Error('file not found');
// 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) {
// logDebug('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 peer = MessageUnique.getPeerByMsgId(cache.msgId);
// let msg = await NTQQMsgApi.getMsgsByMsgId(peer?.Peer!,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);
// let peer = MessageUnique.getPeerByMsgId(cache.msgId);
// msg = await NTQQMsgApi.getMsgsByMsgId(peer?.Peer!,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);
// }
// }
// }
//return res;
}
}
const GetFile_PayloadSchema = {
type: 'object',
properties: {
file_id: { type: 'string' },
file: { type: 'string' }
},
required: ['file_id']
} as const satisfies JSONSchema;
type GetFile_Payload_Internal = FromSchema<typeof GetFile_PayloadSchema>;
interface GetFile_Payload extends GetFile_Payload_Internal {
file: string
}
export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile;
PayloadSchema = GetFile_PayloadSchema;
protected async _handle(payload: GetFile_Payload): Promise<GetFileResponse> {
payload.file = payload.file_id;
return super._handle(payload);
}
}

View File

@@ -0,0 +1,23 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi, NTQQUserApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupFileCount extends BaseAction<Payload, { count: number }> {
actionName = ActionName.GetGroupFileCount;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQGroupApi.GetGroupFileCount([payload.group_id?.toString()]);
return { count: ret.groupFileCounts[0] };
}
}

View File

@@ -0,0 +1,31 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
start_index: { type: 'number' },
file_count: { type: 'number' },
},
required: ['group_id', 'start_index', 'file_count']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupFileList extends BaseAction<Payload, { FileList: Array<any> }> {
actionName = ActionName.GetGroupFileList;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQMsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: payload.file_count,
startIndex: payload.start_index,
sortOrder: 2,
showOnlinedocFolder: 0
}).catch((e) => { return []; });
return { FileList: ret };
}
}

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,23 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
folder_name: { type: 'string' },
},
required: ['group_id', 'folder_name']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetGroupFileFolder extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupFileFolder;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
return (await NTQQGroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
}
}

View File

@@ -0,0 +1,82 @@
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 { randomUUID } from 'crypto';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
interface FileResponse {
file: string;
}
const SchemaData = {
type: 'object',
properties: {
thread_count: { type: 'number' },
url: { type: 'string' },
base64: { type: 'string' },
name: { type: 'string' },
headers: {
type: ['string', 'array'],
items: {
type: 'string'
}
}
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name;
const name = payload.name || randomUUID();
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,52 @@
import BaseAction from '../BaseAction';
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types';
import { NTQQMsgApi } from '@/core/apis';
import { OB11Constructor } from '../../constructor';
import { ActionName, BaseCheckResult } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'string' },
id: { type: 'string' }
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
interface Response {
messages: (OB11Message & { content: OB11MessageData })[];
}
export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetForwardMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msgId = payload.message_id || payload.id;
if (!msgId) {
throw Error('message_id is required');
}
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId);
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || parseInt(msgId));
if (!rootMsg) {
throw Error('msg not found');
}
const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, 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 MessageUnique.createMsg({ guildId:'',chatType:msg.chatType,peerUid:msg.peerUid },msg.msgId)!;
return resMsg;
}));
messages.map(msg => {
(<OB11ForwardMessage>msg).content = msg.message;
delete (<any>msg).message;
});
return { messages };
}
}

View File

@@ -0,0 +1,57 @@
import BaseAction from '../BaseAction';
import { OB11Message, OB11User } from '../../types';
import { ActionName } from '../types';
import { ChatType, RawMessage } from '@/core/entities';
import { NTQQMsgApi } from '@/core/apis/msg';
import { OB11Constructor } from '../../constructor';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQFriendApi, NTQQUserApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
interface Response {
messages: OB11Message[];
}
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
message_seq: { type: 'number' },
count: { type: 'number' },
reverseOrder: { type: 'boolean' }
},
required: ['user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<Response> {
//处理参数
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const MsgCount = payload.count || 20;
const isReverseOrder = payload.reverseOrder || true;
if (!uid) throw `记录${payload.user_id}不存在`;
const friend = await NTQQFriendApi.isBuddy(uid);
const peer = { chatType: friend ? ChatType.friend : ChatType.temp, peerUid: uid };
//拉取消息
let msgList: RawMessage[];
if (!payload.message_seq || payload.message_seq == 0) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, MsgCount)).msgList;
} else {
const startMsgId = MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq)?.MsgId;
if (!startMsgId) throw `消息${payload.message_seq}不存在`;
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList;
}
if(isReverseOrder) msgList.reverse();
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
//转换消息
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList };
}
}

View File

@@ -0,0 +1,26 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { WebApi, WebHonorType } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
type: { enum: [WebHonorType.ALL, WebHonorType.EMOTION, WebHonorType.LEGEND, WebHonorType.PERFROMER, WebHonorType.STORONGE_NEWBI, WebHonorType.TALKACTIVE] }
},
required: ['group_id']
} as const satisfies JSONSchema;
// enum是不是有点抽象
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
if (!payload.type) {
payload.type = WebHonorType.ALL;
}
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type);
}
}

View File

@@ -0,0 +1,56 @@
import BaseAction from '../BaseAction';
import { OB11Message, OB11User } from '../../types';
import { getGroup, groups } from '@/core/data';
import { ActionName } from '../types';
import { ChatType, Peer, RawMessage } from '@/core/entities';
import { NTQQMsgApi } from '@/core/apis/msg';
import { OB11Constructor } from '../../constructor';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
interface Response {
messages: OB11Message[];
}
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
message_seq: { type: 'number' },
count: { type: 'number' },
reverseOrder: { type: 'boolean' }
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<Response> {
//处理参数
const group = await getGroup(payload.group_id.toString());
const isReverseOrder = payload.reverseOrder || true;
const MsgCount = payload.count || 20;
const peer: Peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() };
if (!group) throw `${payload.group_id}不存在`;
//拉取消息
let msgList: RawMessage[];
if (!payload.message_seq || payload.message_seq == 0) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, MsgCount)).msgList;
} else {
const startMsgId = MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq)?.MsgId;
if (!startMsgId) throw `消息${payload.message_seq}不存在`;
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList;
}
if(isReverseOrder) msgList.reverse();
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
//转换消息
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList };
}
}

View File

@@ -0,0 +1,23 @@
import { DeviceList } from '@/onebot11/main';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { JSONSchema } from 'json-schema-to-ts';
import { NTQQSystemApi } from '@/core';
import { sleep } from '@/common/utils/helper';
const SchemaData = {
type: 'object',
properties: {
no_cache: { type: 'boolean' },
}
} as const satisfies JSONSchema;
export class GetOnlineClient extends BaseAction<void, Array<any>> {
actionName = ActionName.GetOnlineClient;
protected async _handle(payload: void) {
NTQQSystemApi.getOnlineDev();
await sleep(500);
return DeviceList;
}
}

View File

@@ -0,0 +1,66 @@
import BaseAction from '../BaseAction';
import { OB11User, OB11UserSex } from '../../types';
import { OB11Constructor } from '../../constructor';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { calcQQLevel } from '@/common/utils/qqlevel';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
},
required: ['user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo;
protected async _handle(payload: Payload): Promise<OB11User> {
if (!requireMinNTQQBuild('26702')) {
const user_id = payload.user_id.toString();
const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id);
const uid = (await NTQQUserApi.getUidByUin(user_id))!;
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick,
sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0,
uid: ''
};
return ret;
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) };
return OB11Constructor.stranger(data);
} else {
const user_id = payload.user_id.toString();
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id);
//console.log(extendData);
const uid = (await NTQQUserApi.getUidByUin(user_id))!;
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
};
return ret;
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) };
return OB11Constructor.stranger(data);
}
}
}

View File

@@ -0,0 +1,17 @@
import { log } from '@/common/utils/log';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { QuickAction, QuickActionEvent, handleQuickOperation } from '@/onebot11/server/postOB11Event';
interface Payload{
context: QuickActionEvent,
operation: QuickAction
}
export class GoCQHTTPHandleQuickAction extends BaseAction<Payload, null>{
actionName = ActionName.GoCQHTTP_HandleQuickAction;
protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(payload.context, payload.operation).then().catch(log);
return null;
}
}

View File

@@ -0,0 +1,20 @@
import SendMsg, { normalize } 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 = normalize(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,61 @@
import { checkFileReceived, uri2local } from '@/common/utils/file';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi, WebApi } from '@/core/apis';
import { unlink } from 'node:fs';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
content: { type: 'string' },
image: { type: 'string' },
pinned: { type: 'number' },
confirmRequired: { type: 'number' }
},
required: ['group_id', 'content']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SendGroupNotice extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupNotice;
protected async _handle(payload: Payload) {
let UploadImage: { id: string, width: number, height: number } | undefined = undefined;
if (payload.image) {
//公告图逻辑
const { errMsg, path, isLocal, success } = (await uri2local(payload.image));
if (!success) {
throw `群公告${payload.image}设置失败,image字段可能格式不正确`;
}
if (!path) {
throw `群公告${payload.image}设置失败,获取资源失败`;
}
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ImageUploadResult = await NTQQGroupApi.uploadGroupBulletinPic(payload.group_id.toString(), path);
if (ImageUploadResult.errCode != 0) {
throw `群公告${payload.image}设置失败,图片上传失败`;
}
if (!isLocal) {
unlink(path, () => { });
}
UploadImage = ImageUploadResult.picInfo;
}
let Notice_Pinned = 0;
let Notice_confirmRequired = 0;
if (!payload.pinned) {
Notice_Pinned = 0;
}
if (!payload.confirmRequired) {
Notice_confirmRequired = 0;
}
const PublishGroupBulletinResult = await NTQQGroupApi.publishGroupBulletin(payload.group_id.toString(), payload.content, UploadImage, Notice_Pinned, Notice_confirmRequired);
if (PublishGroupBulletinResult.result != 0) {
throw `设置群公告失败,错误信息:${PublishGroupBulletinResult.errMsg}`;
}
// 下面实现扬了
//await WebApi.setGroupNotice(payload.group_id, payload.content) ;
return null;
}
}

View File

@@ -0,0 +1,44 @@
import BaseAction from '../BaseAction';
import { getGroup } from '@/core/data';
import { ActionName } from '../types';
import { SendMsgElementConstructor } from '@/core/entities/constructor';
import { ChatType, SendFileElement } from '@/core/entities';
import fs from 'fs';
import { SendMsg, sendMsg } from '@/onebot11/action/msg/SendMsg';
import { uri2local } from '@/common/utils/file';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
file: { type: 'string' },
name: { type: 'string' },
folder: { type: 'string' },
folder_id: { type: 'string' }//临时扩展
},
required: ['group_id', 'file', 'name']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile;
PayloadSchema = SchemaData;
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.success) {
throw new Error(downloadResult.errMsg);
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id);
await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true);
return null;
}
}

View File

@@ -0,0 +1,51 @@
import BaseAction from '../BaseAction';
import { getGroup } from '@/core/data';
import { ActionName } from '../types';
import { SendMsgElementConstructor } from '@/core/entities/constructor';
import { ChatType, Peer, SendFileElement } from '@/core/entities';
import fs from 'fs';
import { SendMsg, sendMsg } from '@/onebot11/action/msg/SendMsg';
import { uri2local } from '@/common/utils/file';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQFriendApi, NTQQUserApi } from '@/core';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
file: { type: 'string' },
name: { type: 'string' }
},
required: ['user_id', 'file', 'name']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
actionName = ActionName.GOCQHTTP_UploadPrivateFile;
PayloadSchema = SchemaData;
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!peerUid) {
throw `私聊${payload.user_id}不存在`;
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid };
}
throw '缺少参数 user_id';
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload);
let file = payload.file;
if (fs.existsSync(file)) {
file = `file://${file}`;
}
const downloadResult = await uri2local(file);
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg);
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await sendMsg(peer, [sendFileEle], [], true);
return null;
}
}

View File

@@ -0,0 +1,31 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQGroupApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class DelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.DelEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.removeGroupEssence(
msg.Peer.peerUid,
msg.MsgId
);
}
}

View File

@@ -0,0 +1,31 @@
import { getGroup } from '@/core/data';
import { OB11Group } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQMsgApi } from '@/core/apis/msg';
import { GroupEssenceMsgRet, WebApi } from '@/core/apis/webapi';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
pages: { type: 'number' },
},
required: ['group_id', 'pages']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupEssence extends BaseAction<Payload, GroupEssenceMsgRet> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages.toString());
if (!ret) {
throw new Error('获取失败');
}
return ret;
}
}

View File

@@ -0,0 +1,31 @@
import { getGroup } from '@/core/data';
import { OB11Group } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetGroupInfo extends BaseAction<Payload, OB11Group> {
actionName = ActionName.GetGroupInfo;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
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,27 @@
import { OB11Group } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis';
import { Group } from '@/core/entities';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
// no_cache get时传字符串
const SchemaData = {
type: 'object',
properties: {
no_cache: { type: ['boolean', 'string'] },
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const groupList: Group[] = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload.no_cache === 'true');
return OB11Constructor.groups(groupList);
}
}
export default GetGroupList;

View File

@@ -0,0 +1,79 @@
import { OB11GroupMember } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user';
import { logDebug } from '@/common/utils/log';
import { WebApi } from '@/core/apis/webapi';
import { NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { getGroupMember, selfInfo } from '@/core/data';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
no_cache: { type: ['boolean', 'string'] },
},
required: ['group_id', 'user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const isNocache = payload.no_cache == true || payload.no_cache === 'true';
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!uid) {
throw (`Uin2Uid Error ${payload.user_id}不存在`);
}
const member = await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), uid, isNocache);
if (!member) {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
}
try {
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
logDebug('群成员详细信息结果', info);
Object.assign(member, info);
} catch (e) {
logDebug('获取群成员详细信息失败, 只能返回基础信息', e);
}
const date = Math.round(Date.now() / 1000);
const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member);
if (!requireMinNTQQBuild('26702')) {
const SelfInfoInGroup = await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), selfInfo.uid, isNocache);
let isPrivilege = false;
if (SelfInfoInGroup) {
isPrivilege = SelfInfoInGroup.role === 3 || SelfInfoInGroup.role === 4;
}
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) {
retMember.join_time = webGroupMembers[i]?.join_time;
retMember.last_sent_time = webGroupMembers[i]?.last_speak_time;
retMember.qage = webGroupMembers[i]?.qage;
retMember.level = webGroupMembers[i]?.lv.level.toString();
}
}
} else {
const LastestMsgList = await NTQQGroupApi.getLastestMsg(payload.group_id.toString(), [payload.user_id.toString()]);
if (LastestMsgList?.msgList?.length && LastestMsgList?.msgList?.length > 0) {
const last_send_time = LastestMsgList.msgList[0].msgTime;
if (last_send_time && last_send_time != '0' && last_send_time != '') {
retMember.last_sent_time = parseInt(last_send_time);
retMember.join_time = Math.round(Date.now() / 1000);//兜底数据 防止群管乱杀
}
}
}
} else {
retMember.last_sent_time = parseInt((await getGroupMember(payload.group_id.toString(), retMember.user_id))?.lastSpeakTime || date.toString());
retMember.join_time = parseInt((await getGroupMember(payload.group_id.toString(), retMember.user_id))?.joinTime || date.toString());
}
return retMember;
}
}
export default GetGroupMemberInfo;

View File

@@ -0,0 +1,84 @@
import { OB11GroupMember } from '../../types';
import { getGroup, getGroupMember, groupMembers, selfInfo } from '@/core/data';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user';
import { logDebug } from '@/common/utils/log';
import { WebApi } from '@/core/apis/webapi';
import { NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
// no_cache get时传字符串
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
no_cache: { type: ['boolean', 'string'] },
},
required: ['group_id', 'user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
if (requireMinNTQQBuild('26702')) {
const V2Data = await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), payload.user_id.toString(), payload.no_cache == true || payload.no_cache === 'true');
if (V2Data) {
return OB11Constructor.groupMember(payload.group_id.toString(), V2Data);
} else {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
}
}
const group = await getGroup(payload.group_id.toString());
const role = (await getGroupMember(payload.group_id, selfInfo.uin))?.role;
const isPrivilege = role === 3 || role === 4;
if (!group) {
throw (`群(${payload.group_id})不存在`);
}
if (payload.no_cache == true || payload.no_cache === 'true') {
groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString()));
}
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString());
//早返回
if (!member) {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
}
//console.log('GetGroupMemberInfo', JSON.stringify(await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), member.uid, true), null, 4));
try {
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
logDebug('群成员详细信息结果', info);
Object.assign(member, info);
} catch (e) {
logDebug('获取群成员详细信息失败, 只能返回基础信息', e);
}
const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member);
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) {
retMember.join_time = webGroupMembers[i]?.join_time;
retMember.last_sent_time = webGroupMembers[i]?.last_speak_time;
retMember.qage = webGroupMembers[i]?.qage;
retMember.level = webGroupMembers[i]?.lv.level.toString();
}
}
} else {
const LastestMsgList = await NTQQGroupApi.getLastestMsg(payload.group_id.toString(), [payload.user_id.toString()]);
if (LastestMsgList?.msgList?.length && LastestMsgList?.msgList?.length > 0) {
const last_send_time = LastestMsgList.msgList[0].msgTime;
if (last_send_time && last_send_time != '0' && last_send_time != '') {
retMember.last_sent_time = parseInt(last_send_time);
retMember.join_time = Math.round(Date.now() / 1000);//兜底数据 防止群管乱杀
}
}
}
return retMember;
}
}
export default GetGroupMemberInfo;

View File

@@ -0,0 +1,104 @@
import { getGroup, getGroupMember, selfInfo } from '@/core/data';
import { OB11GroupMember } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core';
import { WebApi } from '@/core/apis/webapi';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
no_cache: { type: ['boolean', 'string'] },
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const isNocache = payload.no_cache == true || payload.no_cache === 'true';
const GroupList = await NTQQGroupApi.getGroups(isNocache);
const group = GroupList.find(item => item.groupCode == payload.group_id);
if (!group) {
throw (`${payload.group_id}不存在`);
}
const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString());
let _groupMembers = Array.from(groupMembers.values())
.map(item => { return OB11Constructor.groupMember(group.groupCode, item); });
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>();
// 转为Map 方便索引
const date = Math.round(Date.now() / 1000);
for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date;
_groupMembers[i].last_sent_time = date;
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]);
}
if (!requireMinNTQQBuild('26702')) {
const selfRole = groupMembers.get(selfInfo.uid)?.role;
const isPrivilege = selfRole === 3 || selfRole === 4;
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) {
continue;
}
const MemberData = MemberMap.get(webGroupMembers[i]?.uin);
if (MemberData) {
MemberData.join_time = webGroupMembers[i]?.join_time;
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time;
MemberData.qage = webGroupMembers[i]?.qage;
MemberData.level = webGroupMembers[i]?.lv.level.toString();
MemberMap.set(webGroupMembers[i]?.uin, MemberData);
}
}
} else {
if (isNocache) {
const DateMap = await NTQQGroupApi.getGroupMemberLastestSendTimeCache(payload.group_id.toString());//开始从本地拉取
for (const DateUin of DateMap.keys()) {
const MemberData = MemberMap.get(parseInt(DateUin));
if (MemberData) {
MemberData.last_sent_time = parseInt(DateMap.get(DateUin)!);
//join_time 有基础数据兜底
}
}
} else {
_groupMembers.forEach(item => {
item.last_sent_time = date;
item.join_time = date;
});
}
}
} else {
_groupMembers.forEach(async item => {
item.last_sent_time = parseInt((await getGroupMember(payload.group_id.toString(), item.user_id))?.lastSpeakTime || date.toString());
item.join_time = parseInt((await getGroupMember(payload.group_id.toString(), item.user_id))?.joinTime || date.toString());
});
}
// 还原索引到Array 一同返回
// let retData: any[] = [];
// for (let retMem of MemberMap.values()) {
// retMem.level = TypeConvert.toString(retMem.level) as any;
// retData.push(retMem)
// }
// _groupMembers = Array.from(retData);
_groupMembers = Array.from(MemberMap.values());
return _groupMembers;
}
}
export default GetGroupMemberList;

View File

@@ -0,0 +1,57 @@
import { WebApi, WebApiGroupNoticeFeed, WebApiGroupNoticeRet } from '@/core/apis/webapi';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
interface GroupNotice {
sender_id: number
publish_time: number
message: {
text: string
image: Array<{
height: string
width: string
id: string
}>
}
}
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
type ApiGroupNotice = GroupNotice & WebApiGroupNoticeFeed;
export class GetGroupNotice extends BaseAction<Payload, GroupNotice[]> {
actionName = ActionName.GoCQHTTP_GetGroupNotice;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const group = payload.group_id.toString();
const ret = await WebApi.getGrouptNotice(group);
if (!ret) {
throw new Error('获取公告失败');
}
const retNotices: GroupNotice[] = new Array<ApiGroupNotice>();
for (const key in ret.feeds) {
const retApiNotice: WebApiGroupNoticeFeed = ret.feeds[key];
const retNotice: GroupNotice = {
// ...ret.feeds[key],
sender_id: retApiNotice.u,
publish_time: retApiNotice.pubt,
message: {
text: retApiNotice.msg.text,
image: retApiNotice.msg.pics?.map((pic) => {
return { id: pic.id, height: pic.h, width: pic.w };
}) || []
}
};
retNotices.push(retNotice);
}
return retNotices;
}
}

View File

@@ -0,0 +1,47 @@
import { NTQQGroupApi, NTQQUserApi } from '@/core';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] }
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupSystemMsg extends BaseAction<void, any> {
actionName = ActionName.GetGroupSystemMsg;
protected async _handle(payload: void) {
// 默认10条 该api未完整实现 包括响应数据规范化 类型规范化
const SingleScreenNotifies = await NTQQGroupApi.getSingleScreenNotifies(10);
const retData: any = { InvitedRequest: [], join_requests: [] };
for (const SSNotify of SingleScreenNotifies) {
if (SSNotify.type == 1) {
retData.InvitedRequest.push({
request_id: SSNotify.seq,
invitor_uin: await NTQQUserApi.getUinByUid(SSNotify.user1?.uid),
invitor_nick: SSNotify.user1?.nickName,
group_id: SSNotify.group?.groupCode,
group_name: SSNotify.group?.groupName,
checked: SSNotify.status === 1 ? false : true,
actor: await NTQQUserApi.getUinByUid(SSNotify.user2?.uid) || 0,
});
} else if (SSNotify.type == 7) {
retData.join_requests.push({
request_id: SSNotify.seq,
requester_uin: await NTQQUserApi.getUinByUid(SSNotify.user1?.uid),
requester_nick: SSNotify.user1?.nickName,
group_id: SSNotify.group?.groupCode,
group_name: SSNotify.group?.groupName,
checked: SSNotify.status === 1 ? false : true,
actor: await NTQQUserApi.getUinByUid(SSNotify.user2?.uid) || 0,
});
}
}
return retData;
}
}

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,17 @@
import SendMsg, { ContextMode } from '../msg/SendMsg';
import { ActionName, BaseCheckResult } from '../types';
import { OB11PostSendMsg } from '../../types';
// 未检测参数
class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg;
contextMode: ContextMode = ContextMode.Group;
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,30 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQGroupApi, NTQQMsgApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.SetEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.addGroupEssence(
msg.Peer.peerUid,
msg.MsgId
);
}
}

View File

@@ -0,0 +1,31 @@
import BaseAction from '../BaseAction';
import { GroupRequestOperateTypes } from '@/core/entities';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
flag: { type: 'string' },
approve: { type: ['string', 'boolean'] },
reason: { type: 'string', nullable: true, }
},
required: ['flag'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false';
await NTQQGroupApi.handleGroupRequest(flag,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason
);
return null;
}
}

View File

@@ -0,0 +1,33 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '@/core/data';
import { GroupMemberRole } from '@/core/entities';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
user_id: { type: [ 'number' , 'string' ] },
enable: { type: 'boolean' }
},
required: ['group_id', 'user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupAdmin extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAdmin;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id);
// 已经前置验证类型
const enable = payload.enable?.toString() !== 'false';
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,31 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '@/core/data';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
duration: { type: ['number', 'string'] }
},
required: ['group_id', 'user_id', 'duration']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupBan;
PayloadSchema = SchemaData;
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,30 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '@/core/data';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
user_id: { type: [ 'number' , 'string' ] },
card: { type: 'string' }
},
required: ['group_id', 'user_id', 'card']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupCard extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupCard;
PayloadSchema = SchemaData;
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,32 @@
import BaseAction from '../BaseAction';
import { getGroupMember } from '@/core/data';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
user_id: { type: [ 'number' , 'string' ] },
reject_add_request: { type: [ 'boolean' , 'string' ] }
},
required: ['group_id', 'user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupKick extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupKick;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id);
if (!member) {
throw `群成员${payload.user_id}不存在`;
}
const rejectReq = payload.reject_add_request?.toString() == 'true';
await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], rejectReq);
return null;
}
}

View File

@@ -0,0 +1,29 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { log, logError } from '@/common/utils/log';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { deleteGroup } from '@/core/data';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
is_dismiss: { type: 'boolean' }
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupLeave extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupLeave;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
try {
await NTQQGroupApi.quitGroup(payload.group_id.toString());
deleteGroup(payload.group_id.toString());
} catch (e) {
logError('退群失败', e);
throw e;
}
}
}

View File

@@ -0,0 +1,24 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
group_name: { type: 'string' }
},
required: ['group_id', 'group_name']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupName extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupName;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name);
return null;
}
}

View File

@@ -0,0 +1,25 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
enable: { type: ['boolean','string'] }
},
required: ['group_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
const enable = payload.enable?.toString() !== 'false';
await NTQQGroupApi.banGroup(payload.group_id.toString(), enable);
return null;
}
}

180
src/onebot/action/index.ts Normal file
View File

@@ -0,0 +1,180 @@
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 '@/onebot11/action/extends/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, MarkAllMsgAsRead, MarkGroupMsgAsRead, MarkPrivateMsgAsRead } from './msg/MarkMsgAsRead';
import CleanCache from './system/CleanCache';
import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile';
import { GetConfigAction, SetConfigAction } from '@/onebot11/action/extends/Config';
import GetGroupAddRequest from '@/onebot11/action/extends/GetGroupAddRequest';
import SetQQAvatar from '@/onebot11/action/extends/SetQQAvatar';
import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile';
import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory';
import GetFile from './file/GetFile';
import { GoCQHTTPGetForwardMsgAction } from './go-cqhttp/GetForwardMsg';
import GetFriendMsgHistory from './go-cqhttp/GetFriendMsgHistory';
import { GetCookies } from './user/GetCookies';
import { SetMsgEmojiLike } from '@/onebot11/action/msg/SetMsgEmojiLike';
import { GetRobotUinRange } from './extends/GetRobotUinRange';
import { SetOnlineStatus } from './extends/SetOnlineStatus';
import { GetGroupNotice } from './group/GetGroupNotice';
import { GetGroupEssence } from './group/GetGroupEssence';
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from '@/onebot11/action/msg/ForwardSingleMsg';
import { GetFriendWithCategory } from './extends/GetFriendWithCategory';
import { SendGroupNotice } from './go-cqhttp/SendGroupNotice';
import { Reboot, RebootNormal } from './system/Reboot';
import { GetGroupHonorInfo } from './go-cqhttp/GetGroupHonorInfo';
import { GoCQHTTPHandleQuickAction } from './go-cqhttp/QuickAction';
import { GetGroupSystemMsg } from './group/GetGroupSystemMsg';
import { GetOnlineClient } from './go-cqhttp/GetOnlineClient';
import { IOCRImage, OCRImage } from './extends/OCRImage';
import { GetGroupFileCount } from './file/GetGroupFileCount';
import { GetGroupFileList } from './file/GetGroupFileList';
import { TranslateEnWordToZn } from './extends/TranslateEnWordToZn';
import { SetGroupFileFolder } from './file/SetGroupFileFolder';
import { DelGroupFile } from './file/DelGroupFile';
import { DelGroupFileFolder } from './file/DelGroupFileFolder';
import { SetSelfProfile } from './extends/SetSelfProfile';
import { shareGroupEx, sharePeer } from './extends/sharePeer';
import { CreateCollection } from './extends/CreateCollection';
import { SetLongNick } from './extends/SetLongNick';
import DelEssenceMsg from './group/DelEssenceMsg';
import SetEssenceMsg from './group/SetEssenceMsg';
import GetRecentContact from './user/GetRecentContact';
import { GetProfileLike } from './extends/GetProfileLike';
import SetGroupHeader from './extends/SetGroupHeader';
import { FetchCustomFace } from './extends/FetchCustomFace';
import GoCQHTTPUploadPrivateFile from './go-cqhttp/UploadPrivareFile';
import TestApi01 from './extends/TestApi01';
import { FetchEmojioLike } from './extends/FetchEmojioLike';
export const actionHandlers = [
new FetchEmojioLike(),
new RebootNormal(),
new GetFile(),
new Debug(),
new Reboot(),
new SetSelfProfile(),
new shareGroupEx(),
new sharePeer(),
new CreateCollection(),
new SetLongNick(),
new ForwardFriendSingleMsg(),
new ForwardGroupSingleMsg(),
new MarkGroupMsgAsRead(),
new MarkPrivateMsgAsRead(),
new SetQQAvatar(),
new TranslateEnWordToZn(),
new GetGroupFileCount(),
new GetGroupFileList(),
new SetGroupFileFolder(),
new DelGroupFile(),
new DelGroupFileFolder(),
// 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 SetMsgEmojiLike(),
new GetCookies(),
new SetOnlineStatus(),
new GetRobotUinRange(),
new GetFriendWithCategory(),
//以下为go-cqhttp api
new GetOnlineClient(),
new OCRImage(),
new IOCRImage(),
new GetGroupHonorInfo(),
new SendGroupNotice(),
new GetGroupNotice(),
new GetGroupEssence(),
new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GoCQHTTPDownloadFile(),
new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTPGetForwardMsgAction(),
new GetFriendMsgHistory(),
new GoCQHTTPHandleQuickAction(),
new GetGroupSystemMsg(),
new DelEssenceMsg(),
new SetEssenceMsg(),
new GetRecentContact(),
new MarkAllMsgAsRead(),
new GetProfileLike(),
new SetGroupHeader(),
new FetchCustomFace(),
new GoCQHTTPUploadPrivateFile(),
new TestApi01()
];
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>();
for (const action of actionHandlers) {
actionMap.set(action.actionName, action);
actionMap.set(action.actionName + '_async', action);
actionMap.set(action.actionName + '_rate_limited', action);
}
return actionMap;
}
export const actionMap = initActionMap();

View File

@@ -0,0 +1,54 @@
import { NTQQMsgApi } from '@/core/apis';
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
import { sleep } from '@/common/utils/helper';
import { NTEventDispatch } from '@/common/utils/EventTask';
import { NodeIKernelMsgListener } from '@/core';
const SchemaData = {
type: 'object',
properties: {
message_id: {
oneOf: [
{ type: 'number' },
{ type: 'string' }
]
}
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(Number(payload.message_id));
if (msg) {
let ret = NTEventDispatch.RegisterListen<NodeIKernelMsgListener['onMsgInfoListUpdate']>
(
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgs) => {
if (msgs.find(m => m.msgId === msg.MsgId && m.recallTime !== '0')) {
return true;
}
return false;
}
).catch(e => new Promise<undefined>((resolve, reject) => { resolve(undefined) }));
await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]);
let data = await ret;
if (!data) {
throw new Error('Recall failed');
}
//await sleep(100);
//await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId]);
}
}
}
export default DeleteMsg;

View File

@@ -0,0 +1,57 @@
import BaseAction from '../BaseAction';
import { NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { ChatType, Peer } from '@/core/entities';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'number' },
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class ForwardSingleMsg extends BaseAction<Payload, null> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`);
}
return { chatType: ChatType.friend, peerUid };
}
return { chatType: ChatType.group, peerUid: payload.group_id!.toString() };
}
protected async _handle(payload: Payload): Promise<null> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(payload.message_id);
if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`);
}
const peer = await this.getTargetPeer(payload);
const ret = await NTQQMsgApi.forwardMsg(msg.Peer,
peer,
[msg.MsgId],
);
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`);
}
return null;
}
}
export class ForwardFriendSingleMsg extends ForwardSingleMsg {
PayloadSchema = SchemaData;
actionName = ActionName.ForwardFriendSingleMsg;
}
export class ForwardGroupSingleMsg extends ForwardSingleMsg {
PayloadSchema = SchemaData;
actionName = ActionName.ForwardGroupSingleMsg;
}

View File

@@ -0,0 +1,50 @@
import { OB11Message } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
import { NTQQMsgApi } from '@/core';
export type ReturnDataType = OB11Message
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] },
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetMsg extends BaseAction<Payload, OB11Message> {
actionName = ActionName.GetMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
// log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
const MsgShortId = await MessageUnique.getShortIdByMsgId(payload.message_id.toString());
const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(MsgShortId || parseInt(payload.message_id.toString()));
if (!msgIdWithPeer) {
throw ('消息不存在');
}
const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType };
const msg = await NTQQMsgApi.getMsgsByMsgId(
peer,
[msgIdWithPeer?.MsgId || payload.message_id.toString()]);
const retMsg = await OB11Constructor.message(msg.msgList[0]);
try {
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgList[0].msgId)!;
retMsg.message_seq = retMsg.message_id;
retMsg.real_id = retMsg.message_id;
} catch (e) {
}
return retMsg;
}
}
export default GetMsg;

View File

@@ -0,0 +1,71 @@
import { ChatType, Peer } from '@/core/entities';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQFriendApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
group_id: { type: ['number', 'string'] }
}
} as const satisfies JSONSchema;
type PlayloadType = FromSchema<typeof SchemaData>;
class MarkMsgAsRead extends BaseAction<PlayloadType, null> {
async getPeer(payload: PlayloadType): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!peerUid) {
throw `私聊${payload.user_id}不存在`;
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid };
}
if (!payload.group_id) {
throw '缺少参数 group_id 或 user_id';
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() };
}
protected async _handle(payload: PlayloadType): Promise<null> {
// 调用API
const ret = await NTQQMsgApi.setMsgRead(await this.getPeer(payload));
if (ret.result != 0) {
throw ('设置已读失败,' + ret.errMsg);
}
return null;
}
}
// 以下为非标准实现
export class MarkPrivateMsgAsRead extends MarkMsgAsRead {
PayloadSchema = SchemaData;
actionName = ActionName.MarkPrivateMsgAsRead;
}
export class MarkGroupMsgAsRead extends MarkMsgAsRead {
PayloadSchema = SchemaData;
actionName = ActionName.MarkGroupMsgAsRead;
}
interface Payload {
message_id: number
}
export class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead;
protected async _handle(payload: Payload): Promise<null> {
return null;
}
}
export class MarkAllMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName._MarkAllMsgAsRead;
protected async _handle(payload: Payload): Promise<null> {
await NTQQMsgApi.markallMsgAsRead();
return null;
}
}

View File

@@ -0,0 +1,36 @@
import { OB11MessageData } from '@/onebot11/types';
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;
}

View File

@@ -0,0 +1,248 @@
import { OB11MessageData, OB11MessageDataType, OB11MessageFileBase } from '@/onebot11/types';
import {
AtType,
CustomMusicSignPostData,
Group,
IdMusicSignPostData,
NTQQFileApi,
NTQQMsgApi,
Peer,
SendArkElement,
SendMessageElement,
SendMsgElementConstructor,
SignMusicWrapper
} from '@/core';
import { getGroupMember } from '@/core/data';
import { logError, logWarn } from '@/common/utils/log';
import { uri2local } from '@/common/utils/file';
import { ob11Config } from '@/onebot11/config';
import { RequestUtil } from '@/common/utils/request';
import { MessageUnique } from '@/common/utils/MessageUnique';
console.log(process.pid)
export type MessageContext = {
deleteAfterSentFiles: string[],
peer:Peer
}
async function handleOb11FileLikeMessage(
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: MessageContext
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const { path, isLocal, fileName, errMsg,success } = (await uri2local(inputdata?.url || inputdata.file));
if (!success) {
logError('文件下载失败', errMsg);
throw Error('文件下载失败' + errMsg);
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path);
}
return { path, fileName: inputdata.name || fileName };
}
const _handlers: {
[Key in OB11MessageDataType]: (
sendMsg: Extract<OB11MessageData, { type: Key }>,
// This picks the correct message type out
// How great the type system of TypeScript is!
context: MessageContext
) => Promise<SendMessageElement | undefined>
} = {
[OB11MessageDataType.text]: async ({ data: { text } }) => SendMsgElementConstructor.text(text),
[OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => {
if (!context.peer) return undefined;
if (atQQ === 'all') return SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员');
// then the qq is a group member
const atMember = await getGroupMember(context.peer.peerUid, atQQ);
return atMember ?
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick) :
undefined;
},
[OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) {
logWarn('回复消息不存在', id);
return undefined;
}
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(replyMsgM?.Peer!, [replyMsgM?.MsgId!])).msgList[0];
return replyMsg ?
SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) :
undefined;
},
[OB11MessageDataType.face]: async ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)),
[OB11MessageDataType.mface]: async ({
data: {
emoji_package_id,
emoji_id,
key,
summary
}
}) => SendMsgElementConstructor.mface(emoji_package_id, emoji_id, key, summary),
// File service
[OB11MessageDataType.image]: async (sendMsg, context) => {
const PicEle = await SendMsgElementConstructor.pic(
(await handleOb11FileLikeMessage(sendMsg, context)).path,
sendMsg.data.summary || '',
sendMsg.data.subType || 0
);
context.deleteAfterSentFiles.push(PicEle.picElement.sourcePath);
return PicEle;
}
, // currently not supported
[OB11MessageDataType.file]: async (sendMsg, context) => {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context);
//logDebug('发送文件', path, fileName);
const FileEle = await SendMsgElementConstructor.file(path, fileName);
// 清除Upload的应该
// context.deleteAfterSentFiles.push(fileName || FileEle.fileElement.filePath);
return FileEle;
},
[OB11MessageDataType.video]: async (sendMsg, context) => {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context);
//logDebug('发送视频', path, fileName);
let thumb = sendMsg.data.thumb;
if (thumb) {
const uri2LocalRes = await uri2local(thumb);
if (uri2LocalRes.success) thumb = uri2LocalRes.path;
}
const videoEle = await SendMsgElementConstructor.video(path, fileName, thumb);
//未测试
context.deleteAfterSentFiles.push(videoEle.videoElement.filePath);
return videoEle;
},
[OB11MessageDataType.miniapp]: async ({ data: any }) => SendMsgElementConstructor.miniapp(),
[OB11MessageDataType.voice]: async (sendMsg, context) =>
SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: async ({ data: { data } }) => SendMsgElementConstructor.ark(data),
[OB11MessageDataType.dice]: async ({ data: { result } }) => SendMsgElementConstructor.dice(result),
[OB11MessageDataType.RPS]: async ({ data: { result } }) => SendMsgElementConstructor.rps(result),
[OB11MessageDataType.markdown]: async ({ data: { content } }) => SendMsgElementConstructor.markdown(content),
[OB11MessageDataType.music]: async ({ data }) => {
// 保留, 直到...找到更好的解决方案
if (data.type === 'custom') {
if (!data.url) {
logError('自定义音卡缺少参数url');
return undefined;
}
if (!data.audio) {
logError('自定义音卡缺少参数audio');
return undefined;
}
if (!data.title) {
logError('自定义音卡缺少参数title');
return undefined;
}
} else {
if (!['qq', '163'].includes(data.type)) {
logError('音乐卡片type错误, 只支持qq、163、custom当前type:', data.type);
return undefined;
}
if (!data.id) {
logError('音乐卡片缺少参数id');
return undefined;
}
}
let postData: IdMusicSignPostData | CustomMusicSignPostData;
if (data.type === 'custom' && data.content) {
const { content, ...others } = data;
postData = { singer: content, ...others };
} else {
postData = data;
}
const signUrl = ob11Config.musicSignUrl;
if (!signUrl) {
if (data.type === 'qq') {
const musicJson = (await SignMusicWrapper(data.id.toString())).data.arkResult.slice(0, -1);
return SendMsgElementConstructor.ark(musicJson);
}
throw Error('音乐消息签名地址未配置');
}
try {
const musicJson = await RequestUtil.HttpGetJson<any>(signUrl, 'POST', postData);
return SendMsgElementConstructor.ark(musicJson);
} catch (e) {
logError('生成音乐消息失败', e);
}
},
[OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: async () => undefined,
[OB11MessageDataType.xml]: async () => undefined,
[OB11MessageDataType.poke]: async () => undefined,
[OB11MessageDataType.Location]: async () => {
return SendMsgElementConstructor.location();
}
};
const handlers = <{
[Key in OB11MessageDataType]: (
sendMsg: OB11MessageData,
context: MessageContext
) => Promise<SendMessageElement | undefined>
}>_handlers;
export default async function createSendElements(
messageData: OB11MessageData[],
peer: Peer,
ignoreTypes: OB11MessageDataType[] = []
) {
const deleteAfterSentFiles: string[] = [];
const callResultList: Array<Promise<SendMessageElement | undefined>> = [];
for (const sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue;
}
const callResult = handlers[sendMsg.type](
sendMsg,
{ peer, deleteAfterSentFiles }
)?.catch(undefined);
callResultList.push(callResult);
}
const ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => ele) as SendMessageElement[];
return { sendElements, deleteAfterSentFiles };
}
export async function createSendElementsParallel(
messageData: OB11MessageData[],
peer: Peer,
ignoreTypes: OB11MessageDataType[] = []
) {
const deleteAfterSentFiles: string[] = [];
const sendElements = <SendMessageElement[]>(
await Promise.all(
messageData.map(async sendMsg => ignoreTypes.includes(sendMsg.type) ?
undefined :
handlers[sendMsg.type](sendMsg, { peer, deleteAfterSentFiles }))
).then(
results => results.filter(
element => element !== undefined
)
)
);
return { sendElements, deleteAfterSentFiles };
}

View File

@@ -0,0 +1,120 @@
import { ChatType, ElementType, Group, NTQQMsgApi, Peer, RawMessage, SendMessageElement } from '@/core';
import { OB11MessageDataType, OB11MessageNode } from '@/onebot11/types';
import { selfInfo } from '@/core/data';
import createSendElements from '@/onebot11/action/msg/SendMsg/create-send-elements';
import { logDebug, logError } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import { normalize, sendMsg } from '@/onebot11/action/msg/SendMsg/index';
import { MessageUnique } from '@/common/utils/MessageUnique';
async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
};
//logDebug('克隆的目标消息', msg);
const sendElements: SendMessageElement[] = [];
for (const element of msg.elements) {
sendElements.push(element as SendMessageElement);
}
if (sendElements.length === 0) {
logDebug('需要clone的消息无法解析将会忽略掉', msg);
}
try {
const nodeMsg = await NTQQMsgApi.sendMsg(selfPeer, sendElements, true);
return nodeMsg;
} catch (e) {
logError(e, '克隆转发消息失败,将忽略本条消息', msg);
}
}
export async function handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<RawMessage | null> {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
};
let nodeMsgIds: string[] = [];
for (const messageNode of messageNodes) {
const nodeId = messageNode.data.id;
if (nodeId) {
//对Mgsid和OB11ID混用情况兜底
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId);
if (!nodeMsg) {
logError('转发消息失败,未找到消息', nodeId);
continue;
}
nodeMsgIds.push(nodeMsg.MsgId);
} else {
// 自定义的消息
try {
let OB11Data = normalize(messageNode.data.content);
//筛选node消息
let isNodeMsg = OB11Data.filter(e => e.type === OB11MessageDataType.node).length;//找到子转发消息
if (isNodeMsg !== 0) {
if (isNodeMsg !== OB11Data.length) { logError('子消息中包含非node消息 跳过不合法部分'); continue; }
const nodeMsg = await handleForwardNode(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node));
if (nodeMsg) { nodeMsgIds.push(nodeMsg.msgId); MessageUnique.createMsg(selfPeer, nodeMsg.msgId) };
//完成子卡片生成跳过后续
continue;
}
const { sendElements } = await createSendElements(OB11Data, destPeer);
//拆分消息
let MixElement = sendElements.filter(element => element.elementType !== ElementType.FILE && element.elementType !== ElementType.VIDEO);
let SingleElement = sendElements.filter(element => element.elementType === ElementType.FILE || element.elementType === ElementType.VIDEO).map(e => [e]);
let AllElement: SendMessageElement[][] = [MixElement, ...SingleElement].filter(e => e !== undefined && e.length !== 0);
const MsgNodeList: Promise<RawMessage | undefined>[] = [];
for (const sendElementsSplitElement of AllElement) {
MsgNodeList.push(sendMsg(selfPeer, sendElementsSplitElement, [], true).catch(e => new Promise((resolve, reject) => { resolve(undefined) })));
}
(await Promise.allSettled(MsgNodeList)).map((result) => {
if (result.status === 'fulfilled' && result.value) {
nodeMsgIds.push(result.value.msgId);
MessageUnique.createMsg(selfPeer, result.value.msgId);
}
});
} catch (e) {
logDebug('生成转发消息节点失败', e);
}
}
}
const nodeMsgArray: Array<RawMessage> = [];
let srcPeer: Peer | undefined = undefined;
let needSendSelf = false;
//检测是否处于同一个Peer 不在同一个peer则全部消息由自身发送
for (let msgId of nodeMsgIds) {
const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId);
if (!nodeMsgPeer) {
logError('转发消息失败,未找到消息', msgId);
continue;
}
const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0];
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid };
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true;
}
nodeMsgArray.push(nodeMsg);
}
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
let retMsgIds: string[] = [];
if (needSendSelf) {
for (const [index, msg] of nodeMsgArray.entries()) {
if (msg.peerUid === selfInfo.uid) continue;
const ClonedMsg = await cloneMsg(msg);
if (ClonedMsg) retMsgIds.push(ClonedMsg.msgId);
}
} else {
retMsgIds = nodeMsgIds;
}
if (nodeMsgIds.length === 0) throw Error('转发消息失败,生成节点为空');
try {
logDebug('开发转发', srcPeer, destPeer, nodeMsgIds);
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds);
} catch (e) {
logError('forward failed', e);
return null;
}
}

View File

@@ -0,0 +1,166 @@
import BaseAction from '@/onebot11/action/BaseAction';
import {
OB11MessageData,
OB11MessageDataType,
OB11MessageMixType,
OB11MessageNode,
OB11PostSendMsg
} from '@/onebot11/types';
import { ActionName, BaseCheckResult } from '@/onebot11/action/types';
import { getGroup } from '@/core/data';
import { ChatType, ElementType, Group, NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi, Peer, SendMessageElement, } from '@/core';
import fs from 'node:fs';
import fsPromise from 'node:fs/promises';
import { logDebug, logError } from '@/common/utils/log';
import { decodeCQCode } from '@/onebot11/cqcode';
import createSendElements from './create-send-elements';
import { handleForwardNode } from '@/onebot11/action/msg/SendMsg/handle-forward-node';
import { MessageUnique } from '@/common/utils/MessageUnique';
export interface ReturnDataType {
message_id: number;
}
export enum ContextMode {
Normal = 0,
Private = 1,
Group = 2
}
// Normalizes a mixed type (CQCode/a single segment/segment array) into a segment array.
export function normalize(message: OB11MessageMixType, autoEscape = false): OB11MessageData[] {
return typeof message === 'string' ? (
autoEscape ?
[{ type: OB11MessageDataType.text, data: { text: message } }] :
decodeCQCode(message)
) : Array.isArray(message) ? message : [message];
}
export { createSendElements };
export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) {
throw ('消息体无法解析, 请检查是否发送了不支持的消息类型');
}
let totalSize = 0;
let timeout = 10000;
try {
for (const fileElement of sendElements) {
if (fileElement.elementType === ElementType.PTT) {
totalSize += fs.statSync(fileElement.pttElement.filePath).size;
}
if (fileElement.elementType === ElementType.FILE) {
totalSize += fs.statSync(fileElement.fileElement.filePath).size;
}
if (fileElement.elementType === ElementType.VIDEO) {
totalSize += fs.statSync(fileElement.videoElement.filePath).size;
}
if (fileElement.elementType === ElementType.PIC) {
totalSize += fs.statSync(fileElement.picElement.sourcePath).size;
}
}
//且 PredictTime ((totalSize / 1024 / 512) * 1000)不等于Nan
const PredictTime = totalSize / 1024 / 256 * 1000;
if (!Number.isNaN(PredictTime)) {
timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s )
}
} catch (e) {
logError('发送消息计算预计时间异常', e);
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
try {
returnMsg!.id = await MessageUnique.createMsg({ chatType: peer.chatType, guildId: '', peerUid: peer.peerUid }, returnMsg!.msgId);
} catch (e: any) {
logDebug('发送消息id获取失败', e);
returnMsg!.id = 0;
}
deleteAfterSentFiles.map((f) => { fsPromise.unlink(f).then().catch(e => logError('发送消息删除文件失败', e)); });
return returnMsg;
}
async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
const group = (await getGroup(payload.group_id))!; // checked before
return {
chatType: ChatType.group,
peerUid: group.groupCode
};
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const isBuddy = await NTQQFriendApi.isBuddy(Uid!);
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy);
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!
};
}
throw '请指定 group_id 或 user_id';
}
function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == msgType).length;
}
return 0;
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg;
contextMode = ContextMode.Normal;
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = normalize(payload.message);
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
if (nodeElementLength > 0 && nodeElementLength != 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') {
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const isBuddy = await NTQQFriendApi.isBuddy(uid!);
// 此处有问题
if (!isBuddy) {
//return { valid: false, message: '异常消息' };
}
}
return { valid: true };
}
protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> {
const peer = await createContext(payload, this.contextMode);
const messages = normalize(
payload.message,
payload.auto_escape === true || payload.auto_escape === 'true'
);
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const returnMsg = await handleForwardNode(peer, messages as OB11MessageNode[]);
if (returnMsg) {
const msgShortId = MessageUnique.createMsg({ guildId: '', peerUid: peer.peerUid, chatType: peer.chatType }, returnMsg!.msgId);
return { message_id: msgShortId! };
} else {
throw Error('发送转发消息失败');
}
} else {
// if (getSpecialMsgNum(payload, OB11MessageDataType.music)) {
// const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
// if (music) {
// }
// }
}
// log("send msg:", peer, sendElements)
const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, peer);
//console.log(peer, JSON.stringify(sendElements,null,2));
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles);
return { message_id: returnMsg!.id! };
}
}
export default SendMsg;

View File

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

View File

@@ -0,0 +1,35 @@
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { NTQQMsgApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['string', 'number'] },
emoji_id: { type: ['string', 'number'] }
},
required: ['message_id', 'emoji_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
if (!payload.emoji_id) {
throw new Error('emojiId not found');
}
const msgData = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList;
if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) {
throw new Error('find msg by msgid error');
}
return await NTQQMsgApi.setEmojiLike(msg.Peer, msgData[0].msgSeq, payload.emoji_id.toString(), true);
}
}

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: void): Promise<ReturnType> {
return {
yes: true
};
}
}

View File

@@ -0,0 +1,93 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import fs from 'fs';
import Path from 'path';
import {
ChatType,
ChatCacheListItemBasic,
CacheFileType
} from '@/core/entities';
import { NTQQFileApi, NTQQFileCacheApi } from '@/core/apis/file';
import { logError } from '@/common/utils/log';
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: { value: string; }) => 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: { fileKey: any; }) => file.fileKey));
}
// 一并清除
await NTQQFileCacheApi.clearChatCache(chatCacheList, cacheFileList);
res();
} catch(e) {
logError('清理缓存时发生了错误');
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) => {
});
}

View File

@@ -0,0 +1,17 @@
import { selfInfo } from '@/core/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,17 @@
import BaseAction from '../BaseAction';
import { OB11Status } from '../../types';
import { ActionName } from '../types';
import { selfInfo, stat } from '@/core/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,
stat
};
}
}

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,43 @@
import { rebootWithNormolLogin, rebootWithQuickLogin } from '@/common/utils/reboot';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { selfInfo } from '@/core/data';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
delay: { type: 'number' }
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class Reboot extends BaseAction<Payload, null> {
actionName = ActionName.Reboot;
protected async _handle(payload: Payload): Promise<null> {
if (payload.delay) {
setTimeout(() => {
rebootWithQuickLogin(selfInfo.uin);
}, payload.delay);
} else {
rebootWithQuickLogin(selfInfo.uin);
}
return null;
}
}
export class RebootNormal extends BaseAction<Payload, null> {
actionName = ActionName.RebootNormal;
protected async _handle(payload: Payload): Promise<null> {
if (payload.delay) {
setTimeout(() => {
rebootWithNormolLogin();
}, payload.delay);
} else {
rebootWithNormolLogin();
}
return null;
}
}

107
src/onebot/action/types.ts Normal file
View File

@@ -0,0 +1,107 @@
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 {
// 以下为扩展napcat扩展
SharePeer = 'ArkShareGroup',
ShareGroupEx = 'ArkSharePeer',
RebootNormal = 'reboot_normal',//无快速登录重新启动
GetRobotUinRange = 'get_robot_uin_range',
SetOnlineStatus = 'set_online_status',
GetFriendsWithCategory = 'get_friends_with_category',
GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
SetQQAvatar = 'set_qq_avatar',
GetConfig = 'get_config',
SetConfig = 'set_config',
Debug = 'debug',
GetFile = 'get_file',
ForwardFriendSingleMsg = 'forward_friend_single_msg',
ForwardGroupSingleMsg = 'forward_group_single_msg',
TranslateEnWordToZn = 'translate_en2zh',
GetGroupFileCount = 'get_group_file_count',
GetGroupFileList = 'get_group_file_list',
SetGroupFileFolder = 'set_group_file_folder',
DelGroupFile = 'del_group_file',
DelGroupFileFolder = 'del_group_file_folder',
// onebot 11
Reboot = 'set_restart',
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',
SetMsgEmojiLike = 'set_msg_emoji_like',
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',
GetCookies = 'get_cookies',
// 以下为go-cqhttp api
GoCQHTTP_HandleQuickAction = '.handle_quick_operation',
GetGroupHonorInfo = 'get_group_honor_info',
GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list',
GoCQHTTP_SendGroupNotice = '_send_group_notice',
GoCQHTTP_GetGroupNotice = '_get_group_notice',
GoCQHTTP_SendForwardMsg = 'send_forward_msg',
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
GetGuildList = 'get_guild_list',
MarkPrivateMsgAsRead = 'mark_private_msg_as_read',
MarkGroupMsgAsRead = 'mark_group_msg_as_read',
GoCQHTTP_UploadGroupFile = 'upload_group_file',
GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
GetFriendMsgHistory = 'get_friend_msg_history',
GetGroupSystemMsg = 'get_group_system_msg',
GetOnlineClient = 'get_online_clients',
OCRImage = 'ocr_image',
IOCRImage = '.ocr_image',
SetSelfProfile = 'set_self_profile',
CreateCollection = 'create_collection',
GetCollectionList = 'get_collection_list',
SetLongNick = 'set_self_longnick',
SetEssenceMsg = 'set_essence_msg',
DelEssenceMsg = 'delete_essence_msg',
GetRecentContact = 'get_recent_contact',
_MarkAllMsgAsRead = '_mark_all_as_read',
GetProfileLike = 'get_profile_like',
SetGroupHeader = 'set_group_head',
FetchCustomFace = 'fetch_custom_face',
GOCQHTTP_UploadPrivateFile = 'upload_private_file',
TestApi01 = 'test_api_01',
FetchEmojioLike = 'fetch_emoji_like'
}

View File

@@ -0,0 +1,69 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi, WebApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
interface Response {
cookies: string,
bkn: string
}
const SchemaData = {
type: 'object',
properties: {
domain: { type: 'string' }
},
required: ['domain']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetCookies extends BaseAction<Payload, Response> {
actionName = ActionName.GetCookies;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
// if (!payload.domain) {
// throw new Error('缺少参数 domain');
// }
// if (payload.domain.endsWith('qzone.qq.com')) {
// // 兼容整个 *.qzone.qq.com
// const data = (await NTQQUserApi.getQzoneCookies());
// const Bkn = WebApi.genBkn(data.p_skey);
// const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin;
// return { cookies: CookieValue };
// }
// // 取Skey
// // 先NodeIKernelTicketService.forceFetchClientKey('')
// // 返回值
// // {
// // result: 0,
// // errMsg: '',
// // url: '',
// // keyIndex: '19',
// // clientKey: 'clientKey',
// // expireTime: '7200'
// // }
// // request https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=1627126029&clientkey=key
// // &u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=keyIndex
// const _PSkey = (await NTQQUserApi.getPSkey([payload.domain]))[payload.domain];
// // 取Pskey
// // NodeIKernelTipOffService.getPskey([ 'qun.qq.com' ], true )
// // {
// // domainPskeyMap: 0,
// // errMsg: 'success',
// // domainPskeyMap: Map(1) {
// // 'qun.qq.com' => 'pskey'
// // }
// // }
// if (!_PSkey || !_Skey) {
// throw new Error('获取Cookies失败');
// }
// const cookies = `p_skey=${_PSkey}; skey=${_Skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`;
// return {
// cookies
// };
const cookiesObject = await NTQQUserApi.getCookies(payload.domain);
//把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ');
const bkn = WebApi.genBkn(cookiesObject.p_skey);
return { cookies, bkn };
}
}

View File

@@ -0,0 +1,40 @@
import { OB11User } from '../../types';
import { OB11Constructor } from '../../constructor';
import { friends } from '@/core/data';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQFriendApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
// no_cache get时传字符串
const SchemaData = {
type: 'object',
properties: {
no_cache: { type: ['boolean', 'string'] },
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
if (requireMinNTQQBuild('26702')) {
//全新逻辑
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true'));
}
if (friends.size === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
const _friends = await NTQQFriendApi.getFriends(true);
// log('强制刷新好友列表,结果: ', _friends)
if (_friends.length > 0) {
friends.clear();
for (const friend of _friends) {
friends.set(friend.uid, friend);
}
}
}
return OB11Constructor.friends(Array.from(friends.values()));
}
}

View File

@@ -0,0 +1,52 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQMsgApi, NTQQUserApi } from '@/core';
import { OB11Constructor } from '@/onebot11/constructor';
const SchemaData = {
type: 'object',
properties: {
count: { type: ['number', 'string'] }
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GetRecentContact extends BaseAction<Payload, any> {
actionName = ActionName.GetRecentContact;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQUserApi.getRecentContactListSnapShot(parseInt((payload.count || 10).toString()));
const data = await Promise.all(ret.info.changedList.map(async (t) => {
const FastMsg = await NTQQMsgApi.getMsgsByMsgId({ chatType: t.chatType, peerUid: t.peerUid }, [t.msgId]);
if (FastMsg.msgList.length > 0) {
//扩展ret.info.changedList
const lastestMsg = await OB11Constructor.message(FastMsg.msgList[0]);
return {
lastestMsg: lastestMsg,
peerUin: t.peerUin,
remark: t.remark,
msgTime: t.msgTime,
chatType: t.chatType,
msgId: t.msgId,
sendNickName: t.sendNickName,
sendMemberName: t.sendMemberName,
peerName: t.peerName
};
}
return {
peerUin: t.peerUin,
remark: t.remark,
msgTime: t.msgTime,
chatType: t.chatType,
msgId: t.msgId,
sendNickName: t.sendNickName,
sendMemberName: t.sendMemberName,
peerName: t.peerName
};
}));
return data;
}
}

View File

@@ -0,0 +1,35 @@
import { NTQQUserApi } from '@/core/apis';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
times: { type: ['number', 'string'] }
},
required: ['user_id', 'times']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
//logDebug('点赞参数', payload);
try {
const qq = payload.user_id.toString();
const uid: string = await NTQQUserApi.getUidByUin(qq) || '';
const result = await NTQQUserApi.like(uid, parseInt(payload.times?.toString()) || 1);
//logDebug('点赞结果', result);
if (result.result !== 0) {
throw Error(result.errMsg);
}
} catch (e) {
throw `点赞失败 ${e}`;
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQFriendApi } from '@/core/apis/friend';
const SchemaData = {
type: 'object',
properties: {
flag: { type: 'string' },
approve: { type: ['string', 'boolean'] },
remark: { type: 'string' }
},
required: ['flag']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve?.toString() !== 'false';
await NTQQFriendApi.handleFriendRequest(payload.flag, approve);
return null;
}
}

View File

@@ -0,0 +1,16 @@
import { selfInfo } from '@/core/data';
export enum EventType {
META = 'meta_event',
REQUEST = 'request',
NOTICE = 'notice',
MESSAGE = 'message',
MESSAGE_SENT = 'message_sent',
}
export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000);
self_id = parseInt(selfInfo.uin);
post_type: EventType = EventType.META;
}

View File

@@ -0,0 +1,5 @@
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
export abstract class OB11BaseMessageEvent extends OB11BaseEvent {
post_type = EventType.MESSAGE;
}

View File

@@ -0,0 +1,6 @@
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META;
meta_event_type: string;
}

View File

@@ -0,0 +1,21 @@
import { OB11BaseMetaEvent } from './OB11BaseMetaEvent';
interface HeartbeatStatus {
online: boolean | null,
good: boolean
}
export class OB11HeartbeatEvent extends OB11BaseMetaEvent {
meta_event_type = 'heartbeat';
status: HeartbeatStatus;
interval: number;
public constructor(isOnline: boolean, isGood: boolean, interval: number) {
super();
this.interval = interval;
this.status = {
online: isOnline,
good: isGood
};
}
}

View File

@@ -0,0 +1,17 @@
import { OB11BaseMetaEvent } from './OB11BaseMetaEvent';
export enum LifeCycleSubType {
ENABLE = 'enable',
DISABLE = 'disable',
CONNECT = 'connect'
}
export class OB11LifeCycleEvent extends OB11BaseMetaEvent {
meta_event_type = 'lifecycle';
sub_type: LifeCycleSubType;
public constructor(subType: LifeCycleSubType) {
super();
this.sub_type = subType;
}
}

View File

@@ -0,0 +1,5 @@
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
export abstract class OB11BaseNoticeEvent extends OB11BaseEvent {
post_type = EventType.NOTICE;
}

View File

@@ -0,0 +1,11 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
export class OB11FriendAddNoticeEvent extends OB11BaseNoticeEvent {
notice_type = 'friend_add';
user_id: number;
public constructor(user_Id: number) {
super();
this.user_id = user_Id;
}
}

View File

@@ -0,0 +1,13 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
export class OB11FriendRecallNoticeEvent extends OB11BaseNoticeEvent {
notice_type = 'friend_recall';
user_id: number;
message_id: number;
public constructor(userId: number, messageId: number) {
super();
this.user_id = userId;
this.message_id = messageId;
}
}

View File

@@ -0,0 +1,6 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin';
sub_type: 'set' | 'unset' = "set"; // "set" | "unset"
}

View File

@@ -0,0 +1,17 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
notice_type = 'group_ban';
operator_id: number;
duration: number;
sub_type: 'ban' | 'lift_ban';
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: 'ban' | 'lift_ban') {
super();
this.group_id = groupId;
this.operator_id = operatorId;
this.user_id = userId;
this.duration = duration;
this.sub_type = sub_type;
}
}

View File

@@ -0,0 +1,16 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupCardEvent extends OB11GroupNoticeEvent {
notice_type = 'group_card';
card_new: string;
card_old: string;
constructor(groupId: number, userId: number, cardNew: string, cardOld: string) {
super();
this.group_id = groupId;
this.user_id = userId;
this.card_new = cardNew;
this.card_old = cardOld;
}
}

View File

@@ -0,0 +1,17 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me';
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease';
sub_type: GroupDecreaseSubType = 'leave'; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operator_id: number;
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super();
this.group_id = groupId;
this.operator_id = operatorId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的
this.user_id = userId;
this.sub_type = subType;
}
}

View File

@@ -0,0 +1,14 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
notice_type = 'essence';
message_id: number;
sender_id: number;
sub_type: 'add' | 'delete' = 'add';
constructor(groupId: number, message_id: number, sender_id: number) {
super();
this.group_id = groupId;
this.message_id = message_id;
this.sender_id = sender_id;
}
}

View File

@@ -0,0 +1,15 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
type GroupIncreaseSubType = 'approve' | 'invite';
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_increase';
operator_id: number;
sub_type: GroupIncreaseSubType;
constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = 'approve') {
super();
this.group_id = groupId;
this.operator_id = operatorId;
this.user_id = userId;
this.sub_type = subType;
}
}

Some files were not shown because too many files have changed in this diff Show More