diff --git a/src/core/core.ts b/src/core/core.ts index 5d7fbe80..7e0fca87 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -10,6 +10,7 @@ import { SelfInfo, LineDevice, SelfStatusInfo } from "./entities"; import { LegacyNTEventWrapper } from "@/common/framework/event-legacy"; import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "./apis"; import os from "node:os"; +import { NTQQCollectionApi } from "./apis/collection"; export enum NapCatCoreWorkingEnv { Unknown = 0, Shell = 1, @@ -42,6 +43,7 @@ export class NapCatCore { this.eventWrapper = new LegacyNTEventWrapper(context.wrapper, context.session); this.initNapCatCoreListeners().then().catch(console.error); this.ApiContext = { + CollectionApi:new NTQQCollectionApi(this.context, this), WebApi: new NTQQWebApi(this.context, this), FriendApi: new NTQQFriendApi(this.context, this), MsgApi: new NTQQMsgApi(this.context, this), diff --git a/src/core/wrapper/context.ts b/src/core/wrapper/context.ts index 70e97fea..114f668b 100644 --- a/src/core/wrapper/context.ts +++ b/src/core/wrapper/context.ts @@ -5,6 +5,7 @@ import { SelfInfo } from "../entities"; import { NodeIKernelLoginService } from "../services"; import { WrapperNodeApi, NodeIQQNTWrapperSession } from "@/core"; import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "../apis"; +import { NTQQCollectionApi } from "../apis/collection"; export interface InstanceContext { readonly workingEnv: NapCatCoreWorkingEnv; @@ -15,6 +16,7 @@ export interface InstanceContext { readonly basicInfoWrapper: QQBasicInfoWrapper; } export interface NTApiContext { + CollectionApi: NTQQCollectionApi, WebApi: NTQQWebApi, FriendApi: NTQQFriendApi, MsgApi: NTQQMsgApi, diff --git a/src/onebot/action/BaseAction.ts b/src/onebot/action/BaseAction.ts new file mode 100644 index 00000000..01973e9f --- /dev/null +++ b/src/onebot/action/BaseAction.ts @@ -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 { + actionName!: ActionName; + CoreContext!: NapCatCore; + private validate: undefined | ValidateFunction = undefined; + PayloadSchema: any = undefined; + protected async check(payload: PayloadType): Promise { + 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> { + 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> { + 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 { + throw `pleas override ${this.actionName} _handle`; + } +} + +export default BaseAction; diff --git a/src/onebot/action/OB11Response.ts b/src/onebot/action/OB11Response.ts new file mode 100644 index 00000000..ddc9d72b --- /dev/null +++ b/src/onebot/action/OB11Response.ts @@ -0,0 +1,32 @@ +import { OB11Return } from '../types'; + +import { isNull } from '../../common/utils/helper'; + +export class OB11Response { + static res(data: T, status: string, retcode: number, message: string = ''): OB11Return { + return { + status: status, + retcode: retcode, + data: data, + message: message, + wording: message, + echo: null + }; + } + + static ok(data: T, echo: any = null) { + const res = OB11Response.res(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; + } +} diff --git a/src/onebot/action/extends/CreateCollection.ts b/src/onebot/action/extends/CreateCollection.ts new file mode 100644 index 00000000..257a16fc --- /dev/null +++ b/src/onebot/action/extends/CreateCollection.ts @@ -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; + +export class CreateCollection extends BaseAction { + 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 + ); + } +} diff --git a/src/onebot/action/extends/Debug.ts b/src/onebot/action/extends/Debug.ts new file mode 100644 index 00000000..6f73ae5d --- /dev/null +++ b/src/onebot/action/extends/Debug.ts @@ -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 { + actionName = ActionName.Debug; + + protected async _handle(payload: Payload): Promise { + //logDebug('debug call ntqq api', payload); + const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, + // NTQQFileCacheApi, + ]; + for (const ntqqApiClass of ntqqApi) { + // logDebug('ntqqApiClass', ntqqApiClass); + const method = (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 + } +} diff --git a/src/onebot/action/extends/FetchCustomFace.ts b/src/onebot/action/extends/FetchCustomFace.ts new file mode 100644 index 00000000..dee0cc08 --- /dev/null +++ b/src/onebot/action/extends/FetchCustomFace.ts @@ -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; + +export class FetchCustomFace extends BaseAction { + 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); + } +} diff --git a/src/onebot/action/extends/FetchEmojioLike.ts b/src/onebot/action/extends/FetchEmojioLike.ts new file mode 100644 index 00000000..baf43dc4 --- /dev/null +++ b/src/onebot/action/extends/FetchEmojioLike.ts @@ -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; + +export class FetchEmojioLike extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/extends/GetCollectionList.ts b/src/onebot/action/extends/GetCollectionList.ts new file mode 100644 index 00000000..b768d555 --- /dev/null +++ b/src/onebot/action/extends/GetCollectionList.ts @@ -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; + +export class GetCollectionList extends BaseAction { + actionName = ActionName.GetCollectionList; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return await NTQQCollectionApi.getAllCollection(payload.category, payload.count); + } +} diff --git a/src/onebot/action/extends/GetFriendWithCategory.ts b/src/onebot/action/extends/GetFriendWithCategory.ts new file mode 100644 index 00000000..9f3dae9c --- /dev/null +++ b/src/onebot/action/extends/GetFriendWithCategory.ts @@ -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 { + 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'); + } + } +} diff --git a/src/onebot/action/extends/GetGroupAddRequest.ts b/src/onebot/action/extends/GetGroupAddRequest.ts new file mode 100644 index 00000000..1d6ebe08 --- /dev/null +++ b/src/onebot/action/extends/GetGroupAddRequest.ts @@ -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 { + actionName = ActionName.GetGroupIgnoreAddRequest; + + protected async _handle(payload: null): Promise { + 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; + } +} diff --git a/src/onebot/action/extends/GetProfileLike.ts b/src/onebot/action/extends/GetProfileLike.ts new file mode 100644 index 00000000..80690adc --- /dev/null +++ b/src/onebot/action/extends/GetProfileLike.ts @@ -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 { + 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; + } +} diff --git a/src/onebot/action/extends/GetRobotUinRange.ts b/src/onebot/action/extends/GetRobotUinRange.ts new file mode 100644 index 00000000..d5b85aba --- /dev/null +++ b/src/onebot/action/extends/GetRobotUinRange.ts @@ -0,0 +1,11 @@ +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQUserApi } from '@/core/apis'; +export class GetRobotUinRange extends BaseAction> { + actionName = ActionName.GetRobotUinRange; + + protected async _handle(payload: void) { + // console.log(await NTQQUserApi.getRobotUinRange()); + return await NTQQUserApi.getRobotUinRange(); + } +} diff --git a/src/onebot/action/extends/OCRImage.ts b/src/onebot/action/extends/OCRImage.ts new file mode 100644 index 00000000..8128fe4e --- /dev/null +++ b/src/onebot/action/extends/OCRImage.ts @@ -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; + +export class OCRImage extends BaseAction { + 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; +} diff --git a/src/onebot/action/extends/SetGroupHeader.ts b/src/onebot/action/extends/SetGroupHeader.ts new file mode 100644 index 00000000..1998301b --- /dev/null +++ b/src/onebot/action/extends/SetGroupHeader.ts @@ -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 { + actionName = ActionName.SetGroupHeader; + // 用不着复杂检测 + protected async check(payload: Payload): Promise { + 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 { + 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; + } +} diff --git a/src/onebot/action/extends/SetLongNick.ts b/src/onebot/action/extends/SetLongNick.ts new file mode 100644 index 00000000..2f1f8c5d --- /dev/null +++ b/src/onebot/action/extends/SetLongNick.ts @@ -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; + +export class SetLongNick extends BaseAction { + actionName = ActionName.SetLongNick; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + const ret = await NTQQUserApi.setLongNick(payload.longNick); + return ret; + } +} diff --git a/src/onebot/action/extends/SetOnlineStatus.ts b/src/onebot/action/extends/SetOnlineStatus.ts new file mode 100644 index 00000000..c06a2292 --- /dev/null +++ b/src/onebot/action/extends/SetOnlineStatus.ts @@ -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; + +export class SetOnlineStatus extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/extends/SetQQAvatar.ts b/src/onebot/action/extends/SetQQAvatar.ts new file mode 100644 index 00000000..f8006344 --- /dev/null +++ b/src/onebot/action/extends/SetQQAvatar.ts @@ -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 { + actionName = ActionName.SetQQAvatar; + // 用不着复杂检测 + protected async check(payload: Payload): Promise { + if (!payload.file || typeof payload.file != 'string') { + return { + valid: false, + message: 'file字段不能为空或者类型错误', + }; + } + return { + valid: true, + }; + } + protected async _handle(payload: Payload): Promise { + 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; + } +} diff --git a/src/onebot/action/extends/SetSelfProfile.ts b/src/onebot/action/extends/SetSelfProfile.ts new file mode 100644 index 00000000..5c85e9eb --- /dev/null +++ b/src/onebot/action/extends/SetSelfProfile.ts @@ -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; + +export class SetSelfProfile extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/extends/TestApi01.ts b/src/onebot/action/extends/TestApi01.ts new file mode 100644 index 00000000..566d63d2 --- /dev/null +++ b/src/onebot/action/extends/TestApi01.ts @@ -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; + +export default class TestApi01 extends BaseAction { + actionName = ActionName.TestApi01; + // 用不着复杂检测 + protected async check(payload: Payload): Promise { + return { + valid: true, + }; + } + protected async _handle(payload: Payload): Promise { + return await napCatCore.session.getMsgService().sendSsoCmdReqByContend(payload.cmd, payload.param); + } +} diff --git a/src/onebot/action/extends/TranslateEnWordToZn.ts b/src/onebot/action/extends/TranslateEnWordToZn.ts new file mode 100644 index 00000000..8088ab6d --- /dev/null +++ b/src/onebot/action/extends/TranslateEnWordToZn.ts @@ -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; + +export class TranslateEnWordToZn extends BaseAction | 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; + } +} diff --git a/src/onebot/action/extends/sharePeer.ts b/src/onebot/action/extends/sharePeer.ts new file mode 100644 index 00000000..abb63bff --- /dev/null +++ b/src/onebot/action/extends/sharePeer.ts @@ -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; + + +export class sharePeer extends BaseAction { + 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; +export class shareGroupEx extends BaseAction { + actionName = ActionName.ShareGroupEx; + PayloadSchema = SchemaDataGroupEx; + protected async _handle(payload: PayloadGroupEx) { + return await NTQQGroupApi.getArkJsonGroupShare(payload.group_id); + } +} \ No newline at end of file diff --git a/src/onebot/action/file/DelGroupFile.ts b/src/onebot/action/file/DelGroupFile.ts new file mode 100644 index 00000000..498cfe13 --- /dev/null +++ b/src/onebot/action/file/DelGroupFile.ts @@ -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; + +export class DelGroupFile extends BaseAction { + actionName = ActionName.DelGroupFile; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return await NTQQGroupApi.DelGroupFile(payload.group_id.toString(), [payload.file_id]); + } +} diff --git a/src/onebot/action/file/DelGroupFileFolder.ts b/src/onebot/action/file/DelGroupFileFolder.ts new file mode 100644 index 00000000..31907558 --- /dev/null +++ b/src/onebot/action/file/DelGroupFileFolder.ts @@ -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; + +export class DelGroupFileFolder extends BaseAction { + actionName = ActionName.DelGroupFileFolder; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return (await NTQQGroupApi.DelGroupFileFolder(payload.group_id.toString(), payload.folder_id)).groupFileCommonResult; + } +} diff --git a/src/onebot/action/file/GetFile.ts b/src/onebot/action/file/GetFile.ts new file mode 100644 index 00000000..85672682 --- /dev/null +++ b/src/onebot/action/file/GetFile.ts @@ -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 { + 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 { + 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; + +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 { + payload.file = payload.file_id; + return super._handle(payload); + } +} diff --git a/src/onebot/action/file/GetGroupFileCount.ts b/src/onebot/action/file/GetGroupFileCount.ts new file mode 100644 index 00000000..2b84a2b6 --- /dev/null +++ b/src/onebot/action/file/GetGroupFileCount.ts @@ -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; + +export class GetGroupFileCount extends BaseAction { + actionName = ActionName.GetGroupFileCount; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + const ret = await NTQQGroupApi.GetGroupFileCount([payload.group_id?.toString()]); + return { count: ret.groupFileCounts[0] }; + } +} diff --git a/src/onebot/action/file/GetGroupFileList.ts b/src/onebot/action/file/GetGroupFileList.ts new file mode 100644 index 00000000..0d5a48e7 --- /dev/null +++ b/src/onebot/action/file/GetGroupFileList.ts @@ -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; + +export class GetGroupFileList extends BaseAction }> { + 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 }; + } +} diff --git a/src/onebot/action/file/GetImage.ts b/src/onebot/action/file/GetImage.ts new file mode 100644 index 00000000..457ccb29 --- /dev/null +++ b/src/onebot/action/file/GetImage.ts @@ -0,0 +1,7 @@ +import { GetFileBase } from './GetFile'; +import { ActionName } from '../types'; + + +export default class GetImage extends GetFileBase { + actionName = ActionName.GetImage; +} \ No newline at end of file diff --git a/src/onebot/action/file/GetRecord.ts b/src/onebot/action/file/GetRecord.ts new file mode 100644 index 00000000..25435974 --- /dev/null +++ b/src/onebot/action/file/GetRecord.ts @@ -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 { + const res = super._handle(payload); + return res; + } +} \ No newline at end of file diff --git a/src/onebot/action/file/SetGroupFileFolder.ts b/src/onebot/action/file/SetGroupFileFolder.ts new file mode 100644 index 00000000..856a2ba5 --- /dev/null +++ b/src/onebot/action/file/SetGroupFileFolder.ts @@ -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; + +export class SetGroupFileFolder extends BaseAction { + actionName = ActionName.SetGroupFileFolder; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return (await NTQQGroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; + } +} diff --git a/src/onebot/action/go-cqhttp/DownloadFile.ts b/src/onebot/action/go-cqhttp/DownloadFile.ts new file mode 100644 index 00000000..19fc9446 --- /dev/null +++ b/src/onebot/action/go-cqhttp/DownloadFile.ts @@ -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; + +export default class GoCQHTTPDownloadFile extends BaseAction { + actionName = ActionName.GoCQHTTP_DownloadFile; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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 { + const headers: Record = {}; + 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; + } +} diff --git a/src/onebot/action/go-cqhttp/GetForwardMsg.ts b/src/onebot/action/go-cqhttp/GetForwardMsg.ts new file mode 100644 index 00000000..375aefd1 --- /dev/null +++ b/src/onebot/action/go-cqhttp/GetForwardMsg.ts @@ -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; + +interface Response { + messages: (OB11Message & { content: OB11MessageData })[]; +} + +export class GoCQHTTPGetForwardMsgAction extends BaseAction { + actionName = ActionName.GoCQHTTP_GetForwardMsg; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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 => { + (msg).content = msg.message; + delete (msg).message; + }); + return { messages }; + } +} diff --git a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts new file mode 100644 index 00000000..3ca15764 --- /dev/null +++ b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts @@ -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; + +export default class GetFriendMsgHistory extends BaseAction { + actionName = ActionName.GetFriendMsgHistory; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + //处理参数 + 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 }; + } +} diff --git a/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts b/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts new file mode 100644 index 00000000..2d84f476 --- /dev/null +++ b/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts @@ -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; + +export class GetGroupHonorInfo extends BaseAction> { + 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); + } +} diff --git a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts new file mode 100644 index 00000000..de518621 --- /dev/null +++ b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts @@ -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; + +export default class GoCQHTTPGetGroupMsgHistory extends BaseAction { + actionName = ActionName.GoCQHTTP_GetGroupMsgHistory; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + //处理参数 + 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 }; + } +} diff --git a/src/onebot/action/go-cqhttp/GetOnlineClient.ts b/src/onebot/action/go-cqhttp/GetOnlineClient.ts new file mode 100644 index 00000000..22937d15 --- /dev/null +++ b/src/onebot/action/go-cqhttp/GetOnlineClient.ts @@ -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> { + actionName = ActionName.GetOnlineClient; + + protected async _handle(payload: void) { + NTQQSystemApi.getOnlineDev(); + await sleep(500); + return DeviceList; + } +} diff --git a/src/onebot/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot/action/go-cqhttp/GetStrangerInfo.ts new file mode 100644 index 00000000..b7f1c223 --- /dev/null +++ b/src/onebot/action/go-cqhttp/GetStrangerInfo.ts @@ -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; + +export default class GoCQHTTPGetStrangerInfo extends BaseAction { + actionName = ActionName.GoCQHTTP_GetStrangerInfo; + + protected async _handle(payload: Payload): Promise { + 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); + } + } +} diff --git a/src/onebot/action/go-cqhttp/QuickAction.ts b/src/onebot/action/go-cqhttp/QuickAction.ts new file mode 100644 index 00000000..cf1926cc --- /dev/null +++ b/src/onebot/action/go-cqhttp/QuickAction.ts @@ -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{ + actionName = ActionName.GoCQHTTP_HandleQuickAction; + protected async _handle(payload: Payload): Promise { + handleQuickOperation(payload.context, payload.operation).then().catch(log); + return null; + } +} \ No newline at end of file diff --git a/src/onebot/action/go-cqhttp/SendForwardMsg.ts b/src/onebot/action/go-cqhttp/SendForwardMsg.ts new file mode 100644 index 00000000..fab6d218 --- /dev/null +++ b/src/onebot/action/go-cqhttp/SendForwardMsg.ts @@ -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; +} diff --git a/src/onebot/action/go-cqhttp/SendGroupNotice.ts b/src/onebot/action/go-cqhttp/SendGroupNotice.ts new file mode 100644 index 00000000..4be85a83 --- /dev/null +++ b/src/onebot/action/go-cqhttp/SendGroupNotice.ts @@ -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; + +export class SendGroupNotice extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/go-cqhttp/UploadGroupFile.ts b/src/onebot/action/go-cqhttp/UploadGroupFile.ts new file mode 100644 index 00000000..d00895db --- /dev/null +++ b/src/onebot/action/go-cqhttp/UploadGroupFile.ts @@ -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; + +export default class GoCQHTTPUploadGroupFile extends BaseAction { + actionName = ActionName.GoCQHTTP_UploadGroupFile; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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; + } +} diff --git a/src/onebot/action/go-cqhttp/UploadPrivareFile.ts b/src/onebot/action/go-cqhttp/UploadPrivareFile.ts new file mode 100644 index 00000000..853ca721 --- /dev/null +++ b/src/onebot/action/go-cqhttp/UploadPrivareFile.ts @@ -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; + +export default class GoCQHTTPUploadPrivateFile extends BaseAction { + actionName = ActionName.GOCQHTTP_UploadPrivateFile; + PayloadSchema = SchemaData; + async getPeer(payload: Payload): Promise { + 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 { + 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; + } +} diff --git a/src/onebot/action/group/DelEssenceMsg.ts b/src/onebot/action/group/DelEssenceMsg.ts new file mode 100644 index 00000000..03b422be --- /dev/null +++ b/src/onebot/action/group/DelEssenceMsg.ts @@ -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; + +export default class DelEssenceMsg extends BaseAction { + actionName = ActionName.DelEssenceMsg; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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 + ); + } +} diff --git a/src/onebot/action/group/GetGroupEssence.ts b/src/onebot/action/group/GetGroupEssence.ts new file mode 100644 index 00000000..7e90d6fc --- /dev/null +++ b/src/onebot/action/group/GetGroupEssence.ts @@ -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; + +export class GetGroupEssence extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/group/GetGroupInfo.ts b/src/onebot/action/group/GetGroupInfo.ts new file mode 100644 index 00000000..1a460017 --- /dev/null +++ b/src/onebot/action/group/GetGroupInfo.ts @@ -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; + +class GetGroupInfo extends BaseAction { + 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; diff --git a/src/onebot/action/group/GetGroupList.ts b/src/onebot/action/group/GetGroupList.ts new file mode 100644 index 00000000..396649ff --- /dev/null +++ b/src/onebot/action/group/GetGroupList.ts @@ -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; + +class GetGroupList extends BaseAction { + 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; diff --git a/src/onebot/action/group/GetGroupMemberInfo.ts b/src/onebot/action/group/GetGroupMemberInfo.ts new file mode 100644 index 00000000..06b2c3f0 --- /dev/null +++ b/src/onebot/action/group/GetGroupMemberInfo.ts @@ -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; + +class GetGroupMemberInfo extends BaseAction { + 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; \ No newline at end of file diff --git a/src/onebot/action/group/GetGroupMemberInfoOld.ts b/src/onebot/action/group/GetGroupMemberInfoOld.ts new file mode 100644 index 00000000..7d9708ce --- /dev/null +++ b/src/onebot/action/group/GetGroupMemberInfoOld.ts @@ -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; + +class GetGroupMemberInfo extends BaseAction { + 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; \ No newline at end of file diff --git a/src/onebot/action/group/GetGroupMemberList.ts b/src/onebot/action/group/GetGroupMemberList.ts new file mode 100644 index 00000000..ba1d6e4a --- /dev/null +++ b/src/onebot/action/group/GetGroupMemberList.ts @@ -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; + +class GetGroupMemberList extends BaseAction { + 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 = new Map(); + // 转为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; diff --git a/src/onebot/action/group/GetGroupNotice.ts b/src/onebot/action/group/GetGroupNotice.ts new file mode 100644 index 00000000..cd61e25f --- /dev/null +++ b/src/onebot/action/group/GetGroupNotice.ts @@ -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; + +type ApiGroupNotice = GroupNotice & WebApiGroupNoticeFeed; +export class GetGroupNotice extends BaseAction { + 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(); + 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; + } +} diff --git a/src/onebot/action/group/GetGroupSystemMsg.ts b/src/onebot/action/group/GetGroupSystemMsg.ts new file mode 100644 index 00000000..cb6f4bb7 --- /dev/null +++ b/src/onebot/action/group/GetGroupSystemMsg.ts @@ -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; + +export class GetGroupSystemMsg extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/group/GetGuildList.ts b/src/onebot/action/group/GetGuildList.ts new file mode 100644 index 00000000..ea36304b --- /dev/null +++ b/src/onebot/action/group/GetGuildList.ts @@ -0,0 +1,10 @@ +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; + +export default class GetGuildList extends BaseAction { + actionName = ActionName.GetGuildList; + + protected async _handle(payload: null): Promise { + return null; + } +} \ No newline at end of file diff --git a/src/onebot/action/group/SendGroupMsg.ts b/src/onebot/action/group/SendGroupMsg.ts new file mode 100644 index 00000000..22c06831 --- /dev/null +++ b/src/onebot/action/group/SendGroupMsg.ts @@ -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 { + delete payload.user_id; + payload.message_type = 'group'; + return super.check(payload); + } +} + +export default SendGroupMsg; diff --git a/src/onebot/action/group/SetEssenceMsg.ts b/src/onebot/action/group/SetEssenceMsg.ts new file mode 100644 index 00000000..b8cb2f45 --- /dev/null +++ b/src/onebot/action/group/SetEssenceMsg.ts @@ -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; + +export default class SetEssenceMsg extends BaseAction { + actionName = ActionName.SetEssenceMsg; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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 + ); + } +} diff --git a/src/onebot/action/group/SetGroupAddRequest.ts b/src/onebot/action/group/SetGroupAddRequest.ts new file mode 100644 index 00000000..3f079ff1 --- /dev/null +++ b/src/onebot/action/group/SetGroupAddRequest.ts @@ -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; + +export default class SetGroupAddRequest extends BaseAction { + actionName = ActionName.SetGroupAddRequest; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + const flag = payload.flag.toString(); + const approve = payload.approve?.toString() !== 'false'; + await NTQQGroupApi.handleGroupRequest(flag, + approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, + payload.reason + ); + return null; + } +} diff --git a/src/onebot/action/group/SetGroupAdmin.ts b/src/onebot/action/group/SetGroupAdmin.ts new file mode 100644 index 00000000..42701b62 --- /dev/null +++ b/src/onebot/action/group/SetGroupAdmin.ts @@ -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; + +export default class SetGroupAdmin extends BaseAction { + actionName = ActionName.SetGroupAdmin; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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; + } +} diff --git a/src/onebot/action/group/SetGroupBan.ts b/src/onebot/action/group/SetGroupBan.ts new file mode 100644 index 00000000..27155b46 --- /dev/null +++ b/src/onebot/action/group/SetGroupBan.ts @@ -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; + +export default class SetGroupBan extends BaseAction { + actionName = ActionName.SetGroupBan; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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; + } +} diff --git a/src/onebot/action/group/SetGroupCard.ts b/src/onebot/action/group/SetGroupCard.ts new file mode 100644 index 00000000..aef7274c --- /dev/null +++ b/src/onebot/action/group/SetGroupCard.ts @@ -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; + +export default class SetGroupCard extends BaseAction { + actionName = ActionName.SetGroupCard; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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; + } +} diff --git a/src/onebot/action/group/SetGroupKick.ts b/src/onebot/action/group/SetGroupKick.ts new file mode 100644 index 00000000..2af20234 --- /dev/null +++ b/src/onebot/action/group/SetGroupKick.ts @@ -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; + +export default class SetGroupKick extends BaseAction { + actionName = ActionName.SetGroupKick; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + 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; + } +} diff --git a/src/onebot/action/group/SetGroupLeave.ts b/src/onebot/action/group/SetGroupLeave.ts new file mode 100644 index 00000000..fed7d67c --- /dev/null +++ b/src/onebot/action/group/SetGroupLeave.ts @@ -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; +export default class SetGroupLeave extends BaseAction { + actionName = ActionName.SetGroupLeave; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + try { + await NTQQGroupApi.quitGroup(payload.group_id.toString()); + deleteGroup(payload.group_id.toString()); + } catch (e) { + logError('退群失败', e); + throw e; + } + } +} diff --git a/src/onebot/action/group/SetGroupName.ts b/src/onebot/action/group/SetGroupName.ts new file mode 100644 index 00000000..d65383cb --- /dev/null +++ b/src/onebot/action/group/SetGroupName.ts @@ -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; +export default class SetGroupName extends BaseAction { + actionName = ActionName.SetGroupName; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + + await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name); + return null; + } +} diff --git a/src/onebot/action/group/SetGroupWholeBan.ts b/src/onebot/action/group/SetGroupWholeBan.ts new file mode 100644 index 00000000..38b4ffc7 --- /dev/null +++ b/src/onebot/action/group/SetGroupWholeBan.ts @@ -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; + +export default class SetGroupWholeBan extends BaseAction { + actionName = ActionName.SetGroupWholeBan; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + const enable = payload.enable?.toString() !== 'false'; + await NTQQGroupApi.banGroup(payload.group_id.toString(), enable); + return null; + } +} diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts new file mode 100644 index 00000000..f4df4ea3 --- /dev/null +++ b/src/onebot/action/index.ts @@ -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>(); + 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(); diff --git a/src/onebot/action/msg/DeleteMsg.ts b/src/onebot/action/msg/DeleteMsg.ts new file mode 100644 index 00000000..f180329c --- /dev/null +++ b/src/onebot/action/msg/DeleteMsg.ts @@ -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; + +class DeleteMsg extends BaseAction { + 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', + 1, + 5000, + (msgs) => { + if (msgs.find(m => m.msgId === msg.MsgId && m.recallTime !== '0')) { + return true; + } + return false; + } + ).catch(e => new Promise((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; diff --git a/src/onebot/action/msg/ForwardSingleMsg.ts b/src/onebot/action/msg/ForwardSingleMsg.ts new file mode 100644 index 00000000..1c200437 --- /dev/null +++ b/src/onebot/action/msg/ForwardSingleMsg.ts @@ -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; + +class ForwardSingleMsg extends BaseAction { + protected async getTargetPeer(payload: Payload): Promise { + 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 { + 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; +} diff --git a/src/onebot/action/msg/GetMsg.ts b/src/onebot/action/msg/GetMsg.ts new file mode 100644 index 00000000..29edde9e --- /dev/null +++ b/src/onebot/action/msg/GetMsg.ts @@ -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; + +class GetMsg extends BaseAction { + 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; diff --git a/src/onebot/action/msg/MarkMsgAsRead.ts b/src/onebot/action/msg/MarkMsgAsRead.ts new file mode 100644 index 00000000..740be9cf --- /dev/null +++ b/src/onebot/action/msg/MarkMsgAsRead.ts @@ -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; + +class MarkMsgAsRead extends BaseAction { + async getPeer(payload: PlayloadType): Promise { + 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 { + // 调用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 { + actionName = ActionName.GoCQHTTP_MarkMsgAsRead; + + protected async _handle(payload: Payload): Promise { + return null; + } +} + +export class MarkAllMsgAsRead extends BaseAction { + actionName = ActionName._MarkAllMsgAsRead; + + protected async _handle(payload: Payload): Promise { + await NTQQMsgApi.markallMsgAsRead(); + return null; + } +} diff --git a/src/onebot/action/msg/SendMsg/check-send-message.ts b/src/onebot/action/msg/SendMsg/check-send-message.ts new file mode 100644 index 00000000..299f41dc --- /dev/null +++ b/src/onebot/action/msg/SendMsg/check-send-message.ts @@ -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; +} diff --git a/src/onebot/action/msg/SendMsg/create-send-elements.ts b/src/onebot/action/msg/SendMsg/create-send-elements.ts new file mode 100644 index 00000000..e23b0356 --- /dev/null +++ b/src/onebot/action/msg/SendMsg/create-send-elements.ts @@ -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, + // This picks the correct message type out + // How great the type system of TypeScript is! + context: MessageContext + ) => Promise +} = { + [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(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 +}>_handlers; + +export default async function createSendElements( + messageData: OB11MessageData[], + peer: Peer, + ignoreTypes: OB11MessageDataType[] = [] +) { + const deleteAfterSentFiles: string[] = []; + const callResultList: Array> = []; + 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 = ( + 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 }; +} diff --git a/src/onebot/action/msg/SendMsg/handle-forward-node.ts b/src/onebot/action/msg/SendMsg/handle-forward-node.ts new file mode 100644 index 00000000..c01c26e0 --- /dev/null +++ b/src/onebot/action/msg/SendMsg/handle-forward-node.ts @@ -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 { + 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 { + 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[] = []; + 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 = []; + 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; + } +} diff --git a/src/onebot/action/msg/SendMsg/index.ts b/src/onebot/action/msg/SendMsg/index.ts new file mode 100644 index 00000000..94255d01 --- /dev/null +++ b/src/onebot/action/msg/SendMsg/index.ts @@ -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 { + // 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 { + actionName = ActionName.SendMsg; + contextMode = ContextMode.Normal; + + protected async check(payload: OB11PostSendMsg): Promise { + 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; diff --git a/src/onebot/action/msg/SendPrivateMsg.ts b/src/onebot/action/msg/SendPrivateMsg.ts new file mode 100644 index 00000000..7c5cd7e3 --- /dev/null +++ b/src/onebot/action/msg/SendPrivateMsg.ts @@ -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 { + payload.message_type = 'private'; + return super.check(payload); + } +} + +export default SendPrivateMsg; diff --git a/src/onebot/action/msg/SetMsgEmojiLike.ts b/src/onebot/action/msg/SetMsgEmojiLike.ts new file mode 100644 index 00000000..edb8edcc --- /dev/null +++ b/src/onebot/action/msg/SetMsgEmojiLike.ts @@ -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; + +export class SetMsgEmojiLike extends BaseAction { + 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); + } +} diff --git a/src/onebot/action/system/CanSendImage.ts b/src/onebot/action/system/CanSendImage.ts new file mode 100644 index 00000000..459c3e66 --- /dev/null +++ b/src/onebot/action/system/CanSendImage.ts @@ -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; +} diff --git a/src/onebot/action/system/CanSendRecord.ts b/src/onebot/action/system/CanSendRecord.ts new file mode 100644 index 00000000..07741cfa --- /dev/null +++ b/src/onebot/action/system/CanSendRecord.ts @@ -0,0 +1,16 @@ +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; + +interface ReturnType { + yes: boolean +} + +export default class CanSendRecord extends BaseAction { + actionName = ActionName.CanSendRecord; + + protected async _handle(_payload: void): Promise { + return { + yes: true + }; + } +} diff --git a/src/onebot/action/system/CleanCache.ts b/src/onebot/action/system/CleanCache.ts new file mode 100644 index 00000000..0e0cafc2 --- /dev/null +++ b/src/onebot/action/system/CleanCache.ts @@ -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 { + actionName = ActionName.CleanCache; + + protected _handle(): Promise { + return new Promise(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>((res, rej) => { + }); +} diff --git a/src/onebot/action/system/GetLoginInfo.ts b/src/onebot/action/system/GetLoginInfo.ts new file mode 100644 index 00000000..7d43d3b7 --- /dev/null +++ b/src/onebot/action/system/GetLoginInfo.ts @@ -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 { + actionName = ActionName.GetLoginInfo; + + protected async _handle(payload: null) { + return OB11Constructor.selfInfo(selfInfo); + } +} + +export default GetLoginInfo; diff --git a/src/onebot/action/system/GetStatus.ts b/src/onebot/action/system/GetStatus.ts new file mode 100644 index 00000000..affbe13f --- /dev/null +++ b/src/onebot/action/system/GetStatus.ts @@ -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 { + actionName = ActionName.GetStatus; + + protected async _handle(payload: any): Promise { + return { + online: !!selfInfo.online, + good: true, + stat + }; + } +} diff --git a/src/onebot/action/system/GetVersionInfo.ts b/src/onebot/action/system/GetVersionInfo.ts new file mode 100644 index 00000000..330a1c59 --- /dev/null +++ b/src/onebot/action/system/GetVersionInfo.ts @@ -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 { + actionName = ActionName.GetVersionInfo; + + protected async _handle(payload: any): Promise { + return { + app_name: 'NapCat.Onebot', + protocol_version: 'v11', + app_version: version + }; + } +} diff --git a/src/onebot/action/system/Reboot.ts b/src/onebot/action/system/Reboot.ts new file mode 100644 index 00000000..19d6ad9c --- /dev/null +++ b/src/onebot/action/system/Reboot.ts @@ -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; + +export class Reboot extends BaseAction { + actionName = ActionName.Reboot; + + protected async _handle(payload: Payload): Promise { + if (payload.delay) { + setTimeout(() => { + rebootWithQuickLogin(selfInfo.uin); + }, payload.delay); + } else { + rebootWithQuickLogin(selfInfo.uin); + } + return null; + } +} +export class RebootNormal extends BaseAction { + actionName = ActionName.RebootNormal; + + protected async _handle(payload: Payload): Promise { + if (payload.delay) { + setTimeout(() => { + rebootWithNormolLogin(); + }, payload.delay); + } else { + rebootWithNormolLogin(); + } + return null; + } +} diff --git a/src/onebot/action/types.ts b/src/onebot/action/types.ts new file mode 100644 index 00000000..7925a944 --- /dev/null +++ b/src/onebot/action/types.ts @@ -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' +} diff --git a/src/onebot/action/user/GetCookies.ts b/src/onebot/action/user/GetCookies.ts new file mode 100644 index 00000000..cbd852f1 --- /dev/null +++ b/src/onebot/action/user/GetCookies.ts @@ -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; + +export class GetCookies extends BaseAction { + 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 }; + } +} diff --git a/src/onebot/action/user/GetFriendList.ts b/src/onebot/action/user/GetFriendList.ts new file mode 100644 index 00000000..e72eaa31 --- /dev/null +++ b/src/onebot/action/user/GetFriendList.ts @@ -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; +export default class GetFriendList extends BaseAction { + 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())); + } +} diff --git a/src/onebot/action/user/GetRecentContact.ts b/src/onebot/action/user/GetRecentContact.ts new file mode 100644 index 00000000..9132d244 --- /dev/null +++ b/src/onebot/action/user/GetRecentContact.ts @@ -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; + +export default class GetRecentContact extends BaseAction { + 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; + } +} diff --git a/src/onebot/action/user/SendLike.ts b/src/onebot/action/user/SendLike.ts new file mode 100644 index 00000000..a3197523 --- /dev/null +++ b/src/onebot/action/user/SendLike.ts @@ -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; + +export default class SendLike extends BaseAction { + actionName = ActionName.SendLike; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + //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; + } +} diff --git a/src/onebot/action/user/SetFriendAddRequest.ts b/src/onebot/action/user/SetFriendAddRequest.ts new file mode 100644 index 00000000..1befb65a --- /dev/null +++ b/src/onebot/action/user/SetFriendAddRequest.ts @@ -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; + +export default class SetFriendAddRequest extends BaseAction { + actionName = ActionName.SetFriendAddRequest; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload): Promise { + const approve = payload.approve?.toString() !== 'false'; + await NTQQFriendApi.handleFriendRequest(payload.flag, approve); + return null; + } +} diff --git a/src/onebot/event/OB11BaseEvent.ts b/src/onebot/event/OB11BaseEvent.ts new file mode 100644 index 00000000..377ad9ac --- /dev/null +++ b/src/onebot/event/OB11BaseEvent.ts @@ -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; +} diff --git a/src/onebot/event/message/OB11BaseMessageEvent.ts b/src/onebot/event/message/OB11BaseMessageEvent.ts new file mode 100644 index 00000000..8ed0f8f6 --- /dev/null +++ b/src/onebot/event/message/OB11BaseMessageEvent.ts @@ -0,0 +1,5 @@ +import { EventType, OB11BaseEvent } from '../OB11BaseEvent'; + +export abstract class OB11BaseMessageEvent extends OB11BaseEvent { + post_type = EventType.MESSAGE; +} \ No newline at end of file diff --git a/src/onebot/event/meta/OB11BaseMetaEvent.ts b/src/onebot/event/meta/OB11BaseMetaEvent.ts new file mode 100644 index 00000000..4a1b9344 --- /dev/null +++ b/src/onebot/event/meta/OB11BaseMetaEvent.ts @@ -0,0 +1,6 @@ +import { EventType, OB11BaseEvent } from '../OB11BaseEvent'; + +export abstract class OB11BaseMetaEvent extends OB11BaseEvent { + post_type = EventType.META; + meta_event_type: string; +} \ No newline at end of file diff --git a/src/onebot/event/meta/OB11HeartbeatEvent.ts b/src/onebot/event/meta/OB11HeartbeatEvent.ts new file mode 100644 index 00000000..ad2a0c55 --- /dev/null +++ b/src/onebot/event/meta/OB11HeartbeatEvent.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/src/onebot/event/meta/OB11LifeCycleEvent.ts b/src/onebot/event/meta/OB11LifeCycleEvent.ts new file mode 100644 index 00000000..a28f91eb --- /dev/null +++ b/src/onebot/event/meta/OB11LifeCycleEvent.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11BaseNoticeEvent.ts b/src/onebot/event/notice/OB11BaseNoticeEvent.ts new file mode 100644 index 00000000..15c45893 --- /dev/null +++ b/src/onebot/event/notice/OB11BaseNoticeEvent.ts @@ -0,0 +1,5 @@ +import { EventType, OB11BaseEvent } from '../OB11BaseEvent'; + +export abstract class OB11BaseNoticeEvent extends OB11BaseEvent { + post_type = EventType.NOTICE; +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11FriendAddNoticeEvent.ts b/src/onebot/event/notice/OB11FriendAddNoticeEvent.ts new file mode 100644 index 00000000..185b574f --- /dev/null +++ b/src/onebot/event/notice/OB11FriendAddNoticeEvent.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11FriendRecallNoticeEvent.ts b/src/onebot/event/notice/OB11FriendRecallNoticeEvent.ts new file mode 100644 index 00000000..2e374878 --- /dev/null +++ b/src/onebot/event/notice/OB11FriendRecallNoticeEvent.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts b/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts new file mode 100644 index 00000000..a968de43 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts @@ -0,0 +1,6 @@ +import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'; + +export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent { + notice_type = 'group_admin'; + sub_type: 'set' | 'unset' = "set"; // "set" | "unset" +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11GroupBanEvent.ts b/src/onebot/event/notice/OB11GroupBanEvent.ts new file mode 100644 index 00000000..9b47fc36 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupBanEvent.ts @@ -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; + } +} diff --git a/src/onebot/event/notice/OB11GroupCardEvent.ts b/src/onebot/event/notice/OB11GroupCardEvent.ts new file mode 100644 index 00000000..1c2e4bc0 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupCardEvent.ts @@ -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; + } +} diff --git a/src/onebot/event/notice/OB11GroupDecreaseEvent.ts b/src/onebot/event/notice/OB11GroupDecreaseEvent.ts new file mode 100644 index 00000000..33da8563 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupDecreaseEvent.ts @@ -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; + } +} diff --git a/src/onebot/event/notice/OB11GroupEssenceEvent.ts b/src/onebot/event/notice/OB11GroupEssenceEvent.ts new file mode 100644 index 00000000..ae1053e1 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupEssenceEvent.ts @@ -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; + } +} diff --git a/src/onebot/event/notice/OB11GroupIncreaseEvent.ts b/src/onebot/event/notice/OB11GroupIncreaseEvent.ts new file mode 100644 index 00000000..b0a86880 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupIncreaseEvent.ts @@ -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; + } +} diff --git a/src/onebot/event/notice/OB11GroupNoticeEvent.ts b/src/onebot/event/notice/OB11GroupNoticeEvent.ts new file mode 100644 index 00000000..3e046f87 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupNoticeEvent.ts @@ -0,0 +1,6 @@ +import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; + +export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent { + group_id: number = 0; + user_id: number = 0; +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11GroupRecallNoticeEvent.ts b/src/onebot/event/notice/OB11GroupRecallNoticeEvent.ts new file mode 100644 index 00000000..8d35ca1b --- /dev/null +++ b/src/onebot/event/notice/OB11GroupRecallNoticeEvent.ts @@ -0,0 +1,15 @@ +import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'; + +export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent { + notice_type = 'group_recall'; + operator_id: number; + message_id: number; + + constructor(groupId: number, userId: number, operatorId: number, messageId: number) { + super(); + this.group_id = groupId; + this.user_id = userId; + this.operator_id = operatorId; + this.message_id = messageId; + } +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11GroupTitleEvent.ts b/src/onebot/event/notice/OB11GroupTitleEvent.ts new file mode 100644 index 00000000..41eace84 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupTitleEvent.ts @@ -0,0 +1,15 @@ +import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'; + +export class OB11GroupTitleEvent extends OB11GroupNoticeEvent { + notice_type = 'notify'; + sub_type = 'title'; + title: string; + + + constructor(groupId: number, userId: number, title: string) { + super(); + this.group_id = groupId; + this.user_id = userId; + this.title = title; + } +} diff --git a/src/onebot/event/notice/OB11GroupUploadNoticeEvent.ts b/src/onebot/event/notice/OB11GroupUploadNoticeEvent.ts new file mode 100644 index 00000000..d5514872 --- /dev/null +++ b/src/onebot/event/notice/OB11GroupUploadNoticeEvent.ts @@ -0,0 +1,20 @@ +import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'; + +export interface GroupUploadFile{ + id: string, + name: string, + size: number, + busid: number, +} + +export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent { + notice_type = 'group_upload'; + file: GroupUploadFile; + + constructor(groupId: number, userId: number, file: GroupUploadFile) { + super(); + this.group_id = groupId; + this.user_id = userId; + this.file = file; + } +} \ No newline at end of file diff --git a/src/onebot/event/notice/OB11InputStatusEvent.ts b/src/onebot/event/notice/OB11InputStatusEvent.ts new file mode 100644 index 00000000..ba255bc3 --- /dev/null +++ b/src/onebot/event/notice/OB11InputStatusEvent.ts @@ -0,0 +1,16 @@ +import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; +//输入状态事件 初步完成 Mlikio wa Todo 需要做一些过滤 +export class OB11InputStatusEvent extends OB11BaseNoticeEvent { + notice_type = 'notify'; + sub_type = 'input_status'; + status_text = "对方正在输入..." + eventType = 1; + user_id = 0; + constructor(user_id: number, eventType: number, status_text: string) { + super(); + this.user_id = user_id; + this.eventType = eventType; + this.status_text = status_text; + } +} + diff --git a/src/onebot/event/notice/OB11MsgEmojiLikeEvent.ts b/src/onebot/event/notice/OB11MsgEmojiLikeEvent.ts new file mode 100644 index 00000000..1af5ced7 --- /dev/null +++ b/src/onebot/event/notice/OB11MsgEmojiLikeEvent.ts @@ -0,0 +1,20 @@ +import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'; + +export interface MsgEmojiLike { + emoji_id: string, + count: number +} + +export class OB11GroupMsgEmojiLikeEvent extends OB11GroupNoticeEvent { + notice_type = 'group_msg_emoji_like'; + message_id: number; + likes: MsgEmojiLike[]; + + constructor(groupId: number, userId: number, messageId: number, likes: MsgEmojiLike[]) { + super(); + this.group_id = groupId; + this.user_id = userId; // 可为空,表示是对别人的消息操作,如果是对bot自己的消息则不为空 + this.message_id = messageId; + this.likes = likes; + } +} diff --git a/src/onebot/event/notice/OB11PokeEvent.ts b/src/onebot/event/notice/OB11PokeEvent.ts new file mode 100644 index 00000000..192b558e --- /dev/null +++ b/src/onebot/event/notice/OB11PokeEvent.ts @@ -0,0 +1,32 @@ +import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; + +class OB11PokeEvent extends OB11BaseNoticeEvent { + notice_type = 'notify'; + sub_type = 'poke'; + target_id = 0; + user_id = 0; +} + +export class OB11FriendPokeEvent extends OB11PokeEvent { + raw_info: any; + //raw_message nb等框架标准为string + constructor(user_id: number, target_id: number, raw_message: any) { + super(); + this.target_id = target_id; + this.user_id = user_id; + this.raw_info = raw_message; + } +} + +export class OB11GroupPokeEvent extends OB11PokeEvent { + group_id: number; + raw_info: any; + //raw_message nb等框架标准为string + constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) { + super(); + this.group_id = group_id; + this.target_id = target_id; + this.user_id = user_id; + this.raw_info = raw_message; + } +} diff --git a/src/onebot/event/request/OB11FriendRequest.ts b/src/onebot/event/request/OB11FriendRequest.ts new file mode 100644 index 00000000..522aa483 --- /dev/null +++ b/src/onebot/event/request/OB11FriendRequest.ts @@ -0,0 +1,11 @@ +import { OB11BaseNoticeEvent } from '../notice/OB11BaseNoticeEvent'; +import { EventType } from '../OB11BaseEvent'; + + +export class OB11FriendRequestEvent extends OB11BaseNoticeEvent { + post_type = EventType.REQUEST; + user_id: number = 0; + request_type = 'friend' as const; + comment: string = ''; + flag: string = ''; +} diff --git a/src/onebot/event/request/OB11GroupRequest.ts b/src/onebot/event/request/OB11GroupRequest.ts new file mode 100644 index 00000000..e06677e6 --- /dev/null +++ b/src/onebot/event/request/OB11GroupRequest.ts @@ -0,0 +1,11 @@ +import { OB11GroupNoticeEvent } from '../notice/OB11GroupNoticeEvent'; +import { EventType } from '../OB11BaseEvent'; + + +export class OB11GroupRequestEvent extends OB11GroupNoticeEvent { + post_type = EventType.REQUEST; + request_type = 'group' as const; + sub_type: 'add' | 'invite' = 'add'; + comment: string = ''; + flag: string = ''; +} diff --git a/src/onebot/types/index.ts b/src/onebot/types/index.ts index 4f248b07..36456389 100644 --- a/src/onebot/types/index.ts +++ b/src/onebot/types/index.ts @@ -1 +1,2 @@ export * from './entity'; +export * from './message'; \ No newline at end of file diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts new file mode 100644 index 00000000..84eb425f --- /dev/null +++ b/src/onebot/types/message.ts @@ -0,0 +1,204 @@ +import { OB11Sender } from './entity'; +import { EventType } from '@/onebot/event/OB11BaseEvent'; +import { CustomMusicSignPostData, IdMusicSignPostData, PicSubType, RawMessage } from '@/core'; + +export enum OB11MessageType { + private = 'private', + group = 'group' +} + +export interface OB11Message { + target_id?: number; // 自己发送的消息才有此字段 + self_id?: number, + time: number, + message_id: number, + message_seq: number, // 和message_id一样 + real_id: number, + user_id: number, + group_id?: number, + message_type: 'private' | 'group', + sub_type?: 'friend' | 'group' | 'normal', + sender: OB11Sender, + message: OB11MessageData[] | string, + message_format: 'array' | 'string', + raw_message: string, + font: number, + post_type?: EventType, + raw?: RawMessage +} + +export interface OB11ForwardMessage extends OB11Message { + content: OB11MessageData[] | string; +} + +export interface OB11Return { + status: string + retcode: number + data: DataType + message: string, + echo?: any, // ws调用api才有此字段 + wording?: string, // go-cqhttp字段,错误信息 +} + +export enum OB11MessageDataType { + text = 'text', + image = 'image', + music = 'music', + video = 'video', + voice = 'record', + file = 'file', + at = 'at', + reply = 'reply', + json = 'json', + face = 'face', + mface = 'mface', // 商城表情 + markdown = 'markdown', + node = 'node', // 合并转发消息节点 + forward = 'forward', // 合并转发消息,用于上报 + xml = 'xml', + poke = 'poke', + dice = 'dice', + RPS = 'rps', + miniapp = 'miniapp',//json类 + Location = 'location' +} + +export interface OB11MessageMFace { + type: OB11MessageDataType.mface + data: { + emoji_package_id: number + emoji_id: string + key: string + summary: string + } +} + +export interface OB11MessageText { + type: OB11MessageDataType.text, + data: { + text: string, // 纯文本 + } +} + +export interface OB11MessageFileBase { + data: { + thumb?: string; + name?: string; + file: string, + url?: string; + } +} + + +export interface OB11MessageImage extends OB11MessageFileBase { + type: OB11MessageDataType.image + data: OB11MessageFileBase['data'] & { + summary?: string; // 图片摘要 + subType?: PicSubType + }, +} + +export interface OB11MessageRecord extends OB11MessageFileBase { + type: OB11MessageDataType.voice +} + +export interface OB11MessageFile extends OB11MessageFileBase { + type: OB11MessageDataType.file +} + +export interface OB11MessageVideo extends OB11MessageFileBase { + type: OB11MessageDataType.video +} + +export interface OB11MessageAt { + type: OB11MessageDataType.at + data: { + qq: `${number}` | 'all' + name?: string + } +} + +export interface OB11MessageReply { + type: OB11MessageDataType.reply + data: { + id: string + } +} + +export interface OB11MessageFace { + type: OB11MessageDataType.face + data: { + id: string + } +} + +export type OB11MessageMixType = OB11MessageData[] | string | OB11MessageData; + +export interface OB11MessageNode { + type: OB11MessageDataType.node + data: { + id?: string + user_id?: number + nickname: string + content: OB11MessageMixType + } +} + +export interface OB11MessageIdMusic { + type: OB11MessageDataType.music + data: IdMusicSignPostData +} +export interface OB11MessageCustomMusic { + type: OB11MessageDataType.music + data: Omit & { content?: string } +} + +export interface OB11MessageJson { + type: OB11MessageDataType.json + data: { config: { token: string } } & any +} + +export interface OB11MessageDice { + type: OB11MessageDataType.dice, + data: { + result: number + } +} +export interface OB11MessageRPS { + type: OB11MessageDataType.RPS, + data: { + result: number + } +} + +export interface OB11MessageMarkdown { + type: OB11MessageDataType.markdown + data: { + content: string + } +} + +export interface OB11MessageForward { + type: OB11MessageDataType.forward + data: { + id: string, + content: OB11Message[] + } +} + +export type OB11MessageData = + OB11MessageText | + OB11MessageFace | OB11MessageMFace | + OB11MessageAt | OB11MessageReply | + OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | + OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson | + OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward + +export interface OB11PostSendMsg { + message_type?: 'private' | 'group' + user_id?: string, + group_id?: string, + message: OB11MessageMixType; + messages?: OB11MessageMixType; // 兼容 go-cqhttp + auto_escape?: boolean | string +}