Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jul 5, 2021
2 parents fea4723 + d17c32d commit 6281f2e
Show file tree
Hide file tree
Showing 48 changed files with 560 additions and 435 deletions.
24 changes: 24 additions & 0 deletions docs/api/adapter/discord.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,27 @@ sidebarDepth: 2
- 类型: [`AxiosRequestConfig`](https://github.com/axios/axios#request-config)

用于 discord 适配器的请求配置。

### options.discord.handleExternalAsset

- 可选值: `string`
- 默认值: `'auto'`

指定单独发送外链资源时采用的方法:

- **download:** 先下载后发送
- **direct:** 直接发送链接
- **auto:** 发送一个 HEAD 请求,如果返回的 Content-Type 正确,则直接发送链接,否则先下载后发送

### options.discord.handleMixedContent

- 可选值: `string`
- 默认值: `'auto'`

指定发送图文混合内容时采用的方法:

- **separate:** 将每个不同形式的内容分开发送
- **attach:** 图片前如果有文本内容,则将文本作为图片的附带信息进行发送
- **auto:** 如果图片本身采用直接发送则与前面的文本分开,否则将文本作为图片的附带信息发送

当配置为 `attach` 并且发送文本+图片形式的消息时,无论 [`handleExternalAsset`](#options-discord-handleexternalasset) 配置为何都会先下载后发送。
4 changes: 2 additions & 2 deletions packages/adapter-discord/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "koishi-adapter-discord",
"description": "Discord adapter for Koishi",
"version": "1.3.0",
"version": "1.3.1",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
Expand All @@ -28,7 +28,7 @@
"koishi"
],
"peerDependencies": {
"koishi-core": "^3.12.1"
"koishi-core": "^3.12.2"
},
"devDependencies": {
"@types/ws": "^7.4.2",
Expand Down
239 changes: 123 additions & 116 deletions packages/adapter-discord/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { segment } from 'koishi-utils'
import FormData from 'form-data'
import FileType from 'file-type'

export type HandleExternalAssets = 'auto' | 'download' | 'direct'
export type HandleExternalAsset = 'auto' | 'download' | 'direct'
export type HandleMixedContent = 'auto' | 'separate' | 'attach'

export class SenderError extends Error {
constructor(url: string, data: any, selfId: string) {
Expand All @@ -33,20 +34,21 @@ export class DiscordBot extends Bot<'discord'> {
const { axiosConfig, discord = {} } = this.app.options
const endpoint = discord.endpoint || 'https://discord.com/api/v8'
const url = `${endpoint}${path}`
const headers: Record<string, any> = {
Authorization: `Bot ${this.token}`,
}
try {
const response = await axios({
...axiosConfig,
...discord.axiosConfig,
method,
url,
headers: { ...headers, ...exHeaders },
headers: {
Authorization: `Bot ${this.token}`,
...exHeaders,
},
data,
})
return response.data
} catch (e) {
if (e.response?.data) console.log(e.response.data)
throw new SenderError(url, data, this.selfId)
}
}
Expand All @@ -56,12 +58,21 @@ export class DiscordBot extends Bot<'discord'> {
return adaptUser(data)
}

private async sendEmbedMessage(requestUrl: string, fileBuffer: Buffer, payload_json: Record<string, any> = {}) {
private async _sendEmbed(requestUrl: string, fileBuffer: Buffer, payload_json: Record<string, any> = {}) {
const fd = new FormData()
const type = await FileType.fromBuffer(fileBuffer)
fd.append('file', fileBuffer, 'file.' + type.ext)
fd.append('payload_json', JSON.stringify(payload_json))
return this.request('POST', requestUrl, fd, fd.getHeaders())
const r = await this.request('POST', requestUrl, fd, fd.getHeaders())
return r.id as string
}

private async _sendContent(requestUrl: string, content: string, addition: Record<string, any>) {
const r = await this.request('POST', requestUrl, {
...addition,
content,
})
return r.id as string
}

private parseQuote(chain: segment.Chain) {
Expand All @@ -73,125 +84,108 @@ export class DiscordBot extends Bot<'discord'> {
return this.sendMessage(channelId, content)
}

private async sendFullMessage(requestUrl: string, content: string, addition: Record<string, any> = {}): Promise<string> {
private async _sendAsset(requestUrl: string, type: string, data: Record<string, string>, addition: Record<string, any>) {
const { axiosConfig, discord = {} } = this.app.options

if (discord.handleMixedContent === 'separate' && addition.content) {
await this._sendContent(requestUrl, addition.content, addition)
addition.content = ''
}

if (data.url.startsWith('file://')) {
return this._sendEmbed(requestUrl, readFileSync(data.url.slice(8)), addition)
} else if (data.url.startsWith('base64://')) {
const a = Buffer.from(data.url.slice(9), 'base64')
return await this._sendEmbed(requestUrl, a, addition)
}

const sendDirect = async () => {
if (addition.content) {
await this._sendContent(requestUrl, addition.content, addition)
}
return this._sendContent(requestUrl, data.url, addition)
}

const sendDownload = async () => {
const a = await axios.get(data.url, {
...axiosConfig,
...discord.axiosConfig,
responseType: 'arraybuffer',
headers: {
accept: type + '/*',
},
})
return this._sendEmbed(requestUrl, a.data, addition)
}

const mode = data.mode as HandleExternalAsset || discord.handleExternalAsset
if (mode === 'download' || discord.handleMixedContent === 'attach' && addition.content) {
return sendDownload()
} else if (mode === 'direct') {
return sendDirect()
}

// auto mode
await axios.head(data.url, {
...axiosConfig,
...discord.axiosConfig,
headers: {
accept: type + '/*',
},
}).then(({ headers }) => {
if (headers['content-type'].startsWith(type)) {
return sendDirect()
} else {
return sendDownload()
}
}, sendDownload)
}

private async _sendMessage(requestUrl: string, content: string, addition: Record<string, any> = {}) {
const chain = segment.parse(content)
let sentMessageId = '0'
let needSend = ''
const isWebhook = requestUrl.startsWith('/webhooks/')
const that = this
let messageId = '0'
let textBuffer = ''
delete addition.content
async function sendMessage() {
const r = await that.request('POST', requestUrl, {
content: needSend.trim(),
...addition,
})
sentMessageId = r.id
needSend = ''

const sendBuffer = async () => {
const content = textBuffer.trim()
if (!content) return
messageId = await this._sendContent(requestUrl, content, addition)
textBuffer = ''
}

for (const code of chain) {
const { type, data } = code
if (type === 'text') {
needSend += data.content.trim()
textBuffer += data.content.trim()
} else if (type === 'at' && data.id) {
needSend += `<@${data.id}>`
textBuffer += `<@${data.id}>`
} else if (type === 'at' && data.type === 'all') {
needSend += `@everyone`
textBuffer += `@everyone`
} else if (type === 'at' && data.type === 'here') {
needSend += `@here`
textBuffer += `@here`
} else if (type === 'sharp' && data.id) {
needSend += `<#${data.id}>`
textBuffer += `<#${data.id}>`
} else if (type === 'face' && data.name && data.id) {
needSend += `<:${data.name}:${data.id}>`
} else {
if (needSend.trim()) await sendMessage()
if (type === 'share') {
const sendData = isWebhook ? {
embeds: [{ ...addition, ...data }],
} : {
embed: { ...addition, ...data },
}
const r = await this.request('POST', requestUrl, {
...sendData,
})
sentMessageId = r.id
}
if (type === 'image' || type === 'video' && data.url) {
if (data.url.startsWith('file://')) {
const r = await this.sendEmbedMessage(requestUrl, readFileSync(data.url.slice(8)), {
...addition,
})
sentMessageId = r.id
} else if (data.url.startsWith('base64://')) {
const a = Buffer.from(data.url.slice(9), 'base64')
const r = await this.sendEmbedMessage(requestUrl, a, {
...addition,
})
sentMessageId = r.id
} else {
const { axiosConfig, discord = {} } = this.app.options
const sendMode =
data.mode as HandleExternalAssets || // define in segment
discord.handleExternalAssets || // define in app options
'auto' // default

// Utils
async function sendDownload() {
const a = await axios.get(data.url, {
...axiosConfig,
...discord.axiosConfig,
responseType: 'arraybuffer',
headers: {
accept: 'image/*',
},
})
const r = await that.sendEmbedMessage(requestUrl, a.data, {
...addition,
})
sentMessageId = r.id
}
async function sendDirect() {
const r = await that.request('POST', requestUrl, {
content: data.url,
...addition,
})
sentMessageId = r.id
}

if (sendMode === 'direct') {
// send url directly
await sendDirect()
} else if (sendMode === 'download') {
// download send
await sendDownload()
} else {
// auto mode
await axios
.head(data.url, {
...axiosConfig,
...discord.axiosConfig,
headers: {
accept: 'image/*',
},
})
.then(async ({ headers }) => {
if (headers['content-type'].includes('image')) {
await sendDirect()
} else {
await sendDownload()
}
}, async () => {
await sendDownload()
})
.catch(() => {
throw new SenderError(data.url, data, this.selfId)
})
}
}
}
textBuffer += `<:${data.name}:${data.id}>`
} else if ((type === 'image' || type === 'video') && data.url) {
messageId = await this._sendAsset(requestUrl, type, data, {
...addition,
content: textBuffer.trim(),
})
textBuffer = ''
} else if (type === 'share') {
await sendBuffer()
const r = await this.request('POST', requestUrl, {
...addition,
embeds: [{ ...data }],
})
messageId = r.id
}
}
if (needSend.trim()) await sendMessage()
return sentMessageId

await sendBuffer()
return messageId
}

async sendMessage(channelId: string, content: string, groupId?: string) {
Expand All @@ -204,7 +198,7 @@ export class DiscordBot extends Bot<'discord'> {
message_id: quote,
} : undefined

session.messageId = await this.sendFullMessage(`/channels/${channelId}/messages`, session.content, { message_reference })
session.messageId = await this._sendMessage(`/channels/${channelId}/messages`, session.content, { message_reference })

this.app.emit(session, 'send', session)
return session.messageId
Expand Down Expand Up @@ -246,7 +240,7 @@ export class DiscordBot extends Bot<'discord'> {
result.author.nickname = msg.member?.nick
if (msg.message_reference) {
const quoteMsg = await this.$getMessage(msg.message_reference.channel_id, msg.message_reference.message_id)
result.quote = await adaptMessage(this, quoteMsg)
result.quote = adaptMessage(this, quoteMsg)
}
return result
}
Expand All @@ -266,13 +260,26 @@ export class DiscordBot extends Bot<'discord'> {
return adaptChannel(data)
}

async $createReaction(channelId: string, messageId: string, emoji: string) {
await this.request('PUT', `/channels/${channelId}/messages/${messageId}/reactions/${emoji}/@me`)
}

async $deleteReaction(channelId: string, messageId: string, emoji: string, userId = '@me') {
await this.request('DELETE', `/channels/${channelId}/messages/${messageId}/reactions/${emoji}/${userId}`)
}

async $deleteAllReactions(channelId: string, messageId: string, emoji?: string) {
const path = emoji ? '/' + emoji : ''
await this.request('DELETE', `/channels/${channelId}/messages/${messageId}/reactions${path}`)
}

async $executeWebhook(id: string, token: string, data: DC.ExecuteWebhookBody, wait = false): Promise<string> {
const chain = segment.parse(data.content)
if (chain.filter(v => v.type === 'image').length > 10) {
throw new Error('Up to 10 embed objects')
}

return await this.sendFullMessage(`/webhooks/${id}/${token}?wait=${wait}`, data.content, data)
return await this._sendMessage(`/webhooks/${id}/${token}?wait=${wait}`, data.content, data)
}

$getGuildMember(guildId: string, userId: string) {
Expand Down
17 changes: 15 additions & 2 deletions packages/adapter-discord/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { Adapter } from 'koishi-core'
import { AxiosRequestConfig } from 'axios'
import { DiscordBot, HandleExternalAssets } from './bot'
import { DiscordBot, HandleExternalAsset, HandleMixedContent } from './bot'
import WsClient from './ws'
import * as DC from './types'
export * from './bot'

interface DiscordOptions extends Adapter.WsClientOptions {
endpoint?: string
axiosConfig?: AxiosRequestConfig
handleExternalAssets?: HandleExternalAssets
/**
* 发送外链资源时采用的方法
* - download:先下载后发送
* - direct:直接发送链接
* - auto:发送一个 HEAD 请求,如果返回的 Content-Type 正确,则直接发送链接,否则先下载后发送(默认)
*/
handleExternalAsset?: HandleExternalAsset
/**
* 发送图文等混合内容时采用的方法
* - separate:将每个不同形式的内容分开发送
* - attach:图片前如果有文本内容,则将文本作为图片的附带信息进行发送
* - auto:如果图片本身采用直接发送则与前面的文本分开,否则将文本作为图片的附带信息发送(默认)
*/
handleMixedContent?: HandleMixedContent
}

declare module 'koishi-core' {
Expand Down
Loading

0 comments on commit 6281f2e

Please sign in to comment.