absolutely massive typescript porting time

This commit is contained in:
41666 2019-06-02 18:58:15 -05:00
parent 01f238f515
commit 30d08a630f
No known key found for this signature in database
GPG key ID: BC51D07640DC10AF
159 changed files with 2563 additions and 3861 deletions

View file

@ -1,15 +0,0 @@
{
"presets": [ ["@babel/preset-env", {
"targets": {
"node": true
}
}], "@babel/preset-flow" ],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining",
["@babel/plugin-transform-runtime",
{ "helpers": false }]
],
"ignore": ["ui/**/*"]
}

View file

@ -0,0 +1,14 @@
linters:
lib/*.{js,jsx}:
- standard --fix
- git add
lib/*.d.ts:
- tslint --fix
- git add
src/*.{ts,tsx}:
- tslint --fix
- stylelint --fix
- jest --bail --findRelatedTests
- git add

View file

@ -3,57 +3,43 @@
"private": true,
"version": "2.0.0",
"scripts": {
"start": "NODE_ENV=production node dist/index.js",
"pretest": "standard --fix",
"build": "NODE_ENV=production babel --delete-dir-on-start -d dist ."
"start": "node lib/index.js",
"precommit": "lint-staged",
"build": "tsc"
},
"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",
"chalk": "^2.4.2",
"dotenv": "^7.0.0",
"eris": "^0.9.0",
"erlpack": "discordapp/erlpack",
"eventemitter3": "^3.1.0",
"dotenv": "^8.0.0",
"eventemitter3": "^3.1.2",
"fnv-plus": "^1.2.12",
"glob": "^7.1.3",
"glob": "^7.1.4",
"immutable": "^4.0.0-rc.12",
"keygrip": "^1.0.3",
"keygrip": "^1.1.0",
"koa": "^2.7.0",
"koa-better-router": "^2.1.1",
"koa-bodyparser": "^4.2.1",
"koa-session": "^5.10.1",
"koa-session": "^5.12.0",
"kompression": "^1.0.0",
"ksuid": "^1.2.0",
"lru-cache": "^5.1.1",
"moment": "^2.24.0",
"moniker": "^0.1.2",
"nanoid": "^2.0.1",
"pg": "^7.9.0",
"nanoid": "^2.0.3",
"pg": "^7.11.0",
"pg-hstore": "^2.3.2",
"sequelize": "^5.3.5",
"superagent": "^5.0.2",
"zlib-sync": "^0.1.4"
"sequelize": "^5.8.6",
"superagent": "^5.0.5"
},
"devDependencies": {
"@babel/cli": "^7.4.3",
"@babel/core": "^7.4.3",
"@babel/node": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.4.3",
"@babel/preset-env": "^7.4.3",
"@babel/preset-flow": "^7.0.0",
"@roleypoly/types": "^2.0.0",
"@types/koa": "^2.0.48",
"@types/lru-cache": "^5.1.0",
"@types/nanoid": "^2.0.0",
"await-outside": "^2.1.2",
"babel-eslint": "^10.0.1",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"chokidar": "^2.1.5",
"eslint-plugin-flowtype": "^3.6.1",
"standard": "12.0.1"
"chokidar": "^3.0.0",
"lint-staged": "^8.1.7",
"standard": "12.0.1",
"tslint": "^5.17.0",
"typescript": "^3.5.1"
}
}

View file

@ -1,12 +0,0 @@
// @flow
import { Logger } from '../logger'
import { type AppContext } from '../Roleypoly'
export default class Service {
ctx: AppContext
log: Logger
constructor (ctx: AppContext) {
this.ctx = ctx
this.log = new Logger(this.constructor.name)
}
}

View file

@ -1,346 +0,0 @@
/*
import Service from './Service'
import superagent from 'superagent'
// import invariant from 'invariant'
import { type AppContext } from '../Roleypoly'
import Eris from 'eris'
import Bot from '../bot'
import type { AuthTokens } from './auth'
export type UserPartial = {
id: string,
username: string,
discriminator: string,
avatar: string
}
export type Permissions = {
isAdmin: boolean,
canManageRoles: boolean
}
// type ChatCommandHandler = (message: Message, matches: string[], reply: (text: string) => Promise<Message>) => Promise<void>|void
// type ChatCommand = {
// regex: RegExp,
// handler: ChatCommandHandler
// }
class DiscordService extends Service {
botToken: string = process.env.DISCORD_BOT_TOKEN || ''
clientId: string = process.env.DISCORD_CLIENT_ID || ''
clientSecret: string = process.env.DISCORD_CLIENT_SECRET || ''
oauthCallback: string = process.env.OAUTH_AUTH_CALLBACK || ''
isBot: boolean = process.env.IS_BOT === 'true'
appUrl: string
botCallback: string
rootUsers: Set<string>
client: Eris
cmds: ChatCommand[]
bot: Bot
constructor (ctx: AppContext) {
super(ctx)
this.appUrl = ctx.config.appUrl
this.oauthCallback = `${this.appUrl}/api/oauth/callback`
this.botCallback = `${this.appUrl}/api/oauth/bot/callback`
this.rootUsers = new Set((process.env.ROOT_USERS || '').split(','))
this.bot = new Bot(this)
}
ownGm (server: string) {
return this.gm(server, this.client.user.id)
}
fakeGm ({ id = '0', nickname = '[none]', displayHexColor = '#ffffff' }: $Shape<{ id: string, nickname: string, displayHexColor: string }>) { // eslint-disable-line no-undef
return { id, nickname, displayHexColor, __faked: true, roles: { has () { return false } } }
}
isRoot (id: string): boolean {
return this.rootUsers.has(id)
}
getRelevantServers (userId: string): Collection<string, Guild> {
return this.client.guilds.filter((g) => g.members.has(userId))
}
gm (serverId: string, userId: string): ?GuildMember {
const s = this.client.guilds.get(serverId)
if (s == null) {
return null
}
return s.members.get(userId)
}
getRoles (server: string) {
const s = this.client.guilds.get(server)
if (s == null) {
return null
}
return s.roles
}
getPermissions (gm: GuildMember): Permissions {
if (this.isRoot(gm.id)) {
return {
isAdmin: true,
canManageRoles: true
}
}
return {
isAdmin: gm.permissions.has('ADMINISTRATOR'),
canManageRoles: gm.permissions.has('MANAGE_ROLES', true)
}
}
safeRole (server: string, role: string) {
const rl = this.getRoles(server)
if (rl == null) {
throw new Error(`safeRole can't see ${server}`)
}
const r: ?Role = rl.get(role)
if (r == null) {
throw new Error(`safeRole can't find ${role} in ${server}`)
}
return r.editable && !r.hasPermission('MANAGE_ROLES', false, true)
}
// oauth step 2 flow, grab the auth token via code
async getAuthToken (code: string): Promise<AuthTokens> {
const url = 'https://discordapp.com/api/oauth2/token'
try {
const rsp =
await superagent
.post(url)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'authorization_code',
code: code,
redirect_uri: this.oauthCallback
})
return rsp.body
} catch (e) {
this.log.error('getAuthToken failed', e)
throw e
}
}
async getUserFromToken (authToken?: string): Promise<UserPartial> {
const url = 'https://discordapp.com/api/v6/users/@me'
try {
if (authToken == null || authToken === '') {
throw new Error('not logged in')
}
const rsp =
await superagent
.get(url)
.set('Authorization', `Bearer ${authToken}`)
return rsp.body
} catch (e) {
this.log.error('getUser error', e)
throw e
}
}
getUserPartial (userId: string): ?UserPartial {
const U = this.client.users.get(userId)
if (U == null) {
return null
}
return {
username: U.username,
discriminator: U.discriminator,
avatar: U.displayAvatarURL,
id: U.id
}
}
async refreshOAuth ({ refreshToken }: { refreshToken: string }): Promise<AuthTokens> {
const url = 'https://discordapp.com/api/oauth2/token'
try {
const rsp =
await superagent
.post(url)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
redirect_uri: this.oauthCallback
})
return rsp.body
} catch (e) {
this.log.error('refreshOAuth failed', e)
throw e
}
}
async revokeOAuth ({ accessToken }: { accessToken: string }) {
const url = 'https://discordapp.com/api/oauth2/token/revoke'
try {
const rsp =
await superagent
.post(url)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'access_token',
token: accessToken,
redirect_uri: this.oauthCallback
})
return rsp.body
} catch (e) {
this.log.error('revokeOAuth failed', e)
throw e
}
}
// returns oauth authorize url with IDENTIFY permission
// we only need IDENTIFY because we only use it for matching IDs from the bot
getAuthUrl (state: string): string {
return `https://discordapp.com/oauth2/authorize?client_id=${this.clientId}&redirect_uri=${this.oauthCallback}&response_type=code&scope=identify&state=${state}`
}
// returns the bot join url with MANAGE_ROLES permission
// MANAGE_ROLES is the only permission we really need.
getBotJoinUrl (): string {
return `https://discordapp.com/oauth2/authorize?client_id=${this.clientId}&scope=bot&permissions=268435456`
}
mentionResponse (message: Message) {
message.channel.send(`🔰 Assign your roles here! ${this.appUrl}/s/${message.guild.id}`, { disableEveryone: true })
}
_cmds () {
const cmds = [
{
regex: /say (.*)/,
handler (message, matches, r) {
r(matches[0])
}
},
{
regex: /set username (.*)/,
async handler (message, matches) {
const { username } = this.client.user
await this.client.user.setUsername(matches[0])
message.channel.send(`Username changed from ${username} to ${matches[0]}`)
}
},
{
regex: /stats/,
async handler (message, matches) {
const t = [
`**Stats** 📈`,
'',
`👩‍❤️‍👩 **Users Served:** ${this.client.guilds.reduce((acc, g) => acc + g.memberCount, 0)}`,
`🔰 **Servers:** ${this.client.guilds.size}`,
`💮 **Roles Seen:** ${this.client.guilds.reduce((acc, g) => acc + g.roles.size, 0)}`
]
message.channel.send(t.join('\n'))
}
}
]
// prefix regex with ^ for ease of code
.map(({ regex, ...rest }) => ({ regex: new RegExp(`^${regex.source}`, regex.flags), ...rest }))
this.cmds = cmds
return cmds
}
async handleCommand (message: Message) {
const cmd = message.content.replace(`<@${this.client.user.id}> `, '')
this.log.debug(`got command from ${message.author.username}`, cmd)
for (let { regex, handler } of this.cmds) {
const match = regex.exec(cmd)
if (match !== null) {
this.log.debug('command accepted', { cmd, match })
try {
await handler.call(this, message, match.slice(1))
return
} catch (e) {
this.log.error('command errored', { e, cmd, message })
message.channel.send(`❌ **An error occured.** ${e}`)
return
}
}
}
// nothing matched?
this.mentionResponse(message)
}
async issueChallenge (author: string) {
// Create a challenge
const chall = await this.ctx.auth.createDMChallenge(author)
const randomLines = [
'🐄 A yellow cow is only as bright as it lets itself be. ✨',
'‼ **Did you know?** On this day, at least one second ago, you were right here!',
'<:AkkoC8:437428070849314816> *Reticulating splines...*',
'Also, you look great today <:YumekoWink:439519270376964107>',
'btw, ur bright like a <:diamond:544665968631087125>',
`🌈 psst! pssssst! I'm an expensive bot, would you please spare some change? <https://ko-fi.com/roleypoly>`,
'📣 have suggestions? wanna help out? join my discord! <https://discord.gg/PWQUVsd>\n*(we\'re nice people, i swear!)*',
`🤖 this bot is at least ${Math.random() * 100}% LIT 🔥`,
'💖 wanna contribute to these witty lines? <https://discord.gg/PWQUVsd> suggest them on our discord!',
'🛠 I am completely open source, check me out!~ <https://github.com/kayteh/roleypoly>'
]
return ([
'**Hey there!** <a:StockKyaa:435353158462603266>',
'',
`Use this secret code: **${chall.human}**`,
`Or, click here: <${this.ctx.config.appUrl}/magic/${chall.magic}>`,
'',
'This code will self-destruct in 1 hour.',
'---',
randomLines[Math.floor(Math.random() * randomLines.length)]
].join('\n'))
}
handleDM (message: Message) {
switch (message.content.toLowerCase()) {
case 'login':
case 'auth':
case 'log in':
this.issueChallenge(message)
}
}
handleMessage (message: Message) {
if (message.author.bot) { // drop bot messages
return
}
if (message.channel.type === 'dm') {
// handle dm
return this.handleDM(message)
}
if (message.mentions.users.has(this.client.user.id)) {
if (this.rootUsers.has(message.author.id)) {
this.handleCommand(message)
} else {
this.mentionResponse(message)
}
}
}
async handleJoin (guild: Guild) {
await this.ctx.server.ensure(guild)
}
}
module.exports = DiscordService
*/

View file

@ -1,16 +0,0 @@
// @flow
import type {
User,
Member,
Guild
} from 'eris'
export interface IFetcher {
getUser: (id: string) => Promise<?User>;
getGuild: (id: string) => Promise<?Guild>;
getMember: (server: string, user: string) => Promise<?Member>;
getGuilds: () => Promise<Guild[]>;
}

View file

@ -1,22 +1,19 @@
// @flow
import Sequelize from 'sequelize'
import connector from '@roleypoly/ui/connector'
import type Next from 'next'
import Next from 'next'
import betterRouter from 'koa-better-router'
import type EventEmitter from 'events'
import EventEmitter from 'events'
import logger from './logger'
import ServerService from './services/server'
import DiscordService from './services/discord'
import SessionService from './services/sessions'
import AuthService from './services/auth'
import PresentationService from './services/presentation'
import RPCServer from '@roleypoly/rpc-server'
import fetchModels, { type Models } from './models'
import fetchModels, { Models } from './models'
import fetchApis from './api'
import retry from 'async-retry'
import type SocketIO from 'socket.io'
import type KoaApp, { Context } from 'koa'
import KoaApp, { Context } from 'koa'
const log = logger(__filename)
type HTTPHandler = (path: string, handler: (ctx: Context, next: () => void) => any) => void
@ -31,6 +28,15 @@ export type Router = {
export type RouteHook = (router: Router) => void
export type KoaContextExt = KoaApp.Context & {
request: {
body: any
},
session: {
[x: string]: any
}
}
export type AppContext = {
config: {
appUrl: string,
@ -40,46 +46,42 @@ export type AppContext = {
},
ui: Next,
uiHandler: Next.Handler,
io: SocketIO,
// io: SocketIO,
server: ServerService,
discord: DiscordService,
sessions: SessionService,
P: PresentationService,
RPC: RPCServer,
// RPC: RPCServer,
M: Models,
sql: Sequelize,
sql: Sequelize.Sequelize,
auth: AuthService
}
class Roleypoly {
ctx: AppContext|any
io: SocketIO
export default class Roleypoly {
ctx: AppContext | any
router: Router
M: Models
__app: KoaApp
__initialized: Promise<void>
__apiWatcher: EventEmitter
__rpcWatcher: EventEmitter
// private app: KoaApp
private initialized: Promise<void>
private apiWatcher: EventEmitter
private rpcWatcher: EventEmitter
__routeHooks: Set<RouteHook> = new Set()
constructor (io: SocketIO, app: KoaApp) {
this.io = io
this.__app = app
private routeHooks: Set<RouteHook> = new Set()
constructor (
io: undefined,
private app: KoaApp
) {
// this.io = io
if (log.debugOn) log.warn('debug mode is on')
const dev = process.env.NODE_ENV !== 'production'
// simple check if we're in a compiled situation or not.
const ui = connector({ dev })
const uiHandler = ui.getRequestHandler()
const appUrl = process.env.APP_URL
if (appUrl == null) {
if (appUrl === undefined) {
throw new Error('APP_URL was unset.')
}
@ -90,27 +92,25 @@ class Roleypoly {
hotReload: process.env.NO_HOT_RELOAD !== '1',
sharedSecret: process.env.SHARED_SECRET
},
io,
ui,
uiHandler
io
}
this.__initialized = this._mountServices()
this.initialized = this._mountServices()
}
async awaitServices () {
await this.__initialized
await this.initialized
}
async _mountServices () {
const dbUrl: ?string = process.env.DB_URL
if (dbUrl == null) {
const dbUrl: string = process.env.DB_URL || ''
if (dbUrl === '') {
throw log.fatal('DB_URL not set.')
}
await this.ctx.ui.prepare()
const sequelize = new Sequelize(dbUrl, { logging: log.sql.bind(log, log) })
const sequelize = new Sequelize.Sequelize(dbUrl, { logging: log.sql.bind(log, log) })
this.ctx.sql = sequelize
this.M = fetchModels(sequelize)
this.ctx.M = this.M
@ -133,15 +133,15 @@ class Roleypoly {
this.ctx.sessions = new SessionService(this.ctx)
this.ctx.auth = new AuthService(this.ctx)
this.ctx.P = new PresentationService(this.ctx)
this.ctx.RPC = new RPCServer(this)
// this.ctx.RPC = new RPCServer(this)
}
addRouteHook (hook: RouteHook) {
this.__routeHooks.add(hook)
this.routeHooks.add(hook)
}
hookServiceRoutes (router: Router) {
for (let h of this.__routeHooks) {
for (let h of this.routeHooks) {
h(router)
}
}
@ -172,14 +172,14 @@ class Roleypoly {
const path = require('path')
let hotMiddleware = mw
this.__apiWatcher = chokidar.watch(path.join(__dirname, 'api/**'))
this.__apiWatcher.on('all', async (path) => {
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(path.join(__dirname, 'rpc/**'))
this.__rpcWatcher.on('all', (path) => {
this.rpcWatcher = chokidar.watch(path.join(__dirname, 'rpc/**'))
this.rpcWatcher.on('all', (path) => {
log.info('reloading RPCs...', path)
this.ctx.RPC.reload()
})
@ -190,8 +190,6 @@ class Roleypoly {
}
}
this.__app.use(mw)
this.app.use(mw)
}
}
module.exports = Roleypoly

View file

@ -1,16 +1,15 @@
// @flow
import { type Context } from 'koa'
import { type AppContext, type Router } from '../Roleypoly'
// import { Context } from 'koa'
import { AppContext, Router, KoaContextExt } from '../Roleypoly'
import ksuid from 'ksuid'
import logger from '../logger'
import renderError from '../util/error'
const log = logger(__filename)
export default (R: Router, $: AppContext) => {
R.post('/api/auth/token', async (ctx: Context) => {
const { token } = ((ctx.request.body: any): { token: string })
R.post('/api/auth/token', async (ctx: KoaContextExt) => {
const { token } = ctx.request.body as { token: string | undefined }
if (token == null || token === '') {
if (token === undefined || token === '') {
ctx.body = { err: 'token_missing' }
ctx.status = 400
return
@ -37,8 +36,8 @@ export default (R: Router, $: AppContext) => {
}
})
R.get('/api/auth/user', async (ctx: Context) => {
const { accessToken } = ((ctx.session: any): { accessToken?: string })
R.get('/api/auth/user', async (ctx: KoaContextExt) => {
const { accessToken } = ctx.session as { accessToken?: string }
if (accessToken === undefined) {
ctx.body = { err: 'not_logged_in' }
ctx.status = 401
@ -57,7 +56,7 @@ export default (R: Router, $: AppContext) => {
}
})
R.get('/api/auth/redirect', async (ctx: Context) => {
R.get('/api/auth/redirect', async (ctx: KoaContextExt) => {
const { r } = ctx.query
// check if already authed
if (await $.auth.isLoggedIn(ctx, { refresh: true })) {
@ -75,7 +74,7 @@ export default (R: Router, $: AppContext) => {
ctx.redirect(url)
})
R.get('/api/oauth/callback', async (ctx: Context, next: *) => {
R.get('/api/oauth/callback', async (ctx: KoaContextExt, next: *) => {
const { code, state } = ctx.query
const { oauthRedirect: r } = ctx.session
delete ctx.session.oauthRedirect
@ -93,8 +92,8 @@ export default (R: Router, $: AppContext) => {
if (state != null) {
try {
const ksState = ksuid.parse(state)
const fiveMinAgo = new Date() - 1000 * 60 * 5
if (ksState.date < fiveMinAgo) {
const fiveMinAgo = Date.now() - 1000 * 60 * 5
if (+ksState.date < fiveMinAgo) {
ctx.status = 419
await renderError($, ctx)
return
@ -119,22 +118,22 @@ export default (R: Router, $: AppContext) => {
}
})
R.post('/api/auth/logout', async (ctx: Context) => {
ctx.session = null
R.post('/api/auth/logout', async (ctx: KoaContextExt) => {
ctx.session = {}
})
R.get('/api/auth/logout', async (ctx: Context) => {
R.get('/api/auth/logout', async (ctx: KoaContextExt) => {
if (await $.auth.isLoggedIn(ctx)) {
if (ctx.session.authType === 'oauth') {
await $.discord.revokeOAuth(ctx.session)
await $.discord.revokeOAuth(ctx.session )
}
}
ctx.session = null
ctx.session = {}
return ctx.redirect('/')
})
R.get('/api/oauth/bot', async (ctx: Context) => {
R.get('/api/oauth/bot', async (ctx: KoaContextExt) => {
const url = $.discord.getBotJoinUrl()
if (ctx.query.url === '✔️') {
ctx.body = { url }
@ -144,11 +143,11 @@ export default (R: Router, $: AppContext) => {
ctx.redirect(url)
})
R.get('/api/oauth/bot/callback', async (ctx: Context) => {
R.get('/api/oauth/bot/callback', async (ctx: KoaContextExt) => {
// console.log(ctx.request)
})
R.get('/magic/:challenge', async (ctx: Context) => {
R.get('/magic/:challenge', async (ctx: KoaContextExt) => {
if (ctx.request.headers['user-agent'].includes('Discordbot')) {
return $.ui.render(ctx.req, ctx.res, '/_internal/_discordbot/_magic', {})
}

View file

@ -1,12 +1,11 @@
// @flow
import glob from 'glob'
import type { Router, AppContext } from '../Roleypoly'
import { Router, AppContext } from '../Roleypoly'
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 } = {}) => {
export default async (router: Router, ctx: AppContext, { forceClear }: { forceClear: boolean } = { forceClear: false }) => {
const apis = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.'))
log.debug('found apis', apis)
@ -20,7 +19,6 @@ export default async (router: Router, ctx: AppContext, { forceClear = false }: {
if (forceClear) {
delete require.cache[require.resolve(a)]
}
// $FlowFixMe this isn't an important error. potentially dangerous, but irrelevant.
require(a).default(router, ctx)
} catch (e) {
log.error(`couldn't mount ${a}`, e)

View file

@ -1,7 +1,8 @@
// @flow
import { type Context } from 'koa'
import { type AppContext, type Router } from '../Roleypoly'
import { type ServerModel } from '../models/Server'
import { Context } from 'koa'
import { AppContext, Router, KoaContextExt } from '../Roleypoly'
import { ServerModel } from '../models/Server'
import { MemberExt } from '../services/discord'
import { Member } from 'eris'
export default (R: Router, $: AppContext) => {
R.get('/api/servers', async (ctx: Context) => {
@ -22,28 +23,27 @@ export default (R: Router, $: AppContext) => {
const srv = $.discord.client.guilds.get(id)
if (srv == null) {
if (srv === undefined) {
ctx.body = { err: 'not found' }
ctx.status = 404
return
}
let gm
let gm: Partial<MemberExt> | undefined
if (srv.members.has(userId)) {
gm = $.discord.gm(id, userId)
gm = await $.discord.gm(id, userId)
} else if ($.discord.isRoot(userId)) {
gm = $.discord.fakeGm({ id: userId })
}
if (gm == null) {
if (gm === undefined) {
ctx.body = { err: 'not_a_member' }
ctx.status = 400
return
}
const server = await $.P.presentableServer(srv, gm)
const server = await $.P.presentableServer(srv, gm as Member)
// $FlowFixMe bad koa type
ctx.body = server
})
@ -55,22 +55,22 @@ export default (R: Router, $: AppContext) => {
console.log(srv)
if (srv == null) {
if (srv === undefined) {
ctx.body = { err: 'not found' }
ctx.status = 404
return
}
// $FlowFixMe bad koa type
ctx.body = await $.P.serverSlug(srv)
ctx.body = $.P.serverSlug(srv)
})
R.patch('/api/server/:id', async (ctx: Context) => {
const { userId } = (ctx.session: { userId: string })
const { id } = (ctx.params: { id: string })
R.patch('/api/server/:id', async (ctx: KoaContextExt) => {
const { userId } = (ctx.session as { userId: string })
const { id } = (ctx.params as { id: string })
let gm = $.discord.gm(id, userId)
if (gm == null) {
let gm = await $.discord.gm(id, userId)
if (gm === undefined) {
if ($.discord.isRoot(userId)) {
gm = $.discord.fakeGm({ id: userId })
} else {
@ -83,29 +83,30 @@ export default (R: Router, $: AppContext) => {
}
// check perms
if (!$.discord.getPermissions(gm).canManageRoles) {
if (!$.discord.getPermissions(gm as Member).canManageRoles) {
ctx.status = 403
ctx.body = { err: 'cannot_manage_roles' }
return
}
const { message, categories } = ((ctx.request.body: any): $Shape<ServerModel>)
const { message, categories } = ctx.request.body as Partial<ServerModel>
// todo make less nasty
await $.server.update(id, {
...((message != null) ? { message } : {}),
...((categories != null) ? { categories } : {})
...((message !== undefined) ? { message } : {}),
...((categories !== undefined) ? { categories } : {})
})
ctx.body = { ok: true }
})
R.patch('/api/servers/:server/roles', async (ctx: Context) => {
const { userId } = (ctx.session: { userId: string })
const { server } = (ctx.params: { server: string })
R.patch('/api/servers/:server/roles', async (ctx: KoaContextExt) => {
const { userId } = (ctx.session as { userId: string })
const { server } = (ctx.params as { server: string })
let gm = $.discord.gm(server, userId)
if (gm == null) {
let gm = await $.discord.gm(server, userId)
if (gm === undefined) {
if ($.discord.isRoot(userId)) {
gm = $.discord.fakeGm({ id: userId })
} else {
@ -117,8 +118,8 @@ export default (R: Router, $: AppContext) => {
}
}
const originalRoles = gm.roles
let { added, removed } = ((ctx.request.body: any): { added: string[], removed: string[] })
const originalRoles = gm.roles || []
let { added, removed } = ctx.request.body as { added: string[], removed: string[] }
const allowedRoles: string[] = await $.server.getAllowedRoles(server)
@ -128,7 +129,9 @@ export default (R: Router, $: AppContext) => {
removed = removed.filter(isSafe)
const newRoles = originalRoles.concat(added).filter(x => !removed.includes(x))
gm.edit({
// force cast this because we know this will be correct.
await (gm as Member).edit({
roles: newRoles
})

View file

@ -1,6 +1,6 @@
// @flow
import { type Context } from 'koa'
import { type AppContext, type Router } from '../Roleypoly'
import { Context } from 'koa'
import { AppContext, Router } from '../Roleypoly'
export default (R: Router, $: AppContext) => {
R.get('/api/~/relevant-servers/:user', async (ctx: Context, next: () => void) => {
// ctx.body = 'ok'

View file

@ -1,6 +1,5 @@
// @flow
import { type Context } from 'koa'
import { type AppContext, type Router } from '../Roleypoly'
import { Context } from 'koa'
import { AppContext, Router } from '../Roleypoly'
export default (R: Router, $: AppContext) => {
// note, this file only contains stuff for complicated routes.
// next.js will handle anything beyond this.

View file

@ -18,8 +18,8 @@ const server = http.createServer(app.callback())
const M = new Roleypoly(null, app) // eslint-disable-line no-unused-vars
const appKey = process.env.APP_KEY
if (appKey == null || appKey === '') {
const appKey = process.env.APP_KEY || ''
if (appKey === '') {
throw new Error('APP_KEY not set')
}
app.keys = new Keygrip([ appKey ])
@ -37,7 +37,7 @@ async function start () {
// Request logger
app.use(async (ctx, next) => {
let timeStart = new Date()
let timeStart = Date.now()
try {
await next()
} catch (e) {
@ -51,7 +51,7 @@ async function start () {
}
}
}
let timeElapsed = new Date() - timeStart
let timeElapsed = Date.now() - timeStart
log.request(`${ctx.status} ${ctx.method} ${ctx.url} - ${ctx.ip} - took ${timeElapsed}ms`)
// return null

View file

@ -1,7 +1,6 @@
// @flow
import type Sequelize, { DataTypes as DT } from 'sequelize'
import Sequelize from 'sequelize'
export default (sql: Sequelize, DataTypes: DT) => {
export default (sql: Sequelize.Sequelize, DataTypes: typeof Sequelize.DataTypes) => {
return sql.define('auth_challenge', {
userId: DataTypes.TEXT,
issuedAt: DataTypes.DATE,

View file

@ -1,5 +1,5 @@
// @flow
import type Sequelize, { DataTypes as DT } from 'sequelize'
import Sequelize from 'sequelize'
export type Category = {
hidden: boolean,
@ -17,7 +17,7 @@ export type ServerModel = {
message: string
}
export default (sql: Sequelize, DataTypes: DT) => {
export default (sql: Sequelize.Sequelize, DataTypes: typeof Sequelize.DataTypes) => {
return sql.define('server', {
id: { // discord snowflake
type: DataTypes.TEXT,

View file

@ -1,4 +1,6 @@
module.exports = (sequelize, DataTypes) => {
import Sequelize from 'sequelize'
export default (sequelize: Sequelize.Sequelize, DataTypes: typeof Sequelize.DataTypes) => {
return sequelize.define('session', {
id: { type: DataTypes.TEXT, primaryKey: true },
maxAge: DataTypes.BIGINT,

View file

@ -1,16 +1,15 @@
// @flow
import logger from '../logger'
import glob from 'glob'
import path from 'path'
import type Sequelize, { Model } from 'sequelize'
import { Sequelize, Model } from 'sequelize'
const log = logger(__filename)
export type Models = {
[modelName: string]: Model<any> & {
__associations: (models: Models) => void,
__instanceMethods: (model: Model<any>) => void,
}
[modelName: string]: typeof Model & Partial<{
__associations: (models: Models) => void
__instanceMethods: (model: typeof Model) => void
}>
}
export default (sql: Sequelize): Models => {
@ -18,12 +17,13 @@ export default (sql: Sequelize): Models => {
const modelFiles = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.'))
log.debug('found models', modelFiles)
modelFiles.forEach((v) => {
for (const v of modelFiles) {
let name = path.basename(v).replace('.js', '')
if (v === `./index.js`) {
log.debug('index.js hit, skipped')
return
continue
}
try {
log.debug('importing..', v)
let model = sql.import(v)
@ -32,16 +32,17 @@ export default (sql: Sequelize): Models => {
log.fatal('error importing model ' + v, err)
process.exit(-1)
}
})
}
Object.keys(models).forEach((v) => {
if (models[v].hasOwnProperty('__associations')) {
models[v].__associations(models)
for (const model of Object.values(models)) {
if (model.__associations !== undefined) {
model.__associations(models)
}
if (models[v].hasOwnProperty('__instanceMethods')) {
models[v].__instanceMethods(models[v])
if (model.__instanceMethods !== undefined) {
model.__instanceMethods(model)
}
})
}
return models
}

View file

@ -1,6 +1,5 @@
// @flow
import { type AppContext } from '../Roleypoly'
import { type Context } from 'koa'
import { AppContext } from '../Roleypoly'
import { Context } from 'koa'
import { bot } from './_security'
export default ($: AppContext) => ({

View file

@ -0,0 +1,9 @@
// @flow
import { Logger } from '../logger'
import { AppContext } from '../Roleypoly'
export default class Service {
// @ts-ignore log is used in sub-classes
protected log: Logger = new Logger(this.constructor.name)
constructor (public ctx: AppContext) {}
}

View file

@ -1,9 +1,9 @@
// @flow
import Service from './Service'
import nanoid from 'nanoid'
import moniker from 'moniker'
import type { AppContext } from '../Roleypoly'
import type { Context } from 'koa'
import { AppContext } from '../Roleypoly'
import { Context } from 'koa'
import Sequelize from 'sequelize'
// import type { UserPartial } from './discord'
// import type { Models } from '../models'
@ -11,7 +11,7 @@ export type DMChallenge = {
userId: string, // snowflake of the user
human: string, // humanized string for input elsewhere, adjective adjective noun
magic: string, // magic URL, a nanoid
issuedAt: Date
issuedAt: number // Date.now()
}
export type AuthTokens = {
@ -20,15 +20,17 @@ export type AuthTokens = {
expires_in: string
}
type AuthModels = { AuthChallenge: Sequelize.ModelCtor<any>, Session: Sequelize.ModelCtor<any> }
export default class AuthService extends Service {
M: { AuthChallenge: any, Session: any }
M: AuthModels
monikerGen = moniker.generator([ moniker.adjective, moniker.adjective, moniker.noun ], { glue: ' ' })
constructor (ctx: AppContext) {
super(ctx)
this.M = ctx.M
this.M = ctx.M as AuthModels
}
async isLoggedIn (ctx: Context, { refresh = false }: { refresh: boolean } = {}) {
async isLoggedIn (ctx: Context, { refresh }: { refresh: boolean } = { refresh: false }) {
const { userId, expiresAt, authType } = ctx.session
this.log.debug('isLoggedIn session', ctx.session)
if (userId == null) {
@ -54,8 +56,8 @@ export default class AuthService extends Service {
return true
}
async createDMChallenge (userId: string): Promise<DMChallenge> {
if (userId == null || userId === '') {
async createDMChallenge (userId: string | undefined): Promise<DMChallenge> {
if (userId === undefined || userId === '') {
throw new Error('userId was not set')
}
@ -67,7 +69,7 @@ export default class AuthService extends Service {
userId,
human: this.monikerGen.choose(),
magic: nanoid(10),
issuedAt: new Date()
issuedAt: Date.now()
}
await this.M.AuthChallenge.build({ ...out, type: 'dm' }).save()
@ -75,15 +77,15 @@ export default class AuthService extends Service {
return out
}
async fetchDMChallenge (input: { human: string } | { magic: string }): Promise<?DMChallenge> {
const challenge: ?DMChallenge = this.M.AuthChallenge.findOne({ where: input })
if (challenge == null) {
async fetchDMChallenge (input: { human: string } | { magic: string }): Promise<DMChallenge | null> {
const challenge: DMChallenge | undefined = this.M.AuthChallenge.findOne({ where: input })
if (challenge === undefined) {
this.log.debug('challenge not found', challenge)
return null
}
// if issued more than 1 hour ago, it doesn't matter.
if (+challenge.issuedAt + 3.6e5 < new Date()) {
if (+challenge.issuedAt + 3.6e5 < Date.now()) {
this.log.debug('challenge expired', challenge)
return null
}
@ -109,6 +111,7 @@ export default class AuthService extends Service {
injectSessionFromOAuth (ctx: Context, tokens: AuthTokens, userId: string) {
const { expires_in: expiresIn, access_token: accessToken, refresh_token: refreshToken } = tokens
ctx.session = {
...ctx.session,
userId,
authType: 'oauth',
expiresAt: Date.now() + expiresIn,

View file

@ -1,14 +1,14 @@
// @flow
import Service from './Service'
import type { AppContext } from '../Roleypoly'
import Eris, { type Member, Role, type Guild, type Permission as ErisPermission } from 'eris'
import { AppContext } from '../Roleypoly'
import Eris, { Member, Role, Guild, Permission as ErisPermission } from 'eris'
import LRU from 'lru-cache'
// $FlowFixMe
import { OrderedSet, OrderedMap } from 'immutable'
import superagent from 'superagent'
import type { AuthTokens } from './auth'
import type { IFetcher } from './discord/types'
import { AuthTokens } from './auth'
import { IFetcher } from './discord/types'
import RestFetcher from './discord/restFetcher'
import { oc } from 'ts-optchain'
type DiscordServiceConfig = {
token: string,
@ -57,7 +57,7 @@ export default class DiscordService extends Service {
static _guildExpiration = +(process.env.GUILD_INVALIDATION_TIME || 36e5)
ctx: AppContext
client: Eris
client: Eris.Client
cfg: DiscordServiceConfig
@ -69,7 +69,8 @@ export default class DiscordService extends Service {
fetcher: IFetcher
guilds: OrderedMap<Guild> = OrderedMap<Guild>()
guilds: OrderedMap<string, Guild> = OrderedMap<string, Guild>()
_lastGuildFetch: number
constructor (ctx: AppContext) {
super(ctx)
@ -87,30 +88,30 @@ export default class DiscordService extends Service {
this.ownRoleCache = new LRU()
this.topRoleCache = new LRU()
this.client = new Eris(`Bot ${this.cfg.token}`, {
this.client = new Eris.Client(`Bot ${this.cfg.token}`, {
restMode: true
})
this.fetcher = new RestFetcher(this)
this.fetchGuilds(true)
this.fetchGuilds(true).then(() => null).catch(e => this.log.error)
}
isRoot (id: string): boolean {
return this.cfg.rootUsers.has(id)
}
getRelevantServers (user: string): OrderedSet<Guild> {
getRelevantServers (user: string): OrderedMap<string, Guild> {
return this.guilds.filter(guild => guild.members.has(user))
}
async gm (serverId: string, userId: string, { canFake = false }: { canFake: boolean } = {}): Promise<?MemberExt> {
const gm: ?Member = await this.fetcher.getMember(serverId, userId)
if (gm == null && this.isRoot(userId)) {
async gm (serverId: string, userId: string, { canFake }: { canFake: boolean } = { canFake: false }): Promise<Partial<MemberExt> | MemberExt | undefined> {
const gm: Member | undefined = await this.fetcher.getMember(serverId, userId)
if (gm === undefined && this.isRoot(userId)) {
return this.fakeGm({ id: userId })
}
if (gm == null) {
return null
if (gm === undefined) {
return undefined
}
const out: MemberExt = gm
@ -122,24 +123,35 @@ export default class DiscordService extends Service {
return this.gm(serverId, this.client.user.id)
}
fakeGm ({ id = '0', nick = '[none]', color = 0 }: $Shape<MemberExt>): $Shape<MemberExt> {
fakeGm ({ id = '0', nick = '[none]', color = 0 }: Partial<MemberExt>): Partial<MemberExt> {
return { id, nick, color, __faked: true, roles: [] }
}
getRoles (server: string) {
return this.client.guilds.get(server)?.roles
const s = this.client.guilds.get(server)
if (s === undefined) {
return new Map<string, Eris.Role>()
}
return new Map(s.roles)
}
async getOwnPermHeight (server: Guild): Promise<number> {
if (this.ownRoleCache.has(server)) {
const r = this.ownRoleCache.get(server)
async getOwnPermHeight (server: Eris.Guild): Promise<number> {
if (this.ownRoleCache.has(server.id)) {
const r = this.ownRoleCache.get(server.id) as CachedRole // we know this exists.
return r.position
}
const gm = await this.ownGm(server.id)
const g = gm?.guild
const r: Role = OrderedSet(gm?.roles).map(id => g?.roles.get(id)).minBy(r => r.position)
this.ownRoleCache.set(server, {
const gm = oc(await this.ownGm(server.id))
const g = gm.guild()
if (g === undefined) {
throw new Error('guild undefined.')
}
const rs = OrderedSet(gm.roles([])).map(id => g.roles.get(id) as Eris.Role)
const r = rs.minBy(role => role.position) as Eris.Role
this.ownRoleCache.set(server.id, {
id: r.id,
position: r.position
})
@ -149,15 +161,16 @@ export default class DiscordService extends Service {
getHighestRole (gm: MemberExt): Role {
const trk = `${gm.guild.id}:${gm.id}`
if (this.topRoleCache.has(trk)) {
const r = gm.guild.roles.get(this.topRoleCache.get(trk).id)
if (r != null) {
const r = gm.guild.roles.get((this.topRoleCache.get(trk) as CachedRole).id)
if (r !== undefined) {
return r
}
}
const g = gm.guild
const top = OrderedSet(gm.roles).map(id => g.roles.get(id)).minBy(r => r.position)
const top = OrderedSet(gm.roles).map(id => g.roles.get(id) as Eris.Role).minBy(r => r.position) as Eris.Role
this.topRoleCache.set(trk, {
id: top.id,
position: top.position,
@ -191,8 +204,8 @@ export default class DiscordService extends Service {
}
async safeRole (server: string, role: string) {
const r = this.getRoles(server)?.get(role)
if (r == null) {
const r = this.getRoles(server).get(role)
if (r === undefined) {
throw new Error(`safeRole can't find ${role} in ${server}`)
}
@ -205,7 +218,7 @@ export default class DiscordService extends Service {
return ownPh > role.position
}
async oauthRequest (path: string, data: OAuthRequestData) {
async oauthRequest (path: string, data: OAuthRequestData): Promise<AuthTokens> {
const url = `https://discordapp.com/api/oauth2/${path}`
try {
const rsp = await superagent.post(url)
@ -218,7 +231,7 @@ export default class DiscordService extends Service {
...data
})
return (rsp.body: AuthTokens)
return rsp.body
} catch (e) {
this.log.error('oauthRequest failed', { e, path })
throw e
@ -246,11 +259,11 @@ export default class DiscordService extends Service {
})
}
async getUserPartial (userId: string): Promise<?UserPartial> {
async getUserPartial (userId: string): Promise<UserPartial> {
const u = await this.fetcher.getUser(userId)
if (u == null) {
if (u === undefined) {
this.log.debug('userPartial got a null user', userId, u)
return null
throw new Error('userPartial got a null user')
}
return {
@ -296,19 +309,19 @@ export default class DiscordService extends Service {
}
}
async guild (id: string, invalidate: boolean = false): Promise<?Guild> {
async guild (id: string, invalidate: boolean = false): Promise<Guild | undefined> {
// 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)
return this.guilds.get(id) as Eris.Guild
}
// else let's fetch and cache.
const g = await this.fetcher.getGuild(id)
if (g != null) {
if (g !== undefined) {
this.guilds = this.guilds.set(g.id, g)
}
@ -345,21 +358,21 @@ export default class DiscordService extends Service {
}
async canManageRoles (server: string, user: string): Promise<boolean> {
const gm = await this.gm(server, user)
if (gm == null) {
const gm = await this.gm(server, user) as Member | undefined
if (gm === undefined) {
return false
}
return this.getPermissions(gm).canManageRoles
}
isMember (server: string, user: string): boolean {
return this.gm(server, user) != null
async isMember (server: string, user: string): Promise<boolean> {
return await this.gm(server, user) !== undefined
}
async isValidUser (user: string): Promise<boolean> {
const u = await this.fetcher.getUser(user)
if (u != null) {
if (u !== undefined) {
return true
}

View file

@ -1,16 +1,14 @@
// @flow
import type { IFetcher } from './types'
import type DiscordSvc from '../discord'
import type ErisClient, { User, Member, Guild } from 'eris'
import { IFetcher } from './types'
import DiscordSvc from '../discord'
import { Client, 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 {
ctx: DiscordSvc
client: ErisClient
client: Client
cache: LRU<string, Guild | Member | User>
constructor (ctx: DiscordSvc) {
@ -22,10 +20,10 @@ export default class BotFetcher implements IFetcher {
})
}
getUser = async (id: string): Promise<?User> => {
getUser = async (id: string): Promise<User | undefined> => {
if (this.cache.has(`U:${id}`)) {
log.debug('user cache hit')
return this.cache.get(`U:${id}`)
return this.cache.get(`U:${id}`) as User
}
log.debug('user cache miss')
@ -35,32 +33,32 @@ export default class BotFetcher implements IFetcher {
this.cache.set(`U:${id}`, u)
return u
} catch (e) {
return null
return undefined
}
}
getMember = async (server: string, user: string): Promise<?Member> => {
getMember = async (server: string, user: string): Promise<Member | undefined> => {
if (this.cache.has(`M:${server}:${user}`)) {
log.debug('member cache hit')
return this.cache.get(`M:${server}:${user}`)
// log.debug('member cache hit')
return this.cache.get(`M:${server}:${user}`) as Member
}
log.debug('member cache miss')
// log.debug('member cache miss')
try {
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
m.guild = await this.getGuild(server) as Guild // we have to prefill this for whatever reason
return m
} catch (e) {
return null
return undefined
}
}
getGuild = async (server: string): Promise<?Guild> => {
getGuild = async (server: string): Promise<Guild | undefined> => {
if (this.cache.has(`G:${server}`)) {
log.debug('guild cache hit')
return this.cache.get(`G:${server}`)
return this.cache.get(`G:${server}`) as Guild
}
log.debug('guild cache miss')
@ -70,18 +68,17 @@ export default class BotFetcher implements IFetcher {
this.cache.set(`G:${server}`, g)
return g
} catch (e) {
return null
return undefined
}
}
getGuilds = async (): Promise<Guild[]> => {
const last: ?string = undefined
const last: string | undefined = 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)

View file

@ -0,0 +1,15 @@
import {
User,
Member,
Guild
} from 'eris'
export interface IFetcher {
getUser: (id: string) => Promise<User | undefined>
getGuild: (id: string) => Promise<Guild | undefined>
getMember: (server: string, user: string) => Promise<Member | undefined>
getGuilds: () => Promise<Guild[]>
}

View file

@ -1,24 +1,25 @@
// @flow
import Service from './Service'
import LRU from 'lru-cache'
import { type AppContext } from '../Roleypoly'
import { type Models } from '../models'
import { type ServerModel } from '../models/Server'
import type DiscordService from './discord'
import { AppContext } from '../Roleypoly'
import { Models } from '../models'
import { ServerModel } from '../models/Server'
import DiscordService from './discord'
// $FlowFixMe
import type { Sequence } from 'immutable'
import { Sequence } from 'immutable'
import {
type Guild,
type Collection
Guild,
Collection
} from 'eris'
import type {
import {
Member,
PresentableServer,
ServerSlug,
PresentableRole
} from '@roleypoly/types'
import areduce from '../util/areduce'
class PresentationService extends Service {
export default class PresentationService extends Service {
cache: LRU
M: Models
discord: DiscordService
@ -41,7 +42,7 @@ class PresentationService extends Service {
}
presentableServers (collection: Collection<Guild> | Sequence<Guild>, userId: string): Promise<PresentableServer[]> {
return areduce(Array.from(collection.values()), async (acc, server) => {
return areduce<Guild, PresentableServer>(Array.from(collection.values()), async (acc: PresentableServer[], server: Guild) => {
const gm = server.members.get(userId)
if (gm == null) {
throw new Error(`somehow this guildmember ${userId} of ${server.id} didn't exist.`)
@ -49,17 +50,17 @@ class PresentationService extends Service {
acc.push(await this.presentableServer(server, gm, { incRoles: false }))
return acc
}, [])
})
}
async presentableServer (server: Guild, gm: Member, { incRoles = true }: { incRoles: boolean } = {}): Promise<PresentableServer> {
async presentableServer (server: Guild, gm: Member, { incRoles = true }: { incRoles?: boolean } = {}): Promise<PresentableServer> {
const sd = await this.ctx.server.get(server.id)
return {
id: server.id,
gm: {
nickname: gm.nick || gm.user.username,
color: gm?.color,
color: gm.color || null,
roles: gm.roles
},
server: this.serverSlug(server),
@ -82,5 +83,3 @@ class PresentationService extends Service {
}))
}
}
module.exports = PresentationService

View file

@ -1,14 +1,12 @@
// @flow
import Service from './Service'
import { type AppContext } from '../Roleypoly'
import type PresentationService from './presentation'
import {
type Guild
} from 'eris'
import { type ServerModel, type Category } from '@roleypoly/types'
import { AppContext } from '../Roleypoly'
import PresentationService from './presentation'
import { Guild } from 'eris'
import { ServerModel, Category } from '@roleypoly/types'
export default class ServerService extends Service {
Server: * // Model<ServerModel> but flowtype is bugged
Server: any // Model<ServerModel> but flowtype is bugged
P: PresentationService
constructor (ctx: AppContext) {
super(ctx)
@ -17,14 +15,14 @@ export default class ServerService extends Service {
}
async ensure (server: Guild) {
let srv
let srv: ServerModel | undefined = undefined
try {
srv = await this.get(server.id)
} catch (e) {
this.log.info('creating server', server.id)
}
if (srv == null) {
if (srv === undefined) {
return this.create({
id: server.id,
message: '',
@ -39,7 +37,7 @@ export default class ServerService extends Service {
return srv.save()
}
async update (id: string, newData: $Shape<ServerModel>) { // eslint-disable-line no-undef
async update (id: string, newData: Partial<ServerModel>) { // eslint-disable-line no-undef
const srv = await this.get(id, false)
return srv.update(newData)
@ -61,7 +59,7 @@ export default class ServerService extends Service {
async getAllowedRoles (id: string) {
const server: ServerModel = await this.get(id)
const categories: Category[] = (Object.values(server.categories): any)
const categories: Category[] = Object.values(server.categories)
const allowed: string[] = []
for (const c of categories) {

View file

@ -1,6 +1,6 @@
// @flow
import Service from './Service'
import { type AppContext } from '../Roleypoly'
import { AppContext } from '../Roleypoly'
import Sequelize from 'sequelize'
type SessionData = {
rolling: boolean,
@ -9,7 +9,7 @@ type SessionData = {
}
export default class SessionsService extends Service {
Session: any
Session: Sequelize.ModelCtor<any>
constructor (ctx: AppContext) {
super(ctx)
this.Session = ctx.M.Session
@ -25,7 +25,7 @@ export default class SessionsService extends Service {
return user.data
}
async set (id: string, data: any, { maxAge, rolling, changed }: SessionData) {
async set<T> (id: string, data: any, { maxAge, rolling, changed }: SessionData) {
let session = await this.Session.findOne({ where: { id } })
if (session === null) {
session = this.Session.build({ id })
@ -42,7 +42,7 @@ export default class SessionsService extends Service {
async destroy (id: string) {
const sess = await this.Session.findOne({ where: { id } })
if (sess != null) {
if (sess !== null) {
return sess.destroy()
}
}

View file

@ -0,0 +1,13 @@
const areduce = async <T, V> (
array: T[],
predicate: (acc: V[], val: T) => Promise<V[]>,
acc: undefined | V[] = []
): Promise<Array<V>> => {
for (let i of array) {
acc = await predicate(acc, i)
}
return acc
}
export default areduce

View file

@ -4,7 +4,7 @@ import os from 'os'
import { addAwaitOutsideToReplServer } from 'await-outside'
import Roleypoly from '../Roleypoly'
import chokidar from 'chokidar'
import logger from '../logger'
import logger from '../../logger'
// process.env.DEBUG = false
process.env.IS_BOT = false

View file

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./lib",
"plugins": [
{ "transform": "ts-optchain/transform" }
]
},
"include": [
"./src"
]
}

View file

@ -0,0 +1,3 @@
{
"extends": "tslint-config-standard"
}

View file

@ -1,8 +0,0 @@
// @flow
export default async function<T, V> (array: Array<T>, predicate: (Array<V>, T) => Promise<Array<V>>, acc: Array<V>): Promise<Array<V>> {
for (let i of array) {
acc = await predicate(acc, i)
}
return acc
}