Skip to content

Commit

Permalink
feat(qq): button
Browse files Browse the repository at this point in the history
  • Loading branch information
XxLittleCxX committed Oct 5, 2023
1 parent 2400025 commit 3b5e235
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 41 deletions.
140 changes: 106 additions & 34 deletions adapters/qq/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import FormData from 'form-data'
import { escape } from '@satorijs/element'
import { QQGuildBot } from './bot/guild'

export const escapeMarkdown = (val: string) =>
val
.replace(/([\\`*_[\*_~`\]\-(#!>])/g, '\\$&')

export class QQGuildMessageEncoder extends MessageEncoder<QQGuildBot> {
private content: string = ''
private file: Buffer
Expand All @@ -24,34 +28,39 @@ export class QQGuildMessageEncoder extends MessageEncoder<QQGuildBot> {
const useFormData = Boolean(this.file)
let r: QQ.Message
this.bot.ctx.logger('qq').debug('use form data %s', useFormData)
if (useFormData) {
const form = new FormData()
form.append('content', this.content)
if (this.options?.session) {
form.append('msg_id', this.options?.session?.messageId)
}
if (this.file) {
form.append('file_image', this.file, this.filename)
}
// if (this.fileUrl) {
// form.append('image', this.fileUrl)
// }
r = await this.bot.http.post<QQ.Message>(endpoint, form, {
headers: form.getHeaders(),
})
} else {
r = await this.bot.http.post<QQ.Message>(endpoint, {
...{
content: this.content,
msg_id: this.options?.session?.messageId ?? this.options?.session?.id,
image: this.fileUrl,
},
...(this.reference ? {
messageReference: {
message_id: this.reference,
try {
if (useFormData) {
const form = new FormData()
form.append('content', this.content)
if (this.options?.session) {
form.append('msg_id', this.options?.session?.messageId)
}
if (this.file) {
form.append('file_image', this.file, this.filename)
}
// if (this.fileUrl) {
// form.append('image', this.fileUrl)
// }
r = await this.bot.http.post<QQ.Message>(endpoint, form, {
headers: form.getHeaders(),
})
} else {
r = await this.bot.http.post<QQ.Message>(endpoint, {
...{
content: this.content,
msg_id: this.options?.session?.messageId ?? this.options?.session?.id,
image: this.fileUrl,
},
} : {}),
})
...(this.reference ? {
messageReference: {
message_id: this.reference,
},
} : {}),
})
}
} catch (e) {
this.bot.ctx.logger('qq').error(e)
this.bot.ctx.logger('qq').error('[response] %o', e.response?.data)
}

this.bot.ctx.logger('qq').debug(require('util').inspect(r, false, null, true))
Expand Down Expand Up @@ -121,33 +130,58 @@ export class QQGuildMessageEncoder extends MessageEncoder<QQGuildBot> {

export class QQMessageEncoder extends MessageEncoder<QQBot> {
private content: string = ''
private useMarkdown = false
private rows: QQ.InlineKeyboardRow[] = []
private buttonGroupState = false
async flush() {
if (!this.content.trim()) return
if (!this.content.trim() && !this.rows.map(v => v.buttons).flat().length) return
const data: QQ.SendMessageParams = {
content: this.content,
msg_type: 0,
timestamp: Math.floor(Date.now() / 1000),
msg_id: this.options?.session?.messageId,
}

if (this.useMarkdown) {
data.msg_type = 2
delete data.content
data.markdown = {
content: escapeMarkdown(this.content) || ' ',
}
if (this.rows.length) {
data.keyboard = {
content: {
rows: this.rows,
},
}
}
}
const session = this.bot.session()
session.type = 'send'
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
await this.bot.internal.sendMessage(this.guildId, data)
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
await this.bot.internal.sendMessage(this.guildId, data)
}
} catch (e) {
this.bot.ctx.logger('qq').error(e)
this.bot.ctx.logger('qq').error('[response] %o', e.response?.data)
}

// this.results.push(session.event.message)
// session.app.emit(session, 'send', session)
this.content = ''
this.rows = []
}

async sendFile(type: string, attrs: Dict) {
if (!attrs.url.startsWith('http')) {
return this.bot.ctx.logger('qq').warn('unsupported file url')
}
await this.flush()
let file_type = 0
if (type === 'image') file_type = 1
else if (type === 'video') file_type = 2
Expand All @@ -164,6 +198,26 @@ export class QQMessageEncoder extends MessageEncoder<QQBot> {
}
}

decodeButton(attrs: Dict, label: string) {
const result: QQ.Button = {
id: attrs.id,
render_data: {
label,
visited_label: label,
style: 0,
},
action: {
type: attrs.type === 'completion' ? 2
: (attrs.type === 'link' ? 0 : 1),
permission: {
type: 2,
},
data: attrs.data,
},
}
return result
}

async visit(element: h) {
const { type, attrs, children } = element
if (type === 'text') {
Expand All @@ -172,6 +226,24 @@ export class QQMessageEncoder extends MessageEncoder<QQBot> {
await this.sendFile(type, attrs)
} else if (type === 'video' && attrs.url) {
await this.sendFile(type, attrs)
} else if (type === 'button-group') {
this.useMarkdown = true
this.buttonGroupState = true
this.rows.push({ buttons: [] })
await this.render(children)
this.buttonGroupState = false
} else if (type === 'button') {
this.useMarkdown = true
if (this.buttonGroupState) {
const last = this.rows[this.rows.length - 1]
last.buttons.push(this.decodeButton(attrs, children.join('')))
} else {
this.rows.push({
buttons: [
this.decodeButton(attrs, children.join('')),
],
})
}
} else if (type === 'message') {
await this.flush()
await this.render(children)
Expand Down
47 changes: 44 additions & 3 deletions adapters/qq/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1173,8 +1173,10 @@ export interface SendMessageParams {
* 当发送 md,ark,embed 的时候 centent 字段需要填入随意内容,否则发送失败
*/
msg_type: MessageType
markdown?: object
keyboard?: object
markdown?: {
content: string
}
keyboard?: Partial<MessageKeyboard>
ark?: object
image?: unknown
message_reference?: object
Expand Down Expand Up @@ -1209,6 +1211,12 @@ export interface UserMessage {
attachments?: Attachment[] // not listed in document?
}

export enum ChatType {
GROUP = 1,
DIRECT = 2,
CHANNEL = 3
}

export interface Interaction {
id: string
type: 11
Expand All @@ -1217,7 +1225,7 @@ export interface Interaction {
guild_id: string
channel_id: string
group_open_id: string
chat_type: number // @TODO enum
chat_type: ChatType
data: {
resolved: {
button_data: string
Expand All @@ -1238,3 +1246,36 @@ export interface UserEvent {
timestamp: number
openid: string
}

export interface MessageKeyboard {
id: string
content: InlineKeyboard
}

export interface InlineKeyboard {
rows: InlineKeyboardRow[]
}

export interface InlineKeyboardRow {
buttons: Button[]
}

export interface Button {
id?: string
render_data: {
label: string
visited_label: string
style: number
}
action: {
type: number
permission: {
type: number
specify_role_ids?: string[]
specify_user_ids?: string
}
click_limit?: number
data: string
at_bot_show_channel_list?: string
}
}
24 changes: 23 additions & 1 deletion adapters/qq/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function adaptSession(bot: QQBot, input: QQ.DispatchPayload) {
let session = bot.session()

if (!['GROUP_AT_MESSAGE_CREATE', 'C2C_MESSAGE_CREATE', 'FRIEND_ADD', 'FRIEND_DEL',
'GROUP_ADD_ROBOT', 'GROUP_DEL_ROBOT'].includes(input.t)) {
'GROUP_ADD_ROBOT', 'GROUP_DEL_ROBOT', 'INTERACTION_CREATE'].includes(input.t)) {
session = bot.guildBot.session()
}

Expand Down Expand Up @@ -187,6 +187,28 @@ export async function adaptSession(bot: QQBot, input: QQ.DispatchPayload) {
session.timestamp = input.d.timestamp
session.guildId = input.d.group_openid
session.operatorId = input.d.op_member_openid
} else if (input.t === 'INTERACTION_CREATE') {
session.type = 'interaction/button'
session.userId = input.d.data.resolved.user_id
if (input.d.chat_type === QQ.ChatType.GROUP) {
session.guildId = input.d.group_open_id
session.channelId = session.guildId
session.isDirect = false
} else if (input.d.chat_type === QQ.ChatType.CHANNEL) {
session.channelId = session.userId
session.isDirect = true
}
session.event.button = {
id: input.d.data.resolved.button_id,
}
// session.messageId = input.d.id // event_id is not supported for sending message

// {message: 'get header appid failed', code: 630006}
try {
await bot.internal.acknowledgeInteraction(input.d.id, 0)
} catch (e) {
bot.ctx.logger('qq').warn(e)
}
} else {
return
}
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pick, remove } from 'cosmokit'
import { Dict, pick, remove } from 'cosmokit'
import { Context, Fragment } from '.'
import { Adapter } from './adapter'
import { MessageEncoder } from './message'
Expand Down Expand Up @@ -26,6 +26,7 @@ export abstract class Bot<T = any> implements Login {
public platform: string
public adapter?: Adapter<this>
public error?: Error
public callbacks: Dict<Function> = {}

protected context: Context
protected _status: Status = Status.OFFLINE
Expand All @@ -42,6 +43,11 @@ export abstract class Bot<T = any> implements Login {
})

ctx.on('dispose', () => this.dispose())

ctx.on('interaction/button', (session: Session) => {
const cb = this.callbacks[session.event.button.id]
if (cb) cb(session)
})
}

update(login: Login) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type EventCallback<T = void, R extends any[] = []> = (this: Session, session: Se
export interface Events<C extends Context = Context> extends cordis.Events<C> {
'internal/session'(session: Session): void
'interaction/command'(session: Session): void
'interaction/button'(session: Session): void
'message'(session: Session): void
'message-created'(session: Session): void
'message-deleted'(session: Session): void
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export abstract class MessageEncoder<B extends Bot = Bot> {
public results: Message[] = []
public session: Session

constructor(public bot: B, public channelId: string, public guildId?: string, public options: SendOptions = {}) {}
constructor(public bot: B, public channelId: string, public guildId?: string, public options: SendOptions = {}) { }

async prepare() {}
async prepare() { }

abstract flush(): Promise<void>
abstract visit(element: h): Promise<void>
Expand All @@ -40,6 +40,12 @@ export abstract class MessageEncoder<B extends Bot = Bot> {
})
await this.prepare()
this.session.elements = h.normalize(content)
const btns = h.select(this.session.elements, 'button').filter(v => v.attrs.type !== 'link' && !v.attrs.id)
for (const btn of btns) {
const r = (Math.random() + 1).toString(36).substring(7)
btn.attrs.id ||= r
if (typeof btn.attrs.action === 'function') this.bot.callbacks[btn.attrs.id] = btn.attrs.action
}
if (await this.session.app.serial(this.session, 'before-send', this.session, this.options)) return
const session = this.options.session ?? this.session
await this.render(await session.transform(this.session.elements))
Expand Down
5 changes: 5 additions & 0 deletions packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ export interface Message {
updatedAt?: number
}

export interface Button {
id: string
}

export interface Command {
name: string
aliases: string[]
Expand Down Expand Up @@ -289,6 +293,7 @@ export interface Event {
operator?: User
role?: GuildRole
user?: User
button?: Button
_type?: string
_data?: any
/** @deprecated */
Expand Down

0 comments on commit 3b5e235

Please sign in to comment.