From 92c471b1ec56a396166678764b805646d433f690 Mon Sep 17 00:00:00 2001 From: _LittleC_ <26459759+XxLittleCxX@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:37:50 +0800 Subject: [PATCH] feat(qq): internal api, logging, retries (#230) --- adapters/qq/src/bot/guild.ts | 13 +- adapters/qq/src/bot/index.ts | 6 +- adapters/qq/src/index.ts | 5 +- adapters/qq/src/internal/group.ts | 80 ++--- adapters/qq/src/internal/guild.ts | 496 ++++++++++++--------------- adapters/qq/src/internal/index.ts | 3 + adapters/qq/src/internal/internal.ts | 49 +++ adapters/qq/src/message.ts | 93 ++--- adapters/qq/src/ws.ts | 5 +- 9 files changed, 380 insertions(+), 370 deletions(-) create mode 100644 adapters/qq/src/internal/index.ts create mode 100644 adapters/qq/src/internal/internal.ts diff --git a/adapters/qq/src/bot/guild.ts b/adapters/qq/src/bot/guild.ts index 8f9c2a97..053a47a7 100644 --- a/adapters/qq/src/bot/guild.ts +++ b/adapters/qq/src/bot/guild.ts @@ -1,7 +1,7 @@ import { Bot, Context, Quester, Universal } from '@satorijs/satori' import { QQBot } from '.' import { decodeChannel, decodeGuild, decodeGuildMember, decodeMessage, decodeUser } from '../utils' -import { GuildInternal } from '../internal/guild' +import { GuildInternal } from '../internal' import { QQGuildMessageEncoder } from '../message' export namespace QQGuildBot { @@ -22,7 +22,7 @@ export class QQGuildBot extends Bot { this.parent = config.parent this.parent.guildBot = this this.platform = 'qqguild' - this.internal = new GuildInternal(() => config.parent.guildHttp) + this.internal = new GuildInternal(this, () => config.parent.guildHttp) this.http = config.parent.guildHttp } @@ -77,7 +77,9 @@ export class QQGuildBot extends Bot { } async muteGuildMember(guildId: string, userId: string, duration: number) { - await this.internal.muteGuildMember(guildId, userId, duration) + await this.internal.muteGuildMember(guildId, userId, { + mute_seconds: Math.floor(duration / 1000), + }) } async getReactionList(channelId: string, messageId: string, emoji: string, next?: string): Promise> { @@ -121,7 +123,10 @@ export class QQGuildBot extends Bot { async createDirectChannel(id: string, guild_id?: string) { let input_guild_id = guild_id if (guild_id?.includes('_')) input_guild_id = guild_id.split('_')[0] // call sendPM directly from DM channel - const dms = await this.internal.createDMS(id, input_guild_id) + const dms = await this.internal.createDMS({ + recipient_id: id, + source_guild_id: input_guild_id, + }) return { id: `${dms.guild_id}_${input_guild_id}`, type: Universal.Channel.Type.DIRECT } } } diff --git a/adapters/qq/src/bot/index.ts b/adapters/qq/src/bot/index.ts index 589b15bb..91f2b2e0 100644 --- a/adapters/qq/src/bot/index.ts +++ b/adapters/qq/src/bot/index.ts @@ -2,8 +2,8 @@ import { Bot, Context, Quester, Schema, Universal } from '@satorijs/satori' import { WsClient } from '../ws' import * as QQ from '../types' import { QQGuildBot } from './guild' -import { GroupInternal } from '../internal/group' import { QQMessageEncoder } from '../message' +import { GroupInternal } from '../internal' interface GetAppAccessTokenResult { access_token: string @@ -39,7 +39,7 @@ export class QQBot extends Bot { this.ctx.plugin(QQGuildBot, { parent: this, }) - this.internal = new GroupInternal(() => this.groupHttp) + this.internal = new GroupInternal(this, () => this.groupHttp) this.ctx.plugin(WsClient, this) } @@ -103,6 +103,7 @@ export class QQBot extends Bot { export namespace QQBot { export interface Config extends QQ.Options, WsClient.Config { intents?: number + retryWhen: number[] } export const Config: Schema = Schema.intersect([ @@ -115,6 +116,7 @@ export namespace QQBot { endpoint: Schema.string().role('link').description('要连接的服务器地址。').default('https://api.sgroup.qq.com/'), authType: Schema.union(['bot', 'bearer'] as const).description('采用的验证方式。').default('bot'), intents: Schema.bitset(QQ.Intents).description('需要订阅的机器人事件。'), + retryWhen: Schema.array(Number).description('发送消息遇到平台错误码时重试。').default([]), }), WsClient.Config, ] as const) diff --git a/adapters/qq/src/index.ts b/adapters/qq/src/index.ts index d865086c..a9d8a2b4 100644 --- a/adapters/qq/src/index.ts +++ b/adapters/qq/src/index.ts @@ -1,7 +1,6 @@ import * as QQ from './types' import { QQBot } from './bot' -import { GroupInternal } from './internal/group' -import { GuildInternal } from './internal/guild' +import { GroupInternal, GuildInternal } from './internal' export { QQ } @@ -27,5 +26,5 @@ declare module '@satorijs/core' { qqguild?: QQ.Payload & GuildInternal } - interface Events extends QQEvents {} + interface Events extends QQEvents { } } diff --git a/adapters/qq/src/internal/group.ts b/adapters/qq/src/internal/group.ts index 27378af0..82ba1213 100644 --- a/adapters/qq/src/internal/group.ts +++ b/adapters/qq/src/internal/group.ts @@ -1,59 +1,37 @@ -import { Quester } from '@satorijs/satori' import * as QQ from '../types' +import { GroupInternal } from '.' -export class GroupInternal { - constructor(private http: () => Quester) { } - - async sendPrivateMessage(openid: string, data: QQ.Message.Request) { - return this.http().post<{ - sendResult: { - msg_id: string - err_msg: string - index: string - } - // id: string - // timestamp: number - }>(`/v2/users/${openid}/messages`, data) - } - - async sendMessage(group_openid: string, data: QQ.Message.Request) { - return this.http().post<{ - group_code: string - msg: string +declare module './internal' { + interface GroupInternal { + sendMessage(channel_id: string, data: QQ.Message.Request): Promise<{ + id: string + timestamp: string } & { code: number message: string data: any - }>(`/v2/groups/${group_openid}/messages`, data) + }> + sendPrivateMessage(openid: string, data: QQ.Message.Request): Promise + sendFilePrivate(openid: string, data: QQ.Message.File.Request): Promise + sendFileGuild(group_openid: string, data: QQ.Message.File.Request): Promise + acknowledgeInteraction(interaction_id: string, code: number): Promise } - - async sendFilePrivate(openid: string, data: QQ.Message.File.Request) { - return this.http().post(`/v2/users/${openid}/files`, data) - } - - async sendFileGuild(group_openid: string, data: QQ.Message.File.Request) { - return this.http().post(`/v2/groups/${group_openid}/files`, data) - } - - // @TODO enum - async acknowledgeInteraction(interaction_id: string, code: number) { - return this.http().put(`/interactions/${interaction_id}`, { - code, - }) - } - - // async getGuildMembers(group_openid: string, start_index: 0) { - // return this.http().get<{ - // members: { - // member_openid: string - // join_timestamp: number - // }[] - // next_index: number - // }>(`/v2/groups/${group_openid}/members`, { - // params: { - // limit: 500, - // start_index, - // }, - // }) - // } } + +GroupInternal.define(false, { + '/v2/groups/{channel.id}/messages': { + POST: 'sendMessage', + }, + '/v2/users/{user.id}/messages': { + POST: 'sendPrivateMessage', + }, + '/v2/users/{user.id}/files': { + POST: 'sendFilePrivate', + }, + '/v2/groups/{channel.id}/files': { + POST: 'sendFileGuild', + }, + '/interactions/{interaction.id}/{interaction.token}/callback': { + POST: 'acknowledgeInteraction', + }, +}) diff --git a/adapters/qq/src/internal/guild.ts b/adapters/qq/src/internal/guild.ts index d7d56fbb..8723d7e6 100644 --- a/adapters/qq/src/internal/guild.ts +++ b/adapters/qq/src/internal/guild.ts @@ -1,289 +1,247 @@ -import { Quester } from '@satorijs/satori' import * as QQ from '../types' - -export class GuildInternal { - constructor(private http: () => Quester) { } - - async getMe() { - return this.http().get('/users/@me') - } - - async getGuilds(params?: Partial<{ - before: string - after: string - limit: number - }>) { - return this.http().get('/users/@me/guilds', { - params, - }) - } - - async getGuild(guild_id: string) { - return this.http().get(`/guilds/${guild_id}`) - } - - async getChannels(guild_id: string) { - return this.http().get(`/guilds/${guild_id}/channels`) - } - - async getChannel(channel_id: string) { - return this.http().get(`/channels/${channel_id}`) - } - - async createGuildChannel(guild_id: string, params: QQ.CreateGuildParams) { - return this.http().post(`/guilds/${guild_id}/channels`, params) - } - - async modifyChannel(channel_id: string, params: QQ.ModifyGuildParams) { - return this.http().patch(`/channels/${channel_id}/channels`, params) - } - - async deleteChannel(channel_id: string) { - return this.http().delete(`/channels/${channel_id}`) - } - - async getChannelOnlineNums(channel_id: string) { - return this.http().get<{ +import { GuildInternal } from './internal' + +declare module './internal' { + interface GuildInternal { + sendMessage(channelId: string, data: QQ.Message.ChannelRequest): Promise + + getMe(): Promise + + getGuilds(params?: Partial<{ + before: string + after: string + limit: number + }>): Promise + + getGuild(guild_id: string): Promise + getChannels(guild_id: string): Promise + getChannel(channel_id: string): Promise + createGuildChannel(guild_id: string, params: QQ.CreateGuildParams): Promise + modifyChannel(channel_id: string, params: QQ.ModifyGuildParams): Promise + deleteChannel(channel_id: string): Promise + getChannelOnlineNums(channel_id: string): Promise<{ online_nums: number - }>(`/channels/${channel_id}/online_nums`) - } - - async getGuildMembers(guild_id: string, params?: Partial<{ - after: string - limit: number - }>) { - return this.http().get(`/guilds/${guild_id}/members`, { params }) - } - - async getGuildRoleMembers(guild_id: string, role_id: string, nextInput = '0') { - return await this.http().get<{ + }> + getGuildMembers(guild_id: string, params?: Partial<{ + after: string + limit: number + }>): Promise + + getGuildRoleMembers(guild_id: string, role_id: string, params?: Partial<{ + start_index: string + limit: number + }>): Promise<{ data: QQ.Member[] next: string - }>(`/guilds/${guild_id}/roles/${role_id}/members`, { - params: { - start_index: nextInput, - limit: 0, - }, - }) - } - - async getGuildMember(guild_id: string, user_id: string) { - return this.http().get(`/guilds/${guild_id}/members/${user_id}`) - } - - async removeGuildMember(guild_id: string, user_id: string) { - return this.http().delete(`/guilds/${guild_id}/members/${user_id}`) - } - - async getGuildRoles(guild_id: string) { - return this.http().get(`/guilds/${guild_id}/roles`) - } - - async createGuildRole(guild_id: string, params: QQ.CreateGuildRoleParams) { - return this.http().post<{ + }> + getGuildMember(guild_id: string, user_id: string): Promise + removeGuildMember(guild_id: string, user_id: string): Promise + getGuildRoles(guild_id: string): Promise + createGuildRole(guild_id: string, params: QQ.CreateGuildRoleParams): Promise<{ role_id: string role: QQ.Role - }>(`/guilds/${guild_id}/roles`, params) - } - - async modifyGuildRole(guild_id: string, role_id: string, params: QQ.CreateGuildRoleParams) { - return this.http().patch<{ + }> + modifyGuildRole(guild_id: string, role_id: string, params: QQ.CreateGuildRoleParams): Promise<{ guild_id: string role_id: string role: QQ.Role - }>(`/guilds/${guild_id}/roles/${role_id}`, params) - } - - async removeGuildRole(guild_id: string, role_id: string) { - return this.http().delete(`/guilds/${guild_id}/roles/${role_id}`) - } - - async addGuildMemberRole(guild_id: string, user_id: string, role_id: string, channel_id?: string) { - return this.http().put(`/guilds/${guild_id}/members/${user_id}/roles/${role_id}`, channel_id ? { - channel: { id: channel_id }, - } : {}) - } - - async removeGuildMemberRole(guild_id: string, user_id: string, role_id: string, channel_id?: string) { - return this.http().axios({ - url: `/guilds/${guild_id}/members/${user_id}/roles/${role_id}`, - method: 'delete', - data: channel_id ? { - channel: { id: channel_id }, - } : {}, - }) - } - - async getChannelMemberPermissions(channel_id: string, user_id: string) { - return this.http().get(`/channels/${channel_id}/members/${user_id}/permissions`) - } - - async modifyChannelMemberPermissions(channel_id: string, user_id: string, params: { - add: string - remove: string - }) { - return this.http().put(`/channels/${channel_id}/members/${user_id}/permissions`, params) - } - - async getChannelRole(channel_id: string, role_id: string) { - return this.http().get(`/channels/${channel_id}/roles/${role_id}/permissions`) - } - - async modifyChannelRole(channel_id: string, role_id: string, params: { - add: string - remove: string - }) { - return this.http().put(`/channels/${channel_id}/roles/${role_id}/permissions`, params) - } - - async getMessage(channelId: string, messageId: string) { - const { message } = await this.http().get<{ - message: QQ.Message - }>(`/channels/${channelId}/messages/${messageId}`) - return message - } - - async sendMessage(channelId: string, data: QQ.Message.ChannelRequest) { - return this.http().post(`/channels/${channelId}/messages`, data) - } - - async sendDM(guildId: string, data: QQ.Message.ChannelRequest) { - return this.http().post(`/dms/${guildId}/messages`, data) - } - - async deleteMessage(channel_id: string, message_id: string, hidetip = false) { - return this.http().delete(`/channels/${channel_id}/messages/${message_id}?hidetip=${hidetip.toString()}`) - } - - async getMessageSetting(guild_id: string) { - return this.http().get(`/guilds/${guild_id}/messages/setting`) - } - - async createDMS(recipient_id: string, source_guild_id: string) { - return this.http().post('/users/@me/dms', { - recipient_id, source_guild_id, - }) - } - - async deleteDM(guild_id: string, message_id: string, hidetip = false) { - // guild_id 是 createDMS 之后的 id - return this.http().delete(`/dms/${guild_id}/messages/${message_id}?hidetip=${hidetip.toString()}`) - } - - async muteGuild(guild_id: string, duration: number) { - return this.http().patch(`/guilds/${guild_id}/mute`, { - mute_seconds: duration / 1000, - }) - } - - async muteGuildMember(guild_id: string, user_id: string, duration: number) { - return this.http().patch(`/guilds/${guild_id}/members/${user_id}/mute`, { - mute_seconds: duration / 1000, - }) - } - - async muteGuildMembers(guild_id: string, user_ids: string[], duration: number) { - return this.http().patch<{ - user_ids: string[] - }>(`/guilds/${guild_id}/mute`, { - mute_seconds: duration / 1000, - user_ids, - }) - } - - async createGuildAnnounce(guild_id: string, params: Partial) { - return this.http().post(`/guilds/${guild_id}/announces`, params) - } - - async removeGuildAnnounce(guild_id: string, message_id: string) { - return this.http().delete(`/guilds/${guild_id}/announces/${message_id}`) - } - - async createPinsMessage(channel_id: string, message_id: string) { - return this.http().put(`/channels/${channel_id}/pins/${message_id}`) - } - - async removePinsMessage(channel_id: string, message_id: string) { - return this.http().delete(`/channels/${channel_id}/pins/${message_id}`) - } - - async getPinsMessage(channel_id: string) { - return this.http().get(`/channels/${channel_id}/pins`) - } - - // @TODO test: since - async getSchedules(channel_id: string, since?: number) { - return this.http().get(`/channels/${channel_id}/schedules?since=${since ?? ''}`) - } - - async getSchedule(channel_id: string, schedule_id: string) { - return this.http().get(`/channels/${channel_id}/schedules/${schedule_id}`) - } - - async createSchedule(channel_id: string, schedule: QQ.Schedule) { - return this.http().post(`/channels/${channel_id}/schedules`, schedule) - } - - async modifySchedule(channel_id: string, schedule_id: string, schedule: QQ.Schedule) { - return this.http().patch(`/channels/${channel_id}/schedules/${schedule_id}`, schedule) - } - - async removeSchedule(channel_id: string, schedule_id: string) { - return this.http().delete(`/channels/${channel_id}/schedules/${schedule_id}`) - } - - async createReaction(channel_id: string, message_id: string, type: string, id: string) { - return this.http().put(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`) - } - - async deleteReaction(channel_id: string, message_id: string, type: string, id: string) { - return this.http().delete(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`) - } - - async getReactions(channel_id: string, message_id: string, type: string, id: string, params?: Partial<{ - cookie: string - limit: number - }>) { - return this.http().get<{ + }> + removeGuildRole(guild_id: string, role_id: string): Promise + addGuildMemberRole(guild_id: string, user_id: string, role_id: string, params: { + channel: { id: string } + }): Promise + removeGuildMemberRole(guild_id: string, user_id: string, role_id: string, params?: { + channel: { id: string } + }): Promise + getChannelMemberPermissions(channel_id: string, user_id: string): Promise + modifyChannelMemberPermissions(channel_id: string, user_id: string, params: { + add: string + remove: string + }): Promise + getChannelRole(channel_id: string, role_id: string): Promise + modifyChannelRole(channel_id: string, role_id: string, params: { + add: string + remove: string + }): Promise + getMessage(channelId: string, messageId: string): Promise + sendMessage(channelId: string, data: QQ.Message.ChannelRequest): Promise + sendDM(guildId: string, data: QQ.Message.ChannelRequest): Promise + deleteMessage(channelId: string, messageId: string, params?: { + hidetip?: boolean + }): Promise + getMessageSetting(guildId: string): Promise + createDMS(data: { + recipient_id: string + source_guild_id: string + }): Promise + deleteDM(guildId: string, messageId: string, params?: { + hidetip?: boolean + }): Promise + muteGuildOrMembers(guildId: string, data: { + mute_seconds: number + user_ids?: string[] + }): Promise<{ + user_ids?: string[] + }> + muteGuildMember(guildId: string, userId: string, data: { + mute_seconds: number + }): Promise + createGuildAnnounce(guildId: string, params: Partial): Promise + removeGuildAnnounce(guildId: string, messageId: string): Promise + createPinsMessage(channelId: string, messageId: string): Promise + removePinsMessage(channelId: string, messageId: string): Promise + getPinsMessage(channelId: string): Promise + getSchedules(channelId: string, params?: { + since: number + }): Promise + getSchedule(channelId: string, scheduleId: string): Promise + createSchedule(channelId: string, schedule: QQ.Schedule): Promise + modifySchedule(channelId: string, scheduleId: string, schedule: QQ.Schedule): Promise + removeSchedule(channelId: string, scheduleId: string): Promise + createReaction(channelId: string, messageId: string, type: string, id: string): Promise + deleteReaction(channelId: string, messageId: string, type: string, id: string): Promise + getReactions(channelId: string, messageId: string, type: string, id: string, params?: Partial<{ + cookie: string + limit: number + }>): Promise<{ cookie: string is_end: boolean users: Pick[] - }>(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`, { - params, - }) - } - - async listThreads(channel_id: string) { - return this.http().get<{ + }> + listThreads(channelId: string): Promise<{ threads: QQ.Forum.Thread[] is_finish: number - }>(`/channels/${channel_id}/threads`) - } - - async getThread(channel_id: string, thread_id: string) { - return this.http().get<{ + }> + getThread(channelId: string, threadId: string): Promise<{ thread: QQ.Forum.Thread - }>(`/channels/${channel_id}/threads/${thread_id}`) - } - - async createPost(channel_id: string, data: QQ.Forum.CreatePostRequest) { - return this.http().put<{ + }> + createPost(channelId: string, data: QQ.Forum.CreatePostRequest): Promise<{ task_id: string create_time: string - }>(`/channels/${channel_id}/threads`, data) - } - - async removePost(channel_id: string, thread_id: string) { - return this.http().delete(`/channels/${channel_id}/threads/${thread_id}`) - } - - async getGuildApiPermissions(guild_id: string) { - return this.http().get<{ + }> + removePost(channelId: string, threadId: string): Promise + getGuildApiPermissions(guildId: string): Promise<{ apis: QQ.APIPermission[] - }>(`/guilds/${guild_id}/api_permissions`) - } - - async createGuildApiPermissionDemand(guild_id: string, data: QQ.APIPermissionDemand) { - return this.http().post(`/guilds/${guild_id}/api_permissions/demand`, data) + }> + createGuildApiPermissionDemand(guildId: string, data: QQ.APIPermissionDemand): Promise } } + +GuildInternal.define(true, { + '/users/@me': { + GET: 'getMe', + }, + '/users/@me/guilds': { + GET: 'getGuilds', + }, + '/guilds/{guild.id}': { + GET: 'getGuild', + }, + '/guilds/{guild.id}/channels': { + GET: 'getChannels', + POST: 'createGuildChannel', + }, + '/channels/{channel.id}': { + GET: 'getChannel', + PATCH: 'modifyChannel', + DELETE: 'deleteChannel', + }, + '/channels/{channel.id}/online_nums': { + GET: 'getChannelOnlineNums', + }, + '/guilds/{guild.id}/members': { + GET: 'getGuildMembers', + }, + '/guilds/{guild.id}/roles/{role.id}/members': { + GET: 'getGuildRoleMembers', + }, + '/guilds/{guild.id}/members/{user.id}': { + GET: 'getGuildMember', + DELETE: 'removeGuildMember', + }, + '/guilds/{guild.id}/roles': { + GET: 'getGuildRoles', + POST: 'createGuildRole', + }, + '/guilds/{guild.id}/roles/{role.id}': { + PATCH: 'modifyGuildRole', + DELETE: 'removeGuildRole', + }, + '/guilds/{guild.id}/members/{user.id}/roles/{role.id}': { + PUT: 'addGuildMemberRole', + DELETE: 'removeGuildMemberRole', + }, + '/channels/{channel.id}/members/{user.id}/permissions': { + GET: 'getChannelMemberPermissions', + PUT: 'modifyChannelMemberPermissions', + }, + '/channels/{channel.id}/roles/{role.id}/permissions': { + GET: 'getChannelRole', + PUT: 'modifyChannelRole', + }, + '/channels/{channel.id}/messages/{message.id}': { + GET: 'getMessage', + DELETE: 'deleteMessage', + }, + '/channels/{channel.id}/messages': { + POST: 'sendMessage', + }, + '/dms/{guild.id}/messages': { + POST: 'sendDM', + }, + '/guilds/{guild.id}/messages/setting': { + GET: 'getMessageSetting', + }, + '/users/@me/dms': { + POST: 'createDMS', + }, + '/dms/{guild.id}/messages/{message.id}': { + DELETE: 'deleteDM', + }, + '/guilds/{guild.id}/mute': { + PATCH: 'muteGuildOrMembers', + }, + '/guilds/{guild.id}/members/{user.id}/mute': { + PATCH: 'muteGuildMember', + }, + '/guilds/{guild.id}/announces': { + POST: 'createGuildAnnounce', + }, + '/guilds/{guild.id}/announces/{message.id}': { + DELETE: 'removeGuildAnnounce', + }, + '/channels/{channel.id}/pins/{message.id}': { + PUT: 'createPinsMessage', + DELETE: 'removePinsMessage', + }, + '/channels/{channel.id}/pins': { + GET: 'getPinsMessage', + }, + '/channels/{channel.id}/schedules': { + GET: 'getSchedules', + POST: 'createSchedule', + }, + '/channels/{channel.id}/schedules/{schedule.id}': { + GET: 'getSchedule', + PATCH: 'modifySchedule', + DELETE: 'removeSchedule', + }, + '/channels/{channel.id}/messages/{message.id}/reactions/{type}/{id}': { + PUT: 'createReaction', + DELETE: 'deleteReaction', + GET: 'getReactions', + }, + '/channels/{channel.id}/threads': { + GET: 'listThreads', + PUT: 'createPost', + }, + '/channels/{channel.id}/threads/{thread.id}': { + DELETE: 'removePost', + }, + '/guilds/{guild.id}/api_permissions': { + GET: 'getGuildApiPermissions', + }, + '/guilds/{guild.id}/api_permissions/demand': { + POST: 'createGuildApiPermissionDemand', + }, +}) diff --git a/adapters/qq/src/internal/index.ts b/adapters/qq/src/internal/index.ts new file mode 100644 index 00000000..b306563a --- /dev/null +++ b/adapters/qq/src/internal/index.ts @@ -0,0 +1,3 @@ +export * from './internal' +export * from './group' +export * from './guild' diff --git a/adapters/qq/src/internal/internal.ts b/adapters/qq/src/internal/internal.ts new file mode 100644 index 00000000..0224b4eb --- /dev/null +++ b/adapters/qq/src/internal/internal.ts @@ -0,0 +1,49 @@ +import { Bot, Dict, makeArray, Quester } from '@satorijs/satori' + +export class Internal { + constructor(private bot: Bot, private http: () => Quester) { } + + static define(isGuild: boolean, routes: Dict>>) { + for (const path in routes) { + for (const key in routes[path]) { + const method = key as Quester.Method + for (const name of makeArray(routes[path][method])) { + (isGuild ? GuildInternal : GroupInternal).prototype[name] = async function (this: Internal, ...args: any[]) { + const raw = args.join(', ') + const url = path.replace(/\{([^}]+)\}/g, () => { + if (!args.length) throw new Error(`too few arguments for ${path}, received ${raw}`) + return args.shift() + }) + const config: Quester.AxiosRequestConfig = {} + if (args.length === 1) { + if (method === 'GET' || method === 'DELETE') { + config.params = args[0] + } else { + config.data = args[0] + } + } else if (args.length === 2 && method !== 'GET' && method !== 'DELETE') { + config.data = args[0] + config.params = args[1] + } else if (args.length > 1) { + throw new Error(`too many arguments for ${path}, received ${raw}`) + } + try { + this.bot.logger.debug(`${method} ${url} request: %o`, config) + const response = await this.http().axios(url, { ...config, method }) + this.bot.logger.debug(`${method} ${url} response: %o, trace id: %s`, response.data, response.headers['x-tps-trace-id']) + return response.data + } catch (error) { + if (!Quester.isAxiosError(error) || !error.response) throw error + this.bot.logger.debug(`${method} ${url} response: %o, trace id: %s`, error.response.data, error.response.headers['x-tps-trace-id']) + throw error + } + } + } + } + } + } +} + +export class GroupInternal extends Internal { } + +export class GuildInternal extends Internal { } diff --git a/adapters/qq/src/message.ts b/adapters/qq/src/message.ts index ec33a90c..b0b46dea 100644 --- a/adapters/qq/src/message.ts +++ b/adapters/qq/src/message.ts @@ -27,7 +27,7 @@ export class QQGuildMessageEncoder extends MessageE let endpoint = `/channels/${this.channelId}/messages` if (isDirect) endpoint = `/dms/${this.channelId.split('_')[0]}/messages` const useFormData = Boolean(this.file) - let msg_id = this.options?.session?.messageId ?? this.options?.session?.id + let msg_id = this.options?.session?.messageId if (this.options?.session && (Date.now() - this.options?.session?.timestamp) > MSG_TIMEOUT) { msg_id = null } @@ -52,7 +52,7 @@ export class QQGuildMessageEncoder extends MessageE headers: form.getHeaders(), }) } else { - r = await this.bot.http.post(endpoint, { + const payload: QQ.Message.ChannelRequest = { ...{ content: this.content, msg_id, @@ -63,20 +63,22 @@ export class QQGuildMessageEncoder extends MessageE message_id: this.reference, }, } : {}), - }) + } + if (isDirect) r = await this.bot.internal.sendDM(this.channelId.split('_')[0], payload) + else r = await this.bot.internal.sendMessage(this.channelId, payload) } } catch (e) { - this.bot.logger.error(e) - this.bot.logger.error('[response] %o', e.response?.data) - if ((e.repsonse?.data?.code === 40004 || e.response?.data?.code === 102) && !this.retry && this.fileUrl) { - this.bot.logger.warn('retry image sending') - this.retry = true - await this.resolveFile(null, true) - await this.flush() + if (Quester.isAxiosError(e)) { + if (this.bot.parent.config.retryWhen.includes(e.response.data.code) && !this.retry && this.fileUrl) { + this.bot.logger.warn('retry image sending') + this.retry = true + await this.resolveFile(null, true) + await this.flush() + } } } - this.bot.logger.debug(r) + // this.bot.logger.debug(r) const session = this.bot.session() session.type = 'send' // await decodeMessage(this.bot, r, session.event.message = {}, session.event) @@ -91,11 +93,11 @@ export class QQGuildMessageEncoder extends MessageE * active msg, http 202: {"code":304023,"message":"push message is waiting for audit now","data":{"message_audit":{"audit_id":"xxx"}}} * passive msg, http 200: Partial */ - if (r.id) { + if (r?.id) { session.messageId = r.id session.app.emit(session, 'send', session) this.results.push(session.event.message) - } else if (r.code === 304023 && this.bot.config.parent.intents & QQ.Intents.MESSAGE_AUDIT) { + } else if (r?.code === 304023 && this.bot.config.parent.intents & QQ.Intents.MESSAGE_AUDIT) { try { const auditData: QQ.MessageAudited = await this.audit(r.data.message_audit.audit_id) session.messageId = auditData.message_id @@ -135,7 +137,7 @@ export class QQGuildMessageEncoder extends MessageE if (!download && !await this.bot.ctx.http.isPrivate(attrs.src || attrs.url)) { return this.fileUrl = attrs.src || attrs.url } - const { data, filename } = await this.bot.ctx.http.file(attrs.src || attrs.url, attrs) + const { data, filename } = await this.bot.ctx.http.file(this.fileUrl || attrs.src || attrs.url, attrs) this.file = Buffer.from(data) this.filename = filename this.fileUrl = null @@ -189,6 +191,7 @@ export class QQMessageEncoder extends MessageEncode private useMarkdown = false private rows: QQ.Button[][] = [] private attachedFile: QQ.Message.File.Response + private retry = false // 先图后文 async flush() { @@ -230,38 +233,44 @@ export class QQMessageEncoder extends MessageEncode } const session = this.bot.session() session.type = 'send' - try { - if (this.session.isDirect) { - const { sendResult: { msg_id } } = await this.bot.internal.sendPrivateMessage(this.session.channelId, data) - session.messageId = msg_id - } else { - // FIXME: missing message id - const resp = await this.bot.internal.sendMessage(this.session.channelId, data) - if (resp.msg !== 'success') { - this.bot.logger.warn(resp) - } - if (resp.code === 304023 && this.bot.config.intents & QQ.Intents.MESSAGE_AUDIT) { - try { - const auditData: QQ.MessageAudited = await this.audit(resp.data.message_audit.audit_id) - session.messageId = auditData.message_id + const send = async () => { + try { + if (this.session.isDirect) { + const { sendResult: { msg_id } } = await this.bot.internal.sendPrivateMessage(this.session.channelId, data) + session.messageId = msg_id + } else { + const resp = await this.bot.internal.sendMessage(this.session.channelId, data) + if (resp.id) { + session.messageId = resp.id + session.timestamp = new Date(resp.timestamp).valueOf() session.app.emit(session, 'send', session) this.results.push(session.event.message) - } catch (e) { - this.bot.logger.error(e) + } else if (resp.code === 304023 && this.bot.config.intents & QQ.Intents.MESSAGE_AUDIT) { + try { + const auditData: QQ.MessageAudited = await this.audit(resp.data.message_audit.audit_id) + session.messageId = auditData.message_id + session.app.emit(session, 'send', session) + this.results.push(session.event.message) + } catch (e) { + this.bot.logger.error(e) + } } } + } catch (e) { + if (!Quester.isAxiosError(e)) throw e + this.errors.push(e) + if (!this.retry && this.bot.config.retryWhen.includes(e.response.data.code)) { + this.bot.logger.warn('%s retry message sending', this.session.cid) + this.retry = true + await send() + } } - } catch (e) { - if (!Quester.isAxiosError(e)) throw e - this.errors.push(e) - this.bot.logger.warn('[response] %s %o', e.response?.status, e.response?.data) } - - // this.results.push(session.event.message) - // session.app.emit(session, 'send', session) + await send() this.content = '' this.attachedFile = null this.rows = [] + this.retry = false } async audit(audit_id: string): Promise { @@ -297,6 +306,7 @@ export class QQMessageEncoder extends MessageEncode let file_type = 0 if (type === 'img' || type === 'image') file_type = 1 else if (type === 'video') file_type = 2 + else if (type === 'audio') file_type = 3 else return const data: QQ.Message.File.Request = { file_type, @@ -313,9 +323,14 @@ export class QQMessageEncoder extends MessageEncode } catch (e) { if (!Quester.isAxiosError(e)) throw e this.errors.push(e) - this.bot.logger.warn('[response] %s %o', e.response?.status, e.response?.data) + if (!this.retry && this.bot.config.retryWhen.includes(e.response.data.code)) { + this.bot.logger.warn('%s retry message sending', this.session.cid) + this.retry = true + await this.sendFile(type, attrs) + } } entry?.dispose?.() + this.retry = false return res } @@ -372,7 +387,7 @@ export class QQMessageEncoder extends MessageEncode await this.flush() const data = await this.sendFile(type, attrs) if (data) this.attachedFile = data - } else if (type === 'video' && (attrs.src || attrs.url)) { + } else if ((type === 'video' || type === 'audio') && (attrs.src || attrs.url)) { await this.flush() const data = await this.sendFile(type, attrs) if (data) this.attachedFile = data diff --git a/adapters/qq/src/ws.ts b/adapters/qq/src/ws.ts index 09c7a68c..6d1561df 100644 --- a/adapters/qq/src/ws.ts +++ b/adapters/qq/src/ws.ts @@ -26,7 +26,7 @@ export class WsClient extends Adapter.WsClient { const parsed: Payload = JSON.parse(data.toString()) - this.bot.logger.debug(parsed) + this.bot.logger.debug('websocket receives %o', parsed) if (parsed.op === Opcode.HELLO) { const token = await this.bot.getAccessToken() if (this._sessionId) { @@ -78,11 +78,12 @@ export class WsClient extends Adapter.WsClient { + this.bot.logger.debug('websocket closed, code %o, reason: %s', e.code, e.reason) clearInterval(this._ping) }) }