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

View file

@ -100,15 +100,6 @@ export default (R: Router, $: AppContext) => {
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) => {
const { userId } = (ctx.session: { userId: 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) {
gm = await gm.addRoles(added.filter(pred))
}
added = added.filter(isSafe)
removed = removed.filter(isSafe)
setTimeout(() => {
if (gm == null) {
ctx.body = {
err: 'guild member disappeared on remove, this should never happen.'
}
ctx.status = 500
return
}
if (removed.length > 0) {
gm.removeRoles(removed.filter(pred))
}
}, 1000)
const newRoles = originalRoles.concat(added).filter(x => !removed.includes(x))
gm.edit({
roles: newRoles
})
ctx.body = { ok: true }
})

View file

@ -3,5 +3,5 @@ module.exports = {
'.',
'./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.log = log
}
}

BIN
branding/Untitled.sketch Normal file

Binary file not shown.

View file

@ -38,7 +38,7 @@
"moment": "^2.24.0",
"moniker": "^0.1.2",
"nanoid": "^2.0.1",
"next": "^8.0.3",
"next": "8.0.3",
"next-redux-wrapper": "^3.0.0-alpha.2",
"pg": "^7.9.0",
"pg-hstore": "^2.3.2",
@ -75,9 +75,9 @@
"enzyme-adapter-react-16": "^1.11.2",
"enzyme-to-json": "^3.3.5",
"eslint-plugin-flowtype": "^3.4.2",
"flow-bin": "^0.95.1",
"flow-bin": "^0.96.0",
"flow-typed": "^2.5.1",
"jest": "^24.5.0",
"jest": "^24.6.0",
"jest-styled-components": "^6.3.1",
"npm-run-all": "^4.1.5",
"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) {
log.error('rpc error', { msg, err })
// log.error('rpc error', { msg, err })
ctx.body = {
msg,
msg: err?.message || msg,
error: true
}
if (err instanceof RPCError) {
// 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 = `${msg || 'RPC Error'}: ${err.message}`
} else {
if (msg == null && err != null) {
ctx.body.msg = msg = err.message
ctx.status = code || 500
if (err != null) {
if (err instanceof RPCError || err.constructor.name === 'RPCError') {
// $FlowFixMe
ctx.status = err.code
console.log(err.code)
}
// 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) => ({
getCurrentUser: secureAs.authed($, (ctx: Context) => {
return $.discord.getUserPartial(ctx.session.userId)
getCurrentUser: secureAs.authed($, async (ctx: Context) => {
return await $.discord.getUserPartial(ctx.session.userId)
}),
isRoot: secureAs.root($, () => {
return true
})
})

View file

@ -27,7 +27,8 @@ export type Permissions = {
type CachedRole = {
id: string,
position: number
position: number,
color?: number
}
type OAuthRequestData = {
@ -48,6 +49,11 @@ export type UserPartial = {
avatar: string
}
export type MemberExt = Member & {
color?: number,
__faked?: true
}
export default class DiscordService extends Service {
ctx: AppContext
bot: Bot
@ -57,6 +63,7 @@ export default class DiscordService extends Service {
// a small cache of role data for checking viability
ownRoleCache: LRU<string, CachedRole>
topRoleCache: LRU<string, CachedRole>
oauthCallback: string
@ -77,6 +84,7 @@ export default class DiscordService extends Service {
this.oauthCallback = `${ctx.config.appUrl}/api/oauth/callback`
this.ownRoleCache = new LRU()
this.topRoleCache = new LRU()
if (this.cfg.isBot) {
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))
}
gm (serverId: string, userId: string): ?Member {
return this.client.guilds.get(serverId)?.members.get(userId)
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)) {
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) {
return this.gm(serverId, this.client.user.id)
}
fakeGm ({ id = '0', nickname = '[none]', displayHexColor = '#ffffff' }: $Shape<Member>): $Shape<Member> {
return { id, nickname, displayHexColor, __faked: true, roles: { has () { return false } } }
fakeGm ({ id = '0', nick = '[none]', color = 0 }: $Shape<MemberExt>): $Shape<MemberExt> {
return { id, nick, color, __faked: true, roles: [] }
}
getRoles (server: string) {
return this.client.guilds.get(server)?.roles
}
getOwnPermHeight (server: Guild): number {
async getOwnPermHeight (server: Guild): Promise<number> {
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 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, {
id: r.id,
position: r.position
@ -150,6 +170,26 @@ export default class DiscordService extends Service {
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 {
const p: ErisPermission = (permable instanceof Role) ? permable.permissions : permable.permission
return {
@ -173,18 +213,19 @@ export default class DiscordService extends Service {
return real
}
safeRole (server: string, role: string) {
async safeRole (server: string, role: string) {
const r = this.getRoles(server)?.get(role)
if (r == null) {
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
return this.getOwnPermHeight(role.guild) > role.position
const ownPh = await this.getOwnPermHeight(role.guild)
return ownPh > role.position
}
async oauthRequest (path: string, data: OAuthRequestData) {
@ -228,8 +269,8 @@ export default class DiscordService extends Service {
})
}
getUserPartial (userId: string): ?UserPartial {
const u = this.client.users.get(userId)
async getUserPartial (userId: string): Promise<?UserPartial> {
const u = await this.fetcher.getUser(userId)
if (u == null) {
return null
}
@ -295,11 +336,16 @@ export default class DiscordService extends Service {
].join('\n'))
}
canManageRoles (server: string, user: string) {
return this.getPermissions(this.gm(server, user)).canManageRoles
async canManageRoles (server: string, user: string): Promise<boolean> {
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
}
}

View file

@ -1,7 +1,7 @@
// @flow
import type { IFetcher } from './types'
import type DiscordSvc from '../discord'
import type ErisClient from 'eris'
import type ErisClient, { User, Member, Guild } from 'eris'
export default class BotFetcher implements IFetcher {
ctx: DiscordSvc
@ -11,7 +11,12 @@ export default class BotFetcher implements IFetcher {
this.client = ctx.client
}
getUser = async (id: string) => this.client.users.get(id)
getMember = async (server: string, user: string) => this.client.guilds.get(server)?.members.get(user)
getGuild = async (server: string) => this.client.guilds.get(server)
getUser = async (id: string): Promise<?User> =>
this.client.users.get(id)
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 RoleStyled from './role.styled'
import Tooltip from '../tooltip'
import logger from '../../../logger'
const log = logger(__filename)
// import logger from '../../../logger'
// const log = logger(__filename)
const fromColors = (colors) => Object.entries(colors).reduce(
(acc, [v, val]) => ({ ...acc, [`--role-color-${v}`]: val })
@ -45,15 +45,15 @@ export default class Role extends React.Component<RoleProps, RoleState> {
}
onMouseOver = () => {
log.debug('caught hovering')
// log.debug('caught hovering')
if (this.props.disabled && this.state.hovering === false) {
log.debug('set hovering')
// log.debug('set hovering')
this.setState({ hovering: true })
}
}
onMouseOut = () => {
log.debug('out hovering')
// log.debug('out hovering')
this.setState({ hovering: false })
}

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import { Provider } from 'react-redux'
import ErrorP, { Overlay } from './_error'
import styled from 'styled-components'
import { withRedux } from '../config/redux'
import type { UserPartial } from '../../services/discord'
type NextPage = React.Component<any> & React.StatelessFunctionalComponent<any> & {
getInitialProps: (ctx: any, ...args: any) => any
@ -35,9 +36,18 @@ class RoleypolyApp extends App {
let pageProps = {}
const rpc = withCookies(ctx)
const user = await rpc.getCurrentUser()
ctx.user = user
let user: ?UserPartial
try {
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.layout = {

View file

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

View file

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

1561
yarn.lock

File diff suppressed because it is too large Load diff