lerna: starting point

This commit is contained in:
41666 2019-04-02 23:05:19 -05:00
parent f7e2898633
commit cb0b1d2410
No known key found for this signature in database
GPG key ID: BC51D07640DC10AF
17 changed files with 730 additions and 1067 deletions

View file

@ -61,6 +61,7 @@ export default (R: Router, $: AppContext) => {
const { r } = ctx.query const { r } = ctx.query
// check if already authed // check if already authed
if (await $.auth.isLoggedIn(ctx, { refresh: true })) { if (await $.auth.isLoggedIn(ctx, { refresh: true })) {
log.debug('already authed.', ctx.session)
return ctx.redirect(r || '/') return ctx.redirect(r || '/')
} }
@ -79,6 +80,7 @@ export default (R: Router, $: AppContext) => {
const { oauthRedirect: r } = ctx.session const { oauthRedirect: r } = ctx.session
delete ctx.session.oauthRedirect delete ctx.session.oauthRedirect
if (await $.auth.isLoggedIn(ctx)) { if (await $.auth.isLoggedIn(ctx)) {
log.debug('user was logged in')
return ctx.redirect(r || '/') return ctx.redirect(r || '/')
} }
@ -108,6 +110,7 @@ export default (R: Router, $: AppContext) => {
const tokens = await $.discord.initializeOAuth(code) const tokens = await $.discord.initializeOAuth(code)
const user = await $.discord.getUserFromToken(tokens.access_token) const user = await $.discord.getUserFromToken(tokens.access_token)
$.auth.injectSessionFromOAuth(ctx, tokens, user.id) $.auth.injectSessionFromOAuth(ctx, tokens, user.id)
log.debug('user logged in', { tokens, user, s: ctx.session })
return ctx.redirect(r || '/') return ctx.redirect(r || '/')
} catch (e) { } catch (e) {
log.error('token and auth fetch failure', e) log.error('token and auth fetch failure', e)

View file

@ -100,15 +100,6 @@ export default (R: Router, $: AppContext) => {
ctx.body = { ok: true } ctx.body = { ok: true }
}) })
R.get('/api/admin/servers', async (ctx: Context) => {
const { userId } = (ctx.session: { userId: string })
if (!$.discord.isRoot(userId)) {
return
}
ctx.body = $.discord.client.guilds.map(g => ({ url: `${$.config.appUrl}/s/${g.id}`, name: g.name, members: g.members.array().length, roles: g.roles.array().length }))
})
R.patch('/api/servers/:server/roles', async (ctx: Context) => { R.patch('/api/servers/:server/roles', async (ctx: Context) => {
const { userId } = (ctx.session: { userId: string }) const { userId } = (ctx.session: { userId: string })
const { server } = (ctx.params: { server: string }) const { server } = (ctx.params: { server: string })
@ -126,30 +117,20 @@ export default (R: Router, $: AppContext) => {
} }
} }
const { added, removed } = ((ctx.request.body: any): { added: string[], removed: string[] }) const originalRoles = gm.roles
let { added, removed } = ((ctx.request.body: any): { added: string[], removed: string[] })
const allowedRoles = await $.server.getAllowedRoles(server) const allowedRoles: string[] = await $.server.getAllowedRoles(server)
const pred = r => $.discord.safeRole(server, r) && allowedRoles.indexOf(r) !== -1 const isSafe = (r: string) => $.discord.safeRole(server, r) && allowedRoles.includes(r)
if (added.length > 0) { added = added.filter(isSafe)
gm = await gm.addRoles(added.filter(pred)) removed = removed.filter(isSafe)
}
setTimeout(() => { const newRoles = originalRoles.concat(added).filter(x => !removed.includes(x))
if (gm == null) { gm.edit({
ctx.body = { roles: newRoles
err: 'guild member disappeared on remove, this should never happen.' })
}
ctx.status = 500
return
}
if (removed.length > 0) {
gm.removeRoles(removed.filter(pred))
}
}, 1000)
ctx.body = { ok: true } ctx.body = { ok: true }
}) })

View file

@ -3,5 +3,5 @@ module.exports = {
'.', '.',
'./ui' './ui'
], ],
ignore: [ './node_modules', './flow-typed' ] ignore: [ './node_modules', './flow-typed', './ui' ]
} }

View file

@ -10,4 +10,6 @@ export default class Bot {
this.svc = DS this.svc = DS
this.log = log this.log = log
} }
} }

BIN
branding/Untitled.sketch Normal file

Binary file not shown.

View file

@ -38,7 +38,7 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"moniker": "^0.1.2", "moniker": "^0.1.2",
"nanoid": "^2.0.1", "nanoid": "^2.0.1",
"next": "^8.0.3", "next": "8.0.3",
"next-redux-wrapper": "^3.0.0-alpha.2", "next-redux-wrapper": "^3.0.0-alpha.2",
"pg": "^7.9.0", "pg": "^7.9.0",
"pg-hstore": "^2.3.2", "pg-hstore": "^2.3.2",
@ -75,9 +75,9 @@
"enzyme-adapter-react-16": "^1.11.2", "enzyme-adapter-react-16": "^1.11.2",
"enzyme-to-json": "^3.3.5", "enzyme-to-json": "^3.3.5",
"eslint-plugin-flowtype": "^3.4.2", "eslint-plugin-flowtype": "^3.4.2",
"flow-bin": "^0.95.1", "flow-bin": "^0.96.0",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"jest": "^24.5.0", "jest": "^24.6.0",
"jest-styled-components": "^6.3.1", "jest-styled-components": "^6.3.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"react-test-renderer": "^16.8.6", "react-test-renderer": "^16.8.6",

View file

@ -109,25 +109,35 @@ export default class RPCServer {
} }
rpcError (ctx: Context & {body: any}, msg: ?string, err: ?Error = null, code: ?number = null) { rpcError (ctx: Context & {body: any}, msg: ?string, err: ?Error = null, code: ?number = null) {
log.error('rpc error', { msg, err }) // log.error('rpc error', { msg, err })
ctx.body = { ctx.body = {
msg, msg: err?.message || msg,
error: true error: true
} }
if (err instanceof RPCError) { ctx.status = code || 500
// this is one of our own errors, so we have a lot of data on this. if (err != null) {
ctx.status = err.code || code || 500 if (err instanceof RPCError || err.constructor.name === 'RPCError') {
ctx.body.msg = `${msg || 'RPC Error'}: ${err.message}` // $FlowFixMe
} else { ctx.status = err.code
if (msg == null && err != null) { console.log(err.code)
ctx.body.msg = msg = err.message
} }
// if not, just play cloudflare, say something was really weird, and move on.
ctx.status = code || 520
ctx.message = 'Unknown Error'
} }
// if (err != null && err.constructor.name === 'RPCError') {
// console.log({ status: err.code })
// // this is one of our own errors, so we have a lot of data on this.
// ctx.status = err.code // || code || 500
// ctx.body.msg = `${err.message || msg || 'RPC Error'}`
// } else {
// if (msg == null && err != null) {
// ctx.body.msg = err.message
// }
// // if not, just play cloudflare, say something was really weird, and move on.
// ctx.status = code || 520
// ctx.message = 'Unknown Error'
// }
} }
} }

View file

@ -5,14 +5,12 @@ import * as secureAs from './_security'
export default ($: AppContext) => ({ export default ($: AppContext) => ({
getCurrentUser: secureAs.authed($, (ctx: Context) => { getCurrentUser: secureAs.authed($, async (ctx: Context) => {
return $.discord.getUserPartial(ctx.session.userId) return await $.discord.getUserPartial(ctx.session.userId)
}), }),
isRoot: secureAs.root($, () => { isRoot: secureAs.root($, () => {
return true return true
}) })
}) })

View file

@ -27,7 +27,8 @@ export type Permissions = {
type CachedRole = { type CachedRole = {
id: string, id: string,
position: number position: number,
color?: number
} }
type OAuthRequestData = { type OAuthRequestData = {
@ -48,6 +49,11 @@ export type UserPartial = {
avatar: string avatar: string
} }
export type MemberExt = Member & {
color?: number,
__faked?: true
}
export default class DiscordService extends Service { export default class DiscordService extends Service {
ctx: AppContext ctx: AppContext
bot: Bot bot: Bot
@ -57,6 +63,7 @@ export default class DiscordService extends Service {
// a small cache of role data for checking viability // a small cache of role data for checking viability
ownRoleCache: LRU<string, CachedRole> ownRoleCache: LRU<string, CachedRole>
topRoleCache: LRU<string, CachedRole>
oauthCallback: string oauthCallback: string
@ -77,6 +84,7 @@ export default class DiscordService extends Service {
this.oauthCallback = `${ctx.config.appUrl}/api/oauth/callback` this.oauthCallback = `${ctx.config.appUrl}/api/oauth/callback`
this.ownRoleCache = new LRU() this.ownRoleCache = new LRU()
this.topRoleCache = new LRU()
if (this.cfg.isBot) { if (this.cfg.isBot) {
this.client = new Eris(this.cfg.token, { this.client = new Eris(this.cfg.token, {
@ -118,30 +126,42 @@ export default class DiscordService extends Service {
return this.client.guilds.filter(guild => guild.members.has(user)) return this.client.guilds.filter(guild => guild.members.has(user))
} }
gm (serverId: string, userId: string): ?Member { async gm (serverId: string, userId: string, { canFake = false }: { canFake: boolean } = {}): Promise<?MemberExt> {
return this.client.guilds.get(serverId)?.members.get(userId) const gm: ?Member = await this.fetcher.getMember(serverId, userId)
if (gm == null && this.isRoot(userId)) {
return this.fakeGm({ id: userId })
}
if (gm == null) {
return null
}
const out: MemberExt = gm
out.color = this.getHighestRole(gm).color
return out
} }
ownGm (serverId: string) { ownGm (serverId: string) {
return this.gm(serverId, this.client.user.id) return this.gm(serverId, this.client.user.id)
} }
fakeGm ({ id = '0', nickname = '[none]', displayHexColor = '#ffffff' }: $Shape<Member>): $Shape<Member> { fakeGm ({ id = '0', nick = '[none]', color = 0 }: $Shape<MemberExt>): $Shape<MemberExt> {
return { id, nickname, displayHexColor, __faked: true, roles: { has () { return false } } } return { id, nick, color, __faked: true, roles: [] }
} }
getRoles (server: string) { getRoles (server: string) {
return this.client.guilds.get(server)?.roles return this.client.guilds.get(server)?.roles
} }
getOwnPermHeight (server: Guild): number { async getOwnPermHeight (server: Guild): Promise<number> {
if (this.ownRoleCache.has(server)) { if (this.ownRoleCache.has(server)) {
return this.ownRoleCache.get(server).position const r = this.ownRoleCache.get(server)
return r.position
} }
const gm = this.ownGm(server.id) const gm = await this.ownGm(server.id)
const g = gm?.guild const g = gm?.guild
const r: Role = OrderedSet(gm?.roles).map(id => g?.roles.get(id)).sortBy(r => r.position).last({ position: 0, id: '0' }) const r: Role = OrderedSet(gm?.roles).map(id => g?.roles.get(id)).minBy(r => r.position)
this.ownRoleCache.set(server, { this.ownRoleCache.set(server, {
id: r.id, id: r.id,
position: r.position position: r.position
@ -150,6 +170,26 @@ export default class DiscordService extends Service {
return r.position return r.position
} }
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) {
return r
}
}
const g = gm.guild
const top = OrderedSet(gm.roles).map(id => g.roles.get(id)).minBy(r => r.position)
this.topRoleCache.set(trk, {
id: top.id,
position: top.position,
color: top.color
})
return top
}
calcPerms (permable: Role | Member): Permissions { calcPerms (permable: Role | Member): Permissions {
const p: ErisPermission = (permable instanceof Role) ? permable.permissions : permable.permission const p: ErisPermission = (permable instanceof Role) ? permable.permissions : permable.permission
return { return {
@ -173,18 +213,19 @@ export default class DiscordService extends Service {
return real return real
} }
safeRole (server: string, role: string) { async safeRole (server: string, role: string) {
const r = this.getRoles(server)?.get(role) const r = this.getRoles(server)?.get(role)
if (r == null) { if (r == null) {
throw new Error(`safeRole can't find ${role} in ${server}`) throw new Error(`safeRole can't find ${role} in ${server}`)
} }
return this.roleIsEditable(r) && !this.calcPerms(r).canManageRoles return (await this.roleIsEditable(r)) && !this.calcPerms(r).canManageRoles
} }
roleIsEditable (role: Role): boolean { async roleIsEditable (role: Role): Promise<boolean> {
// role will be LOWER than my own // role will be LOWER than my own
return this.getOwnPermHeight(role.guild) > role.position const ownPh = await this.getOwnPermHeight(role.guild)
return ownPh > role.position
} }
async oauthRequest (path: string, data: OAuthRequestData) { async oauthRequest (path: string, data: OAuthRequestData) {
@ -228,8 +269,8 @@ export default class DiscordService extends Service {
}) })
} }
getUserPartial (userId: string): ?UserPartial { async getUserPartial (userId: string): Promise<?UserPartial> {
const u = this.client.users.get(userId) const u = await this.fetcher.getUser(userId)
if (u == null) { if (u == null) {
return null return null
} }
@ -295,11 +336,16 @@ export default class DiscordService extends Service {
].join('\n')) ].join('\n'))
} }
canManageRoles (server: string, user: string) { async canManageRoles (server: string, user: string): Promise<boolean> {
return this.getPermissions(this.gm(server, user)).canManageRoles const gm = await this.gm(server, user)
if (gm == null) {
return false
}
return this.getPermissions(gm).canManageRoles
} }
isMember (server: string, user: string) { isMember (server: string, user: string): boolean {
return this.gm(server, user) != null return this.gm(server, user) != null
} }
} }

View file

@ -1,7 +1,7 @@
// @flow // @flow
import type { IFetcher } from './types' import type { IFetcher } from './types'
import type DiscordSvc from '../discord' import type DiscordSvc from '../discord'
import type ErisClient from 'eris' import type ErisClient, { User, Member, Guild } from 'eris'
export default class BotFetcher implements IFetcher { export default class BotFetcher implements IFetcher {
ctx: DiscordSvc ctx: DiscordSvc
@ -11,7 +11,12 @@ export default class BotFetcher implements IFetcher {
this.client = ctx.client this.client = ctx.client
} }
getUser = async (id: string) => this.client.users.get(id) getUser = async (id: string): Promise<?User> =>
getMember = async (server: string, user: string) => this.client.guilds.get(server)?.members.get(user) this.client.users.get(id)
getGuild = async (server: string) => this.client.guilds.get(server)
getMember = async (server: string, user: string): Promise<?Member> =>
this.client.guilds.get(server)?.members.get(user)
getGuild = async (server: string): Promise<?Guild> =>
this.client.guilds.get(server)
} }

View file

@ -4,8 +4,8 @@ import { colors } from '../global-colors'
import Color from 'color' import Color from 'color'
import RoleStyled from './role.styled' import RoleStyled from './role.styled'
import Tooltip from '../tooltip' import Tooltip from '../tooltip'
import logger from '../../../logger' // import logger from '../../../logger'
const log = logger(__filename) // const log = logger(__filename)
const fromColors = (colors) => Object.entries(colors).reduce( const fromColors = (colors) => Object.entries(colors).reduce(
(acc, [v, val]) => ({ ...acc, [`--role-color-${v}`]: val }) (acc, [v, val]) => ({ ...acc, [`--role-color-${v}`]: val })
@ -45,15 +45,15 @@ export default class Role extends React.Component<RoleProps, RoleState> {
} }
onMouseOver = () => { onMouseOver = () => {
log.debug('caught hovering') // log.debug('caught hovering')
if (this.props.disabled && this.state.hovering === false) { if (this.props.disabled && this.state.hovering === false) {
log.debug('set hovering') // log.debug('set hovering')
this.setState({ hovering: true }) this.setState({ hovering: true })
} }
} }
onMouseOut = () => { onMouseOut = () => {
log.debug('out hovering') // log.debug('out hovering')
this.setState({ hovering: false }) this.setState({ hovering: false })
} }

View file

@ -16,7 +16,7 @@ const defaultProps: SocialCardProps = {
imageSize: 200 imageSize: 200
} }
const SocialCards: React.StatelessFunctionalComponent<SocialCardProps> = (props) => { const SocialCards = (props: SocialCardProps) => {
props = { props = {
...defaultProps, ...defaultProps,
...props ...props

View file

@ -10,7 +10,7 @@ type Props = {
const HeaderBarAuth = dynamic(() => import('../components/header/auth')) const HeaderBarAuth = dynamic(() => import('../components/header/auth'))
const HeaderBarUnauth = dynamic(() => import('../components/header/unauth')) const HeaderBarUnauth = dynamic(() => import('../components/header/unauth'))
const HeaderBar: React.StatelessFunctionalComponent<Props> = (props) => { const HeaderBar = (props: Props) => {
if (props.user == null) { if (props.user == null) {
return <HeaderBarUnauth {...props} /> return <HeaderBarUnauth {...props} />
} else { } else {

View file

@ -8,6 +8,7 @@ import { Provider } from 'react-redux'
import ErrorP, { Overlay } from './_error' import ErrorP, { Overlay } from './_error'
import styled from 'styled-components' import styled from 'styled-components'
import { withRedux } from '../config/redux' import { withRedux } from '../config/redux'
import type { UserPartial } from '../../services/discord'
type NextPage = React.Component<any> & React.StatelessFunctionalComponent<any> & { type NextPage = React.Component<any> & React.StatelessFunctionalComponent<any> & {
getInitialProps: (ctx: any, ...args: any) => any getInitialProps: (ctx: any, ...args: any) => any
@ -35,9 +36,18 @@ class RoleypolyApp extends App {
let pageProps = {} let pageProps = {}
const rpc = withCookies(ctx) const rpc = withCookies(ctx)
let user: ?UserPartial
const user = await rpc.getCurrentUser() try {
ctx.user = user user = await rpc.getCurrentUser()
ctx.user = user
} catch (e) {
if (e.code === 403) {
ctx.user = null
} else {
console.error(e)
throw e
}
}
ctx.robots = 'INDEX, FOLLOW' ctx.robots = 'INDEX, FOLLOW'
ctx.layout = { ctx.layout = {

View file

@ -36,7 +36,7 @@ const RoleHolder = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
` `
class Server extends React.Component<ServerPageProps> { class Server extends React.PureComponent<ServerPageProps> {
static async getInitialProps (ctx: *, rpc: *, router: *) { static async getInitialProps (ctx: *, rpc: *, router: *) {
const isDiscordBot = ctx.req && ctx.req.headers['user-agent'].includes('Discordbot') const isDiscordBot = ctx.req && ctx.req.headers['user-agent'].includes('Discordbot')
if (ctx.user == null) { if (ctx.user == null) {

View file

@ -25,7 +25,7 @@ export default class RPCClient {
cookieHeader: string cookieHeader: string
rpc: { rpc: {
[fn: string]: (...args: any[]) => Promise<mixed> | string [fn: string]: (...args: any[]) => Promise<any>
} = {} } = {}
__rpcAvailable: Array<{ __rpcAvailable: Array<{
@ -42,11 +42,12 @@ export default class RPCClient {
this.dev = process.env.NODE_ENV === 'development' this.dev = process.env.NODE_ENV === 'development'
} }
this.rpc = new Proxy({ this.rpc = new Proxy({}, {
toJSON () { get: this.__rpcCall,
return '{}' has: this.__checkCall,
} ownKeys: this.__listCalls,
}, { get: this.__rpcCall, has: this.__checkCall, ownKeys: this.__listCalls, delete: () => {} }) delete: () => {}
})
if (this.dev) { if (this.dev) {
this.updateCalls() this.updateCalls()
@ -90,7 +91,9 @@ export default class RPCClient {
} }
const rsp = await rq.send(req).ok(() => true) const rsp = await rq.send(req).ok(() => true)
const body: RPCResponse = rsp.body const body: RPCResponse = rsp.body
// console.log(body)
if (body.error === true) { if (body.error === true) {
console.error(body)
throw RPCError.fromResponse(body, rsp.status) throw RPCError.fromResponse(body, rsp.status)
} }

1561
yarn.lock

File diff suppressed because it is too large Load diff