Skip to content

Commit

Permalink
feat(telegram): improve browser compatibility (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma authored Mar 1, 2023
1 parent f8a2a6f commit f3f8998
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 88 deletions.
1 change: 0 additions & 1 deletion adapters/telegram/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@satorijs/satori": "^2.2.0"
},
"dependencies": {
"file-type": "^16.5.4",
"form-data": "^4.0.0"
}
}
21 changes: 8 additions & 13 deletions adapters/telegram/src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Bot, Context, Dict, Fragment, Logger, Quester, Schema, segment, SendOptions, Session, Time, Universal } from '@satorijs/satori'
import { arrayBufferToBase64, Bot, Context, Dict, Fragment, Logger, Quester, Schema, segment, SendOptions, Session, Time, Universal } from '@satorijs/satori'
import * as Telegram from './types'
import { adaptGuildMember, adaptUser } from './utils'
import { TelegramMessenger } from './message'
import { HttpServer } from './server'
import { HttpPolling } from './polling'
import { fromBuffer } from 'file-type'
import fs from 'fs'

const logger = new Logger('telegram')

Expand Down Expand Up @@ -57,9 +55,9 @@ export class TelegramBot<T extends TelegramBot.Config = TelegramBot.Config> exte
const route = `/telegram/${this.selfId}`
this.server = selfUrl + route
ctx.router.get(route + '/:file+', async ctx => {
const { buffer, mime } = await this.$getFile(ctx.params.file)
const { data, mime } = await this.$getFile(ctx.params.file)
ctx.set('content-type', mime)
ctx.body = buffer
ctx.body = data
})
}
}
Expand Down Expand Up @@ -253,14 +251,11 @@ export class TelegramBot<T extends TelegramBot.Config = TelegramBot.Config> exte
}

async $getFile(filePath: string) {
let buffer: Buffer
if (this.local) {
buffer = await fs.promises.readFile(filePath)
return await this.ctx.http.file(filePath)
} else {
buffer = await this.file.get(`/${filePath}`, { responseType: 'arraybuffer' })
return await this.file.file(`/${filePath}`)
}
const { mime } = await fromBuffer(buffer)
return { mime, buffer }
}

async $getFileFromId(file_id: string) {
Expand All @@ -276,8 +271,8 @@ export class TelegramBot<T extends TelegramBot.Config = TelegramBot.Config> exte
if (this.server) {
return { url: `${this.server}/${filePath}` }
}
const { mime, buffer } = await this.$getFile(filePath)
const base64 = `data:${mime};base64,` + buffer.toString('base64')
const { mime, data } = await this.$getFile(filePath)
const base64 = `data:${mime};base64,` + arrayBufferToBase64(data)
return { url: base64 }
}

Expand Down Expand Up @@ -332,6 +327,6 @@ export namespace TelegramBot {
local: Schema.boolean().description('是否启用 [Telegram Bot API](https://github.com/tdlib/telegram-bot-api) 本地模式。'),
server: Schema.boolean().description('是否启用文件代理。若开启将会使用 `selfUrl` 进行反代,否则会下载所有资源文件 (包括图片、视频等)。当配置了 `selfUrl` 时将默认开启。'),
}),
}).description('文件设置'),
}).hidden(process.env.KOISHI_ENV === 'browser').description('文件设置'),
] as const)
}
94 changes: 24 additions & 70 deletions adapters/telegram/src/message.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,28 @@
import { createReadStream } from 'fs'
import { fileURLToPath } from 'url'
import { Dict, Logger, Messenger, SendOptions, segment } from '@satorijs/satori'
import { fromBuffer } from 'file-type'
import { Dict, Messenger, SendOptions, segment } from '@satorijs/satori'
import FormData from 'form-data'
import { TelegramBot } from './bot'
import * as Telegram from './utils'

type RenderMode = 'default' | 'figure'

const logger = new Logger('telegram')

type AssetType = 'photo' | 'audio' | 'document' | 'video' | 'animation'

async function maybeFile(payload: Dict, field: AssetType): Promise<[string?, Buffer?, string?]> {
if (!payload[field]) return []
let content: any
let filename = 'file'

const { protocol } = new URL(payload[field])

// Because the base64 string is not url encoded, so it will contain slash
// and can't parse with URL.pathname
let data = payload[field].split('://')[1]

if (protocol === 'file:') {
content = createReadStream(fileURLToPath(payload[field]))
delete payload[field]
} else if (protocol === 'base64:') {
content = Buffer.from(data, 'base64')
delete payload[field]
} else if (protocol === 'data:') {
data = payload[field].split('base64,')[1]
content = Buffer.from(data, 'base64')
delete payload[field]
}
// add file extension for base64 document (general file)
if (field === 'document' && (protocol === 'base64:' || protocol === 'data:')) {
const type = await fromBuffer(content)
if (!type) {
logger.warn('Can not infer file mime')
} else filename = `file.${type.ext}`
async function appendAsset(bot: TelegramBot, form: FormData, element: segment): Promise<AssetType> {
let assetType: AssetType
const { filename, data, mime } = await bot.ctx.http.file(element.attrs.url)
if (element.type === 'image') {
assetType = mime === 'image/gif' ? 'animation' : 'photo'
} else if (element.type === 'file') {
assetType = 'document'
} else {
assetType = element.type as any
}
return [field, content, filename]
}

async function isGif(url: string) {
if (url.toLowerCase().endsWith('.gif')) return true
const { protocol } = new URL(url)
let data: string
if (protocol === 'base64:') {
data = url.split('://')[1]
} else if (protocol === 'data:') {
data = url.split('base64,')[1]
}
if (data) {
const type = await fromBuffer(Buffer.from(data, 'base64'))
if (!type) {
logger.warn('Can not infer file mime')
} else if (type.ext === 'gif') return true
}
return false
// https://github.com/form-data/form-data/issues/468
const value = process.env.KOISHI_ENV === 'browser'
? new Blob([data], { type: mime })
: Buffer.from(data)
form.append(assetType, value, filename)
return assetType
}

const assetApi = {
Expand All @@ -73,7 +36,7 @@ const assetApi = {
const supportedElements = ['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'del', 'a']

export class TelegramMessenger extends Messenger<TelegramBot> {
private assetType: AssetType = null
private asset: segment = null
private payload: Dict
private mode: RenderMode = 'default'

Expand All @@ -92,22 +55,20 @@ export class TelegramMessenger extends Messenger<TelegramBot> {
}

async sendAsset() {
const [field, content, filename] = await maybeFile(this.payload, this.assetType)
const payload = new FormData()
const form = new FormData()
for (const key in this.payload) {
payload.append(key, this.payload[key].toString())
form.append(key, this.payload[key].toString())
}
if (field && content) payload.append(field, content, filename)
const result = await this.bot.internal[assetApi[this.assetType]](payload as any)
const type = await appendAsset(this.bot, form, this.asset)
const result = await this.bot.internal[assetApi[type]](form as any)
await this.addResult(result)
delete this.payload[this.assetType]
delete this.payload.reply_to_message
this.assetType = null
this.asset = null
this.payload.caption = ''
}

async flush() {
if (this.assetType) {
if (this.asset) {
// send previous asset if there is any
await this.sendAsset()
} else if (this.payload.caption) {
Expand Down Expand Up @@ -149,14 +110,7 @@ export class TelegramMessenger extends Messenger<TelegramBot> {
if (this.mode === 'default') {
await this.flush()
}
if (type === 'image') {
this.assetType = await isGif(attrs.url) ? 'animation' : 'photo'
} else if (type === 'file') {
this.assetType = 'document'
} else {
this.assetType = type as any
}
this.payload[this.assetType] = attrs.url
this.asset = element
} else if (type === 'figure') {
await this.flush()
this.mode = 'figure'
Expand Down
11 changes: 7 additions & 4 deletions packages/axios/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export class Quester {
return pick(this.config, ['timeout', 'headers'])
}

resolve(url: string) {
// do not use new URL(url, this.config.endpoint) here
return trimSlash(this.config.endpoint || '') + url
}

async file(url: string): Promise<Quester.File> {
const capture = /^data:([\w/-]+);base64,(.*)$/.exec(url)
if (capture) {
Expand All @@ -81,7 +86,7 @@ export class Quester {
const name = 'file' + (ext ? '.' + ext : '')
return { mime, filename: name, data: base64ToArrayBuffer(base64) }
}
const [_, name] = new URL(url).pathname.match(/.+\/([^/]*)/)
const [_, name] = this.resolve(url).match(/.+\/([^/]*)/)
const { headers, data } = await this.axios(url, { method: 'GET', responseType: 'arraybuffer' })
const mime = headers['content-type']
return { mime, filename: name, data }
Expand Down Expand Up @@ -109,14 +114,12 @@ export namespace Quester {
}

export function create(this: typeof Quester, config: Quester.Config = {}) {
const endpoint = trimSlash(config.endpoint || '')

const request = async (config: AxiosRequestConfig = {}) => {
const options = http.prepare()
return axios({
...options,
...config,
url: endpoint + config.url,
url: http.resolve(config.url),
headers: {
...options.headers,
...config.headers,
Expand Down

0 comments on commit f3f8998

Please sign in to comment.