diff --git a/adapters/qqguild/src/bot.ts b/adapters/qqguild/src/bot.ts index 2269f55b..5456346b 100644 --- a/adapters/qqguild/src/bot.ts +++ b/adapters/qqguild/src/bot.ts @@ -1,5 +1,5 @@ -import { Bot, Context, defineProperty, Fragment, h, Quester, Schema, SendOptions } from '@satorijs/satori' -import { adaptGuild, adaptUser } from './utils' +import { Bot, Context, defineProperty, Quester, Schema, Universal } from '@satorijs/satori' +import { adaptUser, decodeMessage } from './utils' import { QQGuildMessageEncoder } from './message' import { WsClient } from './ws' import { Internal } from './internal' @@ -45,38 +45,9 @@ export class QQGuildBot extends Bot { // const guilds = await this.internal.guilds // return { data: guilds.map(adaptGuild) } // } - - adaptMessage(msg: QQGuild.Message - , input?: QQGuild.Payload, - ) { - const { id: messageId, author, guild_id, channel_id, timestamp } = msg - const session = this.session({ - type: 'message', - guildId: guild_id, - messageId, - channelId: channel_id, - timestamp: new Date(timestamp).valueOf(), - } - , input, - ) - session.author = adaptUser(msg.author) - session.userId = author.id - if (msg.direct_message) { - session.guildId = msg.src_guild_id - } else { - session.guildId = guild_id - session.channelId = channel_id - } - session.isDirect = !!msg.direct_message - session.content = (msg.content ?? '') - .replace(/<@!(.+)>/, (_, $1) => h.at($1).toString()) - .replace(/<#(.+)>/, (_, $1) => h.sharp($1).toString()) - const { attachments = [] } = msg as { attachments?: any[] } - session.content = attachments - .filter(({ contentType }) => contentType.startsWith('image')) - .reduce((content, attachment) => content + h.image(attachment.url), session.content) - session.elements = h.parse(session.content) - return session + async getMessage(channelId: string, messageId: string): Promise { + const r = await this.internal.getMessage(channelId, messageId) + return decodeMessage(this, r) } } @@ -93,6 +64,7 @@ export namespace QQGuildBot { id: Schema.string().description('机器人 id。').required(), key: Schema.string().description('机器人 key。').role('secret').required(), token: Schema.string().description('机器人令牌。').role('secret').required(), + type: Schema.union(['public', 'private'] as const).description('机器人类型。').required(), }) as any, sandbox: Schema.boolean().description('是否开启沙箱模式。').default(true), endpoint: Schema.string().role('link').description('要连接的服务器地址。').default('https://api.sgroup.qq.com/'), diff --git a/adapters/qqguild/src/internal.ts b/adapters/qqguild/src/internal.ts index 2ccbedef..0e779b7e 100644 --- a/adapters/qqguild/src/internal.ts +++ b/adapters/qqguild/src/internal.ts @@ -1,8 +1,8 @@ import { Quester } from '@satorijs/satori' -import { DMS, User } from './types' +import { DMS, Message, User } from './types' export class Internal { - constructor(private http: Quester) {} + constructor(private http: Quester) { } async getMe() { return this.http.get('/users/@me') @@ -14,4 +14,11 @@ export class Internal { recipient_id, source_guild_id, }) } + + async getMessage(channelId: string, messageId: string) { + const { message } = await this.http.get<{ + message: Message + }>(`/channels/${channelId}/messages/${messageId}`) + return message + } } diff --git a/adapters/qqguild/src/message.ts b/adapters/qqguild/src/message.ts index 6ef6af24..4186ddcd 100644 --- a/adapters/qqguild/src/message.ts +++ b/adapters/qqguild/src/message.ts @@ -1,13 +1,11 @@ -import { fileURLToPath } from 'url' import * as QQGuild from './types' -import { Dict, h, Logger, MessageEncoder, Quester } from '@satorijs/satori' +import { Dict, h, MessageEncoder } from '@satorijs/satori' import { QQGuildBot } from './bot' import FormData from 'form-data' - -const logger = new Logger('satori') +import { decodeMessage } from './utils' export class QQGuildMessageEncoder extends MessageEncoder { - private mode: 'figure' | 'default' = 'default' + // private mode: 'figure' | 'default' = 'default' private content: string = '' private file: Buffer private filename: string @@ -51,7 +49,8 @@ export class QQGuildMessageEncoder extends MessageEncoder { const r = await this.bot.http.post(endpoint, form, { headers: form.getHeaders(), }) - const session = this.bot.adaptMessage(r) + const session = this.bot.session() + await decodeMessage(this.bot, r, session) // https://bot.q.qq.com/wiki/develop/api/gateway/direct_message.html#%E6%B3%A8%E6%84%8F // session.guildId = this.session.guildId diff --git a/adapters/qqguild/src/types.ts b/adapters/qqguild/src/types.ts index 4604fbb7..5d06fa8a 100644 --- a/adapters/qqguild/src/types.ts +++ b/adapters/qqguild/src/types.ts @@ -90,7 +90,7 @@ export enum Opcode { HEARTBEAT_ACK = 11 } -type DispatchPayload = { +export type DispatchPayload = { op: Opcode.DISPATCH s: number t: 'READY' @@ -171,6 +171,16 @@ export type Payload = DispatchPayload | { } } +export interface Attachment { + content_type: string + filename: string + height: number + id: string + size: number + url: string + width: number +} + export interface Message { /** 消息 id */ id: string @@ -188,8 +198,8 @@ export interface Message { edited_timestamp: string /** 是否是@全员消息 */ mention_everyone: boolean - // /** 附件 */ - // attachments: Attachment + /** 附件 */ + attachments: Attachment[] /** embed */ embeds: Message.Embed[] /** 消息中@的人 */ @@ -240,11 +250,11 @@ export namespace Message { /** 描述 */ description: string /** 消息弹窗内容 */ - prompt: string + prompt: string /** 消息创建时间 */ timestamp: Date - /** 对象数组 消息创建时间 */ - fields: EmbedField + /** 对象数组 消息创建时间 */ + fields: EmbedField } export interface Markdown { /** markdown 模板 id */ @@ -262,7 +272,7 @@ export namespace Message { } export interface Reference { /** 需要引用回复的消息 id */ - messageId: string + message_id: string /** 是否忽略获取引用消息详情错误,默认否 */ ignoreGetMessageError?: boolean } @@ -389,9 +399,9 @@ export enum ChannelSubType { export interface ChannelPermissions { /** 子频道 id */ - channelId: string + channel_id: string /** 用户 id */ - userId: string + user_id: string /** 用户拥有的子频道权限 */ permissions: string } @@ -418,7 +428,7 @@ export interface Channel { /** 子频道 id */ id: string /** 频道 id */ - guildId: string + guild_id: string /** 子频道名 */ name: string /** 子频道类型 */ @@ -443,7 +453,7 @@ export interface Channel { export interface MemberWithGuild { /** 频道 id */ - guildId: string + guild_id: string /** 用户基础信息 */ user: User /** 用户在频道内的昵称 */ @@ -459,9 +469,9 @@ export interface MemberWithGuild { */ export interface Announce { /** 频道 id */ - guildId: string + guild_id: string /** 子频道 id */ - channelId: string + channel_id: string /** 消息 id */ messageId: string } @@ -471,11 +481,11 @@ export interface Announce { */ export interface MessageReaction { /** 用户 ID */ - userId: string + user_id: string /** 频道 ID */ - guildId: string + guild_id: string /** 子频道 ID */ - channelId: string + channel_id: string /** 表态对象 */ target: ReactionTarget /** 表态所用表情 */ @@ -497,13 +507,13 @@ export interface ReactionTarget { */ export enum ReactionTargetType { /** 消息 */ - MESSAGE = 0, + MESSAGE = 'ReactionTargetType_MSG', /** 帖子 */ - POST = 1, + POST = 'ReactionTargetType_FEED', /** 评论 */ - COMMENT = 2, + COMMENT = 'ReactionTargetType_COMMNENT', /** 回复 */ - REPLY = 3 + REPLY = 'ReactionTargetType_REPLY' } /** @@ -547,7 +557,7 @@ export interface Schedule { /** 创建者 */ creator: Member /** 日程开始时跳转到的子频道 id */ - jumpChannelId: string + jumpchannel_id: string /** 日程提醒类型,取值参考 RemindType */ remindType: RemindType } @@ -576,7 +586,7 @@ export interface Mute { /** 禁言多少秒(两个字段二选一,默认以 muteEndTimestamp 为准) */ muteSeconds?: number /** 禁言成员的user_id列表,即 User 的id */ - userIds?: string[] + user_ids?: string[] } export enum DeleteHistoryMsgDays { @@ -594,7 +604,7 @@ export interface MessageSetting { /** 是否允许发主动消息 */ disablePushMsg: string /** 子频道 id 数组 */ - channelIds: string + channel_ids: string /** 每个子频道允许主动推送消息最大消息条数 */ channelPushMaxNum: string } @@ -616,9 +626,9 @@ export interface DMS { */ export interface PinsMessage { /** 频道 id */ - guildId: string + guild_id: string /** 子频道 id */ - channelId: string + channel_id: string /** 子频道内精华消息 id 数组 */ messageIds: string[] } @@ -649,9 +659,9 @@ export interface APIPermissionDemandIdentify { */ export interface APIPermissionDemand { /** 申请接口权限的频道 id */ - guildId: string + guild_id: string /** 接口权限需求授权链接发送的子频道 id */ - channelId: string + channel_id: string /** 权限接口唯一标识 */ apiIdentify: APIPermissionDemandIdentify /** 接口权限链接中的接口权限描述信息 */ @@ -664,6 +674,7 @@ export interface AppConfig { id: string key: string token: string + type: 'public' | 'private' } export interface Options { diff --git a/adapters/qqguild/src/utils.ts b/adapters/qqguild/src/utils.ts index 128f5028..6679b9ea 100644 --- a/adapters/qqguild/src/utils.ts +++ b/adapters/qqguild/src/utils.ts @@ -1,5 +1,6 @@ -import { Universal } from '@satorijs/satori' +import { h, Session, Universal } from '@satorijs/satori' import * as QQGuild from './types' +import { QQGuildBot } from './bot' export const adaptGuild = (guild: QQGuild.Guild): Universal.Guild => ({ id: guild.id, @@ -14,3 +15,82 @@ export const adaptUser = (user: QQGuild.User): Universal.User => ({ isBot: user.bot, avatar: user.avatar, }) + +export async function decodeMessage(bot: QQGuildBot, msg: QQGuild.Message, session: Partial = {}): Promise { + const { id: messageId, author, guild_id, channel_id, timestamp } = msg + session.type = 'message' + session.guildId = guild_id + session.messageId = messageId + session.channelId = channel_id + session.timestamp = new Date(timestamp).valueOf() + + session.author = adaptUser(msg.author) + session.userId = author.id + if (msg.direct_message) { + session.guildId = msg.src_guild_id + } else { + session.guildId = guild_id + session.channelId = channel_id + } + session.isDirect = !!msg.direct_message + session.content = (msg.content ?? '') + .replace(/<@!(.+)>/, (_, $1) => h.at($1).toString()) + .replace(/<#(.+)>/, (_, $1) => h.sharp($1).toString()) + const { attachments = [] } = msg + session.content = attachments + .filter(({ content_type }) => content_type.startsWith('image')) + .reduce((content, attachment) => content + h.image('https://' + attachment.url), session.content) + session.elements = h.parse(session.content) + + if (msg.message_reference) { + session.quote = await bot.getMessage(msg.channel_id, msg.message_reference.message_id) + } + + return session +} + +export function setupReaction(session: Partial, data: QQGuild.MessageReaction) { + session.userId = data.user_id + session.guildId = data.guild_id + session.channelId = data.channel_id + session.content = `${data.emoji.type}:${data.emoji.id}` + // https://bot.q.qq.com/wiki/develop/api/openapi/reaction/model.html#reactiontargettype + session.messageId = data.target.id + session.isDirect = false + // @TODO type + return session +} + +export async function adaptSession(bot: QQGuildBot, input: QQGuild.DispatchPayload) { + const session = bot.session({}, input) + if (input.t === 'MESSAGE_CREATE' || input.t === 'AT_MESSAGE_CREATE' || input.t === 'DIRECT_MESSAGE_CREATE') { + if (bot.config.app.type === 'private' && input.t === 'AT_MESSAGE_CREATE') return + await decodeMessage(bot, input.d, session) + } else if (input.t === 'MESSAGE_REACTION_ADD') { + setupReaction(session, input.d) + session.type = 'reaction-added' + } else if (input.t === 'MESSAGE_REACTION_REMOVE') { + setupReaction(session, input.d) + session.type = 'reaction-removed' + } else if (input.t === 'CHANNEL_CREATE' || input.t === 'CHANNEL_UPDATE' || input.t === 'CHANNEL_DELETE') { + session.type = { + CHANNEL_CREATE: 'channel-added', + CHANNEL_UPDATE: 'channel-updated', + CHANNEL_DELETE: 'channel-deleted', + }[input.t] + session.guildId = input.d.guild_id + session.channelId = input.d.id + session.channelName = input.d.name + } else if (input.t === 'GUILD_CREATE' || input.t === 'GUILD_UPDATE' || input.t === 'GUILD_DELETE') { + session.type = { + GUILD_CREATE: 'guild-added', + GUILD_UPDATE: 'guild-updated', + GUILD_DELETE: 'guild-deleted', + }[input.t] + session.guildId = input.d.id + session.guildName = input.d.name + } else { + return + } + return session +} diff --git a/adapters/qqguild/src/ws.ts b/adapters/qqguild/src/ws.ts index a99ebc8f..740e3ac6 100644 --- a/adapters/qqguild/src/ws.ts +++ b/adapters/qqguild/src/ws.ts @@ -1,6 +1,7 @@ import { Adapter, Logger, Schema } from '@satorijs/satori' import { QQGuildBot } from './bot' import { Opcode, Payload } from './types' +import { adaptSession } from './utils' const logger = new Logger('qqguild') export class WsClient extends Adapter.WsClient { @@ -41,14 +42,12 @@ export class WsClient extends Adapter.WsClient { } else if (parsed.op === Opcode.DISPATCH) { this._s = parsed.s if (parsed.t === 'READY') { - bot.online() this._sessionId = parsed.d.sessionId - } else if (parsed.t === 'MESSAGE_CREATE' || parsed.t === 'AT_MESSAGE_CREATE' || parsed.t === 'DIRECT_MESSAGE_CREATE') { - const session = bot.adaptMessage(parsed.d - , parsed, - ) - if (session) bot.dispatch(session) + return bot.online() } + const session = await adaptSession(bot, parsed) + if (session) bot.dispatch(session) + logger.debug(require('util').inspect(session, false, null, true)) } })