Compare commits

..

477 commits

Author SHA1 Message Date
cca0bb5c42 fix padding 2023-06-09 03:21:26 -04:00
8c845d5d31 remove #0 discriminators 2023-06-09 03:16:24 -04:00
45a1f45055
Fix role ordering (#789)
* fix role order in picker

* one of these sorts will flip it omg

* fix role list

* revert forced role sort in api
2023-05-28 11:15:44 -04:00
33a8a0048f fix ci 2023-05-28 09:48:03 -04:00
dc1e7718bc remove bot, force role order 2023-05-28 09:47:19 -04:00
677d91b17b this literally doesn't fail locally, removing tests. 2023-05-22 00:31:20 -04:00
5f7f4c05a5 something is wrong with CI :( 2023-05-22 00:25:24 -04:00
15db46c1b2 something is wrong with CI :( 2023-05-22 00:19:56 -04:00
dc1a65941f update ulid-worker 2023-05-22 00:14:10 -04:00
f7e1c32626 lower guild cache time to 15 minutes 2023-05-22 00:05:40 -04:00
dec4aa9619 Revert "flip role order"
This reverts commit 175144cc7a.
2023-02-19 11:45:30 -05:00
175144cc7a flip role order 2023-02-18 15:07:30 -05:00
a682ade1d3 add terms and privacy 2022-10-21 10:19:15 -04:00
0e4a228f1b chore: remove legacy import -- fix tests, remove old code 2022-09-01 09:28:09 -04:00
c909b01767 chore: remove legacy import 2022-09-01 08:55:24 -04:00
edcd8f0f98 fix(ui): comment out server utilities as none are active features. (fixes #639) 2022-07-26 17:07:21 -04:00
b427907f34 fix(api): remove prompt=none temporarily -- tests 2022-07-26 13:04:24 -04:00
25107951de fix(api): remove prompt=none temporarily 2022-07-26 12:45:04 -04:00
71d35ba36a fix(design-system): molecules/user-popover: discord invite link 2022-07-25 13:21:48 -04:00
f2fe4438d1 send full URL on bot mention, not just public url. 2022-02-08 05:54:40 -05:00
0da1e44147 cleanup ci refresh merge 2022-02-06 23:34:38 -05:00
9b42928608 Merge branch 'ci/refresh' into main 2022-02-06 23:33:46 -05:00
a282f32c29 reduce intents to just GUILD_MESSAGES 2022-02-06 23:33:39 -05:00
c0a11fc9ee create before destroy 2022-02-06 23:18:38 -05:00
54abcaeaa7 potentially ancient typo?? 2022-02-06 23:11:49 -05:00
6f3f805b4a set name of output 2022-02-06 22:59:34 -05:00
06737f4bc7 remove debug, but reduce meta-ing the docker digest 2022-02-06 22:56:44 -05:00
d116caccf2 debug some stuff 2022-02-06 22:46:59 -05:00
2b1bb916cf allow stopping for vm updates 2022-02-06 22:30:51 -05:00
9bdbc98c4f scale prod back up to e2-small 2022-02-06 22:26:19 -05:00
b5cfc13793 upgrade container vm module and surrounding info 2022-02-06 22:06:24 -05:00
6df856ac49 scale down bot since rusty-bot uses less resources 2022-02-06 21:36:20 -05:00
9e057e8692 debian pls 2022-02-06 20:11:35 -05:00
3c49529fe3 rust needs way more than I realized 2022-02-06 20:08:09 -05:00
ce77046d86 cargo bin is named bot, not app 2022-02-06 19:36:28 -05:00
9611b26378 fix docker build context 2022-02-06 19:26:06 -05:00
f5fb729ce7 port bot to rust, upgrade response, and cut away some of the tf cruft 2022-02-06 19:16:08 -05:00
0a37eff047 switch all interactions registrations to global 2022-02-04 21:32:22 -05:00
5aa5a6ae1c add /pick-role and /remove-role, refactor responses 2022-02-04 21:21:32 -05:00
0836d548b2 add an instant cache refresh to the editor 2022-02-04 11:59:11 -05:00
68b2b7323b fix tests, add /roleypoly and /pickable-roles handlers 2022-02-04 11:16:18 -05:00
8c61bfd4c7 add Embed footer to interactions types 2022-02-04 01:39:00 -05:00
5c5258ef5e add /roleypoly and /pickable-roles slash commands, fix framework issues 2022-02-04 01:37:14 -05:00
fd7ed13e9d fix tests as output types changed a little 2022-02-03 22:59:32 -05:00
544ef1b2f0 fix interactions, apparently cfw doesn't speak Buffer 2022-02-03 22:57:37 -05:00
7007cfea9d add a test for when attemptLegacyImport encounters an error 2022-02-02 13:57:00 -05:00
c7bfed8bae attemptLegacyImport should not break the entire request when it fails 2022-02-02 13:52:01 -05:00
140f9d566b debounce session fetch as it literally ddoses cloudflare 2022-02-01 21:05:48 -05:00
fcaf3af875 only finish auth flow when user is actually authenticated 2022-02-01 20:39:43 -05:00
5123fba74d fix ui_public_uri in prod 2022-02-01 18:13:32 -05:00
c28e53c6b4 scroll to bottom when a new category is created 2022-02-01 17:23:17 -05:00
e0a0d1d87a uncacheGuild a second time, prospective fix for state desync 2022-02-01 17:16:56 -05:00
5404047bd2 fix help page not correctly injecting all AppShellProps 2022-02-01 01:28:35 -05:00
7ba0b6316e makes login use location.href instead of navigate() 2022-02-01 00:45:24 -05:00
b3f9f57035 forcibly bump api code so it'll deploy 2022-02-01 00:41:35 -05:00
dependabot[bot]
0c19156a0a
chore(deps): bump dns-packet from 1.3.1 to 1.3.4 (#475)
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

---
updated-dependencies:
- dependency-name: dns-packet
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-01 00:31:46 -05:00
872e9b8734 fix new-session tests, remove newly unused param 2022-02-01 00:25:36 -05:00
9f870139ff prevent dependabot from needing google credentials in CI 2022-02-01 00:00:06 -05:00
dependabot[bot]
5f23498967
chore(deps): bump tmpl from 1.0.4 to 1.0.5 (#366)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Katalina <kayteh@users.noreply.github.com>
2022-01-31 23:54:36 -05:00
dependabot[bot]
2f82b4088f
chore(deps): bump tar from 6.0.5 to 6.1.11 (#356)
Bumps [tar](https://github.com/npm/node-tar) from 6.0.5 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.0.5...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 23:54:17 -05:00
e7994d394b update storybook to 6.4.17 2022-01-31 23:52:45 -05:00
b24f3fd00a normalize auth/callback redirect url 2022-01-31 23:50:05 -05:00
8d7d331c82 fix footer patreon link 2022-01-31 23:34:28 -05:00
1cb04c8b5a update login flow to prevent session leakage 2022-01-31 23:32:41 -05:00
be826b613e fix typo on auth/callback redirect 2022-01-31 21:51:15 -05:00
a008dbcc42 add DISCORD_PUBLIC_KEY binding 2022-01-31 21:50:26 -05:00
3291f9aacc
big overhaul (#474)
* miniflare init

* feat(api): add tests

* chore: more tests, almost 100%

* add sessions/state spec

* add majority of routes and datapaths, start on interactions

* nevermind, no interactions

* nevermind x2, tweetnacl is bad but SubtleCrypto has what we need apparently

* simplify interactions verify

* add brute force interactions tests

* every primary path API route is refactored!

* automatically import from legacy, or die trying.

* check that we only fetch legacy once, ever

* remove old-src, same some historic pieces

* remove interactions & worker-utils package, update misc/types

* update some packages we don't need specific pinning for anymore

* update web references to API routes since they all changed

* fix all linting issues, upgrade most packages

* fix tests, divorce enzyme where-ever possible

* update web, fix integration issues

* pre-build api

* fix tests

* move api pretest to api package.json instead of CI

* remove interactions from terraform, fix deploy side configs

* update to tf 1.1.4

* prevent double writes to worker in GCS, port to newer GCP auth workflow

* fix api.tf var refs, upgrade node action

* change to curl-based script upload for worker script due to terraform provider limitations

* oh no, cloudflare freaked out :(
2022-01-31 20:35:22 -05:00
b644a38aa7 fix(branding): adjust LunarNewYear date captures 2022-01-27 16:55:21 -05:00
8c07ed3123 chore: fix linting 2022-01-22 22:14:16 -05:00
20efb22605 fix(editor): make permalink types more sane 2022-01-22 22:10:57 -05:00
0d96a4f973 feat(editor): add permalink section in editor 2022-01-22 22:07:11 -05:00
f86eaae5e9 chore: rename uses of "Guilds" to "Servers" 2022-01-22 20:27:54 -05:00
8eb4377044 fix(design-system): organisms/editor-shell: add spacer above masthead 2022-01-22 20:22:02 -05:00
4e2616c1a7 fix: upgrade all uses of node 14 to 16 2022-01-22 18:57:14 -05:00
0c9f60ccd9 fix(bot): upgrade node verison to 16 2022-01-22 18:56:11 -05:00
7a52653260 fix(bot): update discord.js to 13, ignore non-direct mentions 2022-01-22 18:49:10 -05:00
cba0d1f35a fix(design-system): molecules/editor-category: handleRoleListUpdate should not be nested function 2022-01-22 18:13:00 -05:00
dac2af43a2 fix(api): disable access control 2021-08-13 08:52:44 -04:00
d5acea4abb fix: missing tools on a composable handler 2021-08-07 18:05:34 -04:00
76a03c2d2c chore: refactor asyncResponse to take a preflight response 2021-08-07 18:04:29 -04:00
26bc74bcbc fix(interactions): add async responses 2021-08-07 18:00:20 -04:00
3601d435b2 chore: update discord-interactions terraform module 2021-08-02 20:21:53 -04:00
285e23c0ed fix(bot-join): fix commands scope 2021-08-01 20:56:06 -04:00
066f68ffef
feat: Slash Commands (#337)
* feat: add discord interactions worker

* feat(interactions): update CI/CD and terraform to add interactions

* chore: fix lint issues

* chore: fix build & emulation

* fix(interactions): deployment + handler

* chore: remove worker-dist via gitignore

* feat: add /pickable-roles and /pick-role basis

* feat: add pick, remove, and update the general /roleypoly command

* fix: lint missing Member import
2021-08-01 20:26:47 -04:00
dde05c402e add feature flag stuff
Signed-off-by: Katalina Okano <git@kat.cafe>
2021-07-27 23:03:00 -04:00
3074db0a21 chore: set new features behind feature flags 2021-07-27 20:34:49 -04:00
c1e0e65823 fix(picker): uncache on role updates 2021-07-18 03:23:12 -04:00
2d88c4bf23 fix(picker): on access control violation, do a navigate rather than redirectTo 2021-07-18 02:37:29 -04:00
9921410c9f chore: remove leaked webhook 2021-07-18 02:28:07 -04:00
3f45153b66 feat: add access control 2021-07-18 02:12:30 -04:00
9c07ff0e54 chore: remove leftover BUILD.bazel files 2021-07-17 20:38:54 -04:00
6708f5c6fc chore: add create-component tool 2021-07-17 20:37:58 -04:00
62687ced78 fix(editor): fix style linting issue 2021-07-17 19:46:16 -04:00
b150462f2b fix(api): remove references to x-create-roleypoly-data 2021-07-17 19:43:21 -04:00
4cc202b62a feat(Editor): make server utilities their own pages
Signed-off-by: Katalina Okano <git@kat.cafe>
2021-07-17 19:23:35 -04:00
d52508a046 feat(api): update-roles will report x-audit-log-reason to discord
Signed-off-by: Katalina Okano <git@kat.cafe>
2021-07-17 19:22:16 -04:00
824fee0703 refactor(api): Abstract discord API base url to config.ts
Signed-off-by: Katalina Okano <git@kat.cafe>
2021-07-17 19:21:38 -04:00
31ea2e2183 refactor(api): asEditor instead of copy-pasted admin/manager/root check 2021-07-17 19:07:10 -04:00
0ed5d696df feat: redesign server listing design language 2021-07-10 15:03:10 -04:00
e5d83bc133
Fix/role tap styling (#321)
* fix(Role): don't show role state svgs on mobile hover

* fix(Role): default color hover state should be white svg fill
2021-07-14 22:08:50 -04:00
8158ae38f1 fix(Popover): fix z-index dismiss handler and mobile positioning 2021-07-13 23:32:01 -04:00
acc604f83f
feat: add audit logging via webhook (#309)
* feat: add audit logging via webhook

* addd missing auditLogWebhook values in various places
2021-07-13 23:01:25 -04:00
5671a408c1 fix(Editor): add reset/delete actions to each category (fixes #302) 2021-07-13 22:59:05 -04:00
5dce2fc949 chore(cfw-emulator): add bin field back ascfpages fixed their chmod issue 2021-07-13 22:21:05 -04:00
85ceb25c17 feat(UserPopover): add copyright and links to user popover 2021-07-09 07:08:51 -05:00
29adda5fd6 chore: ??? 2021-07-09 07:08:23 -05:00
1cd5cd7378 fix(Avatar): fix transparent server icons showing backgrounds 2021-07-09 07:07:53 -05:00
57f58d7333 feat(Editor): refresh cache when in the editor 2021-07-09 07:07:10 -05:00
4d18c0da1e chore(get-picker-data): comment a TODO for __no_cache ratelimit 2021-07-09 07:06:24 -05:00
a3cfe6d78a chore(DynamicBranding): remove LNY21, add LNY23 2021-07-09 05:02:35 -05:00
3d8f1030dc fix(DynamicBranding): fix lesbian pride day start date 2021-07-09 05:01:01 -05:00
51ef551d39 fix(Masthead): send date into branding so it changes properly 2021-07-09 04:57:28 -05:00
6d08548020 fix(Editor): editor linting 2021-07-09 04:41:43 -05:00
b6172b4af0 chore: deploy bot to stage 2021-07-09 04:37:03 -05:00
d8cdc1c62a fix(Editor): add empty role picker help page and link 2021-07-09 04:30:34 -05:00
30b9ea3a59 fix(Editor): add an empty categories message 2021-07-09 03:46:06 -05:00
47fa58fd36 fix(ServerCategoryEditor): unselectedRoles shouldn't include @everyone and unsafe roles 2021-07-08 17:36:38 -05:00
5e6b722290 fix(EditorCategory): unflip hidden state 2021-07-08 17:29:35 -05:00
d39b9db781 feat: bot-join machinery redirect 2021-07-08 17:10:50 -05:00
ab3f718e6d
Feat/editor as preview (#294)
* try editor as preview

* add databinding for editor actions and message

* add actions, reordering base interactions

* add drag and drop ordering for categoriers

* category skeleton

* fix linting issues

* add role list and add button, non-functional

* bump packages

* add role search prototype

* yarn.lock sync

* fix lint

* remove cfw-emulator bin
2021-07-08 16:51:00 -05:00
7d681d69d6
Feat/editor category pass2 (#290)
* feat(design-system): add editor skeletons

* use css media queries rather than JS media queries

* init remake

* feat: add basis of toggle atom

* finish toggle

* use pointer cursor with toggle

* sync

* feat: add server message in editor

* cleanup storybook

* add short editor item and data model for categories

* chore: fix build by moving jest version downward

* chore: remove old category editor

* chore: fix EditorCategoryShort index

* add editor wiring and styling updates

* fix linting issues
2021-07-05 12:18:40 -05:00
a37d481b18 chore: downgrade ts-loader because webpack 4 2021-06-30 08:39:07 -04:00
9799114b7c chore: fix tests, upgrade to node 16 2021-06-30 08:30:49 -04:00
10e095656f chore: update codestyle due to prettier/rule updates 2021-06-30 08:02:36 -04:00
dependabot[bot]
f632bfa6e5
chore(deps): bump actions/upload-artifact from 2.2.2 to 2.2.4 (#265)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.2.2 to 2.2.4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.2.2...v2.2.4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Katalina <kayteh@users.noreply.github.com>
2021-06-30 07:59:58 -04:00
dependabot[bot]
ec53c65378
chore(deps): bump actions/cache from 2.1.4 to 2.1.6 (#264)
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.4 to 2.1.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.4...v2.1.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-30 07:59:39 -04:00
dependabot[bot]
e7a6968abf
chore(deps): bump ssri from 6.0.1 to 6.0.2 (#248)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-30 07:59:28 -04:00
94ab6d9907 chore: bump terraform version to 1.0.1 + packages 2021-06-30 07:57:20 -04:00
ccff2684ad chore: bump packages 2021-06-30 07:37:35 -04:00
3748bb8695 chore: add resolutions for webpack as storybook is too loose 2021-04-03 19:02:58 -04:00
9ee7fb4878 chore: update npm packages 2021-03-24 05:31:49 -04:00
0706d62592 chore: update npm packages 2021-03-24 05:29:35 -04:00
25055fc204 chore: update npm packages 2021-03-24 05:27:07 -04:00
dependabot[bot]
ff1a756b3d
chore(deps): bump @types/react-dom from 17.0.2 to 17.0.3 (#197)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.2 to 17.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-03 18:00:14 -04:00
dependabot[bot]
a6f29458f7
chore(deps): bump react-tooltip from 4.2.15 to 4.2.17 (#203)
Bumps [react-tooltip](https://github.com/wwayne/react-tooltip) from 4.2.15 to 4.2.17.
- [Release notes](https://github.com/wwayne/react-tooltip/releases)
- [Changelog](https://github.com/wwayne/react-tooltip/blob/master/CHANGELOG.md)
- [Commits](https://github.com/wwayne/react-tooltip/compare/v4.2.15...v4.2.17)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-03 18:00:02 -04:00
dependabot[bot]
d640cc3e7d
chore(deps-dev): bump @wojtekmaj/enzyme-adapter-react-17 (#201)
Bumps [@wojtekmaj/enzyme-adapter-react-17](https://github.com/wojtekmaj/enzyme-adapter-react-17) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/wojtekmaj/enzyme-adapter-react-17/releases)
- [Commits](https://github.com/wojtekmaj/enzyme-adapter-react-17/compare/v0.5.0...v0.6.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-03 17:59:49 -04:00
dependabot[bot]
db10cfdd30
chore(deps): bump @testing-library/jest-dom from 5.11.9 to 5.11.10 (#204)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.11.9 to 5.11.10.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.11.9...v5.11.10)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-03 17:59:33 -04:00
dependabot[bot]
0a28c4f38c
chore(deps): bump @testing-library/user-event from 13.0.7 to 13.1.1 (#208)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.0.7 to 13.1.1.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.0.7...v13.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-03 17:59:21 -04:00
3190d41a9e
feat(bot): add dockerfile for JS rewrite, fix some loose ends (#210) 2021-04-03 17:53:05 -04:00
Katie Macke
7ec603cf70
Rewrite the bot process in Node/Discord.js (#206)
* Rewrite the bot process in js using discord.js

* remove old go code
2021-03-31 15:35:07 -04:00
55bc84e045 ci: trigger CI on pull_request 2021-03-29 19:08:55 -04:00
70cd0179c3 fix(web): remove setTimeouts from updateScreenSize in BreakpointsProvider 2021-03-23 23:25:03 -04:00
a9c94eefc3 chore(web): update index.html description for auto-embeds 2021-03-23 23:09:27 -04:00
884be92db3
chore: refactor BreakpointsProvider to use hooks (#199) 2021-03-23 23:01:01 -04:00
a4fd37d71c
feat(api): add rate-limiting and /clear-guild-cache (#198) 2021-03-23 22:14:33 -04:00
57d83699d5
feat(design-system): redesign tab-view around quick nav (#194)
* feat(design-system): redesign tab-view around quick nav

* fix tests
2021-03-23 00:00:34 -04:00
a5f819bc3e
feat(web): add error pages (#193) 2021-03-22 21:18:56 -04:00
f4165f8055 chore: update some dev packages 2021-03-22 00:22:57 -04:00
93d2dba8e8 chore: remove packages/web/yarn.lock as it was accidentally committed 2021-03-22 00:18:07 -04:00
dependabot[bot]
be57e762a1
chore(deps-dev): bump husky from 5.1.3 to 5.2.0 (#191)
Bumps [husky](https://github.com/typicode/husky) from 5.1.3 to 5.2.0.
- [Release notes](https://github.com/typicode/husky/releases)
- [Commits](https://github.com/typicode/husky/compare/v5.1.3...v5.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 19:27:50 -04:00
00ac3ef87a feat(api): /login-bounce adds &prompt=none to skip user interaction 2021-03-21 22:26:59 -04:00
117c73a8a1 fix(api): sync from legacy used wrong type of data 2021-03-21 22:10:27 -04:00
bfc96b0750
feat(api): add /sync-from-legacy route (#192)
* feat(api): add /sync-from-legacy route

* chore: remove extraneous dockerfile

* chore: remove extraneous dockerfile build

* chore: remove extraneous dockerfile build matrix
2021-03-22 16:54:33 -04:00
a983492154 chore: bump npm packages 2021-03-19 15:27:09 -04:00
81b6de7f71
performance pass on /s/:id route (#183)
* chore(web): attempt a slightly faster unauthed /s/ load

* retype RouteWrapper
2021-03-15 20:22:18 -04:00
e0fcfc310e
feat: add skeleton masthead and generic loading page (#182)
* feat: add skeleton masthead and generic loading page

* add generic loader to picker page

* smooth out spinner, add no-motion state
2021-03-15 19:51:56 -04:00
fa85b30cf0 fix(web): new-session should load a new page rather than fix state 2021-03-14 19:47:54 -04:00
42323a46f6
Refactor SessionContext (#181)
* fix(web): refactor session context so it works more consistently

* add warning for when syncSession is locked but called again

* cd

* add tests to new-session
2021-03-14 19:20:30 -04:00
b6ae2abd2f chore: add postauthUrl link to skip broken auth flow 2021-03-14 16:44:28 -04:00
10522e73c2 chore: disable auth flow temporarily 2021-03-14 16:40:39 -04:00
fd02dad62b
Improve pre- and post-auth redirect flows (#180)
* fix(web): add /machinery/new-session/... route, start auth flow from there

* fix(api): change redirect to path-based format

* fix(api): remove extra / from login-callback redirect

* fix(web): /auth/login should skip flows if isAuthenticated is true
2021-03-14 16:28:51 -04:00
8fbf8f2519
feat(web): add /machinery/bot-join temporary page (#179)
* feat(web): add /machinery/bot-join temporary page

* add case for non-id'd bot-join route
2021-03-14 15:35:27 -04:00
311a9c371e
fix(design-system): fix UserPopover icon alignment (#178) 2021-03-14 15:16:18 -04:00
0c586404e8
feat(web): add /machinery/logout (#177) 2021-03-14 15:09:52 -04:00
410e27c2b3
fix(design-system): fix some button styling issues leaking link styles in (#176) 2021-03-14 14:49:29 -04:00
f24d2fcc99
chore: update prettier tab width for consistency (#175) 2021-03-13 22:54:34 -05:00
a931f8c69c chore: redo last commit with yarn.lock 2021-03-13 21:26:28 -05:00
e7702fbcac chore: bump react-icons, @types/node 2021-03-13 21:23:58 -05:00
637be8bfa1
feat(web): add page titles (#170)
* feat(web): add page titles

* add html title
2021-03-13 21:17:00 -05:00
2d9d70734b
feat(web): do a cleanup pass over static HTML (#169) 2021-03-13 20:51:01 -05:00
5e8876a90c
fix(design-system): fix styling regression from removing next/link (#168) 2021-03-13 20:16:40 -05:00
99952aa19f
Feat/recent guilds (#89) (#167)
* feat(web): add server-setup page for when bot isn't in the server picked

* chore: move contexts into their own folder

* feat(web): add recent guilds context, and app shell helper context

* feat(web): show recent guilds in masthead

* feat(web): functionally add recents to servers list

* fix(web): correct styling for servers listing recents/all headers

* fix(web): correct some type issues with appShellProps

* fix(web): don't show ServerListing recents when recents is empty
2021-03-13 19:31:36 -05:00
3a36d7a85d feat(web): wire up discord auth button 2021-03-13 16:48:50 -05:00
0a399938d6 fix(web): simplify callbackHost builder 2021-03-13 16:28:01 -05:00
8431df784f chore: commit forgotten terraform changes for allowed_callback_hosts 2021-03-13 16:02:10 -05:00
3388e091c1 feat(api): add wildcard fallback host filtering for stage/dev 2021-03-13 15:57:28 -05:00
6edfe7455f chore: temporarily loosen CORS, add OAuth state info for backend bouncing 2021-03-13 15:49:36 -05:00
ed82a67594 fix(web): better define stage url matcher to prevent unexpected use-cases :) 2021-03-13 05:57:33 -05:00
2b467b8452 fix(web): api context should pass it's current hostname into API URL selector 2021-03-13 05:21:09 -05:00
eb2537ae35 chore(design-system): fix tsconfig extends path 2021-03-13 05:18:01 -05:00
aeaf14bda1 chore(api): remove extraneous logging from get-slug 2021-03-13 05:16:50 -05:00
fac361d277 chore(web): memoize default url since it'll mostly always be the same no matter what 2021-03-13 05:16:25 -05:00
a87ccd9c54 fix(web): getDefaultApiUrl should handle stage.roleypoly.com as stage 2021-03-13 04:55:20 -05:00
cd448b56c9
Reach parity with last web iteration. (#162)
* feat: add Api and Session contexts

* feat(web): add machinery/new-session

* feat(web): add servers page

* chore(web): AppRouter spacing/ordering

* feat(web): add picker, missing update-roles call for now

* feat(web): add picker saves

* chore: add roleTransactions tests

* feat(web): add auth/login
2021-03-13 04:42:07 -05:00
f65779f925 chore: go mod tidy to fix gomega update 2021-03-12 18:50:24 -05:00
c9fce4d4cb chore: update gomega 2021-03-12 18:48:05 -05:00
558555293c chore: delete root .storybook folder 2021-03-12 18:47:24 -05:00
ff6fe5282b chore: update discordgo + discordclient 2021-03-12 18:47:13 -05:00
e5e031fa27 chore: fix storybook weirdness 2021-03-12 18:43:56 -05:00
da6037cf6d chore: bump packages, remove unused webpack plugin 2021-03-12 18:32:09 -05:00
069930a55e ci: remove ui_tag from deploy trigger 2021-03-12 18:10:03 -05:00
2ff6588030
Refactor node packages to yarn workspaces & ditch next.js for CRA. (#161)
* chore: restructure project into yarn workspaces, remove next

* fix tests, remove webapp from terraform

* remove more ui deployment bits

* remove pages, fix FUNDING.yml

* remove isomorphism

* remove next providers

* fix linting issues

* feat: start basis of new web ui system on CRA

* chore: move types to @roleypoly/types package

* chore: move src/common/utils to @roleypoly/misc-utils

* chore: remove roleypoly/ path remappers

* chore: renmove vercel config

* chore: re-add worker-types to api package

* chore: fix type linting scope for api

* fix(web): craco should include all of packages dir

* fix(ci): change api webpack path for wrangler

* chore: remove GAR actions from CI

* chore: update codeql job

* chore: test better github dar matcher in lint-staged
2021-03-12 18:04:49 -05:00
dependabot[bot]
49e308507e
chore(deps): bump actions/cache from v2 to v2.1.4 (#134)
Bumps [actions/cache](https://github.com/actions/cache) from v2 to v2.1.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2...26968a09c0ea4f3e233fdddbafd1166051a095f6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-09 21:13:26 -05:00
dependabot[bot]
bda44bd6ae
chore(deps-dev): bump chokidar from 3.5.0 to 3.5.1 (#119)
Bumps [chokidar](https://github.com/paulmillr/chokidar) from 3.5.0 to 3.5.1.
- [Release notes](https://github.com/paulmillr/chokidar/releases)
- [Commits](https://github.com/paulmillr/chokidar/compare/3.5.0...3.5.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 21:41:51 +00:00
dependabot[bot]
d9092b2ddb
chore(deps-dev): bump eslint-plugin-jsdoc from 31.0.3 to 32.2.0 (#149)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 31.0.3 to 32.2.0.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v31.0.3...v32.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 21:28:56 +00:00
dependabot[bot]
1024562776
chore(deps-dev): bump eslint from 7.17.0 to 7.20.0 (#137)
Bumps [eslint](https://github.com/eslint/eslint) from 7.17.0 to 7.20.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.17.0...v7.20.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 21:03:14 +00:00
dependabot[bot]
f93df2b151
chore(deps-dev): bump @types/node from 14.14.20 to 14.14.31 (#144)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.20 to 14.14.31.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 20:50:04 +00:00
dependabot[bot]
cd927ada76
chore(deps): bump actions/setup-node from v2.1.4 to v2.1.5 (#146)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from v2.1.4 to v2.1.5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.1.4...46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 20:35:48 +00:00
dependabot[bot]
65f936be48
chore(deps): bump github.com/onsi/gomega from 1.10.4 to 1.10.5 (#130)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.10.4 to 1.10.5.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.10.4...v1.10.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 20:30:10 +00:00
dependabot[bot]
0626a3e4d3
chore(deps): bump react-tooltip from 4.2.11 to 4.2.15 (#150)
Bumps [react-tooltip](https://github.com/wwayne/react-tooltip) from 4.2.11 to 4.2.15.
- [Release notes](https://github.com/wwayne/react-tooltip/releases)
- [Changelog](https://github.com/wwayne/react-tooltip/blob/master/CHANGELOG.md)
- [Commits](https://github.com/wwayne/react-tooltip/compare/v4.2.11...v4.2.15)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-26 15:24:13 -05:00
46769cce55 ci: re-fix discord data quoting 2021-01-12 23:33:10 -05:00
dependabot[bot]
e4f7b4dd97
chore(deps): bump actions/upload-artifact from v2.2.1 to v2.2.2 (#107)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from v2.2.1 to v2.2.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.2.1...e448a9b857ee2131e752b06002bf0e093c65e571)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-13 04:32:03 +00:00
bb82ce519b ci: fix curl data quoting 2021-01-12 23:18:12 -05:00
59bb223049 ci: update codeql 2021-01-12 23:12:05 -05:00
041fc19e95 ci: discord failed() -> failure() 2021-01-12 23:09:10 -05:00
14c9993bf6 ci: discord when -> if 2021-01-12 23:01:04 -05:00
6a5df41a31 ci: add #deployments 2021-01-12 22:51:30 -05:00
6392722a6f fix: server-setup template index.ts missed content 2021-01-12 21:15:29 -05:00
19637c2ddb chore: bump npm packages 2021-01-12 21:04:42 -05:00
8299c548ba feat: add server-setup page 2021-01-12 20:57:11 -05:00
df05e23303 chore: bump terraform providers 2021-01-10 15:21:58 -05:00
334462680e chore: bump packages 2020-12-29 00:29:58 -05:00
888988fce2 chore: lint-staged can use stylelint --fix now 2020-12-23 19:16:12 -05:00
d70eb44963 chore: remove stylelint processor 2020-12-23 19:15:35 -05:00
25615ac38d ci: flip worker check, if found skip=1, not 0 2020-12-22 00:31:49 -05:00
20fec5ea24 chore: bump packages 2020-12-22 00:27:50 -05:00
5e065912de ci: skip worker build if already built. 2020-12-22 00:13:31 -05:00
1495500f2b fix(API): highest role position was incorrectly calculated starting from the highest instead of the lowest. fixes #97 2020-12-21 22:09:05 -05:00
5ef9046e0b chore: remove tf lint-staged rule -- will revisit 2020-12-21 05:49:28 -05:00
8c9243b736 infra: set public_uri vars in repo instead of from secrets 2020-12-21 05:37:36 -05:00
7363b4d359 fix(RolePicker): make sure getInitialProps always returns
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:17:02 -05:00
7f031bb960 chore: prefix un-consumed router.replace() calls with void
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:10:57 -05:00
779095386d feat(auth/login): Login page will set a sessionStorage redirect flag when given an ?r= ID
Also adds HTML titles

Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:10:25 -05:00
25ce18b911 feat(UI): new-session machinery will redirect to known URL if given in sessionStorage
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:09:02 -05:00
9699c313f9 chore: fix logout link -- broke layout, will investigate
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:07:51 -05:00
d11e29d4f5 chore: remove PreauthSecretCode
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:04:44 -05:00
91c27f6b9a fix(utils): apiFetch should tolerate missing authentication
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:04:12 -05:00
6d8f40e30e feat(RolePicker): when not authed, redirect to /auth/login?r=id
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-21 05:03:38 -05:00
765a0f2b22 chore: remove eslint from lint-staged, will circle back 2020-12-21 03:58:08 -05:00
52a8dcdf76 chore: change guild member cache identity to be suffixed with /roles as there is an expected change schema change 2020-12-21 03:58:08 -05:00
dependabot[bot]
226e212cb9
chore(deps-dev): bump eslint-plugin-jsdoc from 30.7.8 to 30.7.9 (#87)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 30.7.8 to 30.7.9.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v30.7.8...v30.7.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-21 16:36:03 +00:00
dependabot[bot]
880dbe4912
chore(deps): bump swr from 0.3.9 to 0.3.11 (#88)
Bumps [swr](https://github.com/vercel/swr) from 0.3.9 to 0.3.11.
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/0.3.9...0.3.11)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-21 11:22:38 -05:00
d66fcd1cde chore: instead of HUSKY=0, just disable scripts. they aren't necessary. 2020-12-20 01:29:49 -05:00
7e007b7e86 chore: add HUSKY=0 to ui dockerfile 2020-12-20 01:04:27 -05:00
b98eb10c12 chore: remove enzyme-react-17 type placeholder 2020-12-19 20:54:15 -05:00
5c36a56385 chore: fix lint-staged issues 2020-12-19 20:49:00 -05:00
89d23cb214 fix(ServerListingCard): remove unreachable return in permission tag 2020-12-19 20:48:22 -05:00
04a3feceae chore: prettier ignore husky stuff 2020-12-19 20:40:39 -05:00
62f57c8d67 chore: lint pre-commit 2020-12-19 20:38:40 -05:00
f89cf38c7b chore: husky is still weird 2020-12-19 20:37:41 -05:00
839d98a2ff chore: update husky to v5 (okay actually maybe for real this time) 2020-12-19 20:36:22 -05:00
179139aa31 chore: update husky to v5 (again) 2020-12-19 20:34:18 -05:00
aa60952663 chore: update husky to v5 2020-12-19 20:33:15 -05:00
53d3c8ad2e chore: setup lint-staged 2020-12-19 20:27:10 -05:00
83f3269b06 chore(ServerListingCard): fix stylelint issue 2020-12-19 20:15:32 -05:00
cc592a9da1 fix(RolePicker): wrap Reset/Submit in a div to fix positioning
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-19 20:12:50 -05:00
c1b9153bf4 chore(get-picker-data): make prettier format respond
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-19 20:12:05 -05:00
455632d72d chore(Breakpoints): remove extraneous comment
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-19 20:11:29 -05:00
d61f5ae59e feat(AppShell): add option to disable my guilds popover 2020-12-19 20:10:57 -05:00
e758c09fbf feat(UI): add initial server picker 2020-12-19 20:10:23 -05:00
d4e8e8330a fix(Role): better role name color mapping 2020-12-19 15:04:45 -05:00
b956f3fa66 chore: package.json prettier 2020-12-18 21:09:29 -05:00
b7f82593df fix(RolePicker): prevent render of hidden categories 2020-12-18 21:03:40 -05:00
95dad8effa fix(Masthead): authed masthead should do index as /servers not /dashboard 2020-12-18 21:03:15 -05:00
4159622226 fix(PickerCategory): sort roles by position 2020-12-18 21:02:41 -05:00
51ae64e664 chore: add placeholder /servers with appshell 2020-12-18 21:02:07 -05:00
0e33aa8112 chore: getGuild should use enum authType 2020-12-18 21:01:39 -05:00
990ac3f3ca chore: bump packages 2020-12-18 21:01:14 -05:00
24996d801e ci: make metadata run parallel to the sync process 2020-12-18 19:57:01 -05:00
1a59972398 feat: support updating roles (closes #83) 2020-12-18 19:56:10 -05:00
c7381c3d66 chore(tf): rename url_map due to upstream bugs, attempt to not modify it 2020-12-18 15:53:38 -05:00
8ddf8e6aac chore(tf): bump google provider 2020-12-18 13:43:55 -05:00
2976b35505 chore(api): re-add roleypoly creation gated by root 2020-12-18 13:17:49 -05:00
ba52f7229d fix(tf): regex for matching domain suffix 2020-12-18 13:17:13 -05:00
692467d47f Merge branch 'feat/blackhole-non-rp-traffic' into main 2020-12-18 12:32:43 -05:00
6580abba1d fix(tf): api worker should have BOT_TOKEN binded 2020-12-18 00:57:01 -05:00
b5585f5ee9 chore: fix lint issues 2020-12-18 00:55:15 -05:00
b20c7a08d4 chore: bump npm packages 2020-12-18 00:43:51 -05:00
034466f447 chore: rip out roleypoly server data seeder 2020-12-18 00:43:34 -05:00
ee7ac47bc2 chore: fix failing tests 2020-12-18 00:18:32 -05:00
e4e4bb9024 feat(UI): add role picker, auth helpers, refactor for viability 2020-12-18 00:14:30 -05:00
3fe3cfc21f feat: support logout flow (closes #85) 2020-12-17 17:58:46 -05:00
9cdefefbcc ci: prospective; figure out why environment URL isn't being set 2020-12-17 17:17:38 -05:00
dependabot[bot]
8f5f8bc99b
Bump actions/setup-node from v2-beta to v2.1.4 (#81)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from v2-beta to v2.1.4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2-beta...c46424eee26de4078d34105d3de3cc4992202b1e)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 22:09:35 +00:00
447876be32 chore: remove mustAuthenticate from get-session 2020-12-17 16:54:56 -05:00
bb0987cab9 chore: remove get-slug commented code 2020-12-17 16:54:37 -05:00
041fe49b05 fix(api): define auth token type as enum, export userAgent and use when discordFetch isn't 2020-12-17 16:46:58 -05:00
e1120fd88a ci: add GH actions environments 2020-12-17 16:39:51 -05:00
529a3a6f2d docs: update with better user story data classification for persistent data 2020-12-17 16:15:48 -05:00
55c2f8615c chore(worker-emulator): fix some QoL issues 2020-12-17 16:15:04 -05:00
89fbb01142 feat(api): add revoke-session 2020-12-17 16:14:27 -05:00
b7921a830a fix(api): login-callback may have null user in rare cases 2020-12-17 15:01:40 -05:00
823a99b4eb fix(api): prevent creation of Response objects outside of request time 2020-12-17 14:23:23 -05:00
9c935f2847 chore: remove unused go code 2020-12-15 21:39:27 -05:00
0b384bfe5c feat(api): add get-picker-data; refactor fully away from old gRPC datatypes 2020-12-15 21:33:26 -05:00
823760dc2f chore: update @types/node, tslint-to-eslint convertors 2020-12-15 15:56:57 -05:00
6d1037f25e fix(types): permissions to string/bigint to prevent int scalability concerns 2020-12-15 15:54:39 -05:00
c55ce3b828 feat(api): add get-slug 2020-12-15 15:29:25 -05:00
a85c4d5ddd add withSession, cacheLayer, and userAgent to discordFetch 2020-12-14 14:43:22 -05:00
12d8e99513 inject fetch into KV emu 2020-12-14 14:10:24 -05:00
22cbde52dd init to fetch guild slug 2020-12-13 22:06:27 -05:00
e25b9c96c6 fix KV emulation ttl 2020-12-13 21:58:55 -05:00
dependabot[bot]
961cdab975
Bump ini from 1.3.5 to 1.3.8 (#75)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Katalina <kayteh@users.noreply.github.com>
2020-12-13 21:41:31 -05:00
dependabot[bot]
036a11803d
Bump github.com/onsi/gomega from 1.10.3 to 1.10.4 (#65)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.10.3 to 1.10.4.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.10.3...v1.10.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Katalina <kayteh@users.noreply.github.com>
2020-12-13 21:41:01 -05:00
dependabot[bot]
2660d1dad9
Bump hashicorp/setup-terraform from v1 to v1.3.2 (#69)
Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from v1 to v1.3.2.
- [Release notes](https://github.com/hashicorp/setup-terraform/releases)
- [Changelog](https://github.com/hashicorp/setup-terraform/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/setup-terraform/compare/v1...3d8debd658c92063839bc97da5c2427100420dec)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Katalina <kayteh@users.noreply.github.com>
2020-12-13 21:40:39 -05:00
8fe3d1dcf2 change KV emulation to leveldb, remove redis 2020-12-13 21:30:55 -05:00
3bca07c7d7 codestyle issues 2020-12-13 18:18:21 -05:00
140da31193 bump go packages 2020-12-13 17:48:16 -05:00
f4bc1ba950 bump npm packages 2020-12-13 17:46:38 -05:00
e0a2711459 change from cmd to entrypoint as buildkit is starting to complain 2020-12-13 17:43:14 -05:00
d9508b0b41 blackhole non-roleypoly traffic 2020-12-13 13:13:13 -05:00
dependabot[bot]
16b614c180
Bump actions/upload-artifact from v1 to v2.2.1 (#60)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from v1 to v2.2.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...726a6dcd0199f578459862705eed35cda05af50b)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-12 00:18:24 -05:00
dependabot[bot]
404a7b651e
Bump @types/styled-components from 5.1.4 to 5.1.5 (#72)
Bumps [@types/styled-components](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/styled-components) from 5.1.4 to 5.1.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/styled-components)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-12 00:06:36 -05:00
c3e5eff1d7 docs: update readme for quickstart 2020-12-11 21:45:59 -05:00
a44983b088 chore: add cloudflare worker emulator and supporting bits. now fully offline! 2020-12-11 21:09:55 -05:00
6ddb3e3192 fix digest.txt typo when getting digests from artifacts 2020-12-11 00:27:56 -05:00
63a377867b tf fmt upon cf origin cert.tf 2020-12-11 00:21:46 -05:00
18583f145a add cf origin certs, swap LB to HTTPS 2020-12-11 00:16:58 -05:00
961989197c add bot deploy 2020-12-11 00:16:03 -05:00
25a94089f8 update ts-eslint and jest types 2020-12-08 01:47:49 -05:00
39efe219c8 always redirect to api:/login-bounce instead of making the user interact with the page -- offically disabling DM auth 2020-12-08 01:30:05 -05:00
5d17105eba load auth login ui config from server only 2020-12-08 01:02:53 -05:00
4dbca37917 fix github.ref matcher 2 2020-12-07 23:51:37 -05:00
f97ced4433 fix github.ref matcher 2020-12-07 23:49:53 -05:00
6ba252d34c wire build to deploy, e2e CICD!@ 2020-12-07 23:48:54 -05:00
473031150b terraform fmt 2020-12-07 22:53:02 -05:00
afdf331070 fix default tagging for cloud run 2020-12-07 22:49:54 -05:00
22b15ec16a oMerge branch 'main' of github.com:roleypoly/roleypoly into main 2020-12-07 22:35:11 -05:00
07e008fb49 add better digest fetcher 2020-12-07 22:33:18 -05:00
181df2df40 use alternate path for getting digest 2020-12-07 00:20:34 -05:00
1bcb571205 fix tag output keys 2020-12-06 23:42:43 -05:00
e4b80db558 removing trailing slash from mappings 2020-12-06 23:26:10 -05:00
853aa3ca00 use region mapper for artifact urls 2020-12-06 23:12:56 -05:00
ecda7e21d2 fix tag output mapping 2020-12-06 23:01:51 -05:00
6ed7bda678 fix tags.tfvars pathname 2020-12-06 22:57:43 -05:00
0e69066aad set correct key for worker artifact path 2020-12-06 22:45:58 -05:00
d8bda6fb43 try pulling secrets from gcloud for tf 2020-12-06 22:36:11 -05:00
441b24045d set working-directory for tf runs 2020-12-06 21:52:15 -05:00
d5094f94f9 attempted TF_DATA fix 2020-12-06 21:39:38 -05:00
84291524dc attempt to fix deploy 2020-12-06 21:30:45 -05:00
3626c43a3d use mhart alpine images because they smol 2020-12-06 21:22:41 -05:00
e305efbd1c strip dev packages for ui docker build, removed before while debugging 2020-12-06 21:14:37 -05:00
754d2fc21e add --quiet to GAR delete cmd 2020-12-06 21:06:54 -05:00
e954031d3e disable fail-fast in GAR job 2020-12-06 21:05:26 -05:00
1b9eabaee5 GAR job might need stderr 2020-12-06 21:04:51 -05:00
25965e0ca8 GAR job doesn't need a needs 2020-12-06 21:03:05 -05:00
e89ce7f10c add GAR cleanup job 2020-12-06 21:02:04 -05:00
465ca36f28 attempt a better default pattern 2020-12-06 20:16:29 -05:00
0af36d6f20 missed a closing brace for bot tag setter 2020-12-06 20:05:28 -05:00
f2437a585a oopsied the pkg.dev url 2020-12-06 19:57:25 -05:00
a3778c7a1e use format to not need concatenation in dynamic secret name 2020-12-06 19:49:48 -05:00
bbdea9c074 event -> github.event 2020-12-06 19:42:01 -05:00
a0031d4c2b don't build when deploy workflow updates 2020-12-06 19:41:46 -05:00
26f00eaf22 update deploy 2020-12-06 19:30:38 -05:00
820f2005c4 im actually super smart and i accidentally named the secret wrong 2020-12-06 19:17:35 -05:00
8a57e10e78 try diff gcs flow 2020-12-06 19:13:26 -05:00
5763b3f279 another try at wrangler stuff 2020-12-06 19:03:12 -05:00
734dbecf6b actually c heckout code you fox 2020-12-06 18:20:14 -05:00
991e691a8c fix prettier 2020-12-06 18:16:29 -05:00
ff973b9349 add wrangler to devDeps so CI can run it 2020-12-06 18:14:27 -05:00
146269944a terraform fmt 2020-12-06 18:07:44 -05:00
ed1db4d899 remove matrix id in ci 2020-12-06 18:05:32 -05:00
43d6b6565c adjust branding contrast bg color 2020-12-06 18:03:33 -05:00
95a5732457 more ci/cd work, build worker and publish 2020-12-06 17:59:24 -05:00
0b769913fb enable dependabot for terraform 2020-12-06 14:14:04 -05:00
ef742bba37 make dynamic logos partial props 2020-12-06 13:49:45 -05:00
f4716584bb update seasonal logo variants 2020-12-06 13:45:27 -05:00
5ae666176b add flag brand styles 2020-12-06 12:15:12 -05:00
c25943dfa0 adjust tf to support digest tags 2020-12-06 08:46:16 -05:00
432922dd21 another attempt for auth login config 2020-12-06 07:43:07 -05:00
bd15d1f6fa fix auth/login base url resolution failure 2020-12-06 07:34:25 -05:00
ab8ae622eb fix terraform web lb IP mappings 2020-12-06 07:25:21 -05:00
4ef4badec3 expose public URI routes in ui 2020-12-06 07:24:46 -05:00
9ba1334e2b ui needs to be receptive to $PORT 2020-12-06 06:41:31 -05:00
b62166abda ui container fixes 2020-12-06 06:26:43 -05:00
3ee0c64e7c fix GAR image path 2020-12-06 05:49:35 -05:00
7ad719895d add GAR docker push for google cloud stuff 2020-12-06 05:41:18 -05:00
e028b64ff8 update infra docs to get around deep dns zone limitations 2020-12-06 04:21:03 -05:00
8870f6b640 make terraform stuff 2020-12-06 04:20:12 -05:00
d4e9f38a65 fix import/export ordering issues 2020-12-06 02:16:22 -05:00
344c6e1c52 update docs, add infra notes 2020-12-06 00:41:37 -05:00
d93592f1a2 automatically go fmt on lint 2020-12-05 23:26:36 -05:00
2e1e63a789 prettier organize imports 2020-12-05 23:20:44 -05:00
899853e24c fix codestyle issues with devcontainer 2020-12-05 23:15:04 -05:00
80db505a11 setup dev containers again 2020-12-03 12:56:12 -05:00
c23b84c4fa re-enable bot 2020-12-03 12:37:06 -05:00
f2c4effd24 set labels on docker build 2020-12-03 12:36:21 -05:00
4d0fcc4ad6 attempt to use ghcr.io instead of old gh packages 2020-12-03 12:26:17 -05:00
dec76a40a5 fix type issues 2020-12-03 12:08:20 -05:00
05d7e5c145 lint with typescript as well 2020-12-03 12:06:14 -05:00
b37e3de378 more storybook fixes due to slugs 2020-12-03 11:51:56 -05:00
0e2c981560 storybook and template fixes 2020-12-03 11:32:09 -05:00
7371b5e490 fix dockerfile path, .hack => ./hack 2020-12-03 10:26:18 -05:00
0340693234 breakout docker builds into matrix 2020-12-03 10:22:46 -05:00
558207872d fix a bunch of build issues 2020-12-03 10:16:15 -05:00
e35b17e685 fix storybooks 2020-12-05 03:40:18 -05:00
75882f4331 fix stylelint issues on Role 2020-12-05 03:32:53 -05:00
9310dc31f9 fix code quality issues 2020-12-05 03:21:02 -05:00
4dc0eeaee3 chore: prettier 2020-12-05 03:14:52 -05:00
156bd8f06e Merge branch 'gcf' into main
Signed-off-by: Katalina Okano <git@kat.cafe>
2020-12-05 03:11:12 -05:00
aad0987dce port full auth flow to cf workers 2020-12-05 03:09:20 -05:00
9eeb946389 initial port to cfworkers i guess 2020-12-03 00:32:07 -05:00
ab9fe30b42 start redoing ci/cd and devops 2020-12-02 21:44:49 -05:00
460770407a fix .env.example and discord-bot env 2020-12-02 18:24:06 -05:00
00f0741e8b add redis-breaker 2020-12-02 18:24:06 -05:00
d3394412db unignore docker-compose 2020-12-02 18:22:18 -05:00
c9cb4c95bc finish login story 2020-12-01 23:14:27 -05:00
a23184efd2 add basic oauth bounces 2020-11-24 22:27:56 -05:00
bebfc862e8 update go.mod 2020-11-23 05:14:47 -05:00
d8a25024de add some gcf scaffolding 2020-11-23 05:09:41 -05:00
3eba2d2de8 merge main 2020-11-23 05:09:14 -05:00
8d23accedc
New Logo with Holiday/Celebration variants (#47)
* update branding

* update branding
2020-11-24 00:58:18 -05:00
8ea7746dd5 fix tests 2020-11-22 01:31:46 -05:00
1dd910a5f6 strip out bazel, grpc 2020-11-22 01:26:41 -05:00
5fcac53be2 feat(editor): add basis of role editor pane 2020-10-25 02:27:55 -04:00
c8adad6c81 feat(design-system): port templates 2020-10-24 22:18:54 -04:00
e61f827645 chore: add react & styled-components to react macro, as most are implied. 2020-10-24 21:46:48 -04:00
ba558ecf91 chore(design-system): cleanup typist tests 2020-10-24 21:26:43 -04:00
652e76241d chore(design-system): cleanmup rolepicker 2020-10-24 21:21:52 -04:00
3bc994da5f chore: fix storybook 2020-10-24 21:21:20 -04:00
d29de5c7ec chore: update dev-container base image 2020-10-24 21:06:31 -04:00
4c3f5de0f6 fix(design-system): fix typist tests 2020-10-24 21:02:41 -04:00
b3c384421b feat(design-system): port molecules 2020-10-24 18:03:55 -04:00
35e4c94e56 chore: update npm packages.. part2 2020-10-24 14:51:50 -04:00
f163350057 chore: update npm packages 2020-10-24 14:40:07 -04:00
91a7d83f3d chore: go.sum sync 2020-10-24 10:14:13 -04:00
dependabot[bot]
e875ca796d
chore(deps): bump actions/cache from v2.1.1 to v2.1.2 (#10)
Bumps [actions/cache](https://github.com/actions/cache) from v2.1.1 to v2.1.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.1...d1255ad9362389eac595a9ae406b8e8cb3331f16)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-16 08:07:44 -04:00
682fe0842d chore: fix dep pinning, update react, @babel/core 2020-10-16 04:34:45 -04:00
4a558bb532 ci: add bazel disk caches to workflows 2020-10-16 04:23:30 -04:00
2f8fdf3b82 chore: add .bazelrc 2020-10-16 04:19:05 -04:00
a0b4392b05 chore: restructure bazel macros 2020-10-15 00:51:53 -04:00
a33aa3841c chore: tslint to eslint 2020-10-15 00:17:53 -04:00
97c09f4aa5 chore: fix codeql alerts 2020-10-15 00:02:50 -04:00
89f237cf22
feat: Add majority of design system components, build system fixes (#11)
* feat(design-system): pre-port of roleypoly/ui

* feat(design-system): port molecules

* chore(design-system): prettier

* feat(design-system): add intro card and MDX components

* fix(common/utils): hack fixtures test data moved to design-system, update accordingly

* chore: document protoReflection.ts

* fix(design-system): some molecules missed the magic fuckery

* ci: keep going on bazel test failures

* fix(design-system): server masthead molecule missed the magic fuckery

* chore: fix ts paths

* chore: fix docker publisher

* chore: fix docker publisher names

* chore(discord-bot): fix publisher

* chore(discord-bot): fix publisher
2020-10-14 22:33:01 -04:00
c41fcabfd0 fix(design-system): make shared-types visible to all of design-system 2020-10-14 14:56:20 -04:00
3d867c1db0 chore: fix storybook for realsies 2020-10-14 12:49:21 -04:00
1bbc61c82f chore: change bazelrc to only set workspace status on test, build, and run 2020-10-12 17:39:20 -04:00
61f6c3b34b fix(common): fix utils tests by improving jest dependency resolution 2020-10-12 17:38:33 -04:00
5b440ffa8d feat(common): port utils, tests currently broken 2020-10-11 19:19:12 -04:00
c7afd84e1e chore: prettier shell stuff 2020-10-11 19:10:05 -04:00
a9af2d10bd feat(ts-protoc-gen): change unary grpcweb responses to promises 2020-10-11 19:08:51 -04:00
c26f3c9ef7 chore(ts-protoc-gen): fix types because lol wtf 2020-10-11 18:55:30 -04:00
783915f057 chore(ts-protoc-gen): prettier 2020-10-11 18:35:22 -04:00
291ec9576f feat(ts-protoc-gen): initial commit, broken 2020-10-11 18:34:46 -04:00
9823670084 chore: fix markdown prettier formatting 2020-10-11 15:56:35 -04:00
efe1e5ea5e ci: move dev-container again, just dont run it with everything else 2020-10-11 15:36:42 -04:00
ac830fc946 chore: add jest tests, cleanup rpc 2020-10-11 15:10:20 -04:00
5977c35d38 ci: fix devcontainer path 2020-10-11 13:55:46 -04:00
cb5ff9602a ci: add -c opt to CI builds 2020-10-11 13:16:43 -04:00
9e6a942018 chore: speed up builds by breaking out dev-container again 2020-10-11 06:26:26 -04:00
2bb7d8666d fix(design-system): tab view selected match was incorrect type 2020-10-11 06:09:20 -04:00
2d919c6053 feat(rpc): port RPC repo to bazel monorepo 2020-10-11 05:53:35 -04:00
e33e6f8574 chore: add rpc stuff i guess idk 2020-10-10 12:54:43 -04:00
00dff464df feat(design-system): use rpcs 2020-10-10 12:52:30 -04:00
f31b32c54a feat(rpc): add rpcs 2020-10-10 12:51:47 -04:00
cdafaa90db fix(design-system): redo tab view to be based on index instead of title 2020-10-10 05:01:12 -04:00
878f94ea93 fix(design-system): remove unused import in avatar stories 2020-10-10 04:47:50 -04:00
67fd42f0d7 feat(design-system): convert button stories 2020-10-10 04:46:26 -04:00
70fa51d4a1 chore: prettier 2020-10-10 04:36:22 -04:00
ccf89d8480 feat(design-system): convert sparkle stories 2020-10-10 04:23:35 -04:00
f7e2d1afef feat(design-system): add space stories 2020-10-10 04:20:01 -04:00
4a4015f765 feat(design-system): convert roles to monorepo and stories, add legacy rpc package 2020-10-10 04:18:23 -04:00
d0afb1488e feat(design-system): convert popover story 2020-10-10 03:54:58 -04:00
9e6f8fd423 ci: revert accidental bazel build change 2020-10-09 15:34:57 -04:00
e66758eaa7 Merge branch 'tf' into main 2020-10-09 15:26:18 -04:00
6373de8ab5 ci: add storybook vercel deploy 2020-10-09 15:21:16 -04:00
72ea639c5d feat(design-system): port most of ui atoms to bazel monorepo and new storybook 2020-10-09 15:17:23 -04:00
d1bb55bb7c temp tf 2020-10-09 10:55:02 -04:00
ec505739c8 temp tf 2020-10-09 10:54:55 -04:00
a5e2fdc7a7 chore(deps): upgrade oauth2 2020-10-07 19:46:04 -04:00
dd8841d0ae chore(dev-container): add buildifier and friends 2020-10-07 23:24:00 +00:00
3f9ac5f275 chore: add dev-container readme 2020-10-07 18:55:52 -04:00
f32b2a2a98 chore: update dev-container doc 2020-10-07 18:41:36 -04:00
3a9ae9278a ci: dev-containers needs better login 2020-10-07 02:25:34 -04:00
f1ea4640f3 ci: dev-container for dockerhub needs stamp status 2020-10-07 02:14:53 -04:00
d4727cd92a chore: fix secret->secrets gh action key 2020-10-07 02:04:05 -04:00
b834066479 chore: redo container publishing, port dev-container to bazel 2020-10-07 02:02:52 -04:00
dependabot[bot]
101c476739
chore(deps-dev): bump typescript from 4.0.2 to 4.0.3 (#7)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.0.2...v4.0.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 16:01:27 -04:00
dependabot[bot]
a8ceb8b3ed
chore(deps-dev): bump prettier from 2.1.1 to 2.1.2 (#8)
Bumps [prettier](https://github.com/prettier/prettier) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.1.1...2.1.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 13:03:45 -04:00
dependabot[bot]
064f5bda47
chore(deps-dev): bump @bazel/typescript from 2.2.0 to 2.2.1 (#9)
Bumps [@bazel/typescript](https://github.com/bazelbuild/rules_nodejs/tree/HEAD/packages/typescript) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/bazelbuild/rules_nodejs/releases)
- [Changelog](https://github.com/bazelbuild/rules_nodejs/blob/stable/CHANGELOG.md)
- [Commits](https://github.com/bazelbuild/rules_nodejs/commits/HEAD/packages/typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 12:59:10 -04:00
dependabot[bot]
e95bfc2f8d
chore(deps): bump actions/cache from v1 to v2.1.1 (#6)
Bumps [actions/cache](https://github.com/actions/cache) from v1 to v2.1.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v1...5ca27f25cb3a0babe750cad7e4fddd3e55f29e9a)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 12:58:53 -04:00
515a32fe44 chore(codeql): use +security-and-quality 2020-10-06 20:44:36 -04:00
d92f3680ff chore: fix dependabot.yml indentation 2020-10-06 20:41:03 -04:00
d5c21b8e65 chore: chaotically define dependabot stuff 2020-10-06 20:15:12 -04:00
a58d07f16a chore: re-run gazelle after gazelle update-repos 2020-10-06 13:29:07 -04:00
0002cefd7f chore: sync tinkering updates 2020-10-06 13:25:16 -04:00
4cef998233 ci: fix dev-container run block indent 2020-09-27 01:09:17 -04:00
584 changed files with 34973 additions and 14203 deletions

3
.babelrc.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
plugins: ['styled-components'],
};

View file

@ -1,10 +0,0 @@
load("@io_bazel_rules_docker//container:container.bzl", "container_push")
container_push(
name = "publish-dev-container",
format = "Docker",
image = "@dev-container//image:dockerfile_image.tar",
registry = "docker.pkg.github.com",
repository = "roleypoly/roleypoly/dev-container",
tag = "{STABLE_GIT_BRANCH}",
)

View file

@ -1,14 +0,0 @@
FROM mcr.microsoft.com/vscode/devcontainers/go:1.15
# Install Bazel
ARG BAZEL_VERSION=3.5.0
ARG BAZEL_DOWNLOAD_SHA=dev-mode
RUN curl -fSsL -o /tmp/bazel-installer.sh https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh \
&& ([ "${BAZEL_DOWNLOAD_SHA}" = "dev-mode" ] || echo "${BAZEL_DOWNLOAD_SHA} */tmp/bazel-installer.sh" | sha256sum --check - ) \
&& /bin/bash /tmp/bazel-installer.sh --base=/usr/local/bazel \
&& rm /tmp/bazel-installer.sh
# Install Node.js
ARG INSTALL_NODE="true"
ARG NODE_VERSION="lts/*"
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

View file

@ -1,28 +1,27 @@
{ {
"name": "Roleypoly (Bazel, Go, Node)", "name": "Roleypoly (Node)",
"build": { "image": "ghcr.io/roleypoly/dev-container:main",
"dockerfile": "Dockerfile",
"args": {
"BAZEL_VERSION": "3.5.0",
}
},
// Set *default* container specific settings.json values on container create. // Set *default* container specific settings.json values on container create.
"settings": { "settings": {
"terminal.integrated.shell.linux": "/bin/bash" "terminal.integrated.shell.linux": "/bin/bash"
}, },
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"bazelbuild.vscode-bazel",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"golang.go",
"hashicorp.terraform", "hashicorp.terraform",
"firsttris.vscode-jest-runner", "firsttris.vscode-jest-runner",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"zxh404.vscode-proto3",
"jpoissonnier.vscode-styled-components", "jpoissonnier.vscode-styled-components",
"eg2.vscode-npm-script", "eg2.vscode-npm-script",
"christian-kohler.npm-intellisense" "christian-kohler.npm-intellisense",
] "ms-azuretools.vscode-docker",
"eamodio.gitlens",
"davidanson.vscode-markdownlint",
"stylelint.vscode-stylelint",
"pflannery.vscode-versionlens",
"visualstudioexptteam.vscodeintellicode",
"bungcip.better-toml"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
@ -30,5 +29,5 @@
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust // Uncomment when using a ptrace-based debugger like C++, Go, and Rust
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], // "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode" "remoteUser": "vscode"
} }

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
node_modules
.next

21
.env.example Normal file
View file

@ -0,0 +1,21 @@
# Make an application at https://discord.com/developers/applications
BOT_CLIENT_ID=000000000000000000
BOT_CLIENT_SECRET=RnX8pXXXXXXXXXXXXXXXXXXXXXXXXXu-
BOT_TOKEN=Mzk2MjI3MTM0MjI3NXXXXXXXXXXXXXXXXXXXXXPUlYoARXXXXXXXXXXXXXX
DISCORD_PUBLIC_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
# Comma separated; put your user ID here. Gives elevated permissions to everything.
ROOT_USERS=62601275618889728
# Comma separated; list any bot IDs that are allowed to operate upon this bot.
ALLOWED_BOTS=
# If 6600 or 6601 is taken, change this, and all other 6600/6601 references.
PORT=6609
UI_PORT=6601
# Again, probably right. Do not put a trailing /
UI_PUBLIC_URI=http://localhost:6601
API_PUBLIC_URI=http://localhost:6609
ALLOWED_CALLBACK_HOSTS=http://localhost:6601,https://stage.roleypoly.com,https://next.roleypoly.com,https://roleypoly.com,https://*.roleypoly.pages.dev

128
.eslintrc.js Normal file
View file

@ -0,0 +1,128 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
extends: ['prettier', 'prettier/@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: [
'eslint-plugin-import',
'eslint-plugin-jsdoc',
'eslint-plugin-react',
'@typescript-eslint',
'@typescript-eslint/tslint',
],
rules: {
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/indent': 'off',
'@typescript-eslint/member-delimiter-style': [
'off',
{
multiline: {
delimiter: 'none',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
},
],
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowTaggedTemplates: true,
allowShortCircuit: true,
},
],
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/quotes': 'off',
'@typescript-eslint/semi': ['off', null],
'@typescript-eslint/triple-slash-reference': [
'error',
{
path: 'always',
types: 'prefer-import',
lib: 'always',
},
],
'@typescript-eslint/type-annotation-spacing': 'off',
'@typescript-eslint/unified-signatures': 'error',
'arrow-parens': ['off', 'always'],
'brace-style': ['off', 'off'],
'comma-dangle': 'off',
curly: ['error', 'multi-line'],
'eol-last': 'off',
eqeqeq: ['error', 'smart'],
'id-blacklist': [
'error',
'any',
'Number',
'number',
'String',
'string',
'Boolean',
'boolean',
'Undefined',
'undefined',
],
'id-match': 'error',
'import/no-deprecated': 'error',
'jsdoc/check-alignment': 'error',
'jsdoc/check-indentation': 'error',
'jsdoc/newline-after-description': 'error',
'linebreak-style': 'off',
'max-len': 'off',
'new-parens': 'off',
'newline-per-chained-call': 'off',
'no-caller': 'error',
'no-cond-assign': 'error',
'no-constant-condition': 'error',
'no-control-regex': 'error',
'no-duplicate-imports': 'error',
'no-empty': 'error',
'no-eval': 'error',
'no-extra-semi': 'off',
'no-fallthrough': 'error',
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'off',
'no-multiple-empty-lines': 'off',
'no-redeclare': 'error',
'no-regex-spaces': 'error',
'no-return-await': 'error',
'no-throw-literal': 'error',
'no-trailing-spaces': 'off',
'no-underscore-dangle': 'error',
'no-unused-labels': 'error',
'no-var': 'error',
'one-var': ['error', 'never'],
'quote-props': 'off',
radix: 'error',
'react/jsx-curly-spacing': 'off',
'react/jsx-equals-spacing': 'off',
'react/jsx-wrap-multilines': 'off',
'space-before-function-paren': 'off',
'space-in-parens': ['off', 'never'],
'spaced-comment': [
'error',
'always',
{
markers: ['/'],
},
],
'use-isnan': 'error',
},
};

2
.github/FUNDING.yml vendored
View file

@ -1,7 +1,7 @@
# These are supported funding model platforms # These are supported funding model platforms
github: kayteh github: kayteh
patreon: kata patreon: roleypoly
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: roleypoly ko_fi: roleypoly
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

21
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
- package-ecosystem: 'gomod'
directory: '/'
schedule:
interval: 'daily'
- package-ecosystem: 'terraform'
directory: '/terraform'
schedule:
interval: 'daily'

View file

@ -1,59 +1,91 @@
name: Bazel Build name: Roleypoly CI
on: push on:
push:
pull_request:
jobs: jobs:
bazel_build: node_test:
name: Bazel Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Node CI
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Mount bazel cache - uses: actions/setup-node@v2.5.1
uses: actions/cache@v1
with: with:
path: "/home/runner/.cache/bazel" node-version: '20'
key: bazel cache: yarn
- name: Install bazelisk - run: yarn install --frozen-lockfile
run: |
curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64"
mkdir -p "${GITHUB_WORKSPACE}/bin/"
mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel"
chmod +x "${GITHUB_WORKSPACE}/bin/bazel"
- name: Test - run: yarn lint
run: |
"${GITHUB_WORKSPACE}/bin/bazel" test \
--stamp \
--workspace_status_command hack/workspace_status.sh \
//src/...
- name: Docker Login # - run: yarn test
run: |
echo ${{github.token}} | docker login -u ${{github.actor}} --password-stdin docker.pkg.github.com
- name: Publish Artifacts worker_build:
run: | runs-on: ubuntu-latest
"${GITHUB_WORKSPACE}/bin/bazel" query //src/... |\ name: Worker Build & Publish
grep +publish |\ if: startsWith(github.ref, 'refs/heads/dependabot/') != true
xargs -l1 "${GITHUB_WORKSPACE}/bin/bazel" run \ needs:
--stamp \ - node_test
--workspace_status_command hack/workspace_status.sh strategy:
matrix:
worker:
- api
steps:
- uses: actions/checkout@master
- name: Write Artifact Manifest - uses: actions/setup-node@v2.5.1
run: |
artifacts=$(${GITHUB_WORKSPACE}/bin/bazel query //src/... | grep +publish)
publishedServices=${artifacts//$'//src/'/}
publishedServices=${publishedServices//$':+publish'/}
manifestJSON='{"services": {}}'
for svc in $publishedServices; do
manifestJSON=$(echo $manifestJSON | jq ".services+={\"$svc\":\"$(cat bazel-bin/src/$svc/+publish.digest)\"}")
done
echo $manifestJSON > manifest.json
- name: Upload Artifact Manifest
uses: actions/upload-artifact@v2
with: with:
name: manifest.json node-version: '20'
path: manifest.json cache: yarn
- id: 'auth'
uses: 'google-github-actions/auth@v0'
with:
credentials_json: '${{ secrets.GCS_TF_KEY }}'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0
with:
project_id: ${{ secrets.GCS_PROJECT_ID }}
export_default_credentials: true
- name: Check if already deployed
id: check
run: |
gsutil stat gs://roleypoly-artifacts/workers/${{ github.sha }}/index.mjs \
&& echo ::set-output name=skip::1 \
|| echo ::set-output name=skip::0
- run: yarn install --frozen-lockfile
if: steps.check.outputs.skip == '0'
- run: |
yarn build:api
if: steps.check.outputs.skip == '0'
- id: upload-file
if: github.event_name == 'push' && steps.check.outputs.skip == '0'
uses: google-github-actions/upload-cloud-storage@main
with:
path: packages/api/dist/index.mjs
destination: roleypoly-artifacts/workers/${{ github.sha }}
trigger_deploy:
name: Deploy to Stage
needs:
- worker_build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Invoke Deploy workflow
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Deploy
token: ${{ secrets.GITOPS_TOKEN }}
inputs: |-
{
"environment": "stage",
"worker_tag": "${{ github.sha }}"
}

View file

@ -1,62 +1,47 @@
name: "CodeQL" name: 'Code Scanning - Action'
on: on:
push: push:
branches: [main]
pull_request: pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule: schedule:
- cron: '0 1 * * 2' # ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: '30 1 * * 0'
jobs: jobs:
analyze: CodeQL-Build:
name: Analyze # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go', 'javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: # Override language selection by uncommenting this and choosing your languages
languages: ${{ matrix.language }} # with:
# If you wish to specify custom queries, you can do so here or in a config file. # languages: go, javascript, csharp, python, cpp, java
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below).
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # ✏️ If the Autobuild fails above, remove it and uncomment the following
# and modify them (or add more) to build your code if your project # three lines and modify them (or add more) to build your code if your
# uses a compiled language # project uses a compiled language
#- run: | #- run: |
# make bootstrap # make bootstrap

128
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,128 @@
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: 'One of: stage, prod'
required: true
default: stage
bot_tag:
description: 'tag/digest reference to a UI container build'
required: false
default: ':main'
worker_tag:
description: 'bucket key to fetch worker from'
required: false
default: '' # Empty will try using current main branch hash
jobs:
deploy_terraform:
name: Deploy Terraform
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: hashicorp/setup-terraform@v1.3.2
with:
terraform_version: ^1.1.4
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0
with:
project_id: ${{ secrets.GCS_PROJECT_ID }}
service_account_key: ${{ secrets.GCS_TF_KEY }}
export_default_credentials: true
- name: Get Google Secrets (they keep them in a box under a tree)
id: secrets
uses: google-github-actions/get-secretmanager-secrets@main
with:
secrets: |-
secretJSON:${{ secrets.GCS_PROJECT_ID }}/${{github.event.inputs.environment}}-tfvars
- name: Pull necessary artifacts
working-directory: ./terraform
run: |
currentHash=${{ github.sha }}
targetArtifact=${{ github.event.inputs.worker_tag }}
selected="${targetArtifact:-$currentHash}"
mkdir worker-dist
gsutil cp -r "gs://roleypoly-artifacts/workers/$selected/*" worker-dist/
- name: Terraform init
working-directory: ./terraform
run: |
terraform init --backend-config "prefix=${{github.event.inputs.environment}}"
- name: Write *.auto.tfvars.json files
working-directory: ./terraform
run: |
echo \
'{"bot_tag": "${{github.event.inputs.bot_tag}}", "worker_tag": "${{github.event.inputs.worker_tag}}", "path_to_worker": "./worker-dist/index.mjs"}' \
| jq . \
| tee tags.auto.tfvars.json
echo ${SECRET_TFVARS} > secrets.auto.tfvars.json
env:
SECRET_TFVARS: ${{ steps.secrets.outputs.secretJSON }}
- name: Terraform plan
working-directory: ./terraform
run: |
terraform plan \
-var-file variables/global.tfvars \
-var-file variables/${{github.event.inputs.environment}}.tfvars \
-out=./deployment.tfplan
- name: Terraform apply
working-directory: ./terraform
run: |
terraform apply \
-auto-approve \
deployment.tfplan
- name: Yell Success at Discord
if: success()
run: |
DATA='{
"embeds": [
{
"title": "Roleypoly Deployment Success",
"description": "Roleypoly was successfully deployed at '$(date)'",
"color": 4634182,
"author": {
"name": "Deployment Notification",
"url": "https://github.com/roleypoly/roleypoly/actions/runs/${{ github.run_id }}"
},
"footer": {
"text": "GitHub Actions"
}
}
]
}'
curl -X POST -H "content-type: application/json" --data "$DATA" ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}
- name: Yell Failure at Discord
if: failure()
run: |
DATA='{
"embeds": [
{
"title": "Roleypoly Deployment Failed",
"description": "Roleypoly failed to be deployed at '$(date)'",
"color": 15291219,
"author": {
"name": "Deployment Notification",
"url": "https://github.com/roleypoly/roleypoly/actions/runs/${{ github.run_id }}"
},
"footer": {
"text": "GitHub Actions"
}
}
]
}'
curl -X POST -H "content-type: application/json" --data "$DATA" ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}

View file

@ -1,34 +1,53 @@
name: Build Dev Container name: Dev Container
on: on:
push: push:
paths: paths:
- .devcontainer/Dockerfile - hack/dockerfiles/dev-container.Dockerfile
- .github/workflows/dev-container.yml
schedule: schedule:
- cron: "0 12 * * 2" # 12 noon every tuesday - cron: '0 12 * * 2' # 12 noon every tuesday
jobs: jobs:
dev_container_build: docker_build:
name: Bazel Build (Dev Container) name: Docker Build & Publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Mount bazel cache
uses: actions/cache@v1 - uses: actions/cache@v2.1.6
with: with:
path: "/home/runner/.cache/bazel" path: /tmp/.buildx-cache
key: bazel key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Install bazelisk - name: Docker meta
run: | id: docker_meta
curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64" uses: crazy-max/ghaction-docker-meta@v1
mkdir -p "${GITHUB_WORKSPACE}/bin/" with:
mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel" images: ghcr.io/roleypoly/dev-container
chmod +x "${GITHUB_WORKSPACE}/bin/bazel" tag-sha: true
- name: Build & Publish Dev Container - name: Set up Docker Buildx
run: | id: buildx
"${GITHUB_WORKSPACE}/bin/bazel" run \ uses: docker/setup-buildx-action@v1
--stamp \ with:
--workspace_status_command hack/workspace_status.sh\ install: true
//.devcontainer:publish-dev-container
- name: Login to GitHub Packages Docker Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: roleypoly
password: ${{ secrets.GHCR_PAT }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./hack/dockerfiles/dev-container.Dockerfile
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View file

@ -1,27 +0,0 @@
name: Release Workflow
on: workflow_dispatch
jobs:
commit_release_tag:
name: Commit Roleypoly Release Tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
with:
ssh-key: ${{secrets.DEPLOY_KEY}}
# - name: Setup Git
# uses: webfactory/ssh-agent@v0.2.0
# with:
# ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- name: Push changes
id: push
run: |
TAG=$(date +v%Y%m%d-%H%M%S)
git config --local user.email "gh-automation@roleypoly.com"
git config --local user.name "Roleypoly Release Automation"
git tag $TAG
git push origin $TAG
echo "::set-output release_tag=${TAG}"

11
.gitignore vendored
View file

@ -1,7 +1,8 @@
bazel-bin
bazel-out
bazel-roleypoly
bazel-testlogs
node_modules node_modules
.env .env
docker-compose.yaml *.log
storybook-static
.next
worker
.devdbs
dist

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn lint-staged

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
16.13.2

9
.prettierignore Normal file
View file

@ -0,0 +1,9 @@
bazel-*
dist
storybook-static
.next
worker
**/dist
terraform
.husky/_
.mf

View file

9
.prettierrc.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
printWidth: 90,
useTabs: false,
tabWidth: 2,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
semi: true,
};

3
.stylelintignore Normal file
View file

@ -0,0 +1,3 @@
bazel-*
dist
storybook-static

16
.stylelintrc Normal file
View file

@ -0,0 +1,16 @@
{
"customSyntax": "@stylelint/postcss-css-in-js",
"extends": [
"stylelint-config-recommended",
"stylelint-config-styled-components"
],
"rules": {
"color-function-notation": "modern",
"shorthand-property-no-redundant-values": true,
"font-weight-notation": "numeric",
"alpha-value-notation": "percentage",
"hue-degree-notation": "angle",
"function-calc-no-unspaced-operator": true,
"length-zero-no-unit": true
}
}

17
.vscode/settings.json vendored
View file

@ -1,10 +1,15 @@
{ {
"go.inferGopath": false,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"bazel.buildifierFixOnFormat": true,
"[starlark]": { "[starlark]": {
"editor.tabSize": 4 "editor.tabSize": 4
} },
"bazel.buildifierFixOnFormat": true,
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"editor.tabSize": 2,
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.preferences.importModuleSpecifier": "non-relative"
} }

View file

@ -1,42 +0,0 @@
load("@bazel_gazelle//:def.bzl", "gazelle")
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
# gazelle:prefix github.com/roleypoly/roleypoly
# gazelle:exclude hack/**
gazelle(name = "gazelle")
exports_files(
["tsconfig.json"],
visibility = ["//visibility:public"],
)
filegroup(
name = "node_modules",
srcs = glob(
include = [
"node_modules/**/*.js",
"node_modules/**/*.d.ts",
"node_modules/**/*.json",
"node_modules/.bin/*",
],
exclude = [
# Files under test & docs may contain file names that
# are not legal Bazel labels (e.g.,
# node_modules/ecstatic/test/public/中文/檔案.html)
"node_modules/**/test/**",
"node_modules/**/docs/**",
# Files with spaces in the name are not legal Bazel labels
"node_modules/**/* */**",
"node_modules/**/* *",
],
),
)
# Create a tsc_wrapped compiler rule to use in the ts_library
# compiler attribute when using self-managed dependencies
nodejs_binary(
name = "@bazel/typescript/tsc_wrapped",
entry_point = "@npm//:node_modules/@bazel/typescript/internal/tsc_wrapped/tsc_wrapped.js",
# Point bazel to your node_modules to find the entry point
node_modules = "//:node_modules",
)

109
README.md
View file

@ -4,26 +4,117 @@ https://roleypoly.com
Tame your Discord roles. Tame your Discord roles.
### Need Help with Roleypoly? ## Need Help with Roleypoly? 💁‍♀️
📚 [Please read through our community documentation.](https://github.com/roleypoly/community-docs) 📚 [Please read through our community documentation.](https://github.com/roleypoly/community-docs)
😕 [Still confused? Talk to us on Discord!](https://discord.gg/PWQUVsd) 😕 [Still confused? Talk to us on Discord!](https://discord.gg/PWQUVsd)
## Developing ## Developing
Roleypoly is a distributed system built with Go, React, Terraform, and Bazel. Roleypoly is a distributed system built with TypeScript, React, Terraform, and Go.
This repo is currently being re-architected into a monorepo, so most processes might not be documented. This app is heavily edge computing-based with the backend being deployed via Cloudflare Workers, UI server on Google Cloud Run with 8 regions, and the mention responder in Google Compute Engine.
### Extra Development Docs
- 🏭 [Infrastructure](docs/infrastructure.md)
- 🧾 [User Stories](docs/user-stories.md)
### Quickstart ### Quickstart
This repo can be quickly setup with [VSCode Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or [GitHub Codespaces](https://github.com/codespaces). This will setup a fully featured Docker container for developing VSCode, including extensions. #### Option 1 🚀: E2E Dockerized Emulation
If you'd like to not use either of those, a docker image can be built from `.devcontainers/Dockerfile`, or used normally via `docker.pkg.github.com/roleypoly/roleypoly/dev-container`. This use case is not actively investigated, but with tinkering, will work. Feel free to document this process and open a PR :) This is the fastest way to start. You must be using MacOS or Linux (WSL2 is ok!) for this to be successful.
### Things to Know - Setup `.env` using [`.env.example`][envexample] as a template and guide.
- When setting up your Discord Application, be sure to set `http://localhost:6609/login-callback` as the OAuth2 callback URL.
- Run: `yarn install`
- Run: `docker-compose up`
- This starts the UI and API servers in hot-reload dev/emulation mode. All changes to TS/TSX files should be properly captured and reloaded for you!
- Develop you a Roleypoly!
Bazel can make some tasks far harder normal. Ideally, these are automated over. #### Option 2 🐱‍👤: Local Emulation
- **Updating `go.mod`?** - With pre-requisites:
- Run `hack/gazelle.sh` to regenerate `deps.bzl`. - Node.js 14, Yarn
- Setup `.env` using [`.env.example`][envexample] as a template and guide.
- When setting up your Discord Application, be sure to set `http://localhost:6609/login-callback` as the OAuth2 callback URL.
- Run: `yarn install`
- Run: `yarn start`
- This starts the Web UI, Storybook, and API servers in hot-reload dev/emulation mode. All changes to TS/TSX files should be properly captured and reloaded for you!
- Develop you a Roleypoly!
#### Option 3 🐄🤠: Wrangler (No emulation)
**Outdated. This won't work, but could give you an idea of what to do.**
This is probably extremely painful and requires you to have a Cloudflare account.
- With pre-requisites:
- Cloudflare Account
- Node.js 14, Yarn
- `npm i -g @cloudflare/wrangler`
- Do `wrangler init`, `wrangler login`, etc...
- Setup Wrangler for the project
- Change `account_id` to your Cloudflare Account ID in `wrangler.toml`
- Add a `dev` environment to `wrangler.toml`, using [`.env.example`][envexample] as a reference for how values should be set
- When setting up your Discord Application, be sure to set `http://localhost:8787/login-callback` as the OAuth2 callback URL.
```toml
[env.dev]
[env.dev.vars]
BOT_CLIENT_ID = ...
UI_PUBLIC_URI = "http://localhost:6601"
API_PUBLIC_URI = "http://localhost:8787"
ROOT_USERS = ...
```
- `wrangler secret put BOT_TOKEN -e dev`
- `wrangler secret put BOT_CLIENT_SECRET -e dev`
- Setup KV Namespaces -- Please follow the instructions listed after the command runs.
- `wrangler kvnamespace create -e dev KV_SESSIONS --preview`
- `wrangler kvnamespace create -e dev KV_GUILD_DATA --preview`
- `wrangler kvnamespace create -e dev KV_GUILDS --preview`
- Setup `.env` using [`.env.example`][envexample] as a template and guide.
- Run `yarn install`
- Run both `wrangler dev -e dev` and `yarn start:web`
- This starts the Web UI and API servers in hot-reload dev mode. All changes to TS/TSX files should be properly captured and reloaded for you!
- Develop you a Roleypoly
- And get a beer or heated plant because oh no.
### Developing Design System Components
For working with the [Roleypoly Design System](https://ui.roleypoly.com), use the below steps as reference. Code lives in `src/design-system` among elsewhere.
Run:
- `yarn` to install deps
- `yarn start:design-system` to open storybook
- `yarn test:design-system` to test
### Developing Web UI
For working with the Next.js frontend components, use the below steps as reference. Code lives in `src/web` among elsewhere.
Run:
- `yarn` to install deps
- `yarn start:web` to run Next.js dev server
- `yarn test:web` to test
### Developing API Components
For working with the API, use the below steps as reference. Code lives in `src/api`.
Run:
- `yarn` to install deps
- `yarn start:api` to start an emulated worker
- `yarn test:api` to test
[envexample]: .env.example

100
WORKSPACE
View file

@ -1,100 +0,0 @@
workspace(
name = "roleypoly",
managed_directories = {
"@npm": ["node_modules"],
},
)
### BAZEL
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "io_bazel_rules_go",
sha256 = "08c3cd71857d58af3cda759112437d9e63339ac9c6e0042add43f4d94caf632d",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.24.2/rules_go-v0.24.2.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.24.2/rules_go-v0.24.2.tar.gz",
],
)
http_archive(
name = "bazel_gazelle",
sha256 = "d4113967ab451dd4d2d767c3ca5f927fec4b30f3b2c6f8135a2033b9c05a5687",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.0/bazel-gazelle-v0.22.0.tar.gz",
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.0/bazel-gazelle-v0.22.0.tar.gz",
],
)
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "b16a03bf63952ae436185c74a5c63bec03c010ed422e230db526af55441a02dd",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.1.0/rules_nodejs-2.1.0.tar.gz"],
)
http_archive(
name = "io_bazel_rules_docker",
sha256 = "4521794f0fba2e20f3bf15846ab5e01d5332e587e9ce81629c7f96c793bb7036",
strip_prefix = "rules_docker-0.14.4",
urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.14.4/rules_docker-v0.14.4.tar.gz"],
)
### GO
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load("//:deps.bzl", "go_repositories")
# gazelle:repository_macro deps.bzl%go_repositories
go_repositories()
go_rules_dependencies()
go_register_toolchains()
gazelle_dependencies()
### NODE
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories")
node_repositories(
package_json = ["//:package.json"],
)
### DOCKER/CONTAINER
load(
"@io_bazel_rules_docker//repositories:repositories.bzl",
container_repositories = "repositories",
)
container_repositories()
load(
"@io_bazel_rules_docker//go:image.bzl",
_go_image_repos = "repositories",
)
_go_image_repos()
load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
container_deps()
load("@io_bazel_rules_docker//repositories:pip_repositories.bzl", "pip_deps")
pip_deps()
# Dev Container stuff
load("@io_bazel_rules_docker//contrib:dockerfile_build.bzl", "dockerfile_image")
load("@io_bazel_rules_docker//container:container.bzl", "container_pull")
container_pull(
name = "devcontainergo",
registry = "mcr.microsoft.com",
repository = "vscode/devcontainers/go",
tag = "1.15",
)
dockerfile_image(
name = "dev-container",
dockerfile = "//.devcontainer:Dockerfile",
)

813
deps.bzl
View file

@ -1,813 +0,0 @@
load("@bazel_gazelle//:deps.bzl", "go_repository")
def go_repositories():
go_repository(
name = "co_honnef_go_tools",
importpath = "honnef.co/go/tools",
sum = "h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=",
version = "v0.0.1-2019.2.3",
)
go_repository(
name = "com_github_alecthomas_template",
importpath = "github.com/alecthomas/template",
sum = "h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=",
version = "v0.0.0-20160405071501-a0175ee3bccc",
)
go_repository(
name = "com_github_alecthomas_units",
importpath = "github.com/alecthomas/units",
sum = "h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=",
version = "v0.0.0-20151022065526-2efee857e7cf",
)
go_repository(
name = "com_github_armon_consul_api",
importpath = "github.com/armon/consul-api",
sum = "h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=",
version = "v0.0.0-20180202201655-eb2c6b5be1b6",
)
go_repository(
name = "com_github_beorn7_perks",
importpath = "github.com/beorn7/perks",
sum = "h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=",
version = "v1.0.0",
)
go_repository(
name = "com_github_burntsushi_toml",
importpath = "github.com/BurntSushi/toml",
sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=",
version = "v0.3.1",
)
go_repository(
name = "com_github_bwmarrin_discordgo",
importpath = "github.com/bwmarrin/discordgo",
sum = "h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM=",
version = "v0.22.0",
)
go_repository(
name = "com_github_cespare_xxhash",
importpath = "github.com/cespare/xxhash",
sum = "h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=",
version = "v1.1.0",
)
go_repository(
name = "com_github_client9_misspell",
importpath = "github.com/client9/misspell",
sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=",
version = "v0.3.4",
)
go_repository(
name = "com_github_coreos_bbolt",
importpath = "github.com/coreos/bbolt",
sum = "h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=",
version = "v1.3.2",
)
go_repository(
name = "com_github_coreos_etcd",
importpath = "github.com/coreos/etcd",
sum = "h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=",
version = "v3.3.10+incompatible",
)
go_repository(
name = "com_github_coreos_go_semver",
importpath = "github.com/coreos/go-semver",
sum = "h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=",
version = "v0.2.0",
)
go_repository(
name = "com_github_coreos_go_systemd",
importpath = "github.com/coreos/go-systemd",
sum = "h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=",
version = "v0.0.0-20190321100706-95778dfbb74e",
)
go_repository(
name = "com_github_coreos_pkg",
importpath = "github.com/coreos/pkg",
sum = "h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=",
version = "v0.0.0-20180928190104-399ea9e2e55f",
)
go_repository(
name = "com_github_cpuguy83_go_md2man_v2",
importpath = "github.com/cpuguy83/go-md2man/v2",
sum = "h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=",
version = "v2.0.0",
)
go_repository(
name = "com_github_data_dog_go_sqlmock",
importpath = "github.com/DATA-DOG/go-sqlmock",
sum = "h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=",
version = "v1.5.0",
)
go_repository(
name = "com_github_davecgh_go_spew",
importpath = "github.com/davecgh/go-spew",
sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=",
version = "v1.1.1",
)
go_repository(
name = "com_github_dghubble_trie",
importpath = "github.com/dghubble/trie",
sum = "h1:euE/xPG0HIg6XiNXYrAHxX9aVwD1gw/yM2kptLOOj6k=",
version = "v0.0.0-20200716043226-5a94efb202d5",
)
go_repository(
name = "com_github_dgrijalva_jwt_go",
importpath = "github.com/dgrijalva/jwt-go",
sum = "h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=",
version = "v3.2.0+incompatible",
)
go_repository(
name = "com_github_dgryski_go_sip13",
importpath = "github.com/dgryski/go-sip13",
sum = "h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4=",
version = "v0.0.0-20181026042036-e10d5fee7954",
)
go_repository(
name = "com_github_facebook_ent",
importpath = "github.com/facebook/ent",
sum = "h1:ds9HENceKzpGBgCRlkZNq6TqBIegwKcF3e5reuV9Z0M=",
version = "v0.4.3",
)
go_repository(
name = "com_github_fsnotify_fsnotify",
importpath = "github.com/fsnotify/fsnotify",
sum = "h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=",
version = "v1.4.7",
)
go_repository(
name = "com_github_ghodss_yaml",
importpath = "github.com/ghodss/yaml",
sum = "h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=",
version = "v1.0.0",
)
go_repository(
name = "com_github_go_bindata_go_bindata",
importpath = "github.com/go-bindata/go-bindata",
sum = "h1:WNHfSP1q2vuAa9vF54RrhCl4nqxCjVcXhlbsRXbGOSY=",
version = "v1.0.1-0.20190711162640-ee3c2418e368",
)
go_repository(
name = "com_github_go_kit_kit",
importpath = "github.com/go-kit/kit",
sum = "h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=",
version = "v0.8.0",
)
go_repository(
name = "com_github_go_logfmt_logfmt",
importpath = "github.com/go-logfmt/logfmt",
sum = "h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=",
version = "v0.4.0",
)
go_repository(
name = "com_github_go_logr_logr",
importpath = "github.com/go-logr/logr",
sum = "h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=",
version = "v0.1.0",
)
go_repository(
name = "com_github_go_openapi_inflect",
importpath = "github.com/go-openapi/inflect",
sum = "h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=",
version = "v0.19.0",
)
go_repository(
name = "com_github_go_sql_driver_mysql",
importpath = "github.com/go-sql-driver/mysql",
sum = "h1:L6V0ANsMIMdLgXly241UXhXNFWYgXbgjHupTAAURrV0=",
version = "v1.5.1-0.20200311113236-681ffa848bae",
)
go_repository(
name = "com_github_go_stack_stack",
importpath = "github.com/go-stack/stack",
sum = "h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=",
version = "v1.8.0",
)
go_repository(
name = "com_github_gogo_protobuf",
importpath = "github.com/gogo/protobuf",
sum = "h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=",
version = "v1.2.1",
)
go_repository(
name = "com_github_golang_glog",
importpath = "github.com/golang/glog",
sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=",
version = "v0.0.0-20160126235308-23def4e6c14b",
)
go_repository(
name = "com_github_golang_groupcache",
importpath = "github.com/golang/groupcache",
sum = "h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=",
version = "v0.0.0-20200121045136-8c9f03a8e57e",
)
go_repository(
name = "com_github_golang_mock",
importpath = "github.com/golang/mock",
sum = "h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=",
version = "v1.1.1",
)
go_repository(
name = "com_github_golang_protobuf",
importpath = "github.com/golang/protobuf",
sum = "h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=",
version = "v1.3.2",
)
go_repository(
name = "com_github_google_btree",
importpath = "github.com/google/btree",
sum = "h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=",
version = "v1.0.0",
)
go_repository(
name = "com_github_google_go_cmp",
importpath = "github.com/google/go-cmp",
sum = "h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=",
version = "v0.3.0",
)
go_repository(
name = "com_github_google_go_github_v32",
importpath = "github.com/google/go-github/v32",
sum = "h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=",
version = "v32.1.0",
)
go_repository(
name = "com_github_google_go_querystring",
importpath = "github.com/google/go-querystring",
sum = "h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=",
version = "v1.0.0",
)
go_repository(
name = "com_github_google_gofuzz",
importpath = "github.com/google/gofuzz",
sum = "h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=",
version = "v1.0.0",
)
go_repository(
name = "com_github_google_renameio",
importpath = "github.com/google/renameio",
sum = "h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=",
version = "v0.1.0",
)
go_repository(
name = "com_github_google_uuid",
importpath = "github.com/google/uuid",
sum = "h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=",
version = "v1.1.2",
)
go_repository(
name = "com_github_gorilla_websocket",
importpath = "github.com/gorilla/websocket",
sum = "h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=",
version = "v1.4.2",
)
go_repository(
name = "com_github_grpc_ecosystem_go_grpc_middleware",
importpath = "github.com/grpc-ecosystem/go-grpc-middleware",
sum = "h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=",
version = "v1.0.0",
)
go_repository(
name = "com_github_grpc_ecosystem_go_grpc_prometheus",
importpath = "github.com/grpc-ecosystem/go-grpc-prometheus",
sum = "h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=",
version = "v1.2.0",
)
go_repository(
name = "com_github_grpc_ecosystem_grpc_gateway",
importpath = "github.com/grpc-ecosystem/grpc-gateway",
sum = "h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI=",
version = "v1.9.0",
)
go_repository(
name = "com_github_hashicorp_hcl",
importpath = "github.com/hashicorp/hcl",
sum = "h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=",
version = "v1.0.0",
)
go_repository(
name = "com_github_inconshreveable_mousetrap",
importpath = "github.com/inconshreveable/mousetrap",
sum = "h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=",
version = "v1.0.0",
)
go_repository(
name = "com_github_jessevdk_go_flags",
importpath = "github.com/jessevdk/go-flags",
sum = "h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=",
version = "v1.4.0",
)
go_repository(
name = "com_github_joho_godotenv",
importpath = "github.com/joho/godotenv",
sum = "h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=",
version = "v1.3.0",
)
go_repository(
name = "com_github_jonboulle_clockwork",
importpath = "github.com/jonboulle/clockwork",
sum = "h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=",
version = "v0.1.0",
)
go_repository(
name = "com_github_json_iterator_go",
importpath = "github.com/json-iterator/go",
sum = "h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=",
version = "v1.1.10",
)
go_repository(
name = "com_github_julienschmidt_httprouter",
importpath = "github.com/julienschmidt/httprouter",
sum = "h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=",
version = "v1.3.0",
)
go_repository(
name = "com_github_kisielk_errcheck",
importpath = "github.com/kisielk/errcheck",
sum = "h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E=",
version = "v1.2.0",
)
go_repository(
name = "com_github_kisielk_gotool",
importpath = "github.com/kisielk/gotool",
sum = "h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=",
version = "v1.0.0",
)
go_repository(
name = "com_github_konsorten_go_windows_terminal_sequences",
importpath = "github.com/konsorten/go-windows-terminal-sequences",
sum = "h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=",
version = "v1.0.1",
)
go_repository(
name = "com_github_kr_logfmt",
importpath = "github.com/kr/logfmt",
sum = "h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=",
version = "v0.0.0-20140226030751-b84e30acd515",
)
go_repository(
name = "com_github_kr_pretty",
importpath = "github.com/kr/pretty",
sum = "h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=",
version = "v0.1.0",
)
go_repository(
name = "com_github_kr_pty",
importpath = "github.com/kr/pty",
sum = "h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=",
version = "v1.1.1",
)
go_repository(
name = "com_github_kr_text",
importpath = "github.com/kr/text",
sum = "h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=",
version = "v0.1.0",
)
go_repository(
name = "com_github_lampjaw_discordclient",
importpath = "github.com/lampjaw/discordclient",
sum = "h1:Y2o9fEOoAYjCw8IDyxUVaBq44AUbOLyPnYSPpM6Ef3M=",
version = "v0.0.0-20200923011548-6558fc9e89df",
)
go_repository(
name = "com_github_lib_pq",
importpath = "github.com/lib/pq",
sum = "h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=",
version = "v1.8.0",
)
go_repository(
name = "com_github_magiconair_properties",
importpath = "github.com/magiconair/properties",
sum = "h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=",
version = "v1.8.0",
)
go_repository(
name = "com_github_mattn_go_runewidth",
importpath = "github.com/mattn/go-runewidth",
sum = "h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=",
version = "v0.0.9",
)
go_repository(
name = "com_github_mattn_go_sqlite3",
importpath = "github.com/mattn/go-sqlite3",
sum = "h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=",
version = "v1.14.3",
)
go_repository(
name = "com_github_matttproud_golang_protobuf_extensions",
importpath = "github.com/matttproud/golang_protobuf_extensions",
sum = "h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=",
version = "v1.0.1",
)
go_repository(
name = "com_github_mitchellh_go_homedir",
importpath = "github.com/mitchellh/go-homedir",
sum = "h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=",
version = "v1.1.0",
)
go_repository(
name = "com_github_mitchellh_mapstructure",
importpath = "github.com/mitchellh/mapstructure",
sum = "h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=",
version = "v1.3.3",
)
go_repository(
name = "com_github_modern_go_concurrent",
importpath = "github.com/modern-go/concurrent",
sum = "h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=",
version = "v0.0.0-20180306012644-bacd9c7ef1dd",
)
go_repository(
name = "com_github_modern_go_reflect2",
importpath = "github.com/modern-go/reflect2",
sum = "h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=",
version = "v1.0.1",
)
go_repository(
name = "com_github_mwitkow_go_conntrack",
importpath = "github.com/mwitkow/go-conntrack",
sum = "h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=",
version = "v0.0.0-20161129095857-cc309e4a2223",
)
go_repository(
name = "com_github_oklog_ulid",
importpath = "github.com/oklog/ulid",
sum = "h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=",
version = "v1.3.1",
)
go_repository(
name = "com_github_olekukonko_tablewriter",
importpath = "github.com/olekukonko/tablewriter",
sum = "h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=",
version = "v0.0.4",
)
go_repository(
name = "com_github_oneofone_xxhash",
importpath = "github.com/OneOfOne/xxhash",
sum = "h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=",
version = "v1.2.2",
)
go_repository(
name = "com_github_pelletier_go_toml",
importpath = "github.com/pelletier/go-toml",
sum = "h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=",
version = "v1.2.0",
)
go_repository(
name = "com_github_pkg_errors",
importpath = "github.com/pkg/errors",
sum = "h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=",
version = "v0.9.1",
)
go_repository(
name = "com_github_pmezard_go_difflib",
importpath = "github.com/pmezard/go-difflib",
sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=",
version = "v1.0.0",
)
go_repository(
name = "com_github_prometheus_client_golang",
importpath = "github.com/prometheus/client_golang",
sum = "h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=",
version = "v0.9.3",
)
go_repository(
name = "com_github_prometheus_client_model",
importpath = "github.com/prometheus/client_model",
sum = "h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=",
version = "v0.0.0-20190129233127-fd36f4220a90",
)
go_repository(
name = "com_github_prometheus_common",
importpath = "github.com/prometheus/common",
sum = "h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=",
version = "v0.4.0",
)
go_repository(
name = "com_github_prometheus_procfs",
importpath = "github.com/prometheus/procfs",
sum = "h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=",
version = "v0.0.0-20190507164030-5867b95ac084",
)
go_repository(
name = "com_github_prometheus_tsdb",
importpath = "github.com/prometheus/tsdb",
sum = "h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=",
version = "v0.7.1",
)
go_repository(
name = "com_github_rogpeppe_fastuuid",
importpath = "github.com/rogpeppe/fastuuid",
sum = "h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=",
version = "v0.0.0-20150106093220-6724a57986af",
)
go_repository(
name = "com_github_rogpeppe_go_internal",
importpath = "github.com/rogpeppe/go-internal",
sum = "h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=",
version = "v1.3.0",
)
go_repository(
name = "com_github_russross_blackfriday_v2",
importpath = "github.com/russross/blackfriday/v2",
sum = "h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=",
version = "v2.0.1",
)
go_repository(
name = "com_github_segmentio_ksuid",
importpath = "github.com/segmentio/ksuid",
sum = "h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY=",
version = "v1.0.3",
)
go_repository(
name = "com_github_shurcool_sanitized_anchor_name",
importpath = "github.com/shurcooL/sanitized_anchor_name",
sum = "h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=",
version = "v1.0.0",
)
go_repository(
name = "com_github_sirupsen_logrus",
importpath = "github.com/sirupsen/logrus",
sum = "h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=",
version = "v1.2.0",
)
go_repository(
name = "com_github_soheilhy_cmux",
importpath = "github.com/soheilhy/cmux",
sum = "h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=",
version = "v0.1.4",
)
go_repository(
name = "com_github_spaolacci_murmur3",
importpath = "github.com/spaolacci/murmur3",
sum = "h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=",
version = "v0.0.0-20180118202830-f09979ecbc72",
)
go_repository(
name = "com_github_spf13_afero",
importpath = "github.com/spf13/afero",
sum = "h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=",
version = "v1.1.2",
)
go_repository(
name = "com_github_spf13_cast",
importpath = "github.com/spf13/cast",
sum = "h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=",
version = "v1.3.0",
)
go_repository(
name = "com_github_spf13_cobra",
importpath = "github.com/spf13/cobra",
sum = "h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=",
version = "v1.0.0",
)
go_repository(
name = "com_github_spf13_jwalterweatherman",
importpath = "github.com/spf13/jwalterweatherman",
sum = "h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=",
version = "v1.0.0",
)
go_repository(
name = "com_github_spf13_pflag",
importpath = "github.com/spf13/pflag",
sum = "h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=",
version = "v1.0.5",
)
go_repository(
name = "com_github_spf13_viper",
importpath = "github.com/spf13/viper",
sum = "h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=",
version = "v1.4.0",
)
go_repository(
name = "com_github_stretchr_objx",
importpath = "github.com/stretchr/objx",
sum = "h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=",
version = "v0.2.0",
)
go_repository(
name = "com_github_stretchr_testify",
importpath = "github.com/stretchr/testify",
sum = "h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=",
version = "v1.6.1",
)
go_repository(
name = "com_github_tmc_grpc_websocket_proxy",
importpath = "github.com/tmc/grpc-websocket-proxy",
sum = "h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=",
version = "v0.0.0-20190109142713-0ad062ec5ee5",
)
go_repository(
name = "com_github_ugorji_go",
importpath = "github.com/ugorji/go",
sum = "h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=",
version = "v1.1.4",
)
go_repository(
name = "com_github_xiang90_probing",
importpath = "github.com/xiang90/probing",
sum = "h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=",
version = "v0.0.0-20190116061207-43a291ad63a2",
)
go_repository(
name = "com_github_xordataexchange_crypt",
importpath = "github.com/xordataexchange/crypt",
sum = "h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow=",
version = "v0.0.3-0.20170626215501-b2862e3d0a77",
)
go_repository(
name = "com_github_yuin_goldmark",
importpath = "github.com/yuin/goldmark",
sum = "h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=",
version = "v1.2.1",
)
go_repository(
name = "com_google_cloud_go",
importpath = "cloud.google.com/go",
sum = "h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=",
version = "v0.26.0",
)
go_repository(
name = "in_gopkg_alecthomas_kingpin_v2",
importpath = "gopkg.in/alecthomas/kingpin.v2",
sum = "h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=",
version = "v2.2.6",
)
go_repository(
name = "in_gopkg_check_v1",
importpath = "gopkg.in/check.v1",
sum = "h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=",
version = "v1.0.0-20180628173108-788fd7840127",
)
go_repository(
name = "in_gopkg_errgo_v2",
importpath = "gopkg.in/errgo.v2",
sum = "h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=",
version = "v2.1.0",
)
go_repository(
name = "in_gopkg_resty_v1",
importpath = "gopkg.in/resty.v1",
sum = "h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=",
version = "v1.12.0",
)
go_repository(
name = "in_gopkg_yaml_v2",
importpath = "gopkg.in/yaml.v2",
sum = "h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=",
version = "v2.2.2",
)
go_repository(
name = "in_gopkg_yaml_v3",
importpath = "gopkg.in/yaml.v3",
sum = "h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=",
version = "v3.0.0-20200313102051-9f266ea9e77c",
)
go_repository(
name = "io_etcd_go_bbolt",
importpath = "go.etcd.io/bbolt",
sum = "h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=",
version = "v1.3.2",
)
go_repository(
name = "io_k8s_klog",
importpath = "k8s.io/klog",
sum = "h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=",
version = "v1.0.0",
)
go_repository(
name = "io_opencensus_go",
importpath = "go.opencensus.io",
sum = "h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=",
version = "v0.22.4",
)
go_repository(
name = "org_golang_google_appengine",
importpath = "google.golang.org/appengine",
sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=",
version = "v1.4.0",
)
go_repository(
name = "org_golang_google_genproto",
importpath = "google.golang.org/genproto",
sum = "h1:i1Ppqkc3WQXikh8bXiwHqAN5Rv3/qDCcRk0/Otx73BY=",
version = "v0.0.0-20190425155659-357c62f0e4bb",
)
go_repository(
name = "org_golang_google_grpc",
importpath = "google.golang.org/grpc",
sum = "h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=",
version = "v1.21.0",
)
go_repository(
name = "org_golang_x_crypto",
importpath = "golang.org/x/crypto",
sum = "h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=",
version = "v0.0.0-20200622213623-75b288015ac9",
)
go_repository(
name = "org_golang_x_exp",
importpath = "golang.org/x/exp",
sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=",
version = "v0.0.0-20190121172915-509febef88a4",
)
go_repository(
name = "org_golang_x_lint",
importpath = "golang.org/x/lint",
sum = "h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=",
version = "v0.0.0-20190930215403-16217165b5de",
)
go_repository(
name = "org_golang_x_mod",
importpath = "golang.org/x/mod",
sum = "h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=",
version = "v0.3.0",
)
go_repository(
name = "org_golang_x_net",
importpath = "golang.org/x/net",
sum = "h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=",
version = "v0.0.0-20200822124328-c89045814202",
)
go_repository(
name = "org_golang_x_oauth2",
importpath = "golang.org/x/oauth2",
sum = "h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=",
version = "v0.0.0-20180821212333-d2e6202438be",
)
go_repository(
name = "org_golang_x_sync",
importpath = "golang.org/x/sync",
sum = "h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=",
version = "v0.0.0-20200625203802-6e8e738ad208",
)
go_repository(
name = "org_golang_x_sys",
importpath = "golang.org/x/sys",
sum = "h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=",
version = "v0.0.0-20200323222414-85ca7c5b95cd",
)
go_repository(
name = "org_golang_x_text",
importpath = "golang.org/x/text",
sum = "h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=",
version = "v0.3.2",
)
go_repository(
name = "org_golang_x_time",
importpath = "golang.org/x/time",
sum = "h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=",
version = "v0.0.0-20190308202827-9d24e82272b4",
)
go_repository(
name = "org_golang_x_tools",
importpath = "golang.org/x/tools",
sum = "h1:xLt+iB5ksWcZVxqc+g9K41ZHy+6MKWfXCDsjSThnsPA=",
version = "v0.0.0-20200904185747-39188db58858",
)
go_repository(
name = "org_golang_x_xerrors",
importpath = "golang.org/x/xerrors",
sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=",
version = "v0.0.0-20200804184101-5ec99f83aff1",
)
go_repository(
name = "org_uber_go_atomic",
importpath = "go.uber.org/atomic",
sum = "h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=",
version = "v1.5.0",
)
go_repository(
name = "org_uber_go_dig",
importpath = "go.uber.org/dig",
sum = "h1:yLmDDj9/zuDjv3gz8GQGviXMs9TfysIUMUilCpgzUJY=",
version = "v1.10.0",
)
go_repository(
name = "org_uber_go_fx",
importpath = "go.uber.org/fx",
sum = "h1:CFNTr1oin5OJ0VCZ8EycL3wzF29Jz2g0xe55RFsf2a4=",
version = "v1.13.1",
)
go_repository(
name = "org_uber_go_goleak",
importpath = "go.uber.org/goleak",
sum = "h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=",
version = "v0.10.0",
)
go_repository(
name = "org_uber_go_multierr",
importpath = "go.uber.org/multierr",
sum = "h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E=",
version = "v1.4.0",
)
go_repository(
name = "org_uber_go_tools",
importpath = "go.uber.org/tools",
sum = "h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=",
version = "v0.0.0-20190618225709-2cfd321de3ee",
)
go_repository(
name = "org_uber_go_zap",
importpath = "go.uber.org/zap",
sum = "h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=",
version = "v1.10.0",
)

14
docker-compose.yaml Normal file
View file

@ -0,0 +1,14 @@
# This is the Docker Compose for setting up a local dev environment.
version: '3.8'
services:
dev:
image: node:14
volumes:
- '.:/src'
ports:
- 6609:6609
- 6601:6601
- 6006:6006
working_dir: /src
command: yarn start

37
docs/getting-started.md Normal file
View file

@ -0,0 +1,37 @@
# Roleypoly Developer Guide
If you would like to help build Roleypoly, this guide will help get you started.
## Prerequisites
- Node.js 14+ & Yarn
- Wrangler CLI
- (Optional): Terraform 0.14+
- (Optional): Go 1.15+
## What things are built with
- **Backend/API**
- Node.js & Typescript
- Cloudflare Workers
- **Frontend**
- Next.js & React & Typescript
- Storybooks
- Homegrown Atomic Design System
- **Discord Bot**
- Go
- Google Cloud Run
- **CI/CD**
- GitHub Actions
- Terraform
## How does stuff fit together
As for infrastructure:
- CI/CD process deploys all pieces.
- Discord Bot is deployed on a Google Cloud VM
- Backend is deployed via a Cloudflare Worker
- UI is deployed via Google Cloud Run
Biggest thing to note: this "discord bot" is an optional piece of the system, and should always remain as such. Giving it responsibility has actual engineering and dollar cost.

84
docs/infrastructure.md Normal file
View file

@ -0,0 +1,84 @@
# Roleypoly Infrastructure
## Ring 0
Any of these missing is a service outage.
### Backend
Backend is a Cloudflare Worker deployment. Edge computing, fuck yeah!
Hosts:
- `api-${env}.roleypoly.com/*`
- ex. for stage: `api-stage.roleypoly.com/*`
- `api.roleypoly.com/*` (only in prod)
It uses 3 KV namespaces per environment:
- Sessions
- Store of data and tokens mapped to sessions
- All data subject to 6 hour TTL
- GuildData
- Store of guild data, e.g. categories, etc
- All data is permanent (maybe doubly persisted to Firestore)
- Guilds
- Cache of Discord guild + guild member data
- All data subject to a 5 minute TTL
### App UI
The Next.js server is a docker container hosted on Google Cloud Run in multiple regions with a load balancer. Region focuses knowing traffic: US (x3), EU (x2), AP (x3). There is a running maximum of 10 containers per region, and a minimum of 0.
Regions:
- `us-east4` (South Carolina)
- `us-central1` (Iowa)
- `us-west1` (Oregon)
- `europe-west2` (London)
- `europe-west3` (Frankfurt)
- `australia-southeast1` (Sydney)
- `asia-northeast1` (Tokyo)
- `asia-southeast1` (Singapore)
Staging is only deployed to `us-east4`.
Hosts:
- `web-${env}.roleypoly.com`
- ex. for stage: `web-stage.roleypoly.com`
- `roleypoly.com` (only in prod, after release)
- `next.roleypoly.com` (only in prod, pre-release)
- `beta.roleypoly.com` (only in stage)
## Ring 1
Ephemeral services that can be tolerably lost, and would not be considered an outage, only degraded.
### v1 Migration Service
Hosted on v1 infrastructure, e.g. DigitalOcean. It is a connector and JSON API to the old v1 PgSQL data.
Host: `migration.v1.roleypoly.com` (Locked down to specific authorization tokens)
Sunset: Roleypoly Next + 3 months
### Bot (Mention Responder)
Bot doesn't do much, it's on a modestly sized Compute Engine VM using container hosting. It has no access to anything related to the real deployment of Roleypoly :)
Region: `us-east4` (South Carolina)
## Ring 2
Not end user applications. These will never be considered an "outage" if they are lost.
### Design System Storybook
Design system is a Vercel deployment
Host: `ui.roleypoly.com`

127
docs/user-stories.md Normal file
View file

@ -0,0 +1,127 @@
# User Stories
Loose doc defining end-to-end functionality for Roleypoly. Each slice has a cloud function or bot handler attached.
Legend
- (Hot) - Denotes cachable data stored for a short time, 2 minutes.
- Typical use cases: User roles; anything volatile.
- (Warm) - Denotes cachable data stored for a medium time, 10 minutes.
- Typical use cases: Guild roles; anything unlikely to change, but not painful to query.
- (Cold) - Denotes cachable data stored for a long time, 1 hour.
- Typical use cases: Guild lists for a session; anything very unlikely to change, and commonly used.
- (Permanent) - Denotes data that is permanent.
- Typical use cases: Guild customization data (categories, message)
- (Sec) - Security-minded data. Should never reach end-users.
- Typical use cases: Access tokens
## Primary
### Logged-in Index
As a user, I'd like to see all the servers I am in that can be used with the app.
- Type: Function
- Auth Level: User
- Flow Type: JSON API
- Data:
- User Current Guilds (Cold)
### Server Role Picker View
As a user, I'd like to see all of the roles I can select in a previously set up server.
- Type: Function
- Auth Level: User
- Flow Type: JSON API
- Data:
- User Current Guilds (Cold)
- Guild Roles (Warm)
- User Roles (Hot)
- Guild Customization (Cold) (Permanent)
### Server Role Picker Action
As a user, I'd like to select roles that have been selected in the role picker view for a server.
- Type: Function
- Auth Level: User
- Flow Type: JSON API
- Data:
- User Current Guilds (Cold)
- Guild Roles (Warm)
- User Roles (Hot)
### Server Editor View
As an admin, I'd like to see all of the settings and options for a server.
- Type: Function
- Auth Level: Admin
- Flow Type: JSON API
- Data:
- User Current Guilds (Cold)
- Guild Roles (Warm)
- Guild Customization (Cold) (Permanent)
### Server Editor Action
As an admin, I'd like to save settings and options that I have set within the editor view.
- Type: Function
- Auth Level: Admin
- Flow Type: JSON API
- Data:
- User Current Guilds (Cold)
- Guild Roles (Warm)
- Guild Customization (Cold) (Permanent)
### Session Pre-warming
As a user, I'd like to warm the cache with my current guild list after I log in.
- Type: Function
- Auth Level: User
- Flow Type: Bounces
- Data:
- User Current Guilds (Cold)
### Login
As a guest, I'd like to login with Discord so I can be authenticated as a user.
- Type: Function
- Auth Level: Guest
- Flow Type: OAuth, Bounces
- Data:
- Access Tokens (Sec)
### Logout
As a user, I'd like to revoke my authentication details.
- Type: Function
- Auth Level: User
- Flow Type: OAuth
- Data:
- Access Tokens (Sec)
### Bot Mention
As a discord user, I'd like to mention Roleypoly's bot account to get a link to my editor view.
- Type: Bot Responder
- Auth Level: N/A
- Flow Type: Command
- Data:
- None
### Bot Join
As a discord server admin, I'd like to follow the flow for adding Roleypoly to my server.
- Type: Function
- Auth Level: Guest
- Flow Type: OAuth
- Data:
- None

17
go.mod
View file

@ -1,17 +0,0 @@
module github.com/roleypoly/roleypoly
go 1.15
require (
github.com/bwmarrin/discordgo v0.22.0
github.com/dghubble/trie v0.0.0-20200716043226-5a94efb202d5
github.com/facebook/ent v0.4.3
github.com/google/go-github/v32 v32.1.0
github.com/joho/godotenv v1.3.0
github.com/julienschmidt/httprouter v1.3.0
github.com/lampjaw/discordclient v0.0.0-20200923011548-6558fc9e89df
github.com/segmentio/ksuid v1.0.3
go.uber.org/fx v1.13.1
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
k8s.io/klog v1.0.0
)

260
go.sum
View file

@ -1,260 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM=
github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dghubble/trie v0.0.0-20200716043226-5a94efb202d5 h1:euE/xPG0HIg6XiNXYrAHxX9aVwD1gw/yM2kptLOOj6k=
github.com/dghubble/trie v0.0.0-20200716043226-5a94efb202d5/go.mod h1:xNBeoT4V92/aNvuC3IJ2g59uxuKP4/kzvkpoHrb7v4A=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/facebook/ent v0.4.3 h1:ds9HENceKzpGBgCRlkZNq6TqBIegwKcF3e5reuV9Z0M=
github.com/facebook/ent v0.4.3/go.mod h1:4e/LKv3FFjj/867jPJYCxycZg0aGeEIgkiQ8jv2j6iQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-bindata/go-bindata v1.0.1-0.20190711162640-ee3c2418e368/go.mod h1:7xCgX1lzlrXPHkfvn3EhumqHkmSlzt8at9q7v0ax19c=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lampjaw/discordclient v0.0.0-20200923011548-6558fc9e89df h1:Y2o9fEOoAYjCw8IDyxUVaBq44AUbOLyPnYSPpM6Ef3M=
github.com/lampjaw/discordclient v0.0.0-20200923011548-6558fc9e89df/go.mod h1:lOfqvGl1HcXws86Sczusw1DyV5d0KHPtTTtdjneekto=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY=
github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/dig v1.10.0 h1:yLmDDj9/zuDjv3gz8GQGviXMs9TfysIUMUilCpgzUJY=
go.uber.org/dig v1.10.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=
go.uber.org/fx v1.13.1 h1:CFNTr1oin5OJ0VCZ8EycL3wzF29Jz2g0xe55RFsf2a4=
go.uber.org/fx v1.13.1/go.mod h1:bREWhavnedxpJeTq9pQT53BbvwhUv7TcpsOqcH4a+3w=
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E=
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200904185747-39188db58858 h1:xLt+iB5ksWcZVxqc+g9K41ZHy+6MKWfXCDsjSThnsPA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=

View file

@ -0,0 +1,8 @@
FROM mcr.microsoft.com/vscode/devcontainers/go:1.15
# Install Node.js
ARG NODE_VERSION="lts/*"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
# Install Wrangler
RUN su vscode -c "npm install -g wrangler"

View file

@ -1,12 +0,0 @@
#!/bin/sh
cd `dirname $(realpath $0)`
bazel run //:gazelle
bazel run //:gazelle -- update-repos -from_file=./go.mod --to_macro=deps.bzl%go_repositories -prune=true
sleep 0.5
echo "Fixing deps.bzl..."
head -n2 ../deps.bzl > ../deps.bzl~
tail -n+3 ../deps.bzl | sed '/^$/d' >> ../deps.bzl~
mv ../deps.bzl~ ../deps.bzl

View file

@ -1,7 +0,0 @@
package hacknotused
//go:generate sh gazelle.sh
func noop() {
}

4
hack/jestSetup.ts Normal file
View file

@ -0,0 +1,4 @@
import enableHooks from 'jest-react-hooks-shallow';
// pass an instance of jest to `enableHooks()`
enableHooks(jest);

View file

@ -1,4 +0,0 @@
#!/bin/bash
echo "STABLE_GIT_COMMIT $(git rev-parse --short HEAD)"
echo "STABLE_GIT_BRANCH $(git rev-parse --abbrev-ref HEAD)"
echo "BUILD_DATE $(date -Iseconds)"

View file

@ -2,28 +2,76 @@
"name": "roleypoly", "name": "roleypoly",
"version": "1.0.0", "version": "1.0.0",
"description": "https://roleypoly.com", "description": "https://roleypoly.com",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/roleypoly/roleypoly.git" "url": "git+https://github.com/roleypoly/roleypoly.git"
}, },
"author": "Katalina Okano <git@kat.cafe>", "homepage": "https://github.com/roleypoly/roleypoly#readme",
"license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/roleypoly/roleypoly/issues" "url": "https://github.com/roleypoly/roleypoly/issues"
}, },
"homepage": "https://github.com/roleypoly/roleypoly#readme", "author": "Katalina Okano <git@kat.cafe>",
"dependencies": { "license": "MIT",
"@improbable-eng/grpc-web": "0.13.0", "private": true,
"google-protobuf": "3.13.0" "workspaces": [
"packages/*"
],
"scripts": {
"build": "run-p -c build:*",
"build:api": "yarn workspace @roleypoly/api run build",
"build:design-system": "yarn workspace @roleypoly/design-system run build",
"build:web": "yarn workspace @roleypoly/web run build",
"create-component": "yarn workspace @roleypoly/design-system run create-component",
"lint": "run-p -c lint:* --",
"lint:eslint": "eslint",
"lint:prettier": "cross-env prettier -c '**/*.{ts,tsx,css,yml,yaml,md,json,js,jsx,sh,gitignore,mdx,Dockerfile}'",
"lint:stylelint": "cross-env stylelint 'packages/{web,design-system}/**/*.{ts,tsx}'",
"lint:terraform": "terraform fmt -check -recursive",
"lint:types": "tsc --noEmit",
"lint:types-api": "yarn workspace @roleypoly/api run lint:types",
"postinstall": "is-ci || husky install",
"start": "run-p -c start:*",
"start:api": "yarn workspace @roleypoly/api start",
"start:bot": "yarn workspace @roleypoly/bot start",
"start:design-system": "yarn workspace @roleypoly/design-system start",
"start:web": "yarn workspace @roleypoly/web start",
"test": "run-p -c test:* --",
"test:api": "yarn workspace @roleypoly/api run test",
"test:design-system": "yarn workspace @roleypoly/design-system run test",
"test:misc-utils": "yarn workspace @roleypoly/misc-utils run test",
"test:web": "yarn workspace @roleypoly/web run test"
}, },
"dependencies": {},
"devDependencies": { "devDependencies": {
"@bazel/typescript": "^2.2.0", "@stylelint/postcss-css-in-js": "^0.37.2",
"prettier": "^2.1.1", "husky": "^7.0.4",
"typescript": "^4.0.2", "is-ci": "^3.0.1",
"@roleypoly/ts-protoc-gen": "^1.0.1-promises.1", "jest-react-hooks-shallow": "^1.5.1",
"@types/google-protobuf": "3.7.3" "lint-staged": "^12.3.2",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.5",
"postcss-syntax": "^0.36.2",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^2.3.4",
"prettier-plugin-pkg": "^0.11.1",
"prettier-plugin-sh": "^0.8.1",
"stylelint": "^14.3.0",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-styled-components": "^0.1.1",
"typescript": "^4.5.5"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"prettier --write"
],
"*.{json,Dockerfile,sh,md,env,mdx,yml,html}": [
"prettier --write"
],
".*/*.{json,Dockerfile,sh,md,env,mdx,yml,html}": [
"prettier --write"
],
".husky/pre-commit": [
"prettier --write"
]
} }
} }

2
packages/api/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
dist
.mf

1
packages/api/.nvmrc Normal file
View file

@ -0,0 +1 @@
17

View file

@ -0,0 +1,252 @@
import { uiPublicURI } from '@roleypoly/api/utils/config';
import {
Category,
DiscordUser,
Embed,
GuildData,
GuildDataUpdate,
GuildSlug,
WebhookValidationStatus,
} from '@roleypoly/types';
import { userAgent } from '@roleypoly/worker-utils';
import deepEqual from 'deep-equal';
import { sortBy, uniq } from 'lodash';
type WebhookPayload = {
username: string;
avatar_url: string;
embeds: Embed[];
provider: {
name: string;
url: string;
};
};
type ChangeHandler = (
oldValue: GuildDataUpdate[keyof GuildDataUpdate],
newValue: GuildData[keyof GuildDataUpdate]
) => Embed[];
const changeHandlers: Record<keyof GuildDataUpdate, ChangeHandler> = {
message: (oldValue, newValue) => [
{
timestamp: new Date().toISOString(),
color: 0x453e3d,
fields: [
{
name: 'Old Message',
value: oldValue as string,
inline: false,
},
{
name: 'New Message',
value: newValue as string,
inline: false,
},
],
title: `Server message was updated...`,
},
],
auditLogWebhook: (oldValue, newValue) => [
{
timestamp: new Date().toISOString(),
color: 0x5d5352,
fields: [
{
name: 'Old Webhook ID',
value: !oldValue ? '*unset*' : (oldValue as string).split('/')[5],
inline: false,
},
{
name: 'New Webhook ID',
value: !newValue ? '*unset*' : (newValue as string).split('/')[5],
inline: false,
},
],
title: `Audit Log webhook URL was changed...`,
},
],
categories: (oldValue, newValue) => [
{
timestamp: new Date().toISOString(),
color: 0xab9b9a,
fields: [
{
name: 'Changed Categories',
value: getChangedCategories(
oldValue as Category[],
newValue as Category[]
).join('\n'),
inline: false,
},
],
title: `Categories were changed...`,
},
],
accessControl: (oldValue, newValue) => [
{
timestamp: new Date().toISOString(),
color: 0xab9b9a,
fields: [
{
name: 'Changed Access Control',
value: getChangedAccessControl(
oldValue as GuildDataUpdate['accessControl'],
newValue as GuildDataUpdate['accessControl']
).join('\n'),
inline: false,
},
],
title: `Access Control was changed...`,
},
],
};
export const sendAuditLog = async (
guild: GuildData,
guildUpdate: GuildDataUpdate,
user: DiscordUser
) => {
const auditLogWebhooks = uniq([
guild.auditLogWebhook || '',
guildUpdate.auditLogWebhook || '',
]).filter((webhook) => webhook !== '');
if (auditLogWebhooks.length === 0) {
return;
}
const keys = Object.keys(guildUpdate) as (keyof GuildDataUpdate)[];
const webhookPayload: WebhookPayload = {
username: 'Roleypoly (Audit Log)',
avatar_url: `https://next.roleypoly.com/logo192.png`, //TODO: change to roleypoly.com when swapped.
embeds: [
{
fields: [],
timestamp: new Date().toISOString(),
title: `${user.username}#${user.discriminator} has edited Roleypoly settings`,
color: 0x332d2d,
author: {
name: user.username,
icon_url: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`,
},
},
],
provider: {
name: 'Roleypoly',
url: uiPublicURI,
},
};
for (let key of keys) {
if (deepEqual(guildUpdate[key], guild[key])) {
continue;
}
const handler = changeHandlers[key];
if (!handler) {
continue;
}
const changeFields = handler(guild[key], guildUpdate[key]);
webhookPayload.embeds.push(...changeFields);
}
if (webhookPayload.embeds.length === 1) {
// No changes, don't bother sending
return;
}
// Colors are in order already, so use them to order the embeds.
webhookPayload.embeds = sortBy(webhookPayload.embeds, 'color');
const doWebhook = (webhook: string) =>
fetch(webhook, {
method: 'POST',
body: JSON.stringify(webhookPayload),
headers: {
'content-type': 'application/json',
'user-agent': userAgent,
},
});
await Promise.all(auditLogWebhooks.map((webhook) => doWebhook(webhook)));
};
export const validateAuditLogWebhook = async (
guild: GuildSlug,
webhook: string | null
): Promise<WebhookValidationStatus> => {
if (!webhook) {
return WebhookValidationStatus.NoneSet;
}
const url = new URL(webhook);
if (
url.hostname !== 'discord.com' ||
url.protocol !== 'https:' ||
url.pathname.startsWith('api/webhooks/')
) {
return WebhookValidationStatus.NotDiscordURL;
}
const response = await fetch(webhook, { method: 'GET' });
if (response.status !== 200) {
return WebhookValidationStatus.DoesNotExist;
}
const webhookData = await response.json();
if (webhookData.guild_id !== guild.id) {
return WebhookValidationStatus.NotSameGuild;
}
return WebhookValidationStatus.Ok;
};
const getChangedCategories = (oldCategories: Category[], newCategories: Category[]) => {
const addedCategories = newCategories.filter(
(c) => !oldCategories.find((o) => o.id === c.id)
);
const removedCategories = oldCategories.filter(
(c) => !newCategories.find((o) => o.id === c.id)
);
const changedCategories = newCategories.filter(
(c) =>
oldCategories.find((o) => o.id === c.id) &&
!deepEqual(
oldCategories.find((o) => o.id === c.id),
newCategories.find((o) => o.id === c.id)
)
);
return [
...addedCategories.map((c) => ` **Added** ${c.name}`),
...removedCategories.map((c) => ` **Removed** ${c.name}`),
...changedCategories.map((c) => `🔧 **Changed** ${c.name}`),
];
};
const getChangedAccessControl = (
oldAccessControl: GuildDataUpdate['accessControl'],
newAccessControl: GuildDataUpdate['accessControl']
) => {
const pendingChanged = newAccessControl.blockPending !== oldAccessControl.blockPending;
return [
`✅ Allowed roles: ${
newAccessControl.allowList.map((role) => `<@&${role}>`).join(', ') || `*all roles*`
}`,
`❌ Blocked roles: ${
newAccessControl.blockList.map((role) => `<@&${role}>`).join(', ') || `*no roles*`
}`,
...(pendingChanged
? [
`🔧 Pending/Welcome Screening users are ${
newAccessControl.blockPending ? 'blocked ❌' : 'allowed ✔'
}`,
]
: []),
];
};

View file

@ -0,0 +1,104 @@
import { CategoryType, Member, RoleSafety } from '@roleypoly/types';
import { AuthType, discordFetch, respond } from '@roleypoly/worker-utils';
import { difference, keyBy } from 'lodash';
import { interactionsEndpoint } from '../utils/api-tools';
import { botToken } from '../utils/config';
import {
getGuild,
getGuildData,
getGuildMember,
updateGuildMember,
} from '../utils/guild';
import { conflict, invalid, notAuthenticated, notFound, ok } from '../utils/responses';
export const InteractionsPickRole = interactionsEndpoint(
async (request: Request): Promise<Response> => {
const mode = request.method === 'PUT' ? 'add' : 'remove';
const reqURL = new URL(request.url);
const [, , guildID, userID, roleID] = reqURL.pathname.split('/');
if (!guildID || !userID || !roleID) {
return invalid();
}
const guildP = getGuild(guildID);
const guildDataP = getGuildData(guildID);
const guildMemberP = getGuildMember(
{ serverID: guildID, userID },
{ skipCachePull: true }
);
const [guild, guildData, guildMember] = await Promise.all([
guildP,
guildDataP,
guildMemberP,
]);
if (!guild || !guildData || !guildMember) {
return notFound();
}
let memberRoles = guildMember.roles;
if (
(mode === 'add' && memberRoles.includes(roleID)) ||
(mode !== 'add' && !memberRoles.includes(roleID))
) {
return conflict();
}
const roleMap = keyBy(guild.roles, 'id');
const category = guildData.categories.find((category) =>
category.roles.includes(roleID)
);
// No category? illegal.
if (!category) {
return notFound();
}
// Category is hidden, this is illegal
if (category.hidden) {
return notFound();
}
// Role is unsafe, super illegal.
if (roleMap[roleID].safety !== RoleSafety.Safe) {
return notAuthenticated();
}
// In add mode, if the category is a single-mode, remove the other roles in the category.
if (mode === 'add' && category.type === CategoryType.Single) {
memberRoles = difference(memberRoles, category.roles);
}
if (mode === 'add') {
memberRoles = [...memberRoles, roleID];
} else {
memberRoles = memberRoles.filter((id) => id !== roleID);
}
const patchMemberRoles = await discordFetch<Member>(
`/guilds/${guildID}/members/${userID}`,
botToken,
AuthType.Bot,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via slash command`,
},
body: JSON.stringify({
roles: memberRoles,
}),
}
);
if (!patchMemberRoles) {
return respond({ error: 'discord rejected the request' }, { status: 500 });
}
await updateGuildMember({ serverID: guildID, userID });
return ok();
}
);

View file

@ -0,0 +1,33 @@
import { Category, CategorySlug } from '@roleypoly/types';
import { respond } from '@roleypoly/worker-utils';
import { interactionsEndpoint } from '../utils/api-tools';
import { getGuildData } from '../utils/guild';
import { notFound } from '../utils/responses';
export const InteractionsPickableRoles = interactionsEndpoint(
async (request: Request): Promise<Response> => {
const reqURL = new URL(request.url);
const [, , serverID] = reqURL.pathname.split('/');
const guildData = await getGuildData(serverID);
if (!guildData) {
return notFound();
}
const roleMap: Record<Category['name'], CategorySlug> = {};
for (let category of guildData.categories) {
if (category.hidden) {
continue;
}
// TODO: role safety?
roleMap[category.name] = {
roles: category.roles,
type: category.type,
};
}
return respond(roleMap);
}
);

View file

@ -0,0 +1,16 @@
import {
InteractionCallbackType,
InteractionRequestCommand,
InteractionResponse,
} from '@roleypoly/types';
export const helloWorld = async (
interaction: InteractionRequestCommand
): Promise<InteractionResponse> => {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Hey there, ${interaction.member?.nick || interaction.user?.username}`,
},
};
};

View file

@ -0,0 +1,86 @@
import { selectRole } from '@roleypoly/interactions/utils/api';
import {
asyncPreflightEphemeral,
asyncResponse,
} from '@roleypoly/interactions/utils/interactions';
import { invalid, mustBeInGuild } from '@roleypoly/interactions/utils/responses';
import {
InteractionCallbackType,
InteractionFlags,
InteractionRequestCommand,
InteractionResponse,
} from '@roleypoly/types';
export const pickRole = (mode: 'add' | 'remove') =>
asyncResponse(
async (interaction: InteractionRequestCommand): Promise<InteractionResponse> => {
if (!interaction.guild_id) {
return mustBeInGuild();
}
const userID = interaction.member?.user?.id;
if (!userID) {
return mustBeInGuild();
}
const roleID = interaction.data.options?.find(
(option) => option.name === 'role'
)?.value;
if (!roleID) {
return invalid();
}
const code = await selectRole(mode, interaction.guild_id, userID, roleID);
if (code === 409) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:x: You ${mode === 'add' ? 'already' : "don't"} have that role.`,
flags: InteractionFlags.EPHEMERAL,
},
};
}
if (code === 404) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:x: <@&${roleID}> isn't pickable.`,
flags: InteractionFlags.EPHEMERAL,
},
};
}
if (code === 403) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:x: <@&${roleID}> has unsafe permissions.`,
flags: InteractionFlags.EPHEMERAL,
},
};
}
if (code !== 200) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:x: Something went wrong, please try again later.`,
flags: InteractionFlags.EPHEMERAL,
},
};
}
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:white_check_mark: You ${
mode === 'add' ? 'got' : 'removed'
} the role: <@&${roleID}>`,
flags: InteractionFlags.EPHEMERAL,
},
};
},
asyncPreflightEphemeral
);

View file

@ -0,0 +1,63 @@
import { getPickableRoles } from '@roleypoly/interactions/utils/api';
import { uiPublicURI } from '@roleypoly/interactions/utils/config';
import {
asyncPreflightEphemeral,
asyncResponse,
} from '@roleypoly/interactions/utils/interactions';
import { mustBeInGuild } from '@roleypoly/interactions/utils/responses';
import {
CategoryType,
Embed,
InteractionCallbackType,
InteractionFlags,
InteractionRequestCommand,
InteractionResponse,
} from '@roleypoly/types';
export const pickableRoles = asyncResponse(
async (interaction: InteractionRequestCommand): Promise<InteractionResponse> => {
if (!interaction.guild_id) {
return mustBeInGuild();
}
const pickableRoles = await getPickableRoles(interaction.guild_id);
const embed: Embed = {
color: 0xab9b9a,
fields: [],
title: 'You can pick any of these roles with /pick-role',
};
for (let categoryName in pickableRoles) {
const { roles, type } = pickableRoles[categoryName];
embed.fields.push({
name: `${categoryName}${type === CategoryType.Single ? ' *(pick one)*' : ''}`,
value: roles.map((role) => `<@&${role}>`).join('\n'),
inline: true,
});
}
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [embed],
flags: InteractionFlags.EPHEMERAL,
components: [
{
type: 1,
components: [
// Link to Roleypoly
{
type: 2,
label: 'Pick roles on your browser',
url: `${uiPublicURI}/s/${interaction.guild_id}`,
style: 5,
},
],
},
],
},
};
},
asyncPreflightEphemeral
);

View file

@ -0,0 +1,56 @@
import { uiPublicURI } from '@roleypoly/interactions/utils/config';
import {
Embed,
InteractionCallbackType,
InteractionFlags,
InteractionRequestCommand,
InteractionResponse,
} from '@roleypoly/types';
export const roleypoly = async (
interaction: InteractionRequestCommand
): Promise<InteractionResponse> => {
if (interaction.guild_id) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: 0x453e3d,
title: `:beginner: Hey there, ${
interaction.member?.nick || interaction.member?.user?.username || 'friend'
}!`,
description: `Try these slash commands, or pick roles from your browser!`,
fields: [
{ name: 'See all the roles', value: '/pickable-roles' },
{ name: 'Pick a role', value: '/pick-role' },
{ name: 'Remove a role', value: '/remove-role' },
],
} as Embed,
],
components: [
{
type: 1,
components: [
// Link to Roleypoly
{
type: 2,
label: `Pick roles on ${new URL(uiPublicURI).hostname}`,
url: `${uiPublicURI}/s/${interaction.guild_id}`,
style: 5,
},
],
},
],
flags: InteractionFlags.EPHEMERAL,
},
};
}
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:beginner: Hey! I don't know what server you're in, so check out ${uiPublicURI}`,
},
};
};

View file

@ -0,0 +1,11 @@
module.exports = {
preset: 'ts-jest/presets/default-esm',
name: 'api',
testEnvironment: 'miniflare',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.test.json',
useESM: true,
},
},
};

31
packages/api/package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "@roleypoly/api",
"version": "0.1.0",
"license": "MIT",
"main": "./src/index.ts",
"scripts": {
"build": "yarn build:dev --minify",
"build:dev": "esbuild --bundle --sourcemap --platform=node --format=esm --outdir=dist --out-extension:.js=.mjs ./src/index.ts",
"lint:types": "tsc --noEmit",
"posttest": "rm .env",
"pretest": "cp ../../.env.example .env && yarn build",
"start": "miniflare --watch --debug",
"test": "jest"
},
"dependencies": {},
"devDependencies": {
"@cloudflare/workers-types": "^3.3.1",
"@roleypoly/misc-utils": "*",
"@roleypoly/types": "*",
"@types/node": "^17.0.13",
"esbuild": "^0.14.16",
"itty-router": "^2.4.10",
"jest-environment-miniflare": "^2.2.0",
"lodash": "^4.17.21",
"miniflare": "^2.2.0",
"normalize-url": "^4.5.1",
"ts-jest": "^27.1.3",
"tweetnacl": "^1.0.3",
"ulid-workers": "^2.1.0"
}
}

View file

@ -0,0 +1,5 @@
it('works', () => {
expect(true).toBeTruthy();
});
export {};

View file

@ -0,0 +1 @@
export {};

View file

@ -0,0 +1,208 @@
jest.mock('../utils/discord');
import { CategoryType, Features, Guild, GuildData, RoleSafety } from '@roleypoly/types';
import { APIGuild, discordFetch } from '../utils/discord';
import { configContext } from '../utils/testHelpers';
import { getGuild, getGuildData, getGuildMember, getPickableRoles } from './getters';
const mockDiscordFetch = discordFetch as jest.Mock;
beforeEach(() => {
mockDiscordFetch.mockReset();
});
describe('getGuild', () => {
it('gets a guild from discord', async () => {
const [config] = configContext();
const guild: APIGuild = {
id: '123',
name: 'test',
icon: 'test',
roles: [],
};
mockDiscordFetch.mockReturnValue(guild);
const result = await getGuild(config, '123');
expect(result).toMatchObject(guild);
});
it('gets a guild from cache automatically', async () => {
const [config] = configContext();
const guild: APIGuild = {
id: '123',
name: 'test',
icon: 'test',
roles: [],
};
await config.kv.guilds.put('guild/123', guild, config.retention.guild);
mockDiscordFetch.mockReturnValue({ ...guild, name: 'test2' });
const result = await getGuild(config, '123');
expect(result).toMatchObject(guild);
expect(result!.name).toBe('test');
});
});
describe('getGuildData', () => {
it('gets guild data from store', async () => {
const [config] = configContext();
const guildData: GuildData = {
id: '123',
message: 'Hello world!',
categories: [],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: true,
},
};
await config.kv.guildData.put('123', guildData);
const result = await getGuildData(config, '123');
expect(result).toMatchObject(guildData);
});
it('adds fields that are missing from the stored data', async () => {
const [config] = configContext();
const guildData: Partial<GuildData> = {
id: '123',
message: 'Hello world!',
categories: [],
features: Features.None,
};
await config.kv.guildData.put('123', guildData);
const result = await getGuildData(config, '123');
expect(result).toMatchObject({
...guildData,
auditLogWebhook: null,
accessControl: expect.any(Object),
});
});
});
describe('getGuildMember', () => {
it('gets a member from discord', async () => {
const [config] = configContext();
const member = {
roles: [],
pending: false,
nick: 'test',
};
mockDiscordFetch.mockReturnValue(member);
const result = await getGuildMember(config, '123', '123');
expect(result).toMatchObject(member);
});
it('gets a member from cache automatically', async () => {
const [config] = configContext();
const member = {
roles: [],
pending: false,
nick: 'test2',
};
await config.kv.guilds.put('member/123/123', member, config.retention.guild);
mockDiscordFetch.mockReturnValue({ ...member, nick: 'test' });
const result = await getGuildMember(config, '123', '123');
expect(result).toMatchObject(member);
expect(result!.nick).toBe('test2');
});
});
describe('getPickableRoles', () => {
it('returns all pickable roles for a given guild', async () => {
const guildData: GuildData = {
id: '123',
message: 'Hello world!',
categories: [
{
id: '123',
name: 'test',
position: 0,
roles: ['role-1', 'role-2', 'role-unsafe'],
hidden: false,
type: CategoryType.Multi,
},
{
id: '123',
name: 'test',
position: 0,
roles: ['role-3', 'role-4'],
hidden: true,
type: CategoryType.Multi,
},
],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: true,
},
};
const guild: Guild = {
id: '123',
name: 'test',
icon: '',
roles: [
{
id: 'role-1',
name: 'test',
position: 0,
managed: false,
color: 0,
safety: RoleSafety.Safe,
permissions: '0',
},
{
id: 'role-3',
name: 'test',
position: 0,
managed: false,
color: 0,
safety: RoleSafety.Safe,
permissions: '0',
},
{
id: 'role-unsafe',
name: 'test',
position: 0,
managed: false,
color: 0,
safety: RoleSafety.DangerousPermissions,
permissions: '0',
},
],
};
const result = getPickableRoles(guildData, guild);
expect(result).toMatchObject([
{
category: guildData.categories[0],
roles: [guild.roles[0]],
},
]);
});
});

View file

@ -0,0 +1,196 @@
import { Config } from '@roleypoly/api/src/utils/config';
import {
APIGuild,
APIMember,
APIRole,
AuthType,
discordFetch,
getHighestRole,
} from '@roleypoly/api/src/utils/discord';
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
import {
Category,
Features,
Guild,
GuildData,
Member,
OwnRoleInfo,
Role,
RoleSafety,
} from '@roleypoly/types';
export const getGuild = async (
config: Config,
id: string,
forceMiss?: boolean
): Promise<(Guild & OwnRoleInfo) | null> =>
config.kv.guilds.cacheThrough(
`guild/${id}`,
async () => {
const guildRaw = await discordFetch<APIGuild>(
`/guilds/${id}`,
config.botToken,
AuthType.Bot
);
if (!guildRaw) {
return null;
}
const botMemberRoles =
(await getGuildMember(config, id, config.botClientID))?.roles || [];
const highestRolePosition =
getHighestRole(
botMemberRoles
.map((r) => guildRaw.roles.find((r2) => r2.id === r))
.filter((x) => x !== undefined) as APIRole[]
)?.position || -1;
const roles = guildRaw.roles.map<Role>((role) => ({
id: role.id,
name: role.name,
color: role.color,
managed: role.managed,
position: role.position,
permissions: role.permissions,
safety: calculateRoleSafety(role, highestRolePosition),
}));
const guild: Guild & OwnRoleInfo = {
id,
name: guildRaw.name,
icon: guildRaw.icon,
roles,
highestRolePosition,
};
return guild;
},
config.retention.guild,
forceMiss
);
export const getGuildData = async (config: Config, id: string): Promise<GuildData> => {
const guildData = await config.kv.guildData.get<GuildData>(id);
const empty = {
id,
message: '',
categories: [],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: true,
},
};
if (!guildData) {
await config.kv.guildData.put(id, empty);
return empty;
}
return {
...empty,
...guildData,
};
};
export const getGuildMember = async (
config: Config,
serverID: string,
userID: string,
forceMiss?: boolean,
overrideRetention?: number // allows for own-member to be cached as long as it's used.
): Promise<Member | null> =>
config.kv.guilds.cacheThrough(
`member/${serverID}/${userID}`,
async () => {
const discordMember = await discordFetch<APIMember>(
`/guilds/${serverID}/members/${userID}`,
config.botToken,
AuthType.Bot
);
if (!discordMember) {
return null;
}
return {
guildid: serverID,
roles: discordMember.roles,
pending: discordMember.pending,
nick: discordMember.nick,
};
},
overrideRetention || config.retention.member,
forceMiss
);
export const updateGuildMember = async (
config: Config,
serverID: string,
member: APIMember
): Promise<void> => {
config.kv.guilds.put(
`members/${serverID}/${member.user.id}`,
{
guildid: serverID,
roles: member.roles,
pending: member.pending,
nick: member.nick,
},
config.retention.member
);
};
const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: number) => {
let safety = RoleSafety.Safe;
if (role.managed) {
safety |= RoleSafety.ManagedRole;
}
if (role.position > highestBotRolePosition) {
safety |= RoleSafety.HigherThanBot;
}
const permBigInt = BigInt(role.permissions);
if (
evaluatePermission(permBigInt, permissions.ADMINISTRATOR) ||
evaluatePermission(permBigInt, permissions.MANAGE_ROLES)
) {
safety |= RoleSafety.DangerousPermissions;
}
return safety;
};
export const getPickableRoles = (
guildData: GuildData,
guild: Guild
): { category: Category; roles: Role[] }[] => {
const pickableRoles: { category: Category; roles: Role[] }[] = [];
for (const category of guildData.categories) {
if (category.roles.length === 0 || category.hidden) {
continue;
}
const roles = category.roles
.map((roleID) => guild.roles.find((r) => r.id === roleID))
.filter((role) => role !== undefined && role.safety === RoleSafety.Safe) as Role[];
if (roles.length > 0) {
pickableRoles.push({
category,
roles,
});
}
}
console.log({ pickableRoles });
return pickableRoles;
};

View file

@ -0,0 +1,78 @@
import { Router } from 'itty-router';
import { json } from '../utils/response';
import { configContext, makeSession } from '../utils/testHelpers';
import { requireEditor } from './middleware';
describe('requireEditor', () => {
it('continues the request when user is an editor', async () => {
const testFn = jest.fn();
const [config, context] = configContext();
const session = await makeSession(config);
const router = Router();
router.all('*', requireEditor).get('/:guildId', (request, context) => {
testFn();
return json({});
});
const response = await router.handle(
new Request(`http://test.local/${session.guilds[1].id}`, {
headers: {
authorization: `Bearer ${session.sessionID}`,
},
}),
{ ...context, session, params: { guildId: session.guilds[1].id } }
);
expect(response.status).toBe(200);
expect(testFn).toHaveBeenCalledTimes(1);
});
it('403s the request when user is not an editor', async () => {
const testFn = jest.fn();
const [config, context] = configContext();
const session = await makeSession(config);
const router = Router();
router.all('*', requireEditor).get('/:guildId', (request, context) => {
testFn();
return json({});
});
const response = await router.handle(
new Request(`http://test.local/${session.guilds[0].id}`, {
headers: {
authorization: `Bearer ${session.sessionID}`,
},
}),
{ ...context, session, params: { guildId: session.guilds[0].id } }
);
expect(response.status).toBe(403);
expect(testFn).not.toHaveBeenCalled();
});
it('404s the request when the guild isnt in session', async () => {
const testFn = jest.fn();
const [config, context] = configContext();
const session = await makeSession(config);
const router = Router();
router.all('*', requireEditor).get('/:guildId', (request, context) => {
testFn();
return json({});
});
const response = await router.handle(
new Request(`http://test.local/invalid-session-id`, {
headers: {
authorization: `Bearer ${session.sessionID}`,
},
}),
{ ...context, session, params: { guildId: 'invalid-session-id' } }
);
expect(response.status).toBe(404);
expect(testFn).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,47 @@
import { Context, RoleypolyMiddleware } from '@roleypoly/api/src/utils/context';
import {
engineeringProblem,
forbidden,
notFound,
} from '@roleypoly/api/src/utils/response';
import { UserGuildPermissions } from '@roleypoly/types';
export const requireEditor: RoleypolyMiddleware = async (
request: Request,
context: Context
) => {
if (!context.params.guildId) {
return engineeringProblem('params not set up correctly');
}
if (!context.session) {
return engineeringProblem('middleware not set up correctly');
}
const guild = context.session.guilds.find((g) => g.id === context.params.guildId);
if (!guild) {
return notFound(); // 404 because we don't want enumeration of guilds
}
if (guild.permissionLevel === UserGuildPermissions.User) {
return forbidden();
}
};
export const requireMember: RoleypolyMiddleware = async (
request: Request,
context: Context
) => {
if (!context.params.guildId) {
return engineeringProblem('params not set up correctly');
}
if (!context.session) {
return engineeringProblem('middleware not set up correctly');
}
const guild = context.session.guilds.find((g) => g.id === context.params.guildId);
if (!guild) {
return notFound(); // 404 because we don't want enumeration of guilds
}
};

99
packages/api/src/index.ts Normal file
View file

@ -0,0 +1,99 @@
import { requireEditor, requireMember } from '@roleypoly/api/src/guilds/middleware';
import { authBot } from '@roleypoly/api/src/routes/auth/bot';
import { authCallback } from '@roleypoly/api/src/routes/auth/callback';
import { authSessionDelete } from '@roleypoly/api/src/routes/auth/delete-session';
import { authSession } from '@roleypoly/api/src/routes/auth/session';
import { guildsGuild } from '@roleypoly/api/src/routes/guilds/guild';
import { guildsCacheDelete } from '@roleypoly/api/src/routes/guilds/guild-cache-delete';
import { guildsRolesPut } from '@roleypoly/api/src/routes/guilds/guild-roles-put';
import { guildsGuildPatch } from '@roleypoly/api/src/routes/guilds/guilds-patch';
import { guildsSlug } from '@roleypoly/api/src/routes/guilds/slug';
import { handleInteraction } from '@roleypoly/api/src/routes/interactions/interactions';
import {
requireSession,
withAuthMode,
withSession,
} from '@roleypoly/api/src/sessions/middleware';
import { injectParams } from '@roleypoly/api/src/utils/request';
import { Router } from 'itty-router';
import { authBounce } from './routes/auth/bounce';
import { Environment, parseEnvironment } from './utils/config';
import { Context, RoleypolyHandler } from './utils/context';
import { corsHeaders, json, notFound, serverError } from './utils/response';
const router = Router();
router.all('*', withAuthMode);
router.get('/auth/bot', authBot);
router.get('/auth/bounce', authBounce);
router.get('/auth/callback', authCallback);
router.get('/auth/session', withSession, requireSession, authSession);
router.delete('/auth/session', withSession, requireSession, authSessionDelete);
const guildsCommon = [injectParams, withSession, requireSession, requireMember];
router.get('/guilds/:guildId', ...guildsCommon, guildsGuild);
router.patch('/guilds/:guildId', ...guildsCommon, requireEditor, guildsGuildPatch);
router.delete(
'/guilds/:guildId/cache',
...guildsCommon,
requireEditor,
guildsCacheDelete
);
router.put('/guilds/:guildId/roles', ...guildsCommon, guildsRolesPut);
router.get('/guilds/:guildId/slug', injectParams, withSession, guildsSlug);
router.post('/interactions', handleInteraction);
router.get('/', ((request: Request, { config }: Context) =>
json({
__warning: '🦊',
this: 'is',
a: 'fox-based',
web: 'application',
please: 'be',
aware: 'of',
your: 'surroundings',
warning__: '🦊',
meta: config.uiPublicURI,
version: 2,
})) as RoleypolyHandler);
router.options('*', (request: Request) => {
return new Response(null, {
headers: {
...corsHeaders,
},
});
});
router.all('/*', notFound);
const scrubURL = (urlStr: string) => {
const url = new URL(urlStr);
url.searchParams.delete('code');
url.searchParams.delete('state');
return url.toString();
};
export default {
async fetch(request: Request, env: Environment, event: Context['fetchContext']) {
const config = parseEnvironment(env);
const context: Context = {
config,
fetchContext: {
waitUntil: event.waitUntil.bind(event),
},
authMode: {
type: 'anonymous',
},
params: {},
};
console.log(`${request.method} ${scrubURL(request.url)}`);
return router
.handle(request, context)
.catch((e: Error) => (!e ? notFound() : serverError(e)));
},
};

View file

@ -0,0 +1,25 @@
import { makeRequest } from '../../utils/testHelpers';
describe('GET /auth/bot', () => {
it('redirects to a Discord OAuth bot flow url', async () => {
const response = await makeRequest('GET', '/auth/bot', undefined, {
BOT_CLIENT_ID: 'test123',
});
expect(response.status).toBe(303);
expect(response.headers.get('Location')).toContain(
'https://discord.com/api/oauth2/authorize?client_id=test123&scope=bot%20applications.commands&permissions=268435456'
);
});
it('redirects to a Discord OAuth bot flow url, forcing a guild when set', async () => {
const response = await makeRequest('GET', '/auth/bot?guild=123456', undefined, {
BOT_CLIENT_ID: 'test123',
});
expect(response.status).toBe(303);
expect(response.headers.get('Location')).toContain(
'https://discord.com/api/oauth2/authorize?client_id=test123&scope=bot%20applications.commands&permissions=268435456&guild_id=123456&disable_guild_select=true'
);
});
});

View file

@ -0,0 +1,43 @@
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { seeOther } from '@roleypoly/api/src/utils/response';
const validGuildID = /^[0-9]+$/;
type URLParams = {
clientID: string;
permissions: number;
guildID?: string;
scopes: string[];
};
const buildURL = (params: URLParams) => {
let url = `https://discord.com/api/oauth2/authorize?client_id=${
params.clientID
}&scope=${params.scopes.join('%20')}&permissions=${params.permissions}`;
if (params.guildID) {
url += `&guild_id=${params.guildID}&disable_guild_select=true`;
}
return url;
};
export const authBot: RoleypolyHandler = (
request: Request,
{ config }: Context
): Response => {
let guildID = new URL(request.url).searchParams.get('guild') || '';
if (guildID && !validGuildID.test(guildID)) {
guildID = '';
}
return seeOther(
buildURL({
clientID: config.botClientID,
permissions: 268435456, // Send messages + manage roles
guildID,
scopes: ['bot', 'applications.commands'],
})
);
};

View file

@ -0,0 +1,54 @@
import { StateSession } from '@roleypoly/types';
import { getBindings, makeRequest } from '../../utils/testHelpers';
describe('GET /auth/bounce', () => {
it('should return a redirect to Discord OAuth', async () => {
const response = await makeRequest('GET', '/auth/bounce', undefined, {
BOT_CLIENT_ID: 'test123',
API_PUBLIC_URI: 'http://test.local/',
});
expect(response.status).toBe(303);
expect(response.headers.get('Location')).toContain(
'https://discord.com/api/oauth2/authorize?client_id=test123&response_type=code&scope=identify%20guilds&redirect_uri=http%3A%2F%2Ftest.local%2Fauth%2Fcallback&state='
);
});
it('should store a state-session', async () => {
const response = await makeRequest('GET', '/auth/bounce', undefined, {
BOT_CLIENT_ID: 'test123',
API_PUBLIC_URI: 'http://test.local/',
});
expect(response.status).toBe(303);
const url = new URL(response.headers.get('Location') || '');
const state = url.searchParams.get('state');
const environment = getBindings();
const session = await environment.KV_SESSIONS.get(`state_${state}`, 'json');
expect(session).not.toBeUndefined();
});
test.each([
['http://web.test.local', 'http://web.test.local', 'http://web.test.local'],
['http://*.test.local', 'http://web.test.local', 'http://web.test.local'],
['http://other.test.local', 'http://web.test.local', undefined],
])(
'should process callback hosts when set to %s',
async (allowlist, input, expected) => {
const response = await makeRequest('GET', `/auth/bounce?cbh=${input}`, undefined, {
BOT_CLIENT_ID: 'test123',
API_PUBLIC_URI: 'http://api.test.local',
ALLOWED_CALLBACK_HOSTS: allowlist,
});
expect(response.status).toBe(303);
const url = new URL(response.headers.get('Location') || '');
const state = url.searchParams.get('state');
const environment = getBindings();
const session = (await environment.KV_SESSIONS.get(`state_${state}`, 'json')) as {
data: StateSession;
};
expect(session).not.toBeUndefined();
expect(session?.data.callbackHost).toBe(expected);
}
);
});

View file

@ -0,0 +1,64 @@
import { setupStateSession } from '@roleypoly/api/src/sessions/state';
import { Config } from '@roleypoly/api/src/utils/config';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { getQuery } from '@roleypoly/api/src/utils/request';
import { seeOther } from '@roleypoly/api/src/utils/response';
import { StateSession } from '@roleypoly/types';
type URLParams = {
clientID: string;
redirectURI: string;
state: string;
};
export const buildURL = (params: URLParams) =>
`https://discord.com/api/oauth2/authorize?client_id=${
params.clientID
}&response_type=code&scope=identify%20guilds&redirect_uri=${encodeURIComponent(
params.redirectURI
)}&state=${params.state}`;
const hostMatch = (a: string, b: string): boolean => {
const aURL = new URL(a);
const bURL = new URL(b);
return aURL.host === bURL.host && aURL.protocol === bURL.protocol;
};
const wildcardMatch = (wildcard: string, host: string): boolean => {
const aURL = new URL(wildcard);
const bURL = new URL(host);
const regex = new RegExp(aURL.hostname.replace('*', '[a-z0-9-]+'));
return regex.test(bURL.hostname);
};
export const isAllowedCallbackHost = (config: Config, host: string): boolean => {
return (
hostMatch(host, config.apiPublicURI) ||
config.allowedCallbackHosts.some((allowedHost) =>
allowedHost.includes('*')
? wildcardMatch(allowedHost, host)
: hostMatch(allowedHost, host)
)
);
};
export const authBounce: RoleypolyHandler = async (
request: Request,
{ config }: Context
) => {
const stateSessionData: StateSession = {};
const { cbh: callbackHost } = getQuery(request);
if (callbackHost && isAllowedCallbackHost(config, callbackHost)) {
stateSessionData.callbackHost = callbackHost;
}
const state = await setupStateSession(config, stateSessionData);
const redirectURI = `${config.apiPublicURI}/auth/callback`;
const clientID = config.botClientID;
return seeOther(buildURL({ state, redirectURI, clientID }));
};

View file

@ -0,0 +1,93 @@
jest.mock('../../utils/discord');
jest.mock('../../sessions/create');
import { createSession } from '../../sessions/create';
import { setupStateSession } from '../../sessions/state';
import { parseEnvironment } from '../../utils/config';
import { discordFetch } from '../../utils/discord';
import { getBindings, makeRequest } from '../../utils/testHelpers';
const mockDiscordFetch = discordFetch as jest.Mock;
const mockCreateSession = createSession as jest.Mock;
describe('GET /auth/callback', () => {
it('should ask Discord to trade code for tokens', async () => {
const env = getBindings();
const config = parseEnvironment(env);
const stateID = await setupStateSession(config, {});
const tokens = {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_in: 3600,
scope: 'identify guilds',
token_type: 'Bearer',
};
mockDiscordFetch.mockReturnValueOnce(tokens);
mockCreateSession.mockReturnValueOnce({
sessionID: 'test-session-id',
tokens,
user: {
id: 'test-user-id',
username: 'test-username',
discriminator: 'test-discriminator',
avatar: 'test-avatar',
bot: false,
},
guilds: [],
});
const response = await makeRequest(
'GET',
`/auth/callback?state=${stateID}&code=1234`,
undefined,
{
BOT_CLIENT_ID: 'test123',
BOT_CLIENT_SECRET: 'test456',
API_PUBLIC_URI: 'http://test.local/',
UI_PUBLIC_URI: 'http://web.test.local/',
}
);
expect(response.status).toBe(303);
expect(mockDiscordFetch).toBeCalledTimes(1);
expect(mockCreateSession).toBeCalledWith(expect.any(Object), tokens);
expect(response.headers.get('Location')).toContain(
'http://web.test.local/machinery/new-session#/test-session-id'
);
});
it('will fail if state is invalid', async () => {
const response = await makeRequest(
'GET',
`/auth/callback?state=invalid-state&code=1234`,
undefined,
{
BOT_CLIENT_ID: 'test123',
BOT_CLIENT_SECRET: 'test456',
API_PUBLIC_URI: 'http://test.local/',
UI_PUBLIC_URI: 'http://web.test.local/',
}
);
expect(response.status).toBe(303);
expect(response.headers.get('Location')).toContain(
'http://web.test.local/error/authFailure?extra=state invalid'
);
});
it('will fail if state is missing', async () => {
const response = await makeRequest('GET', `/auth/callback?code=1234`, undefined, {
BOT_CLIENT_ID: 'test123',
BOT_CLIENT_SECRET: 'test456',
API_PUBLIC_URI: 'http://test.local/',
UI_PUBLIC_URI: 'http://web.test.local/',
});
expect(response.status).toBe(303);
expect(response.headers.get('Location')).toContain(
'http://web.test.local/error/authFailure?extra=state invalid'
);
});
});

View file

@ -0,0 +1,77 @@
import { isAllowedCallbackHost } from '@roleypoly/api/src/routes/auth/bounce';
import { createSession } from '@roleypoly/api/src/sessions/create';
import { getStateSession } from '@roleypoly/api/src/sessions/state';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { dateFromID } from '@roleypoly/api/src/utils/id';
import { formDataRequest, getQuery } from '@roleypoly/api/src/utils/request';
import { seeOther } from '@roleypoly/api/src/utils/response';
import { AuthTokenResponse, StateSession } from '@roleypoly/types';
import normalizeUrl from 'normalize-url';
const authFailure = (uiPublicURI: string, extra?: string) =>
seeOther(uiPublicURI + `/error/authFailure${extra ? `?extra=${extra}` : ''}`);
export const authCallback: RoleypolyHandler = async (
request: Request,
{ config }: Context
) => {
let bounceBaseUrl = config.uiPublicURI;
const { state: stateValue, code } = getQuery(request);
if (stateValue === null) {
return authFailure('state missing');
}
try {
const stateTime = dateFromID(stateValue);
const stateExpiry = stateTime + 1000 * config.retention.session;
const currentTime = Date.now();
if (currentTime > stateExpiry) {
return authFailure('state expired');
}
const stateSession = await getStateSession<StateSession>(config, stateValue);
if (
stateSession?.callbackHost &&
isAllowedCallbackHost(config, stateSession.callbackHost)
) {
bounceBaseUrl = stateSession.callbackHost;
}
} catch (e) {
return authFailure(config.uiPublicURI, 'state invalid');
}
if (!code) {
return authFailure(config.uiPublicURI, 'code missing');
}
const response = await discordFetch<AuthTokenResponse>(
`/oauth2/token`,
'',
AuthType.None,
formDataRequest({
client_id: config.botClientID,
client_secret: config.botClientSecret,
grant_type: 'authorization_code',
code,
redirect_uri: config.apiPublicURI + '/auth/callback',
})
);
if (!response) {
return authFailure(config.uiPublicURI, 'code auth failure');
}
const session = await createSession(config, response);
if (!session) {
return authFailure(config.uiPublicURI, 'session setup failure');
}
const nextURL = normalizeUrl(
bounceBaseUrl + '/machinery/new-session/#/' + session.sessionID
);
return seeOther(nextURL);
};

View file

@ -0,0 +1,63 @@
jest.mock('../../utils/discord');
import { SessionData } from '@roleypoly/types';
import { parseEnvironment } from '../../utils/config';
import { AuthType, discordFetch } from '../../utils/discord';
import { formDataRequest } from '../../utils/request';
import { getBindings, makeRequest } from '../../utils/testHelpers';
const mockDiscordFetch = discordFetch as jest.Mock;
describe('DELETE /auth/session', () => {
it('deletes the current user session when it is valid', async () => {
const config = parseEnvironment(getBindings());
const session: SessionData = {
sessionID: 'test-session-id',
user: {
id: 'test-user-id',
username: 'test-username',
discriminator: 'test-discriminator',
avatar: 'test-avatar',
bot: false,
},
guilds: [],
tokens: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_in: 3600,
scope: 'identify guilds',
token_type: 'Bearer',
},
};
await config.kv.sessions.put(session.sessionID, session);
mockDiscordFetch.mockReturnValue(
new Response(null, {
status: 200,
})
);
const response = await makeRequest('DELETE', '/auth/session', {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(204);
expect(await config.kv.sessions.get(session.sessionID)).toBeNull();
expect(mockDiscordFetch).toHaveBeenCalledWith(
'/oauth2/token/revoke',
'',
AuthType.None,
expect.objectContaining(
formDataRequest({
client_id: config.botClientID,
client_secret: config.botClientSecret,
token: session.tokens.access_token,
})
)
);
});
});

View file

@ -0,0 +1,27 @@
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { formDataRequest } from '@roleypoly/api/src/utils/request';
import { noContent } from '@roleypoly/api/src/utils/response';
export const authSessionDelete: RoleypolyHandler = async (
request: Request,
context: Context
) => {
if (!context.session) {
return noContent();
}
await discordFetch(
'/oauth2/token/revoke',
'',
AuthType.None,
formDataRequest({
client_id: context.config.botClientID,
client_secret: context.config.botClientSecret,
token: context.session.tokens.access_token,
})
);
await context.config.kv.sessions.delete(context.session.sessionID);
return noContent();
};

View file

@ -0,0 +1,53 @@
import { SessionData } from '@roleypoly/types';
import { parseEnvironment } from '../../utils/config';
import { getBindings, makeRequest } from '../../utils/testHelpers';
describe('GET /auth/session', () => {
it('fetches the current user session when it is valid', async () => {
const config = parseEnvironment(getBindings());
const session: SessionData = {
sessionID: 'test-session-id',
user: {
id: 'test-user-id',
username: 'test-username',
discriminator: 'test-discriminator',
avatar: 'test-avatar',
bot: false,
},
guilds: [],
tokens: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_in: 3600,
scope: 'identify guilds',
token_type: 'Bearer',
},
};
await config.kv.sessions.put(session.sessionID, session);
const response = await makeRequest('GET', '/auth/session', {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
sessionID: session.sessionID,
user: session.user,
guilds: session.guilds,
});
});
it('returns 401 when session is not valid', async () => {
const response = await makeRequest('GET', '/auth/session', {
headers: {
Authorization: `Bearer invalid-session-id`,
},
});
expect(response.status).toBe(401);
});
});

View file

@ -0,0 +1,17 @@
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { json, notFound } from '@roleypoly/api/src/utils/response';
export const authSession: RoleypolyHandler = async (
request: Request,
context: Context
) => {
if (context.session) {
return json({
user: context.session.user,
guilds: context.session.guilds,
sessionID: context.session.sessionID,
});
}
return notFound();
};

View file

@ -0,0 +1,32 @@
jest.mock('../../guilds/getters');
import { UserGuildPermissions } from '@roleypoly/types';
import { getGuild } from '../../guilds/getters';
import { configContext, makeRequest, makeSession } from '../../utils/testHelpers';
const mockGetGuild = getGuild as jest.Mock;
describe('DELETE /guilds/:id/cache', () => {
it('calls getGuilds and returns No Content', async () => {
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: UserGuildPermissions.Admin,
},
],
});
const response = await makeRequest('DELETE', `/guilds/123/cache`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(204);
expect(mockGetGuild).toHaveBeenCalledWith(expect.any(Object), '123', true);
});
});

View file

@ -0,0 +1,12 @@
import { getGuild } from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { noContent } from '@roleypoly/api/src/utils/response';
export const guildsCacheDelete: RoleypolyHandler = async (
request: Request,
context: Context
) => {
await getGuild(context.config, context.params.guildId!, true);
return noContent();
};

View file

@ -0,0 +1,370 @@
jest.mock('../../guilds/getters');
jest.mock('../../utils/discord');
import {
CategoryType,
Features,
Guild,
GuildData,
Member,
OwnRoleInfo,
RoleSafety,
RoleUpdate,
TransactionType,
} from '@roleypoly/types';
import { getGuild, getGuildData, getGuildMember } from '../../guilds/getters';
import { AuthType, discordFetch } from '../../utils/discord';
import { json } from '../../utils/response';
import { configContext, makeRequest, makeSession } from '../../utils/testHelpers';
const mockDiscordFetch = discordFetch as jest.Mock;
const mockGetGuild = getGuild as jest.Mock;
const mockGetGuildMember = getGuildMember as jest.Mock;
const mockGetGuildData = getGuildData as jest.Mock;
beforeEach(() => {
jest.resetAllMocks();
doMock();
});
describe('PUT /guilds/:id/roles', () => {
it('adds member roles when called with valid roles', async () => {
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const update: RoleUpdate = {
knownState: ['role-1'],
transactions: [{ id: 'role-2', action: TransactionType.Add }],
};
mockDiscordFetch.mockReturnValueOnce(
json({
roles: ['role-1', 'role-2'],
})
);
const response = await makeRequest(
'PUT',
`/guilds/123/roles`,
{
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify(update),
},
{
BOT_TOKEN: 'test',
}
);
expect(response.status).toBe(200);
expect(mockDiscordFetch).toHaveBeenCalledWith(
`/guilds/123/members/${session.user.id}`,
'test',
AuthType.Bot,
{
body: JSON.stringify({
roles: ['role-1', 'role-2'],
}),
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via ${config.uiPublicURI}`,
},
method: 'PATCH',
}
);
});
it('removes member roles when called with valid roles', async () => {
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const update: RoleUpdate = {
knownState: ['role-1'],
transactions: [{ id: 'role-1', action: TransactionType.Remove }],
};
mockDiscordFetch.mockReturnValueOnce(
json({
roles: [],
})
);
const response = await makeRequest(
'PUT',
`/guilds/123/roles`,
{
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify(update),
},
{
BOT_TOKEN: 'test',
}
);
expect(response.status).toBe(200);
expect(mockDiscordFetch).toHaveBeenCalledWith(
`/guilds/123/members/${session.user.id}`,
'test',
AuthType.Bot,
{
body: JSON.stringify({
roles: [],
}),
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via ${config.uiPublicURI}`,
},
method: 'PATCH',
}
);
});
it('does not update roles when called only with invalid roles', async () => {
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const update: RoleUpdate = {
knownState: ['role-1'],
transactions: [
{ id: 'role-3', action: TransactionType.Add }, // role is in a hidden category
{ id: 'role-5-unsafe', action: TransactionType.Add }, // role is marked unsafe
],
};
const response = await makeRequest(
'PUT',
`/guilds/123/roles`,
{
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify(update),
},
{
BOT_TOKEN: 'test',
}
);
expect(response.status).toBe(400);
expect(mockDiscordFetch).not.toHaveBeenCalled();
});
it('filters roles that are invalid while accepting ones that are valid', async () => {
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const update: RoleUpdate = {
knownState: ['role-1'],
transactions: [
{ id: 'role-3', action: TransactionType.Add }, // role is in a hidden category
{ id: 'role-2', action: TransactionType.Add }, // role is in a hidden category
],
};
const response = await makeRequest(
'PUT',
`/guilds/123/roles`,
{
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify(update),
},
{
BOT_TOKEN: 'test',
}
);
expect(response.status).toBe(200);
expect(mockDiscordFetch).toHaveBeenCalledWith(
`/guilds/123/members/${session.user.id}`,
'test',
AuthType.Bot,
{
body: JSON.stringify({
roles: ['role-1', 'role-2'],
}),
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via ${config.uiPublicURI}`,
},
method: 'PATCH',
}
);
});
it('400s when no transactions are present', async () => {
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const update: RoleUpdate = {
knownState: ['role-1'],
transactions: [],
};
const response = await makeRequest(
'PUT',
`/guilds/123/roles`,
{
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify(update),
},
{
BOT_TOKEN: 'test',
}
);
expect(response.status).toBe(400);
expect(mockDiscordFetch).not.toHaveBeenCalled();
expect(mockGetGuild).not.toHaveBeenCalled();
expect(mockGetGuildData).not.toHaveBeenCalled();
expect(mockGetGuildMember).not.toHaveBeenCalled();
});
});
const doMock = () => {
const guild: Guild & OwnRoleInfo = {
id: '123',
name: 'test',
icon: 'test',
highestRolePosition: 0,
roles: [
{
id: 'role-1',
name: 'Role 1',
color: 0,
position: 17,
permissions: '',
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'role-2',
name: 'Role 2',
color: 0,
position: 16,
permissions: '',
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'role-3',
name: 'Role 3',
color: 0,
position: 15,
permissions: '',
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'role-4',
name: 'Role 4',
color: 0,
position: 14,
permissions: '',
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'role-5-unsafe',
name: 'Role 5 (Unsafe)',
color: 0,
position: 14,
permissions: '',
managed: false,
safety: RoleSafety.DangerousPermissions,
},
],
};
const member: Member = {
roles: ['role-1'],
pending: false,
nick: '',
};
const guildData: GuildData = {
id: '123',
message: 'test',
categories: [
{
id: 'category-1',
name: 'Category 1',
position: 0,
hidden: false,
type: CategoryType.Multi,
roles: ['role-1', 'role-2'],
},
{
id: 'category-2',
name: 'Category 2',
position: 1,
hidden: true,
type: CategoryType.Multi,
roles: ['role-3'],
},
],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: false,
},
};
mockGetGuild.mockReturnValue(guild);
mockGetGuildMember.mockReturnValue(member);
mockGetGuildData.mockReturnValue(guildData);
mockDiscordFetch.mockReturnValue(json({}));
};

View file

@ -0,0 +1,160 @@
import {
getGuild,
getGuildData,
getGuildMember,
updateGuildMember,
} from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { APIMember, AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import {
engineeringProblem,
invalid,
json,
notFound,
serverError,
} from '@roleypoly/api/src/utils/response';
import {
difference,
isIdenticalArray,
keyBy,
union,
} from '@roleypoly/misc-utils/collection-tools';
import {
GuildData,
Member,
Role,
RoleSafety,
RoleTransaction,
RoleUpdate,
TransactionType,
} from '@roleypoly/types';
export const guildsRolesPut: RoleypolyHandler = async (
request: Request,
context: Context
) => {
if (!request.body) {
return invalid();
}
const updateRequest: RoleUpdate = await request.json();
if (updateRequest.transactions.length === 0) {
return invalid();
}
const guildID = context.params.guildId;
if (!guildID) {
return engineeringProblem('params not set up correctly');
}
const userID = context.session!.user.id;
const [member, guildData, guild] = await Promise.all([
getGuildMember(context.config, guildID, userID),
getGuildData(context.config, guildID),
getGuild(context.config, guildID),
]);
if (!guild || !member) {
return notFound();
}
const newRoles = calculateNewRoles({
currentRoles: member.roles,
guildRoles: guild.roles,
guildData,
updateRequest,
});
if (
isIdenticalArray(member.roles, newRoles) ||
isIdenticalArray(updateRequest.knownState, newRoles)
) {
return invalid();
}
const patchMemberRoles = await discordFetch<APIMember>(
`/guilds/${guildID}/members/${userID}`,
context.config.botToken,
AuthType.Bot,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via ${context.config.uiPublicURI}`,
},
body: JSON.stringify({
roles: newRoles,
}),
}
);
if (!patchMemberRoles) {
return serverError(new Error('discord rejected the request'));
}
context.fetchContext.waitUntil(
updateGuildMember(context.config, guildID, patchMemberRoles)
);
const updatedMember: Member = {
roles: patchMemberRoles.roles,
};
return json(updatedMember);
};
export const calculateNewRoles = ({
currentRoles,
guildData,
guildRoles,
updateRequest,
}: {
currentRoles: string[];
guildRoles: Role[];
guildData: GuildData;
updateRequest: RoleUpdate;
}): string[] => {
const roleMap = keyBy(guildRoles, 'id');
// These roles were ones changed between knownState (role picker page load/cache) and current (fresh from discord).
// We could cause issues, so we'll re-add them later.
// const diffRoles = difference(updateRequest.knownState, currentRoles);
// Only these are safe
const allSafeRoles = guildData.categories.reduce<string[]>(
(categorizedRoles, category) =>
!category.hidden
? [
...categorizedRoles,
...category.roles.filter(
(roleID) => roleMap[roleID]?.safety === RoleSafety.Safe
),
]
: categorizedRoles,
[]
);
const safeTransactions = updateRequest.transactions.filter((tx: RoleTransaction) =>
allSafeRoles.includes(tx.id)
);
const changesByAction = safeTransactions.reduce<
Record<TransactionType, RoleTransaction[]>
>((group, value, _1, _2, key = value.action) => (group[key].push(value), group), {
[TransactionType.Add]: [],
[TransactionType.Remove]: [],
});
const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map(
(tx: RoleTransaction) => tx.id
);
const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map(
(tx: RoleTransaction) => tx.id
);
const final = union(difference(currentRoles, rolesToRemove), rolesToAdd);
return final;
};

View file

@ -0,0 +1,164 @@
jest.mock('../../guilds/getters');
import { Features, GuildData, PresentableGuild } from '@roleypoly/types';
import { getGuild, getGuildData, getGuildMember } from '../../guilds/getters';
import { APIGuild, APIMember } from '../../utils/discord';
import { configContext, makeRequest, makeSession } from '../../utils/testHelpers';
const mockGetGuild = getGuild as jest.Mock;
const mockGetGuildMember = getGuildMember as jest.Mock;
const mockGetGuildData = getGuildData as jest.Mock;
beforeEach(() => {
mockGetGuildData.mockReset();
mockGetGuild.mockReset();
mockGetGuildMember.mockReset();
});
describe('GET /guilds/:id', () => {
it('returns a presentable guild', async () => {
const guild: APIGuild = {
id: '123',
name: 'test',
icon: 'test',
roles: [
{
id: 'role-1',
name: 'Role 1',
color: 0,
position: 17,
permissions: '',
managed: false,
},
],
};
const member: APIMember = {
roles: ['role-1'],
pending: false,
nick: '',
user: {
id: 'user-1',
},
};
const guildData: GuildData = {
id: '123',
message: 'test',
categories: [],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: false,
},
};
mockGetGuild.mockReturnValue(guild);
mockGetGuildMember.mockReturnValue(member);
mockGetGuildData.mockReturnValue(guildData);
const [config] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const response = await makeRequest('GET', `/guilds/${guild.id}`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
id: guild.id,
guild: session.guilds[0],
member: {
roles: member.roles,
},
roles: guild.roles,
data: guildData,
} as PresentableGuild);
});
it('returns a 404 when the guild is not in session', async () => {
const [config, context] = configContext();
const session = await makeSession(config);
const response = await makeRequest('GET', `/guilds/123`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(404);
});
it('returns 404 when the guild is not fetchable', async () => {
const [config, context] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const response = await makeRequest('GET', `/guilds/123`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(404);
});
it('returns 404 when the member is no longer in the guild', async () => {
const guild: APIGuild = {
id: '123',
name: 'test',
icon: 'test',
roles: [
{
id: 'role-1',
name: 'Role 1',
color: 0,
position: 17,
permissions: '',
managed: false,
},
],
};
mockGetGuild.mockReturnValue(guild);
mockGetGuildMember.mockReturnValue(null);
const [config, context] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: 0,
},
],
});
const response = await makeRequest('GET', `/guilds/${guild.id}`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(404);
});
});

View file

@ -0,0 +1,45 @@
import {
getGuild,
getGuildData,
getGuildMember,
} from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { getQuery } from '@roleypoly/api/src/utils/request';
import { json, notFound } from '@roleypoly/api/src/utils/response';
import { PresentableGuild } from '@roleypoly/types';
export const guildsGuild: RoleypolyHandler = async (
request: Request,
context: Context
) => {
const { __no_cache: noCache } = getQuery(request);
const guild = await getGuild(context.config, context.params!.guildId!, !!noCache);
if (!guild) {
return notFound();
}
const member = await getGuildMember(
context.config,
context.params!.guildId!,
context.session!.user.id,
!!noCache
);
if (!member) {
return notFound();
}
const data = await getGuildData(context.config, guild.id);
const presentableGuild: PresentableGuild = {
id: guild.id,
guild: context.session?.guilds.find((g) => g.id === guild.id)!,
roles: guild.roles,
member: {
roles: member.roles,
},
data,
};
return json(presentableGuild);
};

View file

@ -0,0 +1,164 @@
jest.mock('../../guilds/getters');
import {
Features,
GuildData,
GuildDataUpdate,
UserGuildPermissions,
} from '@roleypoly/types';
import { getGuildData } from '../../guilds/getters';
import { configContext, makeRequest, makeSession } from '../../utils/testHelpers';
const mockGetGuildData = getGuildData as jest.Mock;
beforeAll(() => {
jest.resetAllMocks();
});
describe('PATCH /guilds/:id', () => {
it('updates guild data when user is an editor', async () => {
const [config, context] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: UserGuildPermissions.Manager,
},
],
});
mockGetGuildData.mockReturnValue({
id: '123',
message: 'test',
categories: [],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: false,
},
} as GuildData);
const response = await makeRequest('PATCH', `/guilds/123`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify({
message: 'hello test world!',
} as GuildDataUpdate),
});
expect(response.status).toBe(200);
const newGuildData = await config.kv.guildData.get('123');
expect(newGuildData).toMatchObject({
message: 'hello test world!',
});
});
it('ignores extraneous fields sent as updates', async () => {
const [config, context] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: UserGuildPermissions.Manager,
},
],
});
mockGetGuildData.mockReturnValue({
id: '123',
message: 'test',
categories: [],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: false,
},
} as GuildData);
const response = await makeRequest('PATCH', `/guilds/123`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify({
fifteen: 'foxes',
}),
});
expect(response.status).toBe(200);
const newGuildData = await config.kv.guildData.get('123');
expect(newGuildData).not.toMatchObject({
fifteen: 'foxes',
});
});
it('403s when user is not an editor', async () => {
const [config, context] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: UserGuildPermissions.User,
},
],
});
mockGetGuildData.mockReturnValue({
id: '123',
message: 'test',
categories: [],
features: Features.None,
auditLogWebhook: null,
accessControl: {
allowList: [],
blockList: [],
blockPending: false,
},
} as GuildData);
const response = await makeRequest('PATCH', `/guilds/123`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
body: JSON.stringify({
message: 'hello test world!',
} as GuildDataUpdate),
});
expect(response.status).toBe(403);
});
it('400s when no body is present', async () => {
const [config, context] = configContext();
const session = await makeSession(config, {
guilds: [
{
id: '123',
name: 'test',
icon: 'test',
permissionLevel: UserGuildPermissions.Manager,
},
],
});
const response = await makeRequest('PATCH', `/guilds/123`, {
headers: {
Authorization: `Bearer ${session.sessionID}`,
},
});
expect(response.status).toBe(400);
});
});

View file

@ -0,0 +1,37 @@
import { getGuildData } from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { invalid, json, notFound } from '@roleypoly/api/src/utils/response';
import { GuildData, GuildDataUpdate } from '@roleypoly/types';
export const guildsGuildPatch: RoleypolyHandler = async (
request: Request,
context: Context
) => {
const id = context.params.guildId!;
if (!request.body) {
return invalid();
}
const update: GuildDataUpdate = await request.json();
const oldGuildData = await getGuildData(context.config, id);
if (!oldGuildData) {
return notFound();
}
const newGuildData: GuildData = {
...oldGuildData,
// TODO: validation
message: update.message || oldGuildData.message,
categories: update.categories || oldGuildData.categories,
accessControl: update.accessControl || oldGuildData.accessControl,
// TODO: audit log webhooks
auditLogWebhook: oldGuildData.auditLogWebhook,
};
await context.config.kv.guildData.put(id, newGuildData);
return json(newGuildData);
};

View file

@ -0,0 +1,52 @@
jest.mock('../../guilds/getters');
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
import { getGuild } from '../../guilds/getters';
import { APIGuild } from '../../utils/discord';
import { makeRequest } from '../../utils/testHelpers';
const mockGetGuild = getGuild as jest.Mock;
beforeEach(() => {
mockGetGuild.mockReset();
});
describe('GET /guilds/:id/slug', () => {
it('returns a valid slug for a given discord server', async () => {
const guild: APIGuild = {
id: '123',
name: 'test',
icon: 'test',
roles: [
{
id: 'role-1',
name: 'Role 1',
color: 0,
position: 17,
permissions: '',
managed: false,
},
],
};
mockGetGuild.mockReturnValue(guild);
const response = await makeRequest('GET', `/guilds/${guild.id}/slug`);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
id: guild.id,
icon: guild.icon,
name: guild.name,
permissionLevel: UserGuildPermissions.User,
} as GuildSlug);
});
it('returns a 404 if the guild cannot be fetched', async () => {
mockGetGuild.mockReturnValue(null);
const response = await makeRequest('GET', `/guilds/slug/123`);
expect(response.status).toBe(404);
});
});

View file

@ -0,0 +1,31 @@
import { getGuild } from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { json, notFound } from '@roleypoly/api/src/utils/response';
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
export const guildsSlug: RoleypolyHandler = async (
request: Request,
context: Context
) => {
const id = context.params.guildId!;
const guildInSession = context.session?.guilds.find((guild) => guild.id === id);
if (guildInSession) {
return json<GuildSlug>(guildInSession);
}
const guild = await getGuild(context.config, id);
if (!guild) {
return notFound();
}
const slug: GuildSlug = {
id,
name: guild.name,
icon: guild.icon,
permissionLevel: UserGuildPermissions.User,
};
return json<GuildSlug>(slug);
};

View file

@ -0,0 +1,60 @@
jest.mock('../../../utils/discord');
import { discordFetch } from '../../../utils/discord';
import { configContext } from '../../../utils/testHelpers';
import {
extractInteractionResponse,
isDeferred,
isEphemeral,
makeInteractionsRequest,
mockUpdateCall,
} from '../testHelpers';
const mockDiscordFetch = discordFetch as jest.Mock;
it('responds with the username when member.nick is missing', async () => {
const [, context] = configContext();
const response = await makeInteractionsRequest(
context,
{
name: 'hello-world',
},
false,
{
member: {
nick: undefined,
roles: [],
},
}
);
expect(response.status).toBe(200);
const interaction = await extractInteractionResponse(response);
expect(isDeferred(interaction)).toBe(true);
expect(isEphemeral(interaction)).toBe(true);
expect(mockDiscordFetch).toBeCalledWith(
...mockUpdateCall(expect, {
content: 'Hey there, test-user',
})
);
});
it('responds with the nickname when member.nick is set', async () => {
const [, context] = configContext();
const response = await makeInteractionsRequest(context, {
name: 'hello-world',
});
expect(response.status).toBe(200);
const interaction = await extractInteractionResponse(response);
expect(isDeferred(interaction)).toBe(true);
expect(isEphemeral(interaction)).toBe(true);
expect(mockDiscordFetch).toBeCalledWith(
...mockUpdateCall(expect, {
content: 'Hey there, test-user-nick',
})
);
});

View file

@ -0,0 +1,27 @@
import { InteractionHandler } from '@roleypoly/api/src/routes/interactions/helpers';
import { Context } from '@roleypoly/api/src/utils/context';
import {
InteractionCallbackType,
InteractionRequest,
InteractionResponse,
} from '@roleypoly/types';
export const helloWorld: InteractionHandler = (
interaction: InteractionRequest,
context: Context
): InteractionResponse => {
console.log({ interaction });
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Hey there, ${
interaction.member?.nick ||
interaction.member?.user?.username ||
interaction.user?.username
}`,
},
};
};
helloWorld.ephemeral = true;
helloWorld.deferred = true;

View file

@ -0,0 +1,18 @@
import { InteractionHandler } from '@roleypoly/api/src/routes/interactions/helpers';
import { rolePickerCommon } from '@roleypoly/api/src/routes/interactions/role-picker-common';
import { Context } from '@roleypoly/api/src/utils/context';
import {
InteractionRequest,
InteractionResponse,
TransactionType,
} from '@roleypoly/types';
export const pickRole: InteractionHandler = async (
interaction: InteractionRequest,
context: Context
): Promise<InteractionResponse> => {
return rolePickerCommon(interaction, context, TransactionType.Add);
};
pickRole.ephemeral = true;
pickRole.deferred = true;

View file

@ -0,0 +1,118 @@
import {
getGuild,
getGuildData,
getGuildMember,
getPickableRoles,
} from '@roleypoly/api/src/guilds/getters';
import {
embedBuilder,
getName,
InteractionHandler,
} from '@roleypoly/api/src/routes/interactions/helpers';
import {
embedPalette,
embedResponse,
} from '@roleypoly/api/src/routes/interactions/responses';
import { Context } from '@roleypoly/api/src/utils/context';
import {
CategoryType,
Embed,
InteractionCallbackType,
InteractionRequest,
InteractionResponse,
Role,
} from '@roleypoly/types';
export const pickableRoles: InteractionHandler = async (
interaction: InteractionRequest,
context: Context
): Promise<InteractionResponse> => {
if (!interaction.guild_id) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`,
embedPalette.error
);
}
const [guildData, guild, member] = await Promise.all([
getGuildData(context.config, interaction.guild_id),
getGuild(context.config, interaction.guild_id),
getGuildMember(context.config, interaction.guild_id, interaction.member?.user?.id!),
]);
if (!guildData || !guild) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. Something's wrong with the server you're in. Try picking your roles at ${
context.config.uiPublicURI
}/s/${interaction.guild_id} instead.`,
embedPalette.error
);
}
const roles = getPickableRoles(guildData, guild);
if (roles.length === 0) {
return embedResponse(
':fire: Error',
`Hey ${getName(
interaction
)}. This server might not be set up to use Roleypoly yet, as there are no roles to pick from.`,
embedPalette.error
);
}
const makeBoldIfMemberHasRole = (role: Role, base: string): string => {
if (member?.roles.includes(role.id)) {
return `__${base}__`;
}
return base;
};
const embed: Embed = {
color: embedPalette.neutral,
fields: roles.map(({ category, roles }) => {
return {
name: `${category.name}${
category.type === CategoryType.Single ? ' *(pick one)*' : ''
}`,
value: roles
.map((role) => makeBoldIfMemberHasRole(role, `<@&${role.id}>`))
.join(', '),
} as Embed['fields'][0];
}),
title: 'You can pick any of these roles with /pick-role',
footer: {
text: `Roles with an __underline__ are already picked by you.`,
},
};
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: embedBuilder(embed),
components: [
{
type: 1,
components: [
// Link to Roleypoly
{
type: 2,
label: 'Pick roles on your browser',
url: `${context.config.uiPublicURI}/s/${interaction.guild_id}`,
style: 5,
},
],
},
],
},
};
};
pickableRoles.ephemeral = true;
pickableRoles.deferred = true;

View file

@ -0,0 +1,18 @@
import { InteractionHandler } from '@roleypoly/api/src/routes/interactions/helpers';
import { rolePickerCommon } from '@roleypoly/api/src/routes/interactions/role-picker-common';
import { Context } from '@roleypoly/api/src/utils/context';
import {
InteractionRequest,
InteractionResponse,
TransactionType,
} from '@roleypoly/types';
export const removeRole: InteractionHandler = async (
interaction: InteractionRequest,
context: Context
): Promise<InteractionResponse> => {
return rolePickerCommon(interaction, context, TransactionType.Remove);
};
removeRole.ephemeral = true;
removeRole.deferred = true;

View file

@ -0,0 +1,60 @@
import {
getName,
InteractionHandler,
} from '@roleypoly/api/src/routes/interactions/helpers';
import { embedResponse } from '@roleypoly/api/src/routes/interactions/responses';
import { Context } from '@roleypoly/api/src/utils/context';
import {
Embed,
InteractionCallbackType,
InteractionRequest,
InteractionResponse,
} from '@roleypoly/types';
export const roleypoly: InteractionHandler = (
interaction: InteractionRequest,
context: Context
): InteractionResponse => {
if (!interaction.guild_id) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`
);
}
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: 0x453e3d,
title: `:beginner: Hey there, ${getName(interaction)}!`,
description: `Try these slash commands, or pick roles from your browser!`,
fields: [
{ name: 'See all the roles', value: '/pickable-roles' },
{ name: 'Pick a role', value: '/pick-role' },
{ name: 'Remove a role', value: '/remove-role' },
],
} as Embed,
],
components: [
{
type: 1,
components: [
// Link to Roleypoly
{
type: 2,
label: `Pick roles on ${new URL(context.config.uiPublicURI).hostname}`,
url: `${context.config.uiPublicURI}/s/${interaction.guild_id}`,
style: 5,
},
],
},
],
},
};
};
roleypoly.ephemeral = true;

View file

@ -0,0 +1,179 @@
import { InteractionRequest, InteractionType } from '@roleypoly/types';
import nacl from 'tweetnacl';
import { configContext } from '../../utils/testHelpers';
import { embedBuilder, verifyRequest } from './helpers';
//
// Q: Why tweetnacl when WebCrypto is available?
// A: Discord uses tweetnacl on their end, thus is also
// used in far more examples of Discord Interactions than WebCrypto.
// We don't actually use it in Workers, as SubtleCrypto using NODE-ED25519
// is better in every way, and still gives us the same effect.
//
describe('verifyRequest', () => {
it('validates a successful Discord interactions request', async () => {
const [config, context] = configContext();
const timestamp = String(Date.now());
const body: InteractionRequest = {
id: '123',
type: InteractionType.APPLICATION_COMMAND,
application_id: '123',
token: '123',
version: 1,
};
const { publicKey, secretKey } = nacl.sign.keyPair();
const signature = nacl.sign.detached(
Buffer.from(timestamp + JSON.stringify(body)),
secretKey
);
config.publicKey = Buffer.from(publicKey).toString('hex');
const request = new Request('http://local.test', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'x-signature-timestamp': timestamp,
'x-signature-ed25519': Buffer.from(signature).toString('hex'),
},
});
expect(await verifyRequest(context.config, request, body)).toBe(true);
});
it('fails to validate a headerless Discord interactions request', async () => {
const [config, context] = configContext();
const body: InteractionRequest = {
id: '123',
type: InteractionType.APPLICATION_COMMAND,
application_id: '123',
token: '123',
version: 1,
};
const { publicKey, secretKey } = nacl.sign.keyPair();
config.publicKey = Buffer.from(publicKey).toString('hex');
const request = new Request('http://local.test', {
method: 'POST',
body: JSON.stringify(body),
headers: {},
});
expect(await verifyRequest(context.config, request, body)).toBe(false);
});
it('fails to validate a bad signature from Discord', async () => {
const [config, context] = configContext();
const timestamp = String(Date.now());
const body: InteractionRequest = {
id: '123',
type: InteractionType.APPLICATION_COMMAND,
application_id: '123',
token: '123',
version: 1,
};
const { publicKey } = nacl.sign.keyPair();
const { secretKey: otherKey } = nacl.sign.keyPair();
const signature = nacl.sign.detached(
Buffer.from(timestamp + JSON.stringify(body)),
otherKey
);
config.publicKey = Buffer.from(publicKey).toString('hex');
const request = new Request('http://local.test', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'x-signature-timestamp': timestamp,
'x-signature-ed25519': Buffer.from(signature).toString('hex'),
},
});
expect(await verifyRequest(context.config, request, body)).toBe(false);
});
it('fails to validate when signature differs from data', async () => {
const [config, context] = configContext();
const timestamp = String(Date.now());
const body: InteractionRequest = {
id: '123',
type: InteractionType.APPLICATION_COMMAND,
application_id: '123',
token: '123',
version: 1,
};
const { publicKey, secretKey } = nacl.sign.keyPair();
const signature = nacl.sign.detached(
Buffer.from(timestamp + JSON.stringify({ ...body, id: '456' })),
secretKey
);
config.publicKey = Buffer.from(publicKey).toString('hex');
const request = new Request('http://local.test', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'x-signature-timestamp': timestamp,
'x-signature-ed25519': Buffer.from(signature).toString('hex'),
},
});
expect(await verifyRequest(context.config, request, body)).toBe(false);
});
});
describe('embedBuilder', () => {
it('builds embeds that discord approves of', () => {
const embeds = embedBuilder({
title: 'Test',
fields: [
{
name: 'Field 1',
value: 'role-1, role-2, role-3, role-4, role-5, '
.repeat(1024 / 30 - 15)
.replace(/, $/, ''),
},
{
name: 'Field 2',
value: 'role-1, role-2, role-3, role-4, role-5, '
.repeat(1024 / 30 + 4)
.replace(/, $/, ''),
},
],
color: 0xff0000,
});
expect(embeds).toMatchInlineSnapshot(`
Array [
Object {
"color": 16711680,
"fields": Array [
Object {
"name": "Field 1",
"value": "role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5",
},
Object {
"name": "Field 2",
"value": "role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3",
},
Object {
"name": "Field 2 (continued)",
"value": "role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5, role-1, role-2, role-3, role-4, role-5",
},
],
"title": "Test",
},
]
`);
expect(embeds.length).toBe(1);
expect(embeds[0].fields.length).toBe(3);
});
});

View file

@ -0,0 +1,203 @@
import { Config } from '@roleypoly/api/src/utils/config';
import { Context } from '@roleypoly/api/src/utils/context';
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { Embed, InteractionRequest, InteractionResponse } from '@roleypoly/types';
export const verifyRequest = async (
config: Config,
request: Request,
interaction: InteractionRequest
): Promise<boolean> => {
try {
const timestamp = request.headers.get('x-signature-timestamp');
const signature = request.headers.get('x-signature-ed25519');
if (!timestamp || !signature) {
return false;
}
const key = await crypto.subtle.importKey(
'raw',
bufferizeHex(config.publicKey),
{ name: 'NODE-ED25519', namedCurve: 'NODE-ED25519', public: true } as any,
false,
['verify']
);
const verified = await crypto.subtle.verify(
'NODE-ED25519',
key,
bufferizeHex(signature),
bufferizeString(timestamp + JSON.stringify(interaction))
);
return verified;
} catch (e) {
return false;
}
};
// Cloudflare Workers + SubtleCrypto has no idea what a Buffer.from() is.
// What the fuck?
const bufferizeHex = (input: string) => {
const buffer = new Uint8Array(input.length / 2);
for (let i = 0; i < input.length; i += 2) {
buffer[i / 2] = parseInt(input.substring(i, i + 2), 16);
}
return buffer;
};
const bufferizeString = (input: string) => {
const encoder = new TextEncoder();
return encoder.encode(input);
};
export type InteractionHandler = ((
interaction: InteractionRequest,
context: Context
) => Promise<InteractionResponse> | InteractionResponse) & {
ephemeral?: boolean;
deferred?: boolean;
};
export const runAsync = async (
handler: InteractionHandler,
interaction: InteractionRequest,
context: Context
): Promise<void> => {
const url = `/webhooks/${interaction.application_id}/${interaction.token}/messages/@original`;
try {
const response = await handler(interaction, context);
if (!response) {
throw new Error('Interaction handler returned no response');
}
console.log({ response });
await discordFetch(url, '', AuthType.None, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...response.data,
}),
});
} catch (e) {
console.error('/interations runAsync failed', {
e,
interaction: {
data: interaction.data,
user: interaction.user,
guild: interaction.guild_id,
},
});
try {
await discordFetch(url, '', AuthType.None, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: "I'm sorry, I'm having trouble processing this request.",
} as InteractionResponse['data']),
});
} catch (e) {}
}
};
export const getName = (interaction: InteractionRequest): string => {
return (
interaction.member?.nick ||
interaction.member?.user?.username ||
interaction.user?.username ||
'friend'
);
};
/**
* Take a single big embed and fit it into Discord limits
* per embed, 25 fields, and 1024 characters per field.
* so we'll make new embeds/fields as the content gets too long.
*/
export const embedBuilder = (embed: Embed): Embed[] => {
const embeds: Embed[] = [];
const titleCorrection = (title: string, withContinued?: boolean) => {
const suffix = withContinued ? '... (continued)' : '...';
const offsetTitle = title.length + suffix.length;
return title.length > 256 - offsetTitle
? title.slice(0, 256 - offsetTitle) + suffix
: withContinued
? `${title} (continued)`
: title;
};
let currentEmbed: Embed = {
color: embed.color,
title: embed.title,
fields: [],
};
let knownFieldTitles: string[] = [];
const commitField = (field: Embed['fields'][0]) => {
if (currentEmbed.fields.length === 25) {
embeds.push(currentEmbed);
currentEmbed = {
color: embed.color,
title: `${embed.title} (continued)`,
fields: [],
};
}
console.warn({ field });
const addContinued = knownFieldTitles.includes(field.name);
if (!addContinued) {
knownFieldTitles.push(field.name);
}
field.name = titleCorrection(`${field.name}`, addContinued);
console.warn({ field, knownFieldTitles });
currentEmbed.fields.push(field);
};
for (let field of embed.fields) {
if (field.value.length <= 1024) {
commitField(field);
continue;
}
const split = field.value.split(', '); // we know we'll be using , as a delimiter
let fieldValue: Embed['fields'][0]['value'] = '';
for (let part of split) {
if (fieldValue.length + part.length > 1024) {
commitField({
name: field.name,
value: fieldValue.replace(/, $/, ''),
});
fieldValue = '';
} else {
fieldValue += part + ', ';
}
}
if (fieldValue.length > 0) {
commitField({
name: field.name,
value: fieldValue.replace(/, $/, ''),
});
}
}
if (currentEmbed.fields.length > 0) {
embeds.push(currentEmbed);
}
return embeds;
};

View file

@ -0,0 +1,45 @@
jest.mock('../../utils/discord');
import { InteractionCallbackType, InteractionFlags } from '@roleypoly/types';
import { AuthType, discordFetch } from '../../utils/discord';
import { configContext } from '../../utils/testHelpers';
import { extractInteractionResponse, makeInteractionsRequest } from './testHelpers';
const mockDiscordFetch = discordFetch as jest.Mock;
it('responds with a simple hello-world!', async () => {
const [config, context] = configContext();
const response = await makeInteractionsRequest(context, {
name: 'hello-world',
});
expect(response.status).toBe(200);
const interaction = await extractInteractionResponse(response);
expect(interaction.type).toEqual(
InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
);
expect(interaction.data).toEqual({
flags: InteractionFlags.EPHEMERAL,
});
expect(mockDiscordFetch).toBeCalledWith(expect.any(String), '', AuthType.None, {
body: JSON.stringify({
content: 'Hey there, test-user-nick',
}),
headers: expect.any(Object),
method: 'PATCH',
});
});
it('does not allow requests that are invalid', async () => {
const [config, context] = configContext();
const response = await makeInteractionsRequest(
context,
{
name: 'hello-world',
},
true
);
expect(response.status).toBe(401);
});

View file

@ -0,0 +1,92 @@
import { helloWorld } from '@roleypoly/api/src/routes/interactions/commands/hello-world';
import { pickRole } from '@roleypoly/api/src/routes/interactions/commands/pick-role';
import { pickableRoles } from '@roleypoly/api/src/routes/interactions/commands/pickable-roles';
import { removeRole } from '@roleypoly/api/src/routes/interactions/commands/remove-role';
import { roleypoly } from '@roleypoly/api/src/routes/interactions/commands/roleypoly';
import {
InteractionHandler,
runAsync,
verifyRequest,
} from '@roleypoly/api/src/routes/interactions/helpers';
import { notImplemented } from '@roleypoly/api/src/routes/interactions/responses';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { invalid, json } from '@roleypoly/api/src/utils/response';
import {
InteractionCallbackType,
InteractionData,
InteractionFlags,
InteractionRequest,
InteractionResponse,
InteractionType,
} from '@roleypoly/types';
const commands: Record<InteractionData['name'], InteractionHandler> = {
'hello-world': helloWorld,
roleypoly: roleypoly,
'pickable-roles': pickableRoles,
'pick-role': pickRole,
'remove-role': removeRole,
};
export const handleInteraction: RoleypolyHandler = async (
request: Request,
context: Context
) => {
const interaction: InteractionRequest = await request.json();
if (!interaction) {
return invalid();
}
if (!(await verifyRequest(context.config, request, interaction))) {
console.warn('interactions: invalid signature');
return new Response('invalid request signature', { status: 401 });
}
if (interaction.type !== InteractionType.APPLICATION_COMMAND) {
if (interaction.type === InteractionType.PING) {
console.info('interactions: ping');
return json({ type: InteractionCallbackType.PONG });
}
console.warn('interactions: not application command');
return json({ err: 'not implemented' }, { status: 400 });
}
if (!interaction.data) {
return json({ err: 'data missing' }, { status: 400 });
}
const handler = commands[interaction.data.name] || notImplemented;
try {
if (handler.deferred) {
context.fetchContext.waitUntil(runAsync(handler, interaction, context));
return json({
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
},
} as InteractionResponse);
}
const response = await handler(interaction, context);
return json({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
...response.data,
},
});
} catch (e) {
console.error('/interactions error:', {
interaction: {
data: interaction.data,
user: interaction.user,
guild: interaction.guild_id,
},
e,
});
return invalid();
}
};

View file

@ -0,0 +1,61 @@
import { InteractionHandler } from '@roleypoly/api/src/routes/interactions/helpers';
import {
InteractionCallbackType,
InteractionFlags,
InteractionResponse,
} from '@roleypoly/types';
export const mustBeInGuild: InteractionHandler = (): InteractionResponse => ({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: ':x: This command has to be used in a server.',
flags: InteractionFlags.EPHEMERAL,
},
});
export const invalid: InteractionHandler = (): InteractionResponse => ({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: ':x: You filled that command out wrong...',
flags: InteractionFlags.EPHEMERAL,
},
});
export const somethingWentWrong: InteractionHandler = (): InteractionResponse => ({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: '<a:promareFlame:624850108667789333> Something went terribly wrong.',
flags: InteractionFlags.EPHEMERAL,
},
});
export const notImplemented: InteractionHandler = (): InteractionResponse => ({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: ':x: This command is not implemented yet.',
flags: InteractionFlags.EPHEMERAL,
},
});
export const embedResponse = (
title: string,
description: string,
color?: number
): InteractionResponse => ({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: color || 0x00ff00,
title,
description,
},
],
},
});
export const embedPalette = {
success: 0x1d8227,
error: 0xf14343,
neutral: 0x453e3d,
};

View file

@ -0,0 +1,155 @@
import { getGuild, getGuildData } from '@roleypoly/api/src/guilds/getters';
import { calculateNewRoles } from '@roleypoly/api/src/routes/guilds/guild-roles-put';
import { getName } from '@roleypoly/api/src/routes/interactions/helpers';
import {
embedPalette,
embedResponse,
} from '@roleypoly/api/src/routes/interactions/responses';
import { Context } from '@roleypoly/api/src/utils/context';
import { APIMember, AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { isIdenticalArray } from '@roleypoly/misc-utils/collection-tools';
import {
CategoryType,
InteractionRequest,
InteractionResponse,
RoleTransaction,
TransactionType,
} from '@roleypoly/types';
export const rolePickerCommon = async (
interaction: InteractionRequest,
context: Context,
action: TransactionType
): Promise<InteractionResponse> => {
if (!interaction.guild_id) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`
);
}
const currentRoles = interaction.member?.roles || [];
const [guildData, guild] = await Promise.all([
getGuildData(context.config, interaction.guild_id),
getGuild(context.config, interaction.guild_id),
]);
if (!guildData || !guild) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. Something's wrong with the server you're in. Try picking your roles at ${
context.config.uiPublicURI
}/s/${interaction.guild_id} instead.`,
embedPalette.error
);
}
const roleToPick = interaction.data?.options?.[0]?.value;
if (!roleToPick) {
return embedResponse(
':fire: Discord sent me the wrong data',
`Hey ${getName(interaction)}. Please try again later.`,
embedPalette.error
);
}
const hasRole = interaction.member?.roles.includes(roleToPick);
if (action === TransactionType.Add && hasRole) {
return embedResponse(
`:white_check_mark: You already have that role.`,
`Hey ${getName(interaction)}. You already have <@&${roleToPick}>!`,
embedPalette.neutral
);
}
if (action === TransactionType.Remove && !hasRole) {
return embedResponse(
`:white_check_mark: You don't have that role.`,
`Hey ${getName(interaction)}. You already don't have <@&${roleToPick}>!`,
embedPalette.neutral
);
}
const extraTransactions: RoleTransaction[] = [];
let isSingle = false;
if (action === TransactionType.Add) {
// For single-type categories, let's also generate the remove rules for the other roles in the category
const category = guildData.categories.find((category) =>
category.roles.includes(roleToPick)
);
if (category?.type === CategoryType.Single) {
const otherRoles = category.roles.filter((role) => role !== roleToPick);
extraTransactions.push(
...otherRoles.map((role) => ({ action: TransactionType.Remove, id: role }))
);
isSingle = true;
}
}
const newRoles = calculateNewRoles({
currentRoles,
guildRoles: guild.roles,
guildData,
updateRequest: {
knownState: currentRoles,
transactions: [{ action, id: roleToPick }, ...extraTransactions],
},
});
if (isIdenticalArray(currentRoles, newRoles)) {
return embedResponse(
':x: You cannot pick this role.',
`Hey ${getName(
interaction
)}. <@&${roleToPick}> isn't pickable. Check /pickable-roles to see which roles you can use.`,
embedPalette.error
);
}
const patchMemberRoles = await discordFetch<APIMember>(
`/guilds/${interaction.guild_id}/members/${interaction.member?.user?.id}`,
context.config.botToken,
AuthType.Bot,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via /${
action === TransactionType.Add ? 'pick' : 'remove'
}-role`,
},
body: JSON.stringify({
roles: newRoles,
}),
}
);
if (!patchMemberRoles) {
return embedResponse(
':x: Discord stopped me from updating your roles.',
`Hey ${getName(
interaction
)}. Discord didn't let me give you <@&${roleToPick}>. Could you try again later?`,
embedPalette.error
);
}
return action === TransactionType.Add
? embedResponse(
':white_check_mark: You got it!',
`Hey ${getName(interaction)}, I gave you <@&${roleToPick}>!${
isSingle ? `\nThe other roles in this category have been removed.` : ''
}`,
embedPalette.success
)
: embedResponse(
":white_check_mark: You (don't) got it!",
`Hey ${getName(interaction)}, I took away <@&${roleToPick}>!`,
embedPalette.success
);
};

View file

@ -0,0 +1,130 @@
import { handleInteraction } from '@roleypoly/api/src/routes/interactions/interactions';
import { Context } from '@roleypoly/api/src/utils/context';
import { AuthType } from '@roleypoly/api/src/utils/discord';
import { getID } from '@roleypoly/api/src/utils/id';
import {
InteractionCallbackData,
InteractionCallbackType,
InteractionData,
InteractionFlags,
InteractionRequest,
InteractionResponse,
InteractionType,
} from '@roleypoly/types';
import nacl from 'tweetnacl';
const { publicKey, secretKey } = nacl.sign.keyPair();
const hexPublicKey = Buffer.from(publicKey).toString('hex');
export const getSignatureHeaders = (
context: Context,
interaction: InteractionRequest
): {
'x-signature-ed25519': string;
'x-signature-timestamp': string;
} => {
const timestamp = Date.now().toString();
const body = JSON.stringify(interaction);
const signature = nacl.sign.detached(Buffer.from(timestamp + body), secretKey);
return {
'x-signature-ed25519': Buffer.from(signature).toString('hex'),
'x-signature-timestamp': timestamp,
};
};
export const makeInteractionsRequest = async (
context: Context,
interactionData: Partial<InteractionData>,
forceInvalid?: boolean,
topLevelMixin?: Partial<InteractionRequest>
): Promise<Response> => {
context.config.publicKey = hexPublicKey;
const interaction: InteractionRequest = {
data: {
id: getID(),
name: 'hello-world',
...interactionData,
} as InteractionData,
id: '123',
type: InteractionType.APPLICATION_COMMAND,
application_id: context.config.botClientID,
token: getID(),
version: 1,
user: {
id: '123',
username: 'test-user',
discriminator: '1234',
bot: false,
avatar: '',
},
member: {
nick: 'test-user-nick',
roles: [],
},
...topLevelMixin,
};
const request = new Request('http://localhost:3000/interactions', {
method: 'POST',
headers: {
'content-type': 'application/json',
...getSignatureHeaders(context, {
...interaction,
...(forceInvalid ? { id: 'invalid-id' } : {}),
}),
},
body: JSON.stringify(interaction),
});
return handleInteraction(request, context);
};
export const extractInteractionResponse = async (
response: Response
): Promise<InteractionResponse> => {
const body = await response.json();
return body as InteractionResponse;
};
export const isDeferred = (response: InteractionResponse): boolean => {
return response.type === InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE;
};
export const isEphemeral = (response: InteractionResponse): boolean => {
return (
(response.data?.flags || 0 & InteractionFlags.EPHEMERAL) ===
InteractionFlags.EPHEMERAL
);
};
export const interactionData = (
response: InteractionResponse
): Omit<InteractionCallbackData, 'flags'> | undefined => {
const { data } = response;
if (!data) return undefined;
delete data.flags;
return response.data;
};
export const mockUpdateCall = (
expect: any,
data: Omit<InteractionCallbackData, 'flags'>
) => {
return [
expect.any(String),
'',
AuthType.None,
{
body: JSON.stringify({
...data,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'PATCH',
},
];
};

View file

@ -0,0 +1,53 @@
jest.mock('../utils/discord');
import { AuthTokenResponse } from '@roleypoly/types';
import { parseEnvironment } from '../utils/config';
import { getTokenGuilds, getTokenUser } from '../utils/discord';
import { getBindings } from '../utils/testHelpers';
import { createSession } from './create';
const mockGetTokenGuilds = getTokenGuilds as jest.Mock;
const mockGetTokenUser = getTokenUser as jest.Mock;
it('creates a session from tokens', async () => {
const config = parseEnvironment(getBindings());
const tokens: AuthTokenResponse = {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_in: 3600,
scope: 'identify guilds',
token_type: 'Bearer',
};
mockGetTokenUser.mockReturnValueOnce({
id: 'test-user-id',
username: 'test-username',
discriminator: 'test-discriminator',
avatar: 'test-avatar',
bot: false,
});
mockGetTokenGuilds.mockReturnValueOnce([]);
const session = await createSession(config, tokens);
expect(session).toEqual({
sessionID: expect.any(String),
user: {
id: 'test-user-id',
discriminator: 'test-discriminator',
avatar: 'test-avatar',
bot: false,
username: 'test-username',
},
guilds: [],
tokens,
});
expect(mockGetTokenUser).toBeCalledWith(tokens.access_token);
expect(mockGetTokenGuilds).toBeCalledWith(tokens.access_token);
const savedSession = await config.kv.sessions.get(session?.sessionID || '');
expect(savedSession).toEqual(session);
});

View file

@ -0,0 +1,31 @@
import { Config } from '@roleypoly/api/src/utils/config';
import { getTokenGuilds, getTokenUser } from '@roleypoly/api/src/utils/discord';
import { getID } from '@roleypoly/api/src/utils/id';
import { AuthTokenResponse, SessionData } from '@roleypoly/types';
export const createSession = async (
config: Config,
tokens: AuthTokenResponse
): Promise<SessionData | null> => {
const [user, guilds] = await Promise.all([
getTokenUser(tokens.access_token),
getTokenGuilds(tokens.access_token),
]);
if (!user) {
return null;
}
const sessionID = getID();
const session: SessionData = {
sessionID,
user,
guilds,
tokens,
};
await config.kv.sessions.put(sessionID, session, config.retention.session);
return session;
};

View file

@ -0,0 +1,172 @@
import { Router } from 'itty-router';
import { Context } from '../utils/context';
import { json } from '../utils/response';
import { configContext, makeSession } from '../utils/testHelpers';
import { requireSession, withAuthMode, withSession } from './middleware';
it('detects anonymous auth mode via middleware', async () => {
const [, context] = configContext();
const router = Router();
const testFn = jest.fn();
router.all('*', withAuthMode).get('/', (request, context) => {
expect(context.authMode.type).toBe('anonymous');
testFn();
return json({});
});
await router.handle(new Request('http://test.local/'), context);
expect(testFn).toHaveBeenCalled();
});
it('detects bearer auth mode via middleware', async () => {
const [, context] = configContext();
const testFn = jest.fn();
const token = 'abc123';
const router = Router();
router.all('*', withAuthMode).get('/', (request, context) => {
expect(context.authMode.type).toBe('bearer');
expect(context.authMode.sessionId).toBe(token);
testFn();
return json({});
});
await router.handle(
new Request('http://test.local/', {
headers: {
authorization: `Bearer ${token}`,
},
}),
context
);
expect(testFn).toHaveBeenCalled();
});
it('detects bot auth mode via middleware', async () => {
const testFn = jest.fn();
const [, context] = configContext();
const token = 'abc123';
const router = Router();
router.all('*', withAuthMode).get('/', (request, context) => {
expect(context.authMode.type).toBe('bot');
expect(context.authMode.identity).toBe(token);
testFn();
return json({});
});
await router.handle(
new Request('http://test.local/', {
headers: {
authorization: `Bot ${token}`,
},
}),
context
);
expect(testFn).toHaveBeenCalled();
});
it('sets Context.session via withSession middleware', async () => {
const testFn = jest.fn();
const [config, context] = configContext();
const session = await makeSession(config);
const router = Router();
router.all('*', withAuthMode, withSession).get('/', (request, context: Context) => {
expect(context.session).toBeDefined();
expect(context.session!.sessionID).toBe(session.sessionID);
testFn();
return json({});
});
await router.handle(
new Request('http://test.local/', {
headers: {
authorization: `Bearer ${session.sessionID}`,
},
}),
context
);
expect(testFn).toHaveBeenCalledTimes(1);
});
it('does not set Context.session when session is invalid', async () => {
const testFn = jest.fn();
const [, context] = configContext();
const router = Router();
router.all('*', withAuthMode, withSession).get('/', (request, context: Context) => {
expect(context.session).not.toBeDefined();
testFn();
return json({});
});
await router.handle(
new Request('http://test.local/', {
headers: {
authorization: `Bearer abc123`,
},
}),
context
);
expect(testFn).toHaveBeenCalledTimes(1);
});
it('errors with 401 when requireSession is coupled with invalid session', async () => {
const [, context] = configContext();
const router = Router();
const testFn = jest.fn();
router
.all('*', withAuthMode, withSession, requireSession)
.get('/', (request, context: Context) => {
testFn();
return json({});
});
const response = await router.handle(
new Request('http://test.local/', {
headers: {
authorization: `Bearer abc123`,
},
}),
context
);
expect(testFn).not.toHaveBeenCalled();
expect(response.status).toBe(401);
});
it('passes through when requireSession is coupled with a valid session', async () => {
const [config, context] = configContext();
const session = await makeSession(config);
const router = Router();
const testFn = jest.fn();
router
.all('*', withAuthMode, withSession, requireSession)
.get('/', (request, context: Context) => {
expect(context.session).toBeDefined();
testFn();
return json({});
});
const response = await router.handle(
new Request('http://test.local/', {
headers: {
authorization: `Bearer ${session.sessionID}`,
},
}),
context
);
expect(response.status).toBe(200);
expect(testFn).toHaveBeenCalled();
});

View file

@ -0,0 +1,67 @@
import { Context, RoleypolyMiddleware } from '@roleypoly/api/src/utils/context';
import { unauthorized } from '@roleypoly/api/src/utils/response';
import { SessionData } from '@roleypoly/types';
export const withSession: RoleypolyMiddleware = async (
request: Request,
context: Context
) => {
if (context.authMode.type !== 'bearer') {
return;
}
const session = await context.config.kv.sessions.get<SessionData>(
context.authMode.sessionId
);
if (!session) {
return;
}
context.session = session;
};
export const requireSession: RoleypolyMiddleware = (
request: Request,
context: Context
) => {
if (context.authMode.type !== 'bearer' || !context.session) {
return unauthorized();
}
};
export const withAuthMode: RoleypolyMiddleware = (request: Request, context: Context) => {
const auth = extractAuthentication(request);
if (auth.authType === 'Bearer') {
context.authMode = {
type: 'bearer',
sessionId: auth.token,
};
return;
}
if (auth.authType === 'Bot') {
context.authMode = {
type: 'bot',
identity: auth.token,
};
return;
}
context.authMode = {
type: 'anonymous',
};
};
export const extractAuthentication = (
request: Request
): { authType: string; token: string } => {
const authHeader = request.headers.get('authorization');
if (!authHeader) {
return { authType: 'None', token: '' };
}
const [authType, token] = authHeader.split(' ');
return { authType, token };
};

View file

@ -0,0 +1,25 @@
import { parseEnvironment } from '../utils/config';
import { getBindings } from '../utils/testHelpers';
import { getStateSession, setupStateSession } from './state';
it('creates and fetches a state session', async () => {
const config = parseEnvironment(getBindings());
const stateID = await setupStateSession(config, {
test: 'test-data',
});
const stateSession = await getStateSession(config, stateID);
expect(stateSession).toEqual({
test: 'test-data',
});
});
it('returns undefined when state is invalid', async () => {
const config = parseEnvironment(getBindings());
const stateSession = await getStateSession(config, 'invalid-state-id');
expect(stateSession).toBeUndefined();
});

View file

@ -0,0 +1,19 @@
import { Config } from '@roleypoly/api/src/utils/config';
import { getID } from '@roleypoly/api/src/utils/id';
export const setupStateSession = async <T>(config: Config, data: T): Promise<string> => {
const stateID = getID();
await config.kv.sessions.put(`state_${stateID}`, { data }, config.retention.session);
return stateID;
};
export const getStateSession = async <T>(
config: Config,
stateID: string
): Promise<T | undefined> => {
const stateSession = await config.kv.sessions.get<{ data: T }>(`state_${stateID}`);
return stateSession?.data;
};

View file

@ -0,0 +1,78 @@
import { WrappedKVNamespace } from './kv';
export type Environment = {
BOT_CLIENT_ID: string;
BOT_CLIENT_SECRET: string;
BOT_TOKEN: string;
UI_PUBLIC_URI: string;
API_PUBLIC_URI: string;
ROOT_USERS: string;
ALLOWED_CALLBACK_HOSTS: string;
BOT_IMPORT_TOKEN: string;
INTERACTIONS_SHARED_KEY: string;
RP_SERVER_ID: string;
RP_HELPER_ROLE_IDS: string;
DISCORD_PUBLIC_KEY: string;
KV_SESSIONS: KVNamespace;
KV_GUILDS: KVNamespace;
KV_GUILD_DATA: KVNamespace;
};
export type Config = {
botClientID: string;
botClientSecret: string;
botToken: string;
publicKey: string;
uiPublicURI: string;
apiPublicURI: string;
rootUsers: string[];
allowedCallbackHosts: string[];
importSharedKey: string;
interactionsSharedKey: string;
roleypolyServerID: string;
helperRoleIDs: string[];
kv: {
sessions: WrappedKVNamespace;
guilds: WrappedKVNamespace;
guildData: WrappedKVNamespace;
};
retention: {
session: number;
sessionState: number;
guild: number;
member: number;
};
_raw: Environment;
};
const toList = (x: string): string[] => String(x).split(',');
const safeURI = (x: string) => String(x).replace(/\/$/, '');
export const parseEnvironment = (env: Environment): Config => {
return {
_raw: env,
botClientID: env.BOT_CLIENT_ID,
botClientSecret: env.BOT_CLIENT_SECRET,
botToken: env.BOT_TOKEN,
publicKey: env.DISCORD_PUBLIC_KEY,
uiPublicURI: safeURI(env.UI_PUBLIC_URI),
apiPublicURI: safeURI(env.API_PUBLIC_URI),
rootUsers: toList(env.ROOT_USERS),
allowedCallbackHosts: toList(env.ALLOWED_CALLBACK_HOSTS),
importSharedKey: env.BOT_IMPORT_TOKEN,
interactionsSharedKey: env.INTERACTIONS_SHARED_KEY,
roleypolyServerID: env.RP_SERVER_ID,
helperRoleIDs: toList(env.RP_HELPER_ROLE_IDS),
kv: {
sessions: new WrappedKVNamespace(env.KV_SESSIONS),
guilds: new WrappedKVNamespace(env.KV_GUILDS),
guildData: new WrappedKVNamespace(env.KV_GUILD_DATA),
},
retention: {
session: 60 * 60 * 6, // 6 hours
sessionState: 60 * 5, // 5 minutes
guild: 60 * 15, // 15 minutes
member: 60 * 5, // 5 minutes
},
};
};

View file

@ -0,0 +1,40 @@
import { Config } from '@roleypoly/api/src/utils/config';
import { SessionData } from '@roleypoly/types';
export type AuthMode =
| {
type: 'anonymous';
}
| {
type: 'bearer';
sessionId: string;
}
| {
type: 'bot';
identity: string;
};
export type Context = {
config: Config;
fetchContext: {
waitUntil: FetchEvent['waitUntil'];
};
authMode: AuthMode;
params: {
guildId?: string;
memberId?: string;
};
// Must include withSession middleware for population
session?: SessionData;
};
export type RoleypolyHandler = (
request: Request,
context: Context
) => Promise<Response> | Response;
export type RoleypolyMiddleware = (
request: Request,
context: Context
) => Promise<Response | void> | Response | void;

View file

@ -0,0 +1,34 @@
import { getHighestRole } from './discord';
describe('getHighestRole', () => {
it('returns the highest role', () => {
const roles = [
{
id: 'role-1',
name: 'Role 1',
color: 0,
position: 17,
permissions: '',
managed: false,
},
{
id: 'role-2',
name: 'Role 2',
color: 0,
position: 2,
permissions: '',
managed: false,
},
{
id: 'role-3',
name: 'Role 3',
color: 0,
position: 19,
permissions: '',
managed: false,
},
];
expect(getHighestRole(roles)).toEqual(roles[2]);
});
});

Some files were not shown because too many files have changed in this diff Show more