diff --git a/packages/napcat-satori/action/SatoriAction.ts b/packages/napcat-satori/action/SatoriAction.ts index f0fb7f8f..cd505796 100644 --- a/packages/napcat-satori/action/SatoriAction.ts +++ b/packages/napcat-satori/action/SatoriAction.ts @@ -1,17 +1,84 @@ import { NapCatCore } from 'napcat-core'; import { NapCatSatoriAdapter } from '../index'; +import Ajv, { ErrorObject, ValidateFunction } from 'ajv'; +import { TSchema } from '@sinclair/typebox'; + +export interface SatoriCheckResult { + valid: boolean; + message?: string; +} + +export interface SatoriResponse { + data?: T; + error?: { + code: number; + message: string; + }; +} + +export class SatoriResponseHelper { + static success (data: T): SatoriResponse { + return { data }; + } + + static error (code: number, message: string): SatoriResponse { + return { error: { code, message } }; + } +} export abstract class SatoriAction { abstract actionName: string; protected satoriAdapter: NapCatSatoriAdapter; protected core: NapCatCore; + payloadSchema?: TSchema = undefined; + private validate?: ValidateFunction = undefined; constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { this.satoriAdapter = satoriAdapter; this.core = core; } - abstract handle (payload: PayloadType): Promise; + /** + * 验证请求参数 + */ + protected async check (payload: PayloadType): Promise { + if (this.payloadSchema) { + this.validate = new Ajv({ + allowUnionTypes: true, + useDefaults: true, + coerceTypes: true, + }).compile(this.payloadSchema); + } + + if (this.validate && !this.validate(payload)) { + const errors = this.validate.errors as ErrorObject[]; + const errorMessages = errors.map( + (e) => `Key: ${e.instancePath.split('/').slice(1).join('.')}, Message: ${e.message}` + ); + return { + valid: false, + message: errorMessages.join('\n') ?? '未知错误', + }; + } + + return { valid: true }; + } + + /** + * 处理请求入口(带验证) + */ + async handle (payload: PayloadType): Promise { + const checkResult = await this.check(payload); + if (!checkResult.valid) { + throw new Error(checkResult.message || '参数验证失败'); + } + return this._handle(payload); + } + + /** + * 实际处理逻辑(子类实现) + */ + protected abstract _handle (payload: PayloadType): Promise; protected get logger () { return this.core.context.logger; diff --git a/packages/napcat-satori/action/channel/ChannelGet.ts b/packages/napcat-satori/action/channel/ChannelGet.ts index c88049e6..ac03ab9a 100644 --- a/packages/napcat-satori/action/channel/ChannelGet.ts +++ b/packages/napcat-satori/action/channel/ChannelGet.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriChannel, SatoriChannelType } from '../../types'; -interface ChannelGetPayload { - channel_id: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), +}); -export class ChannelGetAction extends SatoriAction { - actionName = 'channel.get'; +type Payload = Static; - async handle (payload: ChannelGetPayload): Promise { +export class ChannelGetAction extends SatoriAction { + actionName = SatoriActionName.ChannelGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id } = payload; const parts = channel_id.split(':'); @@ -31,7 +36,7 @@ export class ChannelGetAction extends SatoriAction e.groupCode === id); + const group = groups.find((e) => e.groupCode === id); if (!group) { // 如果缓存中没有,尝试获取详细信息 diff --git a/packages/napcat-satori/action/channel/ChannelList.ts b/packages/napcat-satori/action/channel/ChannelList.ts index 55f47d56..b0de47c0 100644 --- a/packages/napcat-satori/action/channel/ChannelList.ts +++ b/packages/napcat-satori/action/channel/ChannelList.ts @@ -1,21 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriChannel, SatoriChannelType, SatoriPageResult } from '../../types'; -interface ChannelListPayload { - guild_id: string; - next?: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + next: Type.Optional(Type.String()), +}); -export class ChannelListAction extends SatoriAction> { - actionName = 'channel.list'; +type Payload = Static; - async handle (payload: ChannelListPayload): Promise> { +export class ChannelListAction extends SatoriAction> { + actionName = SatoriActionName.ChannelList; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise> { const { guild_id } = payload; // 在 QQ 中,群组只有一个文本频道 // 先从群列表缓存中查找 const groups = await this.core.apis.GroupApi.getGroups(); - let group = groups.find(e => e.groupCode === guild_id); + const group = groups.find((e) => e.groupCode === guild_id); let groupName: string; if (!group) { diff --git a/packages/napcat-satori/action/guild/GuildApprove.ts b/packages/napcat-satori/action/guild/GuildApprove.ts new file mode 100644 index 00000000..caf1b880 --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildApprove.ts @@ -0,0 +1,39 @@ +import { Static, Type } from '@sinclair/typebox'; +import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; +import { GroupNotifyMsgType, NTGroupRequestOperateTypes } from 'napcat-core'; + +const SchemaData = Type.Object({ + message_id: Type.String(), // 邀请请求的 seq + approve: Type.Boolean(), + comment: Type.Optional(Type.String()), +}); + +type Payload = Static; + +export class GuildApproveAction extends SatoriAction { + actionName = SatoriActionName.GuildApprove; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { + const { message_id, approve, comment } = payload; + + // message_id 是邀请请求的 seq + const notifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100); + const notify = notifies.find( + (e) => + e.seq == message_id && // 使用 loose equality 以防类型不匹配 + (e.type === GroupNotifyMsgType.INVITED_BY_MEMBER || e.type === GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS) + ); + + if (!notify) { + throw new Error(`未找到加群邀请: ${message_id}`); + } + + const operateType = approve + ? NTGroupRequestOperateTypes.KAGREE + : NTGroupRequestOperateTypes.KREFUSE; + + await this.core.apis.GroupApi.handleGroupRequest(false, notify, operateType, comment); + } +} diff --git a/packages/napcat-satori/action/guild/GuildGet.ts b/packages/napcat-satori/action/guild/GuildGet.ts index d254902c..521451e5 100644 --- a/packages/napcat-satori/action/guild/GuildGet.ts +++ b/packages/napcat-satori/action/guild/GuildGet.ts @@ -1,19 +1,24 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuild } from '../../types'; -interface GuildGetPayload { - guild_id: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), +}); -export class GuildGetAction extends SatoriAction { - actionName = 'guild.get'; +type Payload = Static; - async handle (payload: GuildGetPayload): Promise { +export class GuildGetAction extends SatoriAction { + actionName = SatoriActionName.GuildGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id } = payload; // 先从群列表缓存中查找 const groups = await this.core.apis.GroupApi.getGroups(); - let group = groups.find(e => e.groupCode === guild_id); + const group = groups.find((e) => e.groupCode === guild_id); if (!group) { // 如果缓存中没有,尝试获取详细信息 diff --git a/packages/napcat-satori/action/guild/GuildList.ts b/packages/napcat-satori/action/guild/GuildList.ts index 5b5bc20e..934357ce 100644 --- a/packages/napcat-satori/action/guild/GuildList.ts +++ b/packages/napcat-satori/action/guild/GuildList.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuild, SatoriPageResult } from '../../types'; -interface GuildListPayload { - next?: string; -} +const SchemaData = Type.Object({ + next: Type.Optional(Type.String()), +}); -export class GuildListAction extends SatoriAction> { - actionName = 'guild.list'; +type Payload = Static; - async handle (_payload: GuildListPayload): Promise> { +export class GuildListAction extends SatoriAction> { + actionName = SatoriActionName.GuildList; + override payloadSchema = SchemaData; + + protected async _handle (_payload: Payload): Promise> { const groups = await this.core.apis.GroupApi.getGroups(true); const guilds: SatoriGuild[] = groups.map((group) => ({ diff --git a/packages/napcat-satori/action/guild/GuildMemberApprove.ts b/packages/napcat-satori/action/guild/GuildMemberApprove.ts new file mode 100644 index 00000000..6e2189ea --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildMemberApprove.ts @@ -0,0 +1,39 @@ +import { Static, Type } from '@sinclair/typebox'; +import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; +import { GroupNotifyMsgType, NTGroupRequestOperateTypes } from 'napcat-core'; + +const SchemaData = Type.Object({ + message_id: Type.String(), // 入群请求的 seq + approve: Type.Boolean(), + comment: Type.Optional(Type.String()), +}); + +type Payload = Static; + +export class GuildMemberApproveAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberApprove; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { + const { message_id, approve, comment } = payload; + + // message_id 是入群请求的 seq + const notifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100); + const notify = notifies.find( + (e) => + e.seq === message_id && + e.type === GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS + ); + + if (!notify) { + throw new Error(`未找到入群请求: ${message_id}`); + } + + const operateType = approve + ? NTGroupRequestOperateTypes.KAGREE + : NTGroupRequestOperateTypes.KREFUSE; + + await this.core.apis.GroupApi.handleGroupRequest(false, notify, operateType, comment); + } +} diff --git a/packages/napcat-satori/action/guild/GuildMemberGet.ts b/packages/napcat-satori/action/guild/GuildMemberGet.ts index 07616fa8..8750daeb 100644 --- a/packages/napcat-satori/action/guild/GuildMemberGet.ts +++ b/packages/napcat-satori/action/guild/GuildMemberGet.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuildMember } from '../../types'; -interface GuildMemberGetPayload { - guild_id: string; - user_id: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + user_id: Type.String(), +}); -export class GuildMemberGetAction extends SatoriAction { - actionName = 'guild.member.get'; +type Payload = Static; - async handle (payload: GuildMemberGetPayload): Promise { +export class GuildMemberGetAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id, user_id } = payload; const memberInfo = await this.core.apis.GroupApi.getGroupMember(guild_id, user_id); diff --git a/packages/napcat-satori/action/guild/GuildMemberKick.ts b/packages/napcat-satori/action/guild/GuildMemberKick.ts index b0230be0..8fb26305 100644 --- a/packages/napcat-satori/action/guild/GuildMemberKick.ts +++ b/packages/napcat-satori/action/guild/GuildMemberKick.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface GuildMemberKickPayload { - guild_id: string; - user_id: string; - permanent?: boolean; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + user_id: Type.String(), + permanent: Type.Optional(Type.Boolean({ default: false })), +}); -export class GuildMemberKickAction extends SatoriAction { - actionName = 'guild.member.kick'; +type Payload = Static; - async handle (payload: GuildMemberKickPayload): Promise { +export class GuildMemberKickAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberKick; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id, user_id, permanent } = payload; await this.core.apis.GroupApi.kickMember( diff --git a/packages/napcat-satori/action/guild/GuildMemberList.ts b/packages/napcat-satori/action/guild/GuildMemberList.ts index 2f748848..5474ad25 100644 --- a/packages/napcat-satori/action/guild/GuildMemberList.ts +++ b/packages/napcat-satori/action/guild/GuildMemberList.ts @@ -1,16 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuildMember, SatoriPageResult } from '../../types'; import { GroupMember } from 'napcat-core'; -interface GuildMemberListPayload { - guild_id: string; - next?: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + next: Type.Optional(Type.String()), +}); -export class GuildMemberListAction extends SatoriAction> { - actionName = 'guild.member.list'; +type Payload = Static; - async handle (payload: GuildMemberListPayload): Promise> { +export class GuildMemberListAction extends SatoriAction> { + actionName = SatoriActionName.GuildMemberList; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise> { const { guild_id } = payload; // 使用 getGroupMemberAll 获取所有群成员 diff --git a/packages/napcat-satori/action/guild/GuildMemberMute.ts b/packages/napcat-satori/action/guild/GuildMemberMute.ts index a8481ec8..ad7f5b52 100644 --- a/packages/napcat-satori/action/guild/GuildMemberMute.ts +++ b/packages/napcat-satori/action/guild/GuildMemberMute.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface GuildMemberMutePayload { - guild_id: string; - user_id: string; - duration?: number; // 禁言时长(毫秒),0 表示解除禁言 -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + user_id: Type.String(), + duration: Type.Optional(Type.Number({ default: 0 })), // 禁言时长(毫秒),0 表示解除禁言 +}); -export class GuildMemberMuteAction extends SatoriAction { - actionName = 'guild.member.mute'; +type Payload = Static; - async handle (payload: GuildMemberMutePayload): Promise { +export class GuildMemberMuteAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberMute; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id, user_id, duration } = payload; // 将毫秒转换为秒 diff --git a/packages/napcat-satori/action/index.ts b/packages/napcat-satori/action/index.ts index e19bd170..b9d6b3ec 100644 --- a/packages/napcat-satori/action/index.ts +++ b/packages/napcat-satori/action/index.ts @@ -10,13 +10,16 @@ import { ChannelGetAction } from './channel/ChannelGet'; import { ChannelListAction } from './channel/ChannelList'; import { GuildGetAction } from './guild/GuildGet'; import { GuildListAction } from './guild/GuildList'; +import { GuildApproveAction } from './guild/GuildApprove'; import { GuildMemberGetAction } from './guild/GuildMemberGet'; import { GuildMemberListAction } from './guild/GuildMemberList'; import { GuildMemberKickAction } from './guild/GuildMemberKick'; import { GuildMemberMuteAction } from './guild/GuildMemberMute'; +import { GuildMemberApproveAction } from './guild/GuildMemberApprove'; import { UserGetAction } from './user/UserGet'; import { FriendListAction } from './user/FriendList'; import { FriendApproveAction } from './user/FriendApprove'; +import { LoginGetAction } from './login/LoginGet'; import { UploadCreateAction } from './upload/UploadCreate'; export type SatoriActionMap = Map>; @@ -38,14 +41,18 @@ export function createSatoriActionMap ( // 群组相关 new GuildGetAction(satoriAdapter, core), new GuildListAction(satoriAdapter, core), + new GuildApproveAction(satoriAdapter, core), new GuildMemberGetAction(satoriAdapter, core), new GuildMemberListAction(satoriAdapter, core), new GuildMemberKickAction(satoriAdapter, core), new GuildMemberMuteAction(satoriAdapter, core), + new GuildMemberApproveAction(satoriAdapter, core), // 用户相关 new UserGetAction(satoriAdapter, core), new FriendListAction(satoriAdapter, core), new FriendApproveAction(satoriAdapter, core), + // 登录相关 + new LoginGetAction(satoriAdapter, core), // 上传相关 new UploadCreateAction(satoriAdapter, core), ]; @@ -57,4 +64,5 @@ export function createSatoriActionMap ( return actionMap; } -export { SatoriAction } from './SatoriAction'; +export { SatoriAction, SatoriCheckResult, SatoriResponse, SatoriResponseHelper } from './SatoriAction'; +export { SatoriActionName, SatoriActionNameType } from './router'; diff --git a/packages/napcat-satori/action/login/LoginGet.ts b/packages/napcat-satori/action/login/LoginGet.ts new file mode 100644 index 00000000..bc2847f1 --- /dev/null +++ b/packages/napcat-satori/action/login/LoginGet.ts @@ -0,0 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; +import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; +import { SatoriLogin, SatoriLoginStatus } from '../../types'; + +const SchemaData = Type.Object({}); + +type Payload = Static; + +export class LoginGetAction extends SatoriAction { + actionName = SatoriActionName.LoginGet; + override payloadSchema = SchemaData; + + protected async _handle (_payload: Payload): Promise { + return { + user: { + id: this.selfInfo.uin, + name: this.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfInfo.uin}&s=640`, + }, + self_id: this.selfInfo.uin, + platform: this.platform, + status: SatoriLoginStatus.ONLINE, + }; + } +} diff --git a/packages/napcat-satori/action/message/MessageCreate.ts b/packages/napcat-satori/action/message/MessageCreate.ts index 47f724c6..ed19fdc4 100644 --- a/packages/napcat-satori/action/message/MessageCreate.ts +++ b/packages/napcat-satori/action/message/MessageCreate.ts @@ -1,16 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriMessage, SatoriChannelType } from '../../types'; import { ChatType, SendMessageElement } from 'napcat-core'; -interface MessageCreatePayload { - channel_id: string; - content: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), + content: Type.String(), +}); -export class MessageCreateAction extends SatoriAction { - actionName = 'message.create'; +type Payload = Static; - async handle (payload: MessageCreatePayload): Promise { +export class MessageCreateAction extends SatoriAction { + actionName = SatoriActionName.MessageCreate; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id, content } = payload; // 解析 channel_id,格式: private:{user_id} 或 group:{group_id} diff --git a/packages/napcat-satori/action/message/MessageDelete.ts b/packages/napcat-satori/action/message/MessageDelete.ts index ebc408a6..1e03ab57 100644 --- a/packages/napcat-satori/action/message/MessageDelete.ts +++ b/packages/napcat-satori/action/message/MessageDelete.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { ChatType } from 'napcat-core'; -interface MessageDeletePayload { - channel_id: string; - message_id: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), + message_id: Type.String(), +}); -export class MessageDeleteAction extends SatoriAction { - actionName = 'message.delete'; +type Payload = Static; - async handle (payload: MessageDeletePayload): Promise { +export class MessageDeleteAction extends SatoriAction { + actionName = SatoriActionName.MessageDelete; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id, message_id } = payload; const parts = channel_id.split(':'); diff --git a/packages/napcat-satori/action/message/MessageGet.ts b/packages/napcat-satori/action/message/MessageGet.ts index f9bd2c17..35a9ed9d 100644 --- a/packages/napcat-satori/action/message/MessageGet.ts +++ b/packages/napcat-satori/action/message/MessageGet.ts @@ -1,16 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriMessage, SatoriChannelType } from '../../types'; import { ChatType } from 'napcat-core'; -interface MessageGetPayload { - channel_id: string; - message_id: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), + message_id: Type.String(), +}); -export class MessageGetAction extends SatoriAction { - actionName = 'message.get'; +type Payload = Static; - async handle (payload: MessageGetPayload): Promise { +export class MessageGetAction extends SatoriAction { + actionName = SatoriActionName.MessageGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id, message_id } = payload; const parts = channel_id.split(':'); diff --git a/packages/napcat-satori/action/router.ts b/packages/napcat-satori/action/router.ts new file mode 100644 index 00000000..96628354 --- /dev/null +++ b/packages/napcat-satori/action/router.ts @@ -0,0 +1,57 @@ +/** + * Satori Action 名称映射 + */ +export const SatoriActionName = { + // 消息相关 + MessageCreate: 'message.create', + MessageGet: 'message.get', + MessageDelete: 'message.delete', + MessageUpdate: 'message.update', + MessageList: 'message.list', + + // 频道相关 + ChannelGet: 'channel.get', + ChannelList: 'channel.list', + ChannelCreate: 'channel.create', + ChannelUpdate: 'channel.update', + ChannelDelete: 'channel.delete', + ChannelMute: 'channel.mute', + + // 群组/公会相关 + GuildGet: 'guild.get', + GuildList: 'guild.list', + GuildApprove: 'guild.approve', + + // 群成员相关 + GuildMemberGet: 'guild.member.get', + GuildMemberList: 'guild.member.list', + GuildMemberKick: 'guild.member.kick', + GuildMemberMute: 'guild.member.mute', + GuildMemberApprove: 'guild.member.approve', + GuildMemberRole: 'guild.member.role', + + // 角色相关 + GuildRoleList: 'guild.role.list', + GuildRoleCreate: 'guild.role.create', + GuildRoleUpdate: 'guild.role.update', + GuildRoleDelete: 'guild.role.delete', + + // 用户相关 + UserGet: 'user.get', + UserChannelCreate: 'user.channel.create', + + // 好友相关 + FriendList: 'friend.list', + FriendApprove: 'friend.approve', + + // 登录相关 + LoginGet: 'login.get', + + // 上传相关 + UploadCreate: 'upload.create', + + // 内部互操作(Satori 可选) + InternalAction: 'internal.action', +} as const; + +export type SatoriActionNameType = typeof SatoriActionName[keyof typeof SatoriActionName]; diff --git a/packages/napcat-satori/action/upload/UploadCreate.ts b/packages/napcat-satori/action/upload/UploadCreate.ts index 0bb2042e..daa71f34 100644 --- a/packages/napcat-satori/action/upload/UploadCreate.ts +++ b/packages/napcat-satori/action/upload/UploadCreate.ts @@ -1,17 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface UploadCreatePayload { - [key: string]: unknown; -} +const SchemaData = Type.Record(Type.String(), Type.Unknown()); + +type Payload = Static; interface UploadResult { [key: string]: string; } -export class UploadCreateAction extends SatoriAction { - actionName = 'upload.create'; +export class UploadCreateAction extends SatoriAction { + actionName = SatoriActionName.UploadCreate; + override payloadSchema = SchemaData; - async handle (payload: UploadCreatePayload): Promise { + protected async _handle (payload: Payload): Promise { const result: UploadResult = {}; // 处理上传的文件 diff --git a/packages/napcat-satori/action/user/FriendApprove.ts b/packages/napcat-satori/action/user/FriendApprove.ts index 8b3a13f8..50792b87 100644 --- a/packages/napcat-satori/action/user/FriendApprove.ts +++ b/packages/napcat-satori/action/user/FriendApprove.ts @@ -1,21 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface FriendApprovePayload { - message_id: string; - approve: boolean; - comment?: string; -} +const SchemaData = Type.Object({ + message_id: Type.String(), + approve: Type.Boolean(), + comment: Type.Optional(Type.String()), +}); -export class FriendApproveAction extends SatoriAction { - actionName = 'friend.approve'; +type Payload = Static; - async handle (payload: FriendApprovePayload): Promise { +export class FriendApproveAction extends SatoriAction { + actionName = SatoriActionName.FriendApprove; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { message_id, approve } = payload; // message_id 格式: reqTime (好友请求的时间戳) // 需要从好友请求列表中找到对应的请求 const buddyReqData = await this.core.apis.FriendApi.getBuddyReq(); - const notify = buddyReqData.buddyReqs.find(e => e.reqTime === message_id); + const notify = buddyReqData.buddyReqs.find((e) => e.reqTime === message_id); if (!notify) { throw new Error(`未找到好友请求: ${message_id}`); diff --git a/packages/napcat-satori/action/user/FriendList.ts b/packages/napcat-satori/action/user/FriendList.ts index 95145ca6..cbe0e179 100644 --- a/packages/napcat-satori/action/user/FriendList.ts +++ b/packages/napcat-satori/action/user/FriendList.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriUser, SatoriPageResult } from '../../types'; -interface FriendListPayload { - next?: string; -} +const SchemaData = Type.Object({ + next: Type.Optional(Type.String()), +}); -export class FriendListAction extends SatoriAction> { - actionName = 'friend.list'; +type Payload = Static; - async handle (_payload: FriendListPayload): Promise> { +export class FriendListAction extends SatoriAction> { + actionName = SatoriActionName.FriendList; + override payloadSchema = SchemaData; + + protected async _handle (_payload: Payload): Promise> { const friends = await this.core.apis.FriendApi.getBuddy(); const friendList: SatoriUser[] = friends.map((friend) => ({ diff --git a/packages/napcat-satori/action/user/UserGet.ts b/packages/napcat-satori/action/user/UserGet.ts index da5ec042..de23c178 100644 --- a/packages/napcat-satori/action/user/UserGet.ts +++ b/packages/napcat-satori/action/user/UserGet.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriUser } from '../../types'; -interface UserGetPayload { - user_id: string; -} +const SchemaData = Type.Object({ + user_id: Type.String(), +}); -export class UserGetAction extends SatoriAction { - actionName = 'user.get'; +type Payload = Static; - async handle (payload: UserGetPayload): Promise { +export class UserGetAction extends SatoriAction { + actionName = SatoriActionName.UserGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { user_id } = payload; const uid = await this.core.apis.UserApi.getUidByUinV2(user_id); diff --git a/packages/napcat-satori/api/event.ts b/packages/napcat-satori/api/event.ts index 7d90bd80..f30d133c 100644 --- a/packages/napcat-satori/api/event.ts +++ b/packages/napcat-satori/api/event.ts @@ -1,4 +1,4 @@ -import { NapCatCore, RawMessage, ChatType } from 'napcat-core'; +import { NapCatCore, RawMessage, ChatType, GroupNotify, FriendRequest } from 'napcat-core'; import { NapCatSatoriAdapter } from '../index'; import { SatoriEvent, @@ -6,6 +6,51 @@ import { SatoriLoginStatus, } from '../types'; +/** + * Satori 事件类型定义 + */ +export const SatoriEventType = { + // 消息事件 + MESSAGE_CREATED: 'message-created', + MESSAGE_UPDATED: 'message-updated', + MESSAGE_DELETED: 'message-deleted', + + // 频道事件 + CHANNEL_CREATED: 'channel-created', + CHANNEL_UPDATED: 'channel-updated', + CHANNEL_DELETED: 'channel-deleted', + + // 群组/公会事件 + GUILD_ADDED: 'guild-added', + GUILD_UPDATED: 'guild-updated', + GUILD_REMOVED: 'guild-removed', + GUILD_REQUEST: 'guild-request', + + // 群成员事件 + GUILD_MEMBER_ADDED: 'guild-member-added', + GUILD_MEMBER_UPDATED: 'guild-member-updated', + GUILD_MEMBER_REMOVED: 'guild-member-removed', + GUILD_MEMBER_REQUEST: 'guild-member-request', + + // 角色事件 + GUILD_ROLE_CREATED: 'guild-role-created', + GUILD_ROLE_UPDATED: 'guild-role-updated', + GUILD_ROLE_DELETED: 'guild-role-deleted', + + // 好友事件 + FRIEND_REQUEST: 'friend-request', + + // 登录事件 + LOGIN_ADDED: 'login-added', + LOGIN_REMOVED: 'login-removed', + LOGIN_UPDATED: 'login-updated', + + // 内部事件 + INTERNAL: 'internal', +} as const; + +export type SatoriEventTypeName = typeof SatoriEventType[keyof typeof SatoriEventType]; + export class SatoriEventApi { private satoriAdapter: NapCatSatoriAdapter; private core: NapCatCore; @@ -28,6 +73,19 @@ export class SatoriEventApi { return this.core.selfInfo.uin; } + /** + * 创建基础事件结构 + */ + private createBaseEvent (type: SatoriEventTypeName): SatoriEvent { + return { + id: this.getNextEventId(), + type, + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + }; + } + /** * 将 NapCat 消息转换为 Satori 事件 */ @@ -36,25 +94,20 @@ export class SatoriEventApi { const content = await this.satoriAdapter.apis.MsgApi.parseElements(message.elements); const isPrivate = message.chatType === ChatType.KCHATTYPEC2C; - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'message-created', - platform: this.platform, - self_id: this.selfId, - timestamp: parseInt(message.msgTime) * 1000, - channel: { - id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, - type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, - }, - user: { - id: message.senderUin, - name: message.sendNickName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, - }, - message: { - id: message.msgId, - content, - }, + const event = this.createBaseEvent(SatoriEventType.MESSAGE_CREATED); + event.timestamp = parseInt(message.msgTime) * 1000; + event.channel = { + id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }; + event.user = { + id: message.senderUin, + name: message.sendNickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, + }; + event.message = { + id: message.msgId, + content, }; if (!isPrivate) { @@ -75,65 +128,95 @@ export class SatoriEventApi { } } + /** + * 创建消息更新事件 + */ + async createMessageUpdatedEvent (message: RawMessage): Promise { + try { + const content = await this.satoriAdapter.apis.MsgApi.parseElements(message.elements); + const isPrivate = message.chatType === ChatType.KCHATTYPEC2C; + + const event = this.createBaseEvent(SatoriEventType.MESSAGE_UPDATED); + event.channel = { + id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }; + event.user = { + id: message.senderUin, + name: message.sendNickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, + }; + event.message = { + id: message.msgId, + content, + }; + + return event; + } catch (error) { + this.core.context.logger.logError('[Satori] 创建消息更新事件失败:', error); + return null; + } + } + /** * 创建好友请求事件 */ - createFriendRequestEvent ( - userId: string, - userName: string, - comment: string, - flag: string - ): SatoriEvent { - return { - id: this.getNextEventId(), - type: 'friend-request', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, - message: { - id: flag, - content: comment, - }, + createFriendRequestEvent (request: FriendRequest): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.FRIEND_REQUEST); + event.user = { + id: request.friendUid, + name: request.friendNick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${request.friendUid}&s=640`, }; + event.message = { + id: request.reqTime, + content: request.extWords, + }; + return event; } /** * 创建群组加入请求事件 */ - createGuildMemberRequestEvent ( - guildId: string, - guildName: string, - userId: string, - userName: string, - comment: string, - flag: string - ): SatoriEvent { - return { - id: this.getNextEventId(), - type: 'guild-member-request', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - guild: { - id: guildId, - name: guildName, - avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, - }, - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, - message: { - id: flag, - content: comment, - }, + createGuildMemberRequestEvent (notify: GroupNotify): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_MEMBER_REQUEST); + event.guild = { + id: notify.group.groupCode, + name: notify.group.groupName, + avatar: `https://p.qlogo.cn/gh/${notify.group.groupCode}/${notify.group.groupCode}/640`, }; + event.user = { + id: notify.user1.uid, + name: notify.user1.nickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${notify.user1.uid}&s=640`, + }; + event.message = { + id: notify.seq, + content: notify.postscript, + }; + return event; + } + + /** + * 创建群组邀请事件 + */ + createGuildRequestEvent (notify: GroupNotify): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_REQUEST); + event.guild = { + id: notify.group.groupCode, + name: notify.group.groupName, + avatar: `https://p.qlogo.cn/gh/${notify.group.groupCode}/${notify.group.groupCode}/640`, + }; + event.user = { + id: notify.user2.uid, + name: notify.user2.nickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${notify.user2.uid}&s=640`, + }; + event.message = { + id: notify.seq, + content: notify.postscript, + }; + return event; } /** @@ -146,22 +229,16 @@ export class SatoriEventApi { userName: string, operatorId?: string ): SatoriEvent { - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'guild-member-added', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - guild: { - id: guildId, - name: guildName, - avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, - }, - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, + const event = this.createBaseEvent(SatoriEventType.GUILD_MEMBER_ADDED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + event.user = { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, }; if (operatorId) { @@ -184,22 +261,16 @@ export class SatoriEventApi { userName: string, operatorId?: string ): SatoriEvent { - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'guild-member-removed', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - guild: { - id: guildId, - name: guildName, - avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, - }, - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, + const event = this.createBaseEvent(SatoriEventType.GUILD_MEMBER_REMOVED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + event.user = { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, }; if (operatorId) { @@ -212,6 +283,38 @@ export class SatoriEventApi { return event; } + /** + * 创建群添加事件(自己被邀请或加入群) + */ + createGuildAddedEvent ( + guildId: string, + guildName: string + ): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_ADDED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + return event; + } + + /** + * 创建群移除事件(被踢出或退出群) + */ + createGuildRemovedEvent ( + guildId: string, + guildName: string + ): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_REMOVED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + return event; + } + /** * 创建消息删除事件 */ @@ -222,24 +325,18 @@ export class SatoriEventApi { operatorId?: string ): SatoriEvent { const isPrivate = channelId.startsWith('private:'); - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'message-deleted', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - channel: { - id: channelId, - type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, - }, - user: { - id: userId, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, - message: { - id: messageId, - content: '', - }, + const event = this.createBaseEvent(SatoriEventType.MESSAGE_DELETED); + event.channel = { + id: channelId, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }; + event.user = { + id: userId, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }; + event.message = { + id: messageId, + content: '', }; if (operatorId) { @@ -252,26 +349,69 @@ export class SatoriEventApi { return event; } + /** + * 创建登录添加事件 + */ + createLoginAddedEvent (): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.LOGIN_ADDED); + event.login = { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, + }, + self_id: this.selfId, + platform: this.platform, + status: SatoriLoginStatus.ONLINE, + }; + return event; + } + + /** + * 创建登录移除事件 + */ + createLoginRemovedEvent (): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.LOGIN_REMOVED); + event.login = { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, + }, + self_id: this.selfId, + platform: this.platform, + status: SatoriLoginStatus.OFFLINE, + }; + return event; + } + /** * 创建登录状态更新事件 */ createLoginUpdatedEvent (status: SatoriLoginStatus): SatoriEvent { - return { - id: this.getNextEventId(), - type: 'login-updated', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - login: { - user: { - id: this.selfId, - name: this.core.selfInfo.nick, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, - }, - self_id: this.selfId, - platform: this.platform, - status, + const event = this.createBaseEvent(SatoriEventType.LOGIN_UPDATED); + event.login = { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, }, + self_id: this.selfId, + platform: this.platform, + status, }; + return event; + } + + /** + * 创建内部事件(用于扩展) + */ + createInternalEvent (typeName: string, data: Record): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.INTERNAL); + event._type = typeName; + event._data = data; + return event; } } + +export { SatoriEventType as EventType }; diff --git a/packages/napcat-satori/api/msg.ts b/packages/napcat-satori/api/msg.ts index edc7116d..588648bf 100644 --- a/packages/napcat-satori/api/msg.ts +++ b/packages/napcat-satori/api/msg.ts @@ -1,75 +1,32 @@ import { NapCatCore, MessageElement, ElementType, NTMsgAtType } from 'napcat-core'; import { NapCatSatoriAdapter } from '../index'; +import SatoriElement from '@satorijs/element'; +/** + * Satori 消息处理 API + * 使用 @satorijs/element 处理消息格式转换 + */ export class SatoriMsgApi { private core: NapCatCore; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private _adapter: NapCatSatoriAdapter; - constructor (_satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + this._adapter = satoriAdapter; this.core = core; } /** * 解析 Satori 消息内容为 NapCat 消息元素 + * 使用 @satorijs/element 解析 */ async parseContent (content: string): Promise { const elements: MessageElement[] = []; + const parsed = SatoriElement.parse(content); - // 简单的 XML 解析 - const tagRegex = /<(\w+)([^>]*)(?:\/>|>([\s\S]*?)<\/\1>)/g; - let lastIndex = 0; - let match: RegExpExecArray | null; - - while ((match = tagRegex.exec(content)) !== null) { - // 处理标签前的文本 - if (match.index > lastIndex) { - const text = content.slice(lastIndex, match.index); - if (text.trim()) { - elements.push(this.createTextElement(text)); - } - } - - const [, tagName, attrs = '', innerContent] = match; - const parsedAttrs = this.parseAttributes(attrs); - - switch (tagName) { - case 'at': - elements.push(await this.createAtElement(parsedAttrs)); - break; - case 'img': - case 'image': - elements.push(await this.createImageElement(parsedAttrs)); - break; - case 'audio': - elements.push(await this.createAudioElement(parsedAttrs)); - break; - case 'video': - elements.push(await this.createVideoElement(parsedAttrs)); - break; - case 'file': - elements.push(await this.createFileElement(parsedAttrs)); - break; - case 'face': - elements.push(this.createFaceElement(parsedAttrs)); - break; - case 'quote': - elements.push(await this.createQuoteElement(parsedAttrs)); - break; - default: - // 未知标签,作为文本处理 - if (innerContent) { - elements.push(this.createTextElement(innerContent)); - } - } - - lastIndex = match.index + match[0].length; - } - - // 处理剩余文本 - if (lastIndex < content.length) { - const text = content.slice(lastIndex); - if (text.trim()) { - elements.push(this.createTextElement(text)); - } + for (const elem of parsed) { + const parsedElements = await this.parseSatoriElement(elem); + elements.push(...parsedElements); } // 如果没有解析到任何元素,将整个内容作为文本 @@ -81,73 +38,231 @@ export class SatoriMsgApi { } /** - * 解析 NapCat 消息元素为 Satori 消息内容 + * 解析 satorijs 元素为消息元素 */ - async parseElements (elements: MessageElement[]): Promise { - const parts: string[] = []; + private async parseSatoriElement (elem: SatoriElement): Promise { + const elements: MessageElement[] = []; - for (const element of elements) { - switch (element.elementType) { - case ElementType.TEXT: - if (element.textElement) { - parts.push(this.escapeXml(element.textElement.content)); - } - break; - case ElementType.PIC: - if (element.picElement) { - const src = element.picElement.sourcePath || ''; - parts.push(``); - } - break; - case ElementType.PTT: - if (element.pttElement) { - const src = element.pttElement.filePath || ''; - parts.push(`