flowtyped everything, some functional, safety, and structural changes

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

View file

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

View file

@ -1,37 +1,71 @@
const Service = require('./Service')
const discord = require('discord.js')
const superagent = require('superagent')
// @flow
import Service from './Service'
import superagent from 'superagent'
// import invariant from 'invariant'
import { type AppContext } from '../Roleypoly'
import {
type Message,
type Guild,
type Role,
type GuildMember,
type Collection,
Client
} from 'discord.js'
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 {
constructor (ctx) {
super(ctx)
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'
this.botToken = process.env.DISCORD_BOT_TOKEN
this.clientId = process.env.DISCORD_CLIENT_ID
this.clientSecret = process.env.DISCORD_CLIENT_SECRET
this.oauthCallback = process.env.OAUTH_AUTH_CALLBACK
this.botCallback = `${ctx.config.appUrl}/api/oauth/bot/callback`
this.appUrl = process.env.APP_URL
this.isBot = process.env.IS_BOT === 'true' || false
appUrl: string
botCallback: string
rootUsers: Set<string>
client: Client
cmds: ChatCommand[]
constructor (ctx: AppContext) {
super(ctx)
this.appUrl = ctx.config.appUrl
this.botCallback = `${this.appUrl}/api/oauth/bot/callback`
this.rootUsers = new Set((process.env.ROOT_USERS || '').split(','))
this.client = new discord.Client()
this.client = new Client()
this.client.options.disableEveryone = true
this.cmds = this._cmds()
this._cmds()
this.startBot()
}
ownGm (server) {
ownGm (server: string) {
return this.gm(server, this.client.user.id)
}
fakeGm ({ id = 0, nickname = '[none]', displayHexColor = '#ffffff' }) {
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) {
isRoot (id: string): boolean {
return this.rootUsers.has(id)
}
@ -50,19 +84,27 @@ class DiscordService extends Service {
}
}
getRelevantServers (userId) {
getRelevantServers (userId: string): Collection<string, Guild> {
return this.client.guilds.filter((g) => g.members.has(userId))
}
gm (serverId, userId) {
return this.client.guilds.get(serverId).members.get(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) {
return this.client.guilds.get(server).roles
getRoles (server: string) {
const s = this.client.guilds.get(server)
if (s == null) {
return null
}
return s.roles
}
getPermissions (gm) {
getPermissions (gm: GuildMember): Permissions {
if (this.isRoot(gm.id)) {
return {
isAdmin: true,
@ -71,18 +113,26 @@ class DiscordService extends Service {
}
return {
isAdmin: gm.permissions.hasPermission('ADMINISTRATOR'),
canManageRoles: gm.permissions.hasPermission('MANAGE_ROLES', false, true)
isAdmin: gm.permissions.has('ADMINISTRATOR'),
canManageRoles: gm.permissions.has('MANAGE_ROLES', true)
}
}
safeRole (server, role) {
const r = this.getRoles(server).get(role)
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) {
async getAuthToken (code: string) {
const url = 'https://discordapp.com/api/oauth2/token'
try {
const rsp =
@ -104,7 +154,7 @@ class DiscordService extends Service {
}
}
async getUser (authToken) {
async getUser (authToken?: string): Promise<UserPartial> {
const url = 'https://discordapp.com/api/v6/users/@me'
try {
if (authToken == null || authToken === '') {
@ -146,17 +196,17 @@ class DiscordService extends Service {
// returns oauth authorize url with IDENTIFY permission
// we only need IDENTIFY because we only use it for matching IDs from the bot
getAuthUrl (state) {
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 () {
getBotJoinUrl (): string {
return `https://discordapp.com/oauth2/authorize?client_id=${this.clientId}&scope=bot&permissions=268435456`
}
mentionResponse (message) {
mentionResponse (message: Message) {
message.channel.send(`🔰 Assign your roles here! <${this.appUrl}/s/${message.guild.id}>`, { disableEveryone: true })
}
@ -193,10 +243,11 @@ class DiscordService extends Service {
// 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) {
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) {
@ -218,8 +269,8 @@ class DiscordService extends Service {
this.mentionResponse(message)
}
handleMessage (message) {
if (message.author.bot && message.channel.type !== 'text') { // drop bot messages and dms
handleMessage (message: Message) {
if (message.author.bot || message.channel.type !== 'text') { // drop bot messages and dms
return
}
@ -232,7 +283,7 @@ class DiscordService extends Service {
}
}
async handleJoin (guild) {
async handleJoin (guild: Guild) {
await this.ctx.server.ensure(guild)
}
}

View file

@ -1,8 +1,49 @@
const Service = require('./Service')
const LRU = require('lru-cache')
// @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, { Permissions } from './discord'
import {
type Guild,
type GuildMember,
type Collection
} from 'discord.js'
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: {
nickname: string,
color: string
},
server: ServerSlug,
roles: ?PresentableRole[],
perms: Permissions
}
class PresentationService extends Service {
constructor (ctx) {
cache: LRU
M: Models
discord: DiscordService
constructor (ctx: AppContext) {
super(ctx)
this.M = ctx.M
this.discord = ctx.discord
@ -10,7 +51,7 @@ class PresentationService extends Service {
this.cache = new LRU({ max: 500, maxAge: 100 * 60 * 5 })
}
serverSlug (server) {
serverSlug (server: Guild): ServerSlug {
return {
id: server.id,
name: server.name,
@ -19,27 +60,34 @@ class PresentationService extends Service {
}
}
async oldPresentableServers (collection, userId) {
async oldPresentableServers (collection: Collection<string, Guild>, userId: string) {
this.log.deprecated('use presentableServers instead of oldPresentableServers')
let servers = []
for (let server of collection.array()) {
const gm = server.members.get(userId)
// $FlowFixMe this is deprecated, forget adding more check code.
servers.push(await this.presentableServer(server, gm))
}
return servers
}
async presentableServers (collection, userId) {
return collection.array().areduce(async (acc, server) => {
presentableServers (collection: Collection<string, Guild>, userId: string) {
return areduce(collection.array(), 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.`)
}
acc.push(await this.presentableServer(server, gm, { incRoles: false }))
return acc
})
}, [])
}
async presentableServer (server, gm, { incRoles = true } = {}) {
async presentableServer (server: Guild, gm: GuildMember, { incRoles = true }: { incRoles: boolean } = {}): Promise<PresentableServer> {
const sd = await this.ctx.server.get(server.id)
return {
@ -56,7 +104,7 @@ class PresentationService extends Service {
}
}
async rolesByServer (server) {
async rolesByServer (server: Guild, sd: ServerModel): Promise<PresentableRole[]> {
return server.roles
.filter(r => r.id !== server.id) // get rid of @everyone
.map(r => ({

View file

@ -1,13 +1,22 @@
const Service = require('./Service')
// @flow
import Service from './Service'
import { type AppContext } from '../Roleypoly'
import type PresentationService from './presentation'
import {
type Guild
} from 'discord.js'
import { type ServerModel, type Category } from '../models/Server'
class ServerService extends Service {
constructor (ctx) {
export default class ServerService extends Service {
Server: any // Model<ServerModel> but flowtype is bugged
P: PresentationService
constructor (ctx: AppContext) {
super(ctx)
this.Server = ctx.M.Server
this.P = ctx.P
}
async ensure (server) {
async ensure (server: Guild) {
let srv
try {
srv = await this.get(server.id)
@ -24,19 +33,19 @@ class ServerService extends Service {
}
}
create ({ id, message, categories }) {
create ({ id, message, categories }: ServerModel) {
const srv = this.Server.build({ id, message, categories })
return srv.save()
}
async update (id, newData) {
async update (id: string, newData: $Shape<ServerModel>) { // eslint-disable-line no-undef
const srv = await this.get(id, false)
return srv.update(newData)
}
async get (id, plain = true) {
async get (id: string, plain: boolean = true) {
const s = await this.Server.findOne({
where: {
id
@ -50,17 +59,17 @@ class ServerService extends Service {
return s.get({ plain: true })
}
async getAllowedRoles (id) {
const server = await this.get(id)
async getAllowedRoles (id: string) {
const server: ServerModel = await this.get(id)
const categories: Category[] = (Object.values(server.categories): any)
const allowed: string[] = []
return Object.values(server.categories).reduce((acc, c) => {
for (const c of categories) {
if (c.hidden !== true) {
return acc.concat(c.roles)
allowed.concat(c.roles)
}
}
return acc
}, [])
return allowed
}
}
module.exports = ServerService

View file

@ -1,12 +1,21 @@
const Service = require('./Service')
// @flow
import Service from './Service'
import { type AppContext } from '../Roleypoly'
class SessionsService extends Service {
constructor (ctx) {
type SessionData = {
rolling: boolean,
maxAge: number,
changed: boolean
}
export default class SessionsService extends Service {
Session: any
constructor (ctx: AppContext) {
super(ctx)
this.Session = ctx.M.Session
}
async get (id, { rolling }) {
async get (id: string, { rolling }: SessionData) {
const user = await this.Session.findOne({ where: { id } })
if (user === null) {
@ -16,7 +25,7 @@ class SessionsService extends Service {
return user.data
}
async set (id, data, { maxAge, rolling, changed }) {
async set (id: string, data: any, { maxAge, rolling, changed }: SessionData) {
let session = await this.Session.findOne({ where: { id } })
if (session === null) {
session = this.Session.build({ id })
@ -30,7 +39,7 @@ class SessionsService extends Service {
return session.save()
}
async destroy (id) {
async destroy (id: string) {
const sess = await this.Session.findOne({ where: { id } })
if (sess != null) {
@ -38,5 +47,3 @@ class SessionsService extends Service {
}
}
}
module.exports = SessionsService