diff --git a/adapters/discord/src/types/user.ts b/adapters/discord/src/types/user.ts index 85c3431f..b5ed4838 100644 --- a/adapters/discord/src/types/user.ts +++ b/adapters/discord/src/types/user.ts @@ -8,6 +8,8 @@ export interface User { username: string /** the user's 4-digit discord-tag */ discriminator: string + /** the user's display name, if it is set. For bots, this is the application name */ + global_name?: string /** the user's avatar hash */ avatar?: string /** whether the user belongs to an OAuth2 application */ @@ -106,7 +108,7 @@ export enum VisibilityType { EVERYONE = 1, } -export interface UserUpdateEvent extends User {} +export interface UserUpdateEvent extends User { } declare module './gateway' { interface GatewayEvents { diff --git a/adapters/discord/src/utils.ts b/adapters/discord/src/utils.ts index a6f970c9..7f210da8 100644 --- a/adapters/discord/src/utils.ts +++ b/adapters/discord/src/utils.ts @@ -16,6 +16,7 @@ export const sanitizeCode = (val: string) => val.replace(/(?<=`)(?=`)/g, '\u200b export const decodeUser = (user: Discord.User): Universal.User => ({ id: user.id, + nick: user.global_name, name: user.username, userId: user.id, avatar: user.avatar && `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`, @@ -94,6 +95,16 @@ export async function decodeMessage( }) } + if (data.sticker_items) { + message.content += data.sticker_items.map(s => h('sticker', { + id: s.id, + format_type: s.format_type, + name: s.name, + }, [ + h.image(`https://media.discordapp.net/stickers/${s.id}.webp?size=160`), + ])).join('') + } + // embed 的 update event 太阴间了 只有 id embeds channel_id guild_id 四个成员 if (data.attachments?.length) { if (!/\s$/.test(message.content)) message.content += ' ' diff --git a/adapters/qq/src/bot/guild.ts b/adapters/qq/src/bot/guild.ts index 46bc29f3..0d5e1131 100644 --- a/adapters/qq/src/bot/guild.ts +++ b/adapters/qq/src/bot/guild.ts @@ -22,8 +22,8 @@ export class QQGuildBot extends Bot { this.parent = config.parent this.parent.guildBot = this this.platform = 'qqguild' - this.internal = new GuildInternal(this, () => config.parent.guildHttp) - this.http = config.parent.guildHttp + this.internal = new GuildInternal(this, () => config.parent.http) + this.http = config.parent.http } get status() { diff --git a/adapters/qq/src/bot/index.ts b/adapters/qq/src/bot/index.ts index a92d9b76..6b3f796f 100644 --- a/adapters/qq/src/bot/index.ts +++ b/adapters/qq/src/bot/index.ts @@ -18,7 +18,6 @@ export class QQBot extends Bot { internal: GroupInternal http: HTTP - guildHttp: HTTP private _token: string private _timer: NodeJS.Timeout @@ -29,12 +28,16 @@ export class QQBot extends Bot { if (config.sandbox) { endpoint = endpoint.replace(/^(https?:\/\/)/, '$1sandbox.') } - this.guildHttp = ctx.http.extend({ - endpoint, - headers: { - 'Authorization': `Bot ${this.config.id}.${this.config.token}`, - }, - }) + // 如果是 bot 类型, 使用固定 token + if (this.config.authType === 'bot') { + this.http = this.ctx.http.extend({ + endpoint, + headers: { + 'Authorization': `Bot ${this.config.id}.${this.config.token}`, + 'X-Union-Appid': this.config.id, + }, + }) + } this.ctx.plugin(QQGuildBot, { parent: this, @@ -126,6 +129,7 @@ export namespace QQBot { export interface Config extends QQ.Options, WsClient.Options { intents?: number retryWhen: number[] + manualAcknowledge: boolean } export const Config: Schema = Schema.intersect([ @@ -136,10 +140,13 @@ export namespace QQBot { type: Schema.union(['public', 'private'] as const).description('机器人类型。').required(), sandbox: Schema.boolean().description('是否开启沙箱模式。').default(false), endpoint: Schema.string().role('link').description('要连接的服务器地址。').default('https://api.sgroup.qq.com/'), - authType: Schema.union(['bot', 'bearer'] as const).description('采用的验证方式。').default('bot'), + authType: Schema.union(['bot', 'bearer'] as const).description('采用的验证方式。').default('bearer'), intents: Schema.bitset(QQ.Intents).description('需要订阅的机器人事件。'), retryWhen: Schema.array(Number).description('发送消息遇到平台错误码时重试。').default([]), }), WsClient.Options, + Schema.object({ + manualAcknowledge: Schema.boolean().description('手动响应回调消息。').default(false), + }).description('高级设置'), ] as const) } diff --git a/adapters/qq/src/internal/group.ts b/adapters/qq/src/internal/group.ts index 1c911dd4..56cd5f62 100644 --- a/adapters/qq/src/internal/group.ts +++ b/adapters/qq/src/internal/group.ts @@ -11,7 +11,14 @@ declare module './internal' { message: string data: any }> - sendPrivateMessage(openid: string, data: QQ.Message.Request): Promise + sendPrivateMessage(openid: string, data: QQ.Message.Request): Promise<{ + id: string + timestamp: string + } & { + code: number + message: string + data: any + }> sendFilePrivate(openid: string, data: QQ.Message.File.Request): Promise sendFileGuild(group_openid: string, data: QQ.Message.File.Request): Promise acknowledgeInteraction(interaction_id: string, data: { diff --git a/adapters/qq/src/message.ts b/adapters/qq/src/message.ts index 8a450229..d1018501 100644 --- a/adapters/qq/src/message.ts +++ b/adapters/qq/src/message.ts @@ -193,6 +193,7 @@ export class QQMessageEncoder extends MessageEncode private content: string = '' private passiveId: string private passiveSeq: number + private passiveEventId: string private useMarkdown = false private rows: QQ.Button[][] = [] private attachedFile: QQ.Message.File.Response @@ -202,19 +203,23 @@ export class QQMessageEncoder extends MessageEncode async flush() { if (!this.content.trim() && !this.rows.flat().length && !this.attachedFile) return this.trimButtons() - let msg_id: string, msg_seq: number + let msg_id: string, msg_seq: number, event_id: string if (this.options?.session?.messageId && Date.now() - this.options.session.timestamp < MSG_TIMEOUT) { this.options.session['seq'] ||= 0 msg_id = this.options.session.messageId msg_seq = ++this.options.session['seq'] + } else if (this.options?.session?.qq['id'] && Date.now() - this.options.session.timestamp < MSG_TIMEOUT) { + event_id = this.options.session.qq['id'] } if (this.passiveId) msg_id = this.passiveId if (this.passiveSeq) msg_seq = this.passiveSeq + if (this.passiveEventId) event_id = this.passiveEventId const data: QQ.Message.Request = { content: this.content, msg_type: QQ.Message.Type.TEXT, msg_id, msg_seq, + event_id, } if (this.attachedFile) { if (!data.content.length) data.content = ' ' @@ -240,27 +245,24 @@ export class QQMessageEncoder extends MessageEncode session.type = 'send' 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.channelId = this.session.channelId - session.guildId = this.session.guildId + const resp = this.session.isDirect + ? await this.bot.internal.sendPrivateMessage(this.session.channelId, data) + : await this.bot.internal.sendMessage(this.session.channelId, data) + if (resp.id) { + session.messageId = resp.id + session.timestamp = new Date(resp.timestamp).valueOf() + session.channelId = this.session.channelId + session.guildId = this.session.guildId + session.app.emit(session, 'send', session) + this.results.push(session.event.message) + } 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) - } 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) { + this.bot.logger.error(e) } } } catch (e) { @@ -386,8 +388,9 @@ export class QQMessageEncoder extends MessageEncode if (type === 'text') { this.content += attrs.content } else if (type === 'passive') { - this.passiveId = attrs.messageId - this.passiveSeq = Number(attrs.seq) + if (attrs.messageId) this.passiveId = attrs.messageId + if (attrs.seq) this.passiveSeq = Number(attrs.seq) + if (attrs.eventId) this.passiveEventId = attrs.eventId } else if ((type === 'img' || type === 'image') && (attrs.src || attrs.url)) { await this.flush() const data = await this.sendFile(type, attrs) diff --git a/adapters/qq/src/types.ts b/adapters/qq/src/types.ts index b63946df..32357c71 100644 --- a/adapters/qq/src/types.ts +++ b/adapters/qq/src/types.ts @@ -193,6 +193,7 @@ export interface GetGatewayBotResponse { } export interface PayloadStructure { + id: string /** opcode for the payload */ op: O /** event data */ diff --git a/adapters/qq/src/utils.ts b/adapters/qq/src/utils.ts index 405d42de..19d2f4e2 100644 --- a/adapters/qq/src/utils.ts +++ b/adapters/qq/src/utils.ts @@ -34,8 +34,6 @@ export function decodeGroupMessage( payload: Universal.MessageLike = message, ) { message.id = data.id - const date = data.timestamp.slice(0, data.timestamp.indexOf('m=')).trim().replace(/\+(\d{4}) CST/, 'GMT+$1') - message.timestamp = new Date(date).valueOf() message.elements = [] if (data.content.length) message.elements.push(h.text(data.content)) for (const attachment of (data.attachments ?? [])) { @@ -53,8 +51,11 @@ export function decodeGroupMessage( } message.content = message.elements.join('') - message.guild = { id: data.group_id } - message.user = { id: data.author.id } + if (!payload) return message + const date = data.timestamp.slice(0, data.timestamp.indexOf('m=')).trim().replace(/\+(\d{4}) CST/, 'GMT+$1') + payload.timestamp = new Date(date).valueOf() + payload.guild = data.group_id && { id: data.group_id } + payload.user = { id: data.author.id, avatar: `https://q.qlogo.cn/qqapp/${bot.config.id}/${data.author.id}/640` } return message } @@ -165,15 +166,12 @@ export async function adaptSession(bot: QQBot, i session.type = 'message' session.isDirect = false decodeGroupMessage(bot, input.d, session.event.message = {}, session.event) - session.guildId = session.event.message.guild.id session.channelId = session.guildId - session.userId = session.event.message.user.id session.elements.unshift(h.at(session.selfId)) } else if (input.t === 'C2C_MESSAGE_CREATE') { session.type = 'message' session.isDirect = true decodeGroupMessage(bot, input.d, session.event.message = {}, session.event) - session.userId = input.d.author.id session.channelId = session.userId } else if (input.t === 'FRIEND_ADD') { session.type = 'friend-added' @@ -216,7 +214,7 @@ export async function adaptSession(bot: QQBot, i // {message: 'get header appid failed', code: 630006} // {"message":"check app privilege not pass","code":11253 - bot.internal.acknowledgeInteraction(input.d.id, { code: 0 }).catch(() => { }) + if (!bot.config.manualAcknowledge) bot.internal.acknowledgeInteraction(input.d.id, { code: 0 }).catch(() => { }) } else if (input.t === 'GUILD_MEMBER_ADD' || input.t === 'GUILD_MEMBER_DELETE' || input.t === 'GUILD_MEMBER_UPDATE') { session.type = { GUILD_MEMBER_ADD: 'guild-member-added', diff --git a/adapters/qq/src/ws.ts b/adapters/qq/src/ws.ts index 7f8f91c9..8ef56cb7 100644 --- a/adapters/qq/src/ws.ts +++ b/adapters/qq/src/ws.ts @@ -9,7 +9,7 @@ export class WsClient extends Adapter.WsClient extends Adapter.WsClient extends Adapter.WsClient