diff --git a/.gitignore b/.gitignore index 1b92cbe..194c770 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ node_modules yarn-error\.log *.log + +\.next/ diff --git a/packages/roleypoly-server/Roleypoly.js b/packages/roleypoly-server/Roleypoly.js index b3bac15..274aa49 100644 --- a/packages/roleypoly-server/Roleypoly.js +++ b/packages/roleypoly-server/Roleypoly.js @@ -105,6 +105,8 @@ class Roleypoly { throw log.fatal('DB_URL not set.') } + await this.ctx.ui.prepare() + const sequelize = new Sequelize(dbUrl, { logging: log.sql.bind(log, log) }) this.ctx.sql = sequelize this.M = fetchModels(sequelize) @@ -132,8 +134,6 @@ class Roleypoly { } async loadRoutes (forceClear: boolean = false) { - await this.ctx.ui.prepare() - this.router = betterRouter().loadMethods() fetchApis(this.router, this.ctx, { forceClear }) // this.ctx.RPC.hookRoutes(this.router) @@ -156,15 +156,16 @@ class Roleypoly { // hot-reloading system log.info('API hot-reloading is active.') const chokidar = require('chokidar') + const path = require('path') let hotMiddleware = mw - this.__apiWatcher = chokidar.watch('api/**') + this.__apiWatcher = chokidar.watch(path.join(__dirname, 'api/**')) this.__apiWatcher.on('all', async (path) => { log.info('reloading APIs...', path) hotMiddleware = await this.loadRoutes(true) }) - this.__rpcWatcher = chokidar.watch('rpc/**') + this.__rpcWatcher = chokidar.watch(path.join(__dirname, 'rpc/**')) this.__rpcWatcher.on('all', (path) => { log.info('reloading RPCs...', path) this.ctx.RPC.reload() diff --git a/packages/roleypoly-server/api/index.js b/packages/roleypoly-server/api/index.js index baccb6e..6bae630 100644 --- a/packages/roleypoly-server/api/index.js +++ b/packages/roleypoly-server/api/index.js @@ -1,15 +1,14 @@ // @flow -import logger from '../logger' import glob from 'glob' - import type { Router, AppContext } from '../Roleypoly' - +import path from 'path' +import logger from '../logger' const log = logger(__filename) const PROD = process.env.NODE_ENV === 'production' export default async (router: Router, ctx: AppContext, { forceClear = false }: { forceClear: boolean } = {}) => { - const apis = glob.sync(`./api/**/!(index).js`) + const apis = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.')) log.debug('found apis', apis) for (let a of apis) { @@ -19,12 +18,11 @@ export default async (router: Router, ctx: AppContext, { forceClear = false }: { } log.debug(`mounting ${a}`) try { - const pathname = a.replace('api/', '') if (forceClear) { - delete require.cache[require.resolve(pathname)] + delete require.cache[require.resolve(a)] } // $FlowFixMe this isn't an important error. potentially dangerous, but irrelevant. - require(pathname).default(router, ctx) + require(a).default(router, ctx) } catch (e) { log.error(`couldn't mount ${a}`, e) } diff --git a/packages/roleypoly-server/models/index.js b/packages/roleypoly-server/models/index.js index 94c08c6..dc0d9f8 100644 --- a/packages/roleypoly-server/models/index.js +++ b/packages/roleypoly-server/models/index.js @@ -15,18 +15,18 @@ export type Models = { export default (sql: Sequelize): Models => { const models: Models = {} - const modelFiles = glob.sync('./models/**/!(index).js') + const modelFiles = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.')) log.debug('found models', modelFiles) modelFiles.forEach((v) => { let name = path.basename(v).replace('.js', '') - if (v === './models/index.js') { + if (v === `./index.js`) { log.debug('index.js hit, skipped') return } try { - log.debug('importing..', v.replace('models/', '')) - let model = sql.import(v.replace('models/', '')) + log.debug('importing..', v) + let model = sql.import(v) models[name] = model } catch (err) { log.fatal('error importing model ' + v, err) diff --git a/packages/roleypoly-server/rpc/_autoloader.js b/packages/roleypoly-server/rpc/_autoloader.js index bf41f1d..717cc34 100644 --- a/packages/roleypoly-server/rpc/_autoloader.js +++ b/packages/roleypoly-server/rpc/_autoloader.js @@ -11,14 +11,14 @@ export default (ctx: AppContext, forceClear: ?boolean = false): { [rpc: string]: Function } => { let map = {} - const apis = glob.sync(`./rpc/**/!(index).js`) + const apis = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.')) log.debug('found rpcs', apis) for (let a of apis) { const filename = path.basename(a) const dirname = path.dirname(a) - const pathname = a.replace('rpc/', '') + const pathname = a delete require.cache[require.resolve(pathname)] // internal stuff diff --git a/packages/roleypoly-server/rpc/_security.js b/packages/roleypoly-server/rpc/_security.js index ce92529..7cdc365 100644 --- a/packages/roleypoly-server/rpc/_security.js +++ b/packages/roleypoly-server/rpc/_security.js @@ -95,12 +95,12 @@ export const member = ( $: AppContext, fn: (ctx: Context, server: string, ...args: any[]) => any, silent: boolean = false -) => authed($, ( +) => authed($, async ( ctx: Context, server: string, ...args: any[] ) => { - if ($.discord.isMember(server, ctx.session.userId)) { + if (await $.discord.isMember(server, ctx.session.userId)) { return fn(ctx, server, ...args) } diff --git a/packages/roleypoly-server/rpc/servers.js b/packages/roleypoly-server/rpc/servers.js index 2238dcf..2187e2c 100644 --- a/packages/roleypoly-server/rpc/servers.js +++ b/packages/roleypoly-server/rpc/servers.js @@ -3,6 +3,7 @@ import { type AppContext } from '../Roleypoly' import { type Context } from 'koa' import { type Guild } from 'eris' import * as secureAs from './_security' +import RPCError from './_error' export default ($: AppContext) => ({ @@ -24,23 +25,18 @@ export default ($: AppContext) => ({ return $.P.serverSlug(srv) }, - getServer: secureAs.member($, (ctx: Context, id: string) => { + getServer: secureAs.member($, async (ctx: Context, id: string) => { const { userId } = (ctx.session: { userId: string }) - const srv = $.discord.client.guilds.get(id) + const srv = await $.discord.fetcher.getGuild(id) if (srv == null) { - return { err: 'not_found' } + throw new RPCError('server not found', 404) } - let gm - if (srv.members.has(userId)) { - gm = $.discord.gm(id, userId) - } else if ($.discord.isRoot(userId)) { - gm = $.discord.fakeGm({ id: userId }) - } + let gm = await $.discord.gm(id, userId, { canFake: true }) if (gm == null) { - return { err: 'not_found' } + throw new RPCError('server not found', 404) } return $.P.presentableServer(srv, gm) diff --git a/packages/roleypoly-server/services/discord/restFetcher.js b/packages/roleypoly-server/services/discord/restFetcher.js index 26d74b5..ffc3c7c 100644 --- a/packages/roleypoly-server/services/discord/restFetcher.js +++ b/packages/roleypoly-server/services/discord/restFetcher.js @@ -2,33 +2,71 @@ import type { IFetcher } from './types' import type DiscordSvc from '../discord' import type ErisClient, { User, Member, Guild } from 'eris' +import LRU from 'lru-cache' +import logger from '../../logger' +const log = logger(__filename) export default class BotFetcher implements IFetcher { ctx: DiscordSvc client: ErisClient + cache: LRU + constructor (ctx: DiscordSvc) { this.ctx = ctx this.client = ctx.client + this.cache = new LRU({ + max: 50, + maxAge: 1000 * 60 * 10 + }) } getUser = async (id: string): Promise => { + if (this.cache.has(`U:${id}`)) { + log.debug('user cache hit') + return this.cache.get(`U:${id}`) + } + + log.debug('user cache miss') + try { - return await this.client.getRESTUser(id) + const u = await this.client.getRESTUser(id) + this.cache.set(`U:${id}`, u) + return u } catch (e) { return null } } getMember = async (server: string, user: string): Promise => { + if (this.cache.has(`M:${server}:${user}`)) { + log.debug('member cache hit') + return this.cache.get(`M:${server}:${user}`) + } + + log.debug('member cache miss') + try { - return await this.client.getRESTGuildMember(server, user) + const m = await this.client.getRESTGuildMember(server, user) + this.cache.set(`M:${server}:${user}`, m) + // $FlowFixMe + m.guild = await this.getGuild(server) // we have to prefill this for whatever reason + return m } catch (e) { return null } } getGuild = async (server: string): Promise => { + if (this.cache.has(`G:${server}`)) { + log.debug('guild cache hit') + return this.cache.get(`G:${server}`) + } + + log.debug('guild cache miss') + try { - return await this.client.getRESTGuild(server) + const g = await this.client.getRESTGuild(server) + this.cache.set(`G:${server}`, g) + return g } catch (e) { return null } diff --git a/packages/roleypoly-server/services/presentation.js b/packages/roleypoly-server/services/presentation.js index 764e24e..e26fd26 100644 --- a/packages/roleypoly-server/services/presentation.js +++ b/packages/roleypoly-server/services/presentation.js @@ -4,41 +4,18 @@ import LRU from 'lru-cache' import { type AppContext } from '../Roleypoly' import { type Models } from '../models' import { type ServerModel } from '../models/Server' -import type DiscordService, { Permissions } from './discord' +import type DiscordService from './discord' import { type Guild, - type Member, type Collection } from 'eris' +import type { + Member, + PresentableServer, + ServerSlug, + PresentableRole +} from '@roleypoly/types' import areduce from '../util/areduce' - -export type ServerSlug = { - id: string, - name: string, - ownerID: string, - icon: string -} - -export type PresentableRole = { - id: string, - color: number, - name: string, - position: number, - safe: boolean -} - -export type PresentableServer = ServerModel & { - id: string, - gm?: { - color: number | string, - nickname: string, - roles: string[] - }, - server: ServerSlug, - roles: ?PresentableRole[], - perms: Permissions -} - class PresentationService extends Service { cache: LRU M: Models @@ -61,8 +38,8 @@ class PresentationService extends Service { } } - presentableServers (collection: Collection, userId: string) { - return areduce(collection.array(), async (acc, server) => { + presentableServers (collection: Collection, userId: string) { + return areduce(Array.from(collection.values()), async (acc, server) => { const gm = server.members.get(userId) if (gm == null) { throw new Error(`somehow this guildmember ${userId} of ${server.id} didn't exist.`) @@ -79,12 +56,12 @@ class PresentationService extends Service { return { id: server.id, gm: { - nickname: gm.nickname || gm.user.username, - color: gm.displayHexColor, - roles: gm.roles.keyArray() + nickname: gm.nick || gm.user.username, + color: gm?.color, + roles: gm.roles }, server: this.serverSlug(server), - roles: (incRoles) ? (await this.rolesByServer(server, sd)).map(r => ({ ...r, selected: gm.roles.has(r.id) })) : [], + roles: (incRoles) ? (await this.rolesByServer(server, sd)).map(r => ({ ...r, selected: gm.roles.includes(r.id) })) : [], message: sd.message, categories: sd.categories, perms: this.discord.getPermissions(gm) diff --git a/packages/roleypoly-server/util/rpcrepl.js b/packages/roleypoly-server/util/rpcrepl.js index dfbee8c..89c016a 100644 --- a/packages/roleypoly-server/util/rpcrepl.js +++ b/packages/roleypoly-server/util/rpcrepl.js @@ -5,7 +5,7 @@ import { addAwaitOutsideToReplServer } from 'await-outside' import Roleypoly from '../Roleypoly' import chokidar from 'chokidar' import logger from '../logger' -process.env.DEBUG = false +// process.env.DEBUG = false process.env.IS_BOT = false dotenv.config() diff --git a/packages/roleypoly-types/server.js.flow b/packages/roleypoly-types/server.js.flow index 0eb6207..6e56322 100644 --- a/packages/roleypoly-types/server.js.flow +++ b/packages/roleypoly-types/server.js.flow @@ -20,7 +20,7 @@ export type ServerModel = { export type PresentableServer = ServerModel & { id: string, gm?: { - color: number | string, + color?: number | string, nickname: string, roles: string[] }, diff --git a/packages/roleypoly-ui/connector.js b/packages/roleypoly-ui/connector.js index 6ac0b90..f0a6335 100644 --- a/packages/roleypoly-ui/connector.js +++ b/packages/roleypoly-ui/connector.js @@ -1,7 +1,7 @@ const next = require('next') const connector = ({ dev }) => { - return next({ dev }) + return next({ dev, dir: __dirname }) } module.exports = connector