Skip to content

Commit

Permalink
feat(satori): basic support for upload API
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 20, 2024
1 parent 31cb578 commit 4ae699b
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 7 deletions.
26 changes: 23 additions & 3 deletions packages/core/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import h from '@satorijs/element'
import { Adapter } from './adapter'
import { MessageEncoder } from './message'
import { defineAccessor, Session } from './session'
import { Event, List, Login, Methods, SendOptions, Status, User } from '@satorijs/protocol'
import { Universal } from '.'
import { Event, List, Login, Methods, SendOptions, Status, Upload, User } from '@satorijs/protocol'
import { Universal, UploadResult } from '.'

const eventAliases = [
['message-created', 'message'],
Expand All @@ -29,11 +29,12 @@ 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 resourceUrls: string[]
public adapter?: Adapter<C, this>
public error?: Error
public callbacks: Dict<Function> = {}
public logger: Logger
public [Context.current]: C

// Same as `this.ctx`, but with a more specific type.
protected context: Context
Expand All @@ -51,6 +52,7 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
self.platform = platform
}

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

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

update(login: Login) {
// make sure `status` is the last property to be assigned
// so that `login-updated` event can be dispatched after all properties are updated
Expand Down Expand Up @@ -187,6 +193,20 @@ export abstract class Bot<C extends Context = Context, T = any> implements Login
return this.sendMessage(id, content, null, options)
}

async createUpload(data: UploadResult['data'], type: string, 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
const timer = setTimeout(() => dispose(), 600000)
const dispose = () => {
_dispose()
clearTimeout(timer)
delete this.ctx.satori._tempStore[id]
}
const _dispose = this[Context.current].on('dispose', dispose)
return { url: `upload://temp/${this.ctx.satori.uid}/${id}` }
}

async supports(name: string, session: Partial<C[typeof Context.session]> = {}) {
return !!this[Methods[name]?.name]
}
Expand Down
47 changes: 46 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Context, Logger, Service, z } from 'cordis'
import { Awaitable, defineProperty, Dict } from 'cosmokit'
import { Awaitable, defineProperty, Dict, makeArray, remove } from 'cosmokit'
import { ReadableStream } from 'node:stream/web'
import { Bot } from './bot'
import { Session } from './session'
import { HTTP } from '@cordisjs/plugin-http'
Expand Down Expand Up @@ -124,13 +125,35 @@ class SatoriContext extends Context {

export { SatoriContext as Context }

export interface UploadResult {
status: number
data?: ArrayBuffer | ReadableStream
type?: string
name?: string
url?: string
}

export interface UploadRoute {
path: string | string[] | (() => string | string[])
callback: (path: string) => Promise<UploadResult>
}

export class Satori<C extends Context = Context> extends Service<unknown, C> {
static [Service.provide] = 'satori'
static [Service.immediate] = true

public uid = Math.random().toString(36).slice(2)

_uploadRoutes: UploadRoute[] = []
_tempStore: Dict<UploadResult> = Object.create(null)

constructor(ctx?: C) {
super(ctx)
ctx.mixin('satori', ['bots', 'component'])
this.upload(`/temp/${this.uid}/`, async (path) => {
const id = path.split('/').pop()
return this._tempStore[id] ?? { status: 404 }
})
}

public bots = new Proxy([], {
Expand Down Expand Up @@ -161,6 +184,28 @@ 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'][] = []) {
return this[Context.current].effect(() => {
const route: UploadRoute = { path, callback }
this._uploadRoutes.push(route)
resourceUrls.push(path)
return () => {
remove(this._uploadRoutes, route)
remove(resourceUrls, path)
}
})
}

async download(path: string) {
for (const route of this._uploadRoutes) {
const paths = makeArray(typeof route.path === 'function' ? route.path() : route.path)
if (paths.some(prefix => path.startsWith(prefix))) {
return route.callback(path)
}
}
return { status: 404 }
}
}

export default Satori
14 changes: 12 additions & 2 deletions packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ function Field(name: string): Field {
export interface Method {
name: string
fields: Field[]
isForm: boolean
}

function Method(name: string, fields: string[]): Method {
return { name, fields: fields.map(Field) }
function Method(name: string, fields: string[], isForm = false): Method {
return { name, fields: fields.map(Field), isForm }
}

export const Methods: Dict<Method> = {
Expand All @@ -41,6 +42,8 @@ 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', 'name'], true),

'guild.get': Method('getGuild', ['guild_id']),
'guild.list': Method('getGuildList', ['next']),

Expand Down Expand Up @@ -100,6 +103,9 @@ export interface Methods {
getReactionList(channelId: string, messageId: string, emoji: string, next?: string): Promise<List<User>>
getReactionIter(channelId: string, messageId: string, emoji: string): AsyncIterable<User>

// upload
createUpload(data: ArrayBuffer, type: string, name?: string): Promise<Upload>

// user
getLogin(): Promise<Login>
getUser(userId: string, guildId?: string): Promise<User>
Expand Down Expand Up @@ -223,6 +229,10 @@ export const enum Status {
RECONNECT = 4,
}

export interface Upload {
url: string
}

export interface Message {
id?: string
/** @deprecated */
Expand Down
15 changes: 14 additions & 1 deletion packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { camelCase, Context, sanitize, Schema, Session, snakeCase, Time, Universal } from '@satorijs/core'
import {} from '@cordisjs/plugin-server'
import WebSocket from 'ws'
import { Readable } from 'stream'
import { ReadableStream } from 'stream/web'

export const name = 'server'
export const inject = ['server', 'http']
Expand Down Expand Up @@ -85,7 +87,6 @@ export function apply(ctx: Context, config: Config) {
}
}

const json = koa.request.body
const selfId = koa.request.headers['x-self-id']
const platform = koa.request.headers['x-platform']
const bot = ctx.bots.find(bot => bot.selfId === selfId && bot.platform === platform)
Expand All @@ -94,6 +95,7 @@ export function apply(ctx: Context, config: Config) {
return koa.status = 403
}

const json = koa.request.body
const args = method.fields.map(({ name }) => {
return transformKey(json[name], camelCase)
})
Expand Down Expand Up @@ -121,6 +123,17 @@ export function apply(ctx: Context, config: Config) {
koa.status = 200
})

ctx.server.get(path + '/v1/upload/:name(.+)', async (koa) => {
const result = await ctx.satori.download(koa.params.name)
koa.status = result.status
if (result.status >= 300 && result.status < 400) {
koa.set('Location', result.url!)
} else if (result.status >= 200 && result.status < 300) {
koa.body = result.data instanceof ReadableStream ? Readable.fromWeb(result.data) : result.data
koa.type = result.type!
}
})

const buffer: Session[] = []

const timeout = setInterval(() => {
Expand Down

0 comments on commit 4ae699b

Please sign in to comment.