Skip to content

Commit

Permalink
feat(satori): support multi uploads, proxy urls
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 21, 2024
1 parent 9d22205 commit 8a9c6d3
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 43 deletions.
2 changes: 1 addition & 1 deletion adapters/discord/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class DiscordBot<C extends Context = Context> extends Bot<C, DiscordBot.C
},
})
this.internal = new Internal(this)
this.resourceUrls.push('https://cdn.discordapp.com/')
this.proxyUrls.push('https://cdn.discordapp.com/')
ctx.plugin(WsClient, this)
}

Expand Down
2 changes: 1 addition & 1 deletion adapters/kook/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class KookBot<C extends Context = Context, T extends KookBot.Config = Koo
'Authorization': `Bot ${config.token}`,
},
}).extend(config)
this.resourceUrls.push('https://www.kookapp.cn/')
this.proxyUrls.push('https://www.kookapp.cn/')
this.internal = new Kook.Internal(this.http)

if (config.protocol === 'http') {
Expand Down
4 changes: 3 additions & 1 deletion adapters/satori/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ for (const [key, method] of Object.entries(Universal.Methods)) {
let payload: any
if (method.name === 'createUpload') {
payload = new FormData()
payload.append('file', new Blob([args[0]], { type: args[1] }), args[2])
for (const { data, type, filename } of args as Universal.Upload[]) {
payload.append('file', new Blob([data], { type }), filename)
}
} else {
payload = {}
for (const [index, field] of method.fields.entries()) {
Expand Down
2 changes: 1 addition & 1 deletion adapters/satori/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class SatoriAdapter<C extends Context = Context> extends Adapter.WsClient

this.ctx.satori.upload(() => {
return this.bots
.flatMap(bot => bot.resourceUrls)
.flatMap(bot => bot.proxyUrls)
.filter(url => url.startsWith('upload://'))
.map(url => url.replace('upload://', ''))
}, async (path) => {
Expand Down
34 changes: 24 additions & 10 deletions packages/core/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
public hidden = false
public platform: string
public features: string[]
public resourceUrls: string[]
public proxyUrls: string[]
public adapter?: Adapter<C, this>
public error?: Error
public callbacks: Dict<Function> = {}
Expand All @@ -51,7 +51,7 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
self.platform = platform
}

this.resourceUrls = [`upload://temp/${ctx.satori.uid}/`]
this.proxyUrls = [`upload://temp/${ctx.satori.uid}/`]
this.features = Object.entries(Methods)
.filter(([, value]) => this[value.name])
.map(([key]) => key)
Expand All @@ -73,7 +73,7 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
}

registerUpload(path: string, callback: (path: string) => Promise<Response>) {
this.ctx.satori.upload(path, callback, this.resourceUrls)
this.ctx.satori.upload(path, callback, this.proxyUrls)
}

update(login: Login) {
Expand Down Expand Up @@ -192,18 +192,32 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
return this.sendMessage(id, content, null, options)
}

async createUpload(data: ArrayBuffer, type: string | null, name?: string): Promise<Upload> {
const result = { status: 200, data, type, name }
const id = Math.random().toString(36).slice(2)
this.ctx.satori._tempStore[id] = result
async createUpload(...uploads: Upload[]): Promise<string[]> {
const ids: string[] = []
for (const upload of uploads) {
const id = Math.random().toString(36).slice(2)
const headers = new Headers()
headers.set('content-type', upload.type)
if (upload.filename) {
headers.set('content-disposition', `attachment; filename="${upload.filename}"`)
}
this.ctx.satori._tempStore[id] = {
status: 200,
data: upload.data,
headers,
}
ids.push(id)
}
const timer = setTimeout(() => dispose(), 600000)
const dispose = () => {
_dispose()
clearTimeout(timer)
delete this.ctx.satori._tempStore[id]
for (const id of ids) {
delete this.ctx.satori._tempStore[id]
}
}
const _dispose = this[Context.current].on('dispose', dispose)
return { url: `upload://temp/${this.ctx.satori.uid}/${id}` }
return ids.map(id => `upload://temp/${this.ctx.satori.uid}/${id}`)
}

async supports(name: string, session: Partial<C[typeof Context.session]> = {}) {
Expand All @@ -217,7 +231,7 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
}

toJSON(): Login {
return clone(pick(this, ['platform', 'selfId', 'status', 'user', 'hidden', 'features', 'resourceUrls']))
return clone(pick(this, ['platform', 'selfId', 'status', 'user', 'hidden', 'features', 'proxyUrls']))
}

async getLogin() {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,14 @@ export class Satori<C extends Context = Context> extends Service<unknown, C> {
return this.ctx.set('component:' + name, render)
}

upload(path: UploadRoute['path'], callback: UploadRoute['callback'], resourceUrls: UploadRoute['path'][] = []) {
upload(path: UploadRoute['path'], callback: UploadRoute['callback'], proxyUrls: UploadRoute['path'][] = []) {
return this[Context.current].effect(() => {
const route: UploadRoute = { path, callback }
this._uploadRoutes.push(route)
resourceUrls.push(path)
proxyUrls.push(path)
return () => {
remove(this._uploadRoutes, route)
remove(resourceUrls, path)
remove(proxyUrls, path)
}
})
}
Expand Down
16 changes: 9 additions & 7 deletions packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export interface SendOptions {
linkPreview?: boolean
}

export interface Upload {
type: string
filename?: string
data: ArrayBuffer
}

export interface Response {
status: number
statusText?: string
Expand Down Expand Up @@ -49,7 +55,7 @@ export const Methods: Dict<Method> = {
'reaction.clear': Method('clearReaction', ['channel_id', 'message_id', 'emoji']),
'reaction.list': Method('getReactionList', ['channel_id', 'message_id', 'emoji', 'next']),

'upload.create': Method('createUpload', ['data', 'type'], true),
'upload.create': Method('createUpload', [], true),

'guild.get': Method('getGuild', ['guild_id']),
'guild.list': Method('getGuildList', ['next']),
Expand Down Expand Up @@ -111,7 +117,7 @@ export interface Methods {
getReactionIter(channelId: string, messageId: string, emoji: string): AsyncIterable<User>

// upload
createUpload(data: ArrayBuffer, type: string | null, name?: string): Promise<Upload>
createUpload(...uploads: Upload[]): Promise<string[]>

// user
getLogin(): Promise<Login>
Expand Down Expand Up @@ -225,7 +231,7 @@ export interface Login {
hidden?: boolean
status: Status
features?: string[]
resourceUrls?: string[]
proxyUrls?: string[]
}

export const enum Status {
Expand All @@ -236,10 +242,6 @@ export const enum Status {
RECONNECT = 4,
}

export interface Upload {
url: string
}

export interface Message {
id?: string
/** @deprecated */
Expand Down
55 changes: 36 additions & 19 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { camelCase, Context, sanitize, Schema, Session, snakeCase, Time, Universal } from '@satorijs/core'
import { Binary, camelCase, Context, makeArray, sanitize, Schema, Session, snakeCase, Time, Universal } from '@satorijs/core'
import {} from '@cordisjs/plugin-server'
import WebSocket from 'ws'
import { Readable } from 'node:stream'
Expand Down Expand Up @@ -97,15 +97,20 @@ export function apply(ctx: Context, config: Config) {
}

if (method.name === 'createUpload') {
const [file] = Object.values(koa.request.files ?? {}).flat()
if (!file) {
koa.body = 'file not provided'
return koa.status = 400
}
const data = await readFile(file.filepath)
const result = await bot.createUpload(data, file.mimetype, file.newFilename)
koa.body = result
return koa.status = 201
const entries = Object.entries(koa.request.files ?? {}).map(([key, value]) => {
return [key, makeArray(value)[0]] as const
})
const uploads = await Promise.all(entries.map<Promise<Universal.Upload>>(async ([, file]) => {
const buffer = await readFile(file.filepath)
return {
data: Binary.fromSource(buffer),
type: file.mimetype,
filename: file.newFilename,
}
}))
const result = await bot.createUpload(...uploads)
koa.body = Object.fromEntries(entries.map(([key], index) => [key, result[index]]))
return koa.status = 200
}

const json = koa.request.body
Expand Down Expand Up @@ -136,16 +141,28 @@ export function apply(ctx: Context, config: Config) {
koa.status = 200
})

ctx.server.get(path + '/v1/upload/:name(.+)', async (koa) => {
const { status, statusText, data, headers } = await ctx.satori.download(koa.params.name)
koa.status = status
for (const [key, value] of headers || new Headers()) {
koa.set(key, value)
}
if (status >= 200 && status < 300) {
koa.body = data instanceof ReadableStream ? Readable.fromWeb(data) : data
ctx.server.get(path + '/v1/proxy/:url(.+)', async (koa) => {
const url = koa.params.url
koa.header['Access-Control-Allow-Origin'] = ctx.server.config.selfUrl || '*'
if (url.startsWith('upload://')) {
const { status, statusText, data, headers } = await ctx.satori.download(url.slice(9))
koa.status = status
for (const [key, value] of headers || new Headers()) {
koa.set(key, value)
}
if (status >= 200 && status < 300) {
koa.body = data instanceof ReadableStream ? Readable.fromWeb(data) : data
} else {
koa.body = statusText
}
} else {
koa.body = statusText
try {
koa.body = Readable.fromWeb(await ctx.http.get(koa.params.url, { responseType: 'stream' }))
} catch (error) {
if (!ctx.http.isError(error) || !error.response) throw error
koa.status = error.response.status
koa.body = error.response.data
}
}
})

Expand Down

0 comments on commit 8a9c6d3

Please sign in to comment.