mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-06-16 02:19:08 +00:00
temp commit
This commit is contained in:
parent
e86f7ff68e
commit
6fb39d6c4d
22 changed files with 282 additions and 39 deletions
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
|
|
11
packages/roleypoly-rpc-server/.babelrc
Normal file
11
packages/roleypoly-rpc-server/.babelrc
Normal 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 }]
|
||||
]
|
||||
}
|
24
packages/roleypoly-rpc-server/package.json
Normal file
24
packages/roleypoly-rpc-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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}`)
|
|
@ -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))
|
70
packages/roleypoly-rpc-server/src/logger.js
Normal file
70
packages/roleypoly-rpc-server/src/logger.js
Normal 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)
|
||||
}
|
|
@ -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)
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,4 +11,6 @@ export interface IFetcher {
|
|||
getGuild: (id: string) => Promise<?Guild>;
|
||||
|
||||
getMember: (server: string, user: string) => Promise<?Member>;
|
||||
|
||||
getGuilds: () => Promise<Guild[]>;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue