mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-06-16 10:19:10 +00:00
flowtyped everything, some functional, safety, and structural changes
This commit is contained in:
parent
6f3eca7a64
commit
d2aecb38ca
92 changed files with 17554 additions and 1440 deletions
60
rpc/_autoloader.js
Normal file
60
rpc/_autoloader.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import logger from '../logger'
|
||||
import glob from 'glob'
|
||||
import path from 'path'
|
||||
import { type AppContext } from '../Roleypoly'
|
||||
|
||||
const log = logger(__filename)
|
||||
const PROD: boolean = process.env.NODE_ENV === 'production'
|
||||
|
||||
export default (ctx: AppContext, forceClear: ?boolean = false): {
|
||||
[rpc: string]: Function
|
||||
} => {
|
||||
let map = {}
|
||||
const apis = glob.sync(`./rpc/**/!(index).js`)
|
||||
log.debug('found rpcs', apis)
|
||||
|
||||
for (let a of apis) {
|
||||
const filename = path.basename(a)
|
||||
const dirname = path.dirname(a)
|
||||
|
||||
// internal stuff
|
||||
if (filename.startsWith('_')) {
|
||||
log.debug(`skipping ${a}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (dirname === 'client') {
|
||||
log.debug(`skipping ${a}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// testing only
|
||||
if (filename.endsWith('_test.js') && PROD) {
|
||||
log.debug(`skipping ${a}`)
|
||||
continue
|
||||
}
|
||||
|
||||
log.debug(`mounting ${a}`)
|
||||
try {
|
||||
const pathname = a.replace('rpc/', '')
|
||||
|
||||
delete require.cache[require.resolve(pathname)]
|
||||
|
||||
const r = require(pathname)
|
||||
let o = r
|
||||
if (o.default) {
|
||||
o = r.default
|
||||
}
|
||||
|
||||
map = {
|
||||
...map,
|
||||
...o(ctx)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`couldn't mount ${a}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
15
rpc/_error.js
Normal file
15
rpc/_error.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
class RPCError extends Error {
|
||||
constructor (msg, code, ...extra) {
|
||||
super(msg)
|
||||
this.code = code
|
||||
this.extra = extra
|
||||
}
|
||||
|
||||
static fromResponse (body, status) {
|
||||
const e = new RPCError(body.msg, status)
|
||||
e.remoteStack = body.trace
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RPCError
|
111
rpc/index.js
Normal file
111
rpc/index.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
// @flow
|
||||
import logger from '../logger'
|
||||
import fnv from 'fnv-plus'
|
||||
import autoloader from './_autoloader'
|
||||
import RPCError from './_error'
|
||||
import type Roleypoly from '../Roleypoly'
|
||||
import type betterRouter from 'koa-better-router'
|
||||
import { type Context } from 'koa'
|
||||
const log = logger(__filename)
|
||||
|
||||
export default class RPCServer {
|
||||
ctx: Roleypoly
|
||||
|
||||
rpcMap: {
|
||||
[rpc: string]: Function
|
||||
}
|
||||
|
||||
mapHash: string
|
||||
rpcCalls: { name: string, args: number }[]
|
||||
|
||||
constructor (ctx: Roleypoly) {
|
||||
this.ctx = ctx
|
||||
this.reload()
|
||||
}
|
||||
|
||||
reload () {
|
||||
// actual function map
|
||||
this.rpcMap = autoloader(this.ctx.ctx)
|
||||
|
||||
// hash of the map
|
||||
// used for known-reloads in the client.
|
||||
this.mapHash = fnv.hash(Object.keys(this.rpcMap)).str()
|
||||
|
||||
// call map for the client.
|
||||
this.rpcCalls = Object.keys(this.rpcMap).map(fn => ({ name: this.rpcMap[fn].name, args: this.rpcMap[fn].length - 1 }))
|
||||
}
|
||||
|
||||
hookRoutes (router: betterRouter) {
|
||||
// RPC call reporter.
|
||||
// this is NEVER called in prod.
|
||||
// it is used to generate errors if RPC calls don't exist or are malformed in dev.
|
||||
router.get('/api/_rpc', async (ctx) => {
|
||||
ctx.body = {
|
||||
hash: this.mapHash,
|
||||
available: this.rpcCalls
|
||||
}
|
||||
ctx.status = 200
|
||||
return true
|
||||
})
|
||||
|
||||
router.post('/api/_rpc', this.handleRPC.bind(this))
|
||||
}
|
||||
|
||||
async handleRPC (ctx: Context) {
|
||||
// handle an impossible situation
|
||||
if (!(ctx.request.body instanceof Object)) {
|
||||
return this.rpcError(ctx, null, new RPCError('RPC format was very incorrect.', 400))
|
||||
}
|
||||
|
||||
// check if RPC exists
|
||||
const { fn, args } = ctx.request.body
|
||||
|
||||
if (!(fn in this.rpcMap)) {
|
||||
return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}(...) not found.`, 404))
|
||||
}
|
||||
|
||||
const call = this.rpcMap[fn]
|
||||
|
||||
// if call.length (which is the solid args list)
|
||||
// is longer than args, we have too little to call the function.
|
||||
if (args.length < call.length) {
|
||||
return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}() with ${args.length} arguments does not exist.`, 400))
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await call(ctx, ...args)
|
||||
|
||||
ctx.body = {
|
||||
hash: this.mapHash,
|
||||
response
|
||||
}
|
||||
|
||||
ctx.status = 200
|
||||
} catch (err) {
|
||||
return this.rpcError(ctx, 'RPC call errored', err, 500)
|
||||
}
|
||||
}
|
||||
|
||||
rpcError (ctx: Context & {body: any}, msg: ?string, err: ?Error = null, code: ?number = null) {
|
||||
log.error('rpc error', { msg, err })
|
||||
|
||||
ctx.body = {
|
||||
msg,
|
||||
error: true
|
||||
}
|
||||
|
||||
if (err instanceof RPCError) {
|
||||
// this is one of our own errors, so we have a lot of data on this.
|
||||
ctx.status = err.code || code || 500
|
||||
ctx.body.msg = `${msg || 'RPC Error'}: ${err.message}`
|
||||
} else {
|
||||
if (msg == null && err != null) {
|
||||
ctx.body.msg = msg = err.message
|
||||
}
|
||||
|
||||
// if not, just play cloudflare, say something was really weird, and move on.
|
||||
ctx.status = code || 520
|
||||
ctx.message = 'Unknown Error'
|
||||
}
|
||||
}
|
||||
}
|
14
rpc/rpc_test.js
Normal file
14
rpc/rpc_test.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
// @flow
|
||||
import { type AppContext } from '../Roleypoly'
|
||||
import { type Context } from 'koa'
|
||||
export default ($: AppContext) => ({
|
||||
// i want RPC to be *dead* simple.
|
||||
// probably too simple.
|
||||
hello (_: Context, hello: string) {
|
||||
return `hello, ${hello}!`
|
||||
},
|
||||
|
||||
testJSON (_: Context, inobj: {}) {
|
||||
return inobj
|
||||
}
|
||||
})
|
15
rpc/servers.js
Normal file
15
rpc/servers.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import { type AppContext } from '../Roleypoly'
|
||||
import { type Context } from 'koa'
|
||||
import { type Guild } from 'discord.js'
|
||||
|
||||
export default ($: AppContext) => ({
|
||||
listRelevantServers (ctx: Context) {
|
||||
return $.discord.client.guilds.map<{
|
||||
url: string,
|
||||
name: string,
|
||||
members: number,
|
||||
roles: number
|
||||
}>((g: Guild) => ({ url: `${$.config.appUrl}/s/${g.id}`, name: g.name, members: g.members.array().length, roles: g.roles.array().length }))
|
||||
}
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue