flowtyped everything, some functional, safety, and structural changes

This commit is contained in:
41666 2019-03-10 03:18:11 -05:00
parent 6f3eca7a64
commit d2aecb38ca
92 changed files with 17554 additions and 1440 deletions

60
rpc/_autoloader.js Normal file
View 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
View 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
View 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
View 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
View 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 }))
}
})