temp commit

This commit is contained in:
41666 2019-04-20 04:46:46 -05:00
parent e86f7ff68e
commit 6fb39d6c4d
No known key found for this signature in database
GPG key ID: BC51D07640DC10AF
22 changed files with 282 additions and 39 deletions

View file

@ -11,3 +11,4 @@
esproposal.optional_chaining=enable
munge_underscores=true
emoji=true
esproposal.export_star_as=enable

View file

@ -1,6 +1,5 @@
// @flow
import Eris, { type Message, type TextChannel } from 'eris'
import logger from './logger.js'
import Eris, { type Message, type TextChannel, type Guild } from 'eris'
import RPCClient from '@roleypoly/rpc-client'
import randomEmoji from './random-emoji'
import DMCommands from './commands/dm'
@ -8,6 +7,8 @@ import TextCommands from './commands/text'
import RootCommands from './commands/root'
import type { Command } from './commands/_types'
import retry from 'async-retry'
import logger from './logger'
const log = logger(__filename)
export type BotConfig = $Shape<{
@ -228,8 +229,18 @@ export default class Bot {
}
}
async syncGuild (type: string, guild: Guild) {
await this.rpc.syncGuild(type, guild.id)
}
async start () {
this.client.on('messageCreate', this.handleMessage)
const guildSyncTriggers = ['guildCreate', 'guildDelete', 'guildRoleAdd', 'guildRoleDelete', 'guildRoleUpdate']
for (const trigger of guildSyncTriggers) {
this.client.on(trigger, this.syncGuild.bind(this, trigger))
}
await this.client.connect()
log.info('started bot')
}

View file

@ -7,7 +7,6 @@
"dev": "yarn build --watch"
},
"dependencies": {
"@roleypoly/rpc-client": "2.0.0",
"async-retry": "^1.2.3",
"dotenv": "^7.0.0",
"eris": "^0.9.0"

View file

@ -7,7 +7,7 @@
"dev": "yarn build --watch",
"postinstall": "test -d lib || npm run build"
},
"types": "src/",
"types": "src/index.js",
"main": "lib/index.js",
"files": ["lib"],
"dependencies": {

View file

@ -1,8 +1,10 @@
// @flow
import superagent from 'superagent'
import RPCError from './error'
import RPCError from './error.js'
import retry from 'async-retry'
export { RPCError }
export type RPCResponse = {
response?: mixed,
hash?: string,

View file

@ -0,0 +1,11 @@
{
"presets": [ ["@babel/preset-env", { "targets": {"node": "current"} }], "@babel/preset-flow" ],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-export-namespace-from",
["@babel/plugin-transform-runtime",
{ "helpers": false }]
]
}

View file

@ -0,0 +1,24 @@
{
"private": true,
"name": "@roleypoly/rpc-server",
"version": "2.0.0",
"scripts": {
"build": "babel -d ./lib ./src",
"dev": "yarn build --watch",
"postinstall": "test -d lib || npm run build"
},
"types": "src/index.js",
"main": "lib/index.js",
"files": [
"lib"
],
"dependencies": {
"async-retry": "^1.2.3",
"glob": "^7.1.3",
"nats": "^1.2.10"
},
"devDependencies": {
"@babel/cli": "^7.4.3",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0"
}
}

View file

@ -1,22 +1,21 @@
// @flow
import logger from '../logger'
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): {
export default (globString: string, ctx: any, forceClear: ?boolean = false): {
[rpc: string]: Function
} => {
let map = {}
const apis = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.'))
const apis = glob.sync(globString)
log.debug('found rpcs', apis)
for (let a of apis) {
const filename = path.basename(a)
const dirname = path.dirname(a)
// const dirname = path.dirname(a)
const pathname = a
delete require.cache[require.resolve(pathname)]
@ -27,11 +26,6 @@ export default (ctx: AppContext, forceClear: ?boolean = false): {
continue
}
if (dirname === 'client') {
log.debug(`skipping ${a}`)
continue
}
// testing only
if (filename.endsWith('_test.js') && PROD) {
log.debug(`skipping ${a}`)

View file

@ -1,9 +1,10 @@
// @flow
import fnv from 'fnv-plus'
import autoloader from './_autoloader'
import RPCError from '@roleypoly/rpc-client/error'
import type Roleypoly, { Router } from '../Roleypoly'
import autoloader from './autoloader'
import { RPCError } from '@roleypoly/rpc-client'
import type Roleypoly, { Router } from '@roleypoly/server/Roleypoly'
import type { Context } from 'koa'
export * as secureAs from './security'
// import logger from '../logger'
// const log = logger(__filename)
@ -68,7 +69,7 @@ export default class RPCServer {
}
// check if RPC exists
const { fn, args } = (ctx.request.body: RPCIncoming)
const { fn, args } = ((ctx.request.body: any): RPCIncoming)
if (!(fn in this.rpcMap)) {
return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}(...) not found.`, 404))

View file

@ -0,0 +1,70 @@
// @flow
import chalk from 'chalk'
export class Logger {
debugOn: boolean
name: string
quietSql: boolean
constructor (name: string, debugOverride: boolean = false) {
this.name = name
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
this.quietSql = (process.env.DEBUG_SQL !== 'true')
}
fatal (text: string, ...data: any) {
this.error(text, data)
if (typeof data[data.length - 1] === 'number') {
process.exit(data[data.length - 1])
} else {
process.exit(1)
}
}
error (text: string, ...data: any) {
console.error(chalk.red.bold(`ERR ${this.name}:`) + `\n ${text}`, data)
}
warn (text: string, ...data: any) {
console.warn(chalk.yellow.bold(`WARN ${this.name}:`) + `\n ${text}`, data)
}
notice (text: string, ...data: any) {
console.log(chalk.cyan.bold(`NOTICE ${this.name}:`) + `\n ${text}`, data)
}
info (text: string, ...data: any) {
console.info(chalk.blue.bold(`INFO ${this.name}:`) + `\n ${text}`, data)
}
deprecated (text: string, ...data: any) {
console.warn(chalk.yellowBright(`DEPRECATED ${this.name}:`) + `\n ${text}`, data)
console.trace()
}
request (text: string, ...data: any) {
console.info(chalk.green.bold(`HTTP ${this.name}:`) + `\n ${text}`)
}
debug (text: string, ...data: any) {
if (this.debugOn) {
console.log(chalk.gray.bold(`DEBUG ${this.name}:`) + `\n ${text}`, data)
}
}
rpc (call: string, ...data: any) {
console.log(chalk.redBright.bold`RPC` + chalk.redBright(` ${call}():`), data)
}
sql (logger: Logger, ...data: any) {
if (logger.debugOn && !logger.quietSql) {
console.log(chalk.bold('DEBUG SQL:\n '), data)
}
}
}
export default (pathname: string) => {
const name = pathname.replace(__dirname, '').replace('.js', '')
return new Logger(name)
}

View file

@ -1,9 +1,9 @@
// @flow
import { type AppContext } from '../Roleypoly'
import { type AppContext } from '@roleypoly/server/Roleypoly'
import { type Context } from 'koa'
import RPCError from '@roleypoly/rpc-client/error'
import { RPCError } from '@roleypoly/rpc-client'
import logger from '../logger'
import logger from '../../roleypoly-server/logger'
const log = logger(__filename)
const PermissionError = new RPCError('User does not have permission to call this RPC.', 403)

View file

@ -10,7 +10,7 @@ import DiscordService from './services/discord'
import SessionService from './services/sessions'
import AuthService from './services/auth'
import PresentationService from './services/presentation'
import RPCServer from './rpc'
import RPCServer from '@roleypoly/rpc-server'
import fetchModels, { type Models } from './models'
import fetchApis from './api'
import retry from 'async-retry'

View file

@ -10,6 +10,7 @@
"dependencies": {
"@discordjs/uws": "^11.149.1",
"@roleypoly/types": "^2.0.0",
"@roleypoly/rpc-server": "^2.0.0",
"@roleypoly/ui": "^2.0.0",
"async-retry": "^1.2.3",
"bufferutil": "^4.0.1",

View file

@ -2,18 +2,18 @@
import { type AppContext } from '../Roleypoly'
import { type Context } from 'koa'
import { type Guild } from 'eris'
import * as secureAs from './_security'
import RPCError from '@roleypoly/rpc-client/error'
import { secureAs } from '@roleypoly/rpc-server'
import { RPCError } from '@roleypoly/rpc-client'
export default ($: AppContext) => ({
rootGetAllServers: secureAs.root($, (ctx: Context) => {
return $.discord.client.guilds.map<{
return $.discord.guilds.valueSeq().map<{
url: string,
name: string,
members: number,
roles: number
}>((g: Guild) => ({ url: `${$.config.appUrl}/s/${g.id}`, name: g.name, members: g.memberCount, roles: g.roles.size }))
}>((g: Guild) => ({ url: `${$.config.appUrl}/s/${g.id}`, name: g.name, members: g.members.size, roles: g.roles.size })).toJS()
}),
getServerSlug (ctx: Context, id: string) {
@ -40,5 +40,18 @@ export default ($: AppContext) => ({
}
return $.P.presentableServer(srv, gm)
}),
listOwnServers: secureAs.authed($, async (ctx: Context, id: string) => {
const { userId } = (ctx.session: { userId: string })
const srv = $.discord.getRelevantServers(userId)
return $.P.presentableServers(srv, userId)
}),
syncGuild: secureAs.bot($, async (ctx: Context, type: string, guildId: string) => {
const g = await $.discord.guild(guildId, true)
if (g != null && type === 'guildCreate') {
$.server.ensure(g)
}
})
})

View file

@ -4,7 +4,7 @@ import type { AppContext } from '../Roleypoly'
import Eris, { type Member, Role, type Guild, type Permission as ErisPermission } from 'eris'
import LRU from 'lru-cache'
// $FlowFixMe
import { OrderedSet } from 'immutable'
import { OrderedSet, OrderedMap } from 'immutable'
import superagent from 'superagent'
import type { AuthTokens } from './auth'
import type { IFetcher } from './discord/types'
@ -54,6 +54,8 @@ export type MemberExt = Member & {
}
export default class DiscordService extends Service {
static _guildExpiration = +(process.env.GUILD_INVALIDATION_TIME || 36e5)
ctx: AppContext
client: Eris
@ -67,6 +69,8 @@ export default class DiscordService extends Service {
fetcher: IFetcher
guilds: OrderedMap<Guild> = OrderedMap<Guild>()
_lastGuildFetch: number
constructor (ctx: AppContext) {
super(ctx)
this.ctx = ctx
@ -88,14 +92,15 @@ export default class DiscordService extends Service {
})
this.fetcher = new RestFetcher(this)
this.fetchGuilds(true)
}
isRoot (id: string): boolean {
return this.cfg.rootUsers.has(id)
}
getRelevantServers (user: string) {
return this.client.guilds.filter(guild => guild.members.has(user))
getRelevantServers (user: string): OrderedSet<Guild> {
return this.guilds.filter(guild => guild.members.has(user))
}
async gm (serverId: string, userId: string, { canFake = false }: { canFake: boolean } = {}): Promise<?MemberExt> {
@ -280,6 +285,36 @@ export default class DiscordService extends Service {
return `https://discordapp.com/oauth2/authorize?client_id=${this.cfg.clientId}&scope=bot&permissions=268435456`
}
async fetchGuilds (force: boolean = false) {
if (
force ||
this.guilds.isEmpty() ||
this._lastGuildFetch + DiscordService._guildExpiration < Date.now()
) {
const g = await this.fetcher.getGuilds()
this.guilds = OrderedMap(g.reduce((acc, g) => ({ ...acc, [g.id]: g }), {}))
}
}
async guild (id: string, invalidate: boolean = false): Promise<?Guild> {
// fetch if needed
await this.fetchGuilds()
// do we know about it?
// (also don't get this if we're invalidating)
if (invalidate === false && this.guilds.has(id)) {
return this.guilds.get(id)
}
// else let's fetch and cache.
const g = await this.fetcher.getGuild(id)
if (g != null) {
this.guilds = this.guilds.set(g.id, g)
}
return g
}
async issueChallenge (author: string) {
// Create a challenge
const chall = await this.ctx.auth.createDMChallenge(author)

View file

@ -4,6 +4,8 @@ import type DiscordSvc from '../discord'
import type ErisClient, { User, Member, Guild } from 'eris'
import LRU from 'lru-cache'
import logger from '../../logger'
// $FlowFixMe
import { OrderedSet } from 'immutable'
const log = logger(__filename)
export default class BotFetcher implements IFetcher {
@ -71,4 +73,31 @@ export default class BotFetcher implements IFetcher {
return null
}
}
getGuilds = async (): Promise<Guild[]> => {
const last: ?string = undefined
const limit: number = 100
let out = OrderedSet<Guild>()
try {
while (true) {
// $FlowFixMe -- last is optional, but typedef isn't.
const gl = await this.client.getRESTGuilds(limit, last)
out = out.union(gl)
if (gl.length !== limit) {
break
}
}
} catch (e) {
log.error('getAllGuilds failed', e)
throw e
}
return out.toArray()
}
invalidateGuild (id: string) {
this.cache.del(`G:${id}`)
}
}

View file

@ -11,4 +11,6 @@ export interface IFetcher {
getGuild: (id: string) => Promise<?Guild>;
getMember: (server: string, user: string) => Promise<?Member>;
getGuilds: () => Promise<Guild[]>;
}

View file

@ -5,6 +5,8 @@ import { type AppContext } from '../Roleypoly'
import { type Models } from '../models'
import { type ServerModel } from '../models/Server'
import type DiscordService from './discord'
// $FlowFixMe
import type { Sequence } from 'immutable'
import {
type Guild,
type Collection
@ -38,7 +40,7 @@ class PresentationService extends Service {
}
}
presentableServers (collection: Collection<Guild>, userId: string) {
presentableServers (collection: Collection<Guild> | Sequence<Guild>, userId: string): Promise<PresentableServer[]> {
return areduce(Array.from(collection.values()), async (acc, server) => {
const gm = server.members.get(userId)
if (gm == null) {

View file

@ -5,10 +5,10 @@ import type PresentationService from './presentation'
import {
type Guild
} from 'eris'
import { type ServerModel, type Category } from '../models/Server'
import { type ServerModel, type Category } from '@roleypoly/types'
export default class ServerService extends Service {
Server: any // Model<ServerModel> but flowtype is bugged
Server: * // Model<ServerModel> but flowtype is bugged
P: PresentationService
constructor (ctx: AppContext) {
super(ctx)

View file

@ -1,16 +1,29 @@
// @flow
import { namespaceConfig } from 'fast-redux'
import RPC from '../config/rpc'
// import { Map } from 'immutable'
import type { PresentableServer as Server } from '@roleypoly/types'
const DEFAULT_STATE = {}
export type ServersState = {
[id: string]: Server
}
export type ServersState = typeof DEFAULT_STATE
const DEFAULT_STATE: ServersState = {}
export const { action, getState: getServerState } = namespaceConfig('servers', DEFAULT_STATE)
export const updateServers = action('updateServers', (state: ServersState, serverData) => ({
export const updateServers = action('updateServers', (state: ServersState, serverData: Server[]) => ({
...state,
servers: serverData
...serverData.reduce((acc, s) => ({ ...acc, [s.id]: s }), {})
}))
export const updateSingleServer = action('updateSingleServer', (state, data, server) => ({ ...state, [server]: data }))
export const fetchServerList = (rpc?: typeof RPC) => async (dispatch: *) => {
if (rpc == null) {
rpc = RPC
}
const servers: Server[] = await rpc.listOwnServers()
dispatch(updateServers(servers))
}

View file

@ -368,6 +368,14 @@
"@babel/helper-create-class-features-plugin" "^7.4.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-export-namespace-from@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.2.0.tgz#308fd4d04ff257fc3e4be090550840eeabad5dd9"
integrity sha512-DZUxbHYxQ5fUFIkMEnh75ogEdBLPfL+mQUqrO2hNY2LGm+tqFnxE924+mhAcCOh/8za8AaZsWHbq6bBoS3TAzA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-export-namespace-from" "^7.2.0"
"@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
@ -453,6 +461,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-export-namespace-from@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz#8d257838c6b3b779db52c0224443459bd27fb039"
integrity sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-flow@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.2.0.tgz#a765f061f803bc48f240c26f8747faf97c26bf7c"
@ -8173,6 +8188,14 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
nats@^1.2.10:
version "1.2.10"
resolved "https://registry.yarnpkg.com/nats/-/nats-1.2.10.tgz#3f673e80d3a513f7802b0a5c1f227127a3d1d780"
integrity sha512-0FQMINZbyRkFMRbrpc6+IkKMQ+Zi2Ibr4YPhoEBlbP0Gw3ta23e/GB+LvXNqnV3htOPJNJ54+ToMI43BCYATGQ==
dependencies:
nuid "^1.0.0"
ts-nkeys "^1.0.8"
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -8553,6 +8576,11 @@ nth-check@~1.0.1:
dependencies:
boolbase "~1.0.0"
nuid@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nuid/-/nuid-1.1.0.tgz#e0c5746350a25562f58283197f34c50861f44675"
integrity sha512-C/JdZ6PtCqKsCEs4ni76nhBsdmuQgLAT/CTLNprkcLViDAnkk7qx5sSA8PVC2vmSsdBlSsFuGb52v6pwn1oaeg==
num2fraction@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@ -11600,6 +11628,13 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.3.tgz#e29bd1614c6458d44869fc28b255ab7857ef7c24"
integrity sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw==
ts-nkeys@^1.0.8:
version "1.0.12"
resolved "https://registry.yarnpkg.com/ts-nkeys/-/ts-nkeys-1.0.12.tgz#cda47f4842fe2c4f88b1303817050673e16acb89"
integrity sha512-5TgA+wbfxTy/9pdSuAhvneuL65KKoI7phonzNQH2UhnorAQAWehUwHNLEuli596wu/Fxh0SAhMeKZVLNx4s7Ow==
dependencies:
tweetnacl "^1.0.1"
tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
@ -11629,7 +11664,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
tweetnacl@^1.0.0:
tweetnacl@^1.0.0, tweetnacl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.1.tgz#2594d42da73cd036bd0d2a54683dd35a6b55ca17"
integrity sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==