summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--cabal.project3
-rw-r--r--fig-frontend/client/package-lock.json578
-rw-r--r--fig-frontend/client/package.json17
-rw-r--r--fig-frontend/client/src/blueprint.jpgbin0 -> 1308988 bytes
-rw-r--r--fig-frontend/client/src/config.ts3
-rw-r--r--fig-frontend/client/src/index.css30
-rw-r--r--fig-frontend/client/src/index.ts119
-rw-r--r--fig-frontend/client/src/twitch.ts43
-rw-r--r--fig-frontend/client/tsconfig.json18
-rw-r--r--fig-frontend/fig-frontend.cabal62
-rw-r--r--fig-frontend/main/Main.hs25
-rw-r--r--fig-frontend/src/Fig/Frontend.hs42
-rw-r--r--fig-frontend/src/Fig/Frontend/Auth.hs78
-rw-r--r--fig-frontend/src/Fig/Frontend/Utils.hs37
-rw-r--r--flake.nix4
-rw-r--r--hie.yaml4
17 files changed, 1066 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
index de8e882..93328ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
.direnv
dist-newstyle
-fig-monitor-*.toml \ No newline at end of file
+node_modules
+fig-monitor-*.toml
+fig-frontend.toml
+fig-frontend-assets \ No newline at end of file
diff --git a/cabal.project b/cabal.project
index 03dbf5a..45e3777 100644
--- a/cabal.project
+++ b/cabal.project
@@ -6,5 +6,6 @@ packages:
fig-monitor-irc/
fig-monitor-bullfrog/
fig-bridge-irc-discord/
+ fig-frontend/
deps/irc-client/
- deps/irc-conduit/ \ No newline at end of file
+ deps/irc-conduit/
diff --git a/fig-frontend/client/package-lock.json b/fig-frontend/client/package-lock.json
new file mode 100644
index 0000000..01fb3ce
--- /dev/null
+++ b/fig-frontend/client/package-lock.json
@@ -0,0 +1,578 @@
+{
+ "name": "fig-frontend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fig-frontend",
+ "version": "1.0.0",
+ "dependencies": {
+ "interactjs": "^1.10.26",
+ "lit": "^3.0.0"
+ },
+ "devDependencies": {
+ "esbuild": "^0.17.18",
+ "typescript": "^5.0.4",
+ "typescript-formatter": "^7.2.2",
+ "typescript-language-server": "^4.0.0"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@interactjs/types": {
+ "version": "1.10.26",
+ "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.26.tgz",
+ "integrity": "sha512-DekYpdkMV3XJVd/0k3f4pJluZAsCiG86yEtVXvGLK0lS/Fj0+OzYEv7HoMpcBZSkQ8s7//yaeEBgnxy2tV81lA=="
+ },
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz",
+ "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g=="
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz",
+ "integrity": "sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.1.2"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "node_modules/commandpost": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz",
+ "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==",
+ "dev": true
+ },
+ "node_modules/editorconfig": {
+ "version": "0.15.3",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
+ "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^2.19.0",
+ "lru-cache": "^4.1.5",
+ "semver": "^5.6.0",
+ "sigmund": "^1.0.1"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "node_modules/interactjs": {
+ "version": "1.10.26",
+ "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.26.tgz",
+ "integrity": "sha512-5gNTNDTfEHp2EifqtWGi5VkD3CMZVJSTGmtK/IsVRd+rkOk3E63iVs5Z+IeD5K1Lr0qZpU2754VHAwf5i+Z9xg==",
+ "dependencies": {
+ "@interactjs/types": "1.10.26"
+ }
+ },
+ "node_modules/lit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.0.tgz",
+ "integrity": "sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==",
+ "dependencies": {
+ "@lit/reactive-element": "^2.0.0",
+ "lit-element": "^4.0.0",
+ "lit-html": "^3.1.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.2.tgz",
+ "integrity": "sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.1.2",
+ "@lit/reactive-element": "^2.0.0",
+ "lit-html": "^3.1.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.0.tgz",
+ "integrity": "sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "dev": true,
+ "dependencies": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "node_modules/pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
+ "dev": true
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/sigmund": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
+ "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
+ "dev": true
+ },
+ "node_modules/typescript": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-formatter": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz",
+ "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==",
+ "dev": true,
+ "dependencies": {
+ "commandpost": "^1.0.0",
+ "editorconfig": "^0.15.0"
+ },
+ "bin": {
+ "tsfmt": "bin/tsfmt"
+ },
+ "engines": {
+ "node": ">= 4.2.0"
+ },
+ "peerDependencies": {
+ "typescript": "^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev"
+ }
+ },
+ "node_modules/typescript-language-server": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-4.2.0.tgz",
+ "integrity": "sha512-1yKDqKeWLTQkN4mN+CT84aBr7ckp6sNVb8DZg+eXl0TDl14edn6Yh1wPqPA1rQ4AGVJc02fYbXTFsklaVYy4Uw==",
+ "dev": true,
+ "bin": {
+ "typescript-language-server": "lib/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
+ "dev": true
+ }
+ }
+}
diff --git a/fig-frontend/client/package.json b/fig-frontend/client/package.json
new file mode 100644
index 0000000..52b6451
--- /dev/null
+++ b/fig-frontend/client/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "fig-frontend",
+ "version": "1.0.0",
+ "scripts": {
+ "build": "esbuild src/index.ts --loader:.jpg=file --outdir=../../fig-frontend-assets/js --bundle --minify"
+ },
+ "dependencies": {
+ "interactjs": "^1.10.26",
+ "lit": "^3.0.0"
+ },
+ "devDependencies": {
+ "esbuild": "^0.17.18",
+ "typescript": "^5.0.4",
+ "typescript-formatter": "^7.2.2",
+ "typescript-language-server": "^4.0.0"
+ }
+}
diff --git a/fig-frontend/client/src/blueprint.jpg b/fig-frontend/client/src/blueprint.jpg
new file mode 100644
index 0000000..6efe89d
--- /dev/null
+++ b/fig-frontend/client/src/blueprint.jpg
Binary files differ
diff --git a/fig-frontend/client/src/config.ts b/fig-frontend/client/src/config.ts
new file mode 100644
index 0000000..5498e16
--- /dev/null
+++ b/fig-frontend/client/src/config.ts
@@ -0,0 +1,3 @@
+export const URL = "http://localhost:8000";
+export const API_URL = "http://localhost:8000/api";
+export const CLIENT_ID = "q486jugzn2my4iw6l181o006ugye4j";
diff --git a/fig-frontend/client/src/index.css b/fig-frontend/client/src/index.css
new file mode 100644
index 0000000..891c5fd
--- /dev/null
+++ b/fig-frontend/client/src/index.css
@@ -0,0 +1,30 @@
+body {
+ margin: 0px;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100vw;
+ height: 100vh;
+}
+
+fig-window {
+ display: block;
+ position: absolute;
+}
+
+fig-header {
+ position: absolute;
+ text-align: center;
+ border-radius: 100px;
+ border-style: solid;
+ border-color: silver;
+ border-width: 5px;
+ background: linear-gradient(0deg, rgba(89,89,89,1) 35%, rgba(158,158,158,1) 100%);
+ top: 1em;
+ left: 33vw;
+ width: 33vw;
+}
+
+fig-header:hover {
+ filter: brightness(150%);
+}
diff --git a/fig-frontend/client/src/index.ts b/fig-frontend/client/src/index.ts
new file mode 100644
index 0000000..899c7fd
--- /dev/null
+++ b/fig-frontend/client/src/index.ts
@@ -0,0 +1,119 @@
+import { html, css, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+
+import interact from "interactjs";
+
+import * as Config from "./config";
+import * as Twitch from "./twitch";
+
+import "./index.css";
+
+console.log("welcome to \"the junkyard\"");
+
+@customElement("fig-window")
+export class Window extends LitElement {
+ x: number;
+ y: number;
+
+ constructor() {
+ super();
+ this.x = 0;
+ this.y = 0;
+ interact(this).draggable({
+ listeners: {
+ move(event) {
+ event.target.x += event.dx
+ event.target.y += event.dy
+ event.target.style.left = `${event.target.x}px`
+ event.target.style.top = `${event.target.y}px`
+ },
+ }
+ });
+ }
+
+ render() {
+ console.log("render");
+ return html`
+<slot></slot>
+`;
+ }
+}
+
+@customElement("fig-backdrop")
+export class Backdrop extends LitElement {
+ static styles = css`
+#backdrop {
+ z-index: -2;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100vw;
+ height: 100vh;
+ background-image: url("blueprint.jpg");
+}
+#blur {
+ z-index: -1;
+ backdrop-filter: blur(3px);
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100vw;
+ height: 100vh;
+ background-size: 100px 100px;
+ background-position: -20px -20px;
+ background-image:
+ linear-gradient(to right, lightgrey 1px, transparent 1px),
+ linear-gradient(to bottom, lightgrey 1px, transparent 1px);
+}
+`;
+ render() {
+ return html`
+<div id="backdrop"></div>
+<div id="blur"></div>
+`;
+ }
+}
+
+@customElement("fig-login")
+export class Login extends LitElement {
+ static styles = css`
+ `;
+
+ login() {
+ Twitch.startTwitchAuth();
+ }
+
+ async check() {
+ const resp = await fetch(`${Config.API_URL}/check`);
+ console.log(await resp.text());
+ }
+
+ render() {
+ const token = Twitch.getAuthToken();
+ console.log(token);
+ if (token) {
+ return html`
+<button @click=${this.check}>check token</button>
+`;
+ } else {
+ return html`
+<button @click=${this.login}>login</button>
+`;
+ }
+ }
+}
+
+@customElement("fig-header")
+export class Header extends LitElement {
+ static styles = css`
+h1 {
+ color: #b5a642;
+ font-family: "Rubik Maps";
+}
+`;
+ render() {
+ return html`
+<h1>Welcome To "The JunkYard"</h1>
+`;
+ }
+}
diff --git a/fig-frontend/client/src/twitch.ts b/fig-frontend/client/src/twitch.ts
new file mode 100644
index 0000000..4c264be
--- /dev/null
+++ b/fig-frontend/client/src/twitch.ts
@@ -0,0 +1,43 @@
+import * as Config from "./config";
+
+function generateNonce(): string {
+ var arr = new Uint8Array(20);
+ window.crypto.getRandomValues(arr);
+ return Array.from(arr, b => b.toString(16).padStart(2, "0")).join("");
+}
+
+export function startTwitchAuth() {
+ const nonce = generateNonce();
+ document.cookie = `authnonce=${nonce}; path=/;`;
+ window.location.href =
+ `https://id.twitch.tv/oauth2/authorize?response_type=id_token`
+ + `&client_id=${Config.CLIENT_ID}`
+ + `&redirect_uri=${Config.URL}`
+ + `&scope=openid`
+ + `&nonce=${nonce}`
+ + `&claims=${{id_token: {preferred_username: null}}}`
+ ;
+}
+
+export function getFragmentQuery(): Map<string, string> {
+ let query = new Map();
+ const hashQuery = document.location.hash.slice(1).split("&");
+ for (let equals of hashQuery) {
+ const pair = equals.split("=");
+ query.set(decodeURIComponent(pair[0]), decodeURIComponent(pair[1]));
+ }
+ return query;
+}
+
+export function getAuthToken(): String | null {
+ const frag = getFragmentQuery();
+ const token = frag.get("id_token");
+ if (token) {
+ document.cookie = `id_token=${token}; path=/; SameSite=Strict`;
+ }
+ for (let c of document.cookie.split("; ")) {
+ const [k, v] = c.split("=");
+ if (k === "id_token") return v;
+ }
+ return null;
+}
diff --git a/fig-frontend/client/tsconfig.json b/fig-frontend/client/tsconfig.json
new file mode 100644
index 0000000..d529f64
--- /dev/null
+++ b/fig-frontend/client/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "target": "es6",
+ "lib": ["ESNext", "dom"],
+ "skipLibCheck": true,
+ "useDefineForClassFields": false,
+ "experimentalDecorators": true,
+ "strictPropertyInitialization": false,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "removeComments": true,
+ "preserveConstEnums": true,
+ "sourceMap": true
+ }
+}
diff --git a/fig-frontend/fig-frontend.cabal b/fig-frontend/fig-frontend.cabal
new file mode 100644
index 0000000..b71911e
--- /dev/null
+++ b/fig-frontend/fig-frontend.cabal
@@ -0,0 +1,62 @@
+cabal-version: 3.4
+name: fig-frontend
+version: 0.1.0.0
+
+common defaults
+ ghc-options: -Wall
+ default-language: GHC2021
+ default-extensions: NoImplicitPrelude PackageImports LambdaCase MultiWayIf OverloadedStrings OverloadedLists OverloadedRecordDot DuplicateRecordFields RecordWildCards NoFieldSelectors BlockArguments ViewPatterns TypeFamilies DataKinds GADTs
+
+common deps
+ build-depends:
+ base
+ , aeson
+ , base64
+ , binary
+ , bytestring
+ , containers
+ , data-default-class
+ , directory
+ , filepath
+ , http-types
+ , http-client
+ , http-client-tls
+ , jose-jwt
+ , lucid2
+ , megaparsec
+ , mtl
+ , network
+ , req
+ , safe-exceptions
+ , text
+ , time
+ , tomland
+ , transformers
+ , twain
+ , unordered-containers
+ , vector
+ , wai
+ , wai-extra
+ , wai-middleware-static
+ , warp
+ , websockets
+ , wuss
+ , fig-utils
+ , fig-bus
+
+library
+ import: defaults
+ import: deps
+ hs-source-dirs: src
+ exposed-modules:
+ Fig.Frontend
+ Fig.Frontend.Utils
+ Fig.Frontend.Auth
+
+executable fig-frontend
+ import: defaults
+ import: deps
+ build-depends: fig-frontend, optparse-applicative
+ hs-source-dirs:
+ main
+ main-is: Main.hs
diff --git a/fig-frontend/main/Main.hs b/fig-frontend/main/Main.hs
new file mode 100644
index 0000000..47d10ab
--- /dev/null
+++ b/fig-frontend/main/Main.hs
@@ -0,0 +1,25 @@
+module Main where
+
+import Fig.Prelude
+
+import Options.Applicative
+
+import Fig.Frontend
+import Fig.Frontend.Utils
+
+newtype Opts = Opts
+ { config :: FilePath
+ }
+
+parseOpts :: Parser Opts
+parseOpts = Opts
+ <$> strOption (long "config" <> metavar "PATH" <> help "Path to config file" <> showDefault <> value "fig-frontend.toml")
+
+main :: IO ()
+main = do
+ opts <- execParser $ info (parseOpts <**> helper)
+ ( fullDesc
+ <> header "fig-frontend - public-facing web applications"
+ )
+ cfg <- loadConfig opts.config
+ server cfg
diff --git a/fig-frontend/src/Fig/Frontend.hs b/fig-frontend/src/Fig/Frontend.hs
new file mode 100644
index 0000000..fa1965a
--- /dev/null
+++ b/fig-frontend/src/Fig/Frontend.hs
@@ -0,0 +1,42 @@
+module Fig.Frontend where
+
+import Fig.Prelude
+
+import qualified Network.Wai.Middleware.Static as Wai.Static
+import qualified Network.Wai.Handler.Warp as Warp
+
+import qualified Web.Twain as Tw
+
+import qualified Lucid as L
+
+import Fig.Frontend.Utils
+import Fig.Frontend.Auth
+
+server :: Config -> IO ()
+server cfg = do
+ log $ "Frontend server running on port " <> tshow cfg.port
+ Warp.run cfg.port $ app cfg
+
+app :: Config -> Tw.Application
+app cfg = foldr' @[] ($)
+ (Tw.notFound . Tw.send $ Tw.text "not found")
+ [ Wai.Static.staticPolicy $ Wai.Static.addBase cfg.assetPath
+ , Tw.get "/"
+ . Tw.send . Tw.html
+ . L.renderBS
+ $ L.doctypehtml_ do
+ L.head_ do
+ L.title_ "The Junkyard"
+ L.link_ [L.rel_ "stylesheet", L.href_ "js/index.css"]
+ L.link_ [L.rel_ "stylesheet", L.href_ "https://fonts.googleapis.com/css?family=Rubik+Maps"]
+ L.link_ [L.rel_ "icon", L.href_ "data:;base64,iVBORw0KGgo="]
+ L.script_ [L.type_ "module", L.src_ "js/index.js"] ("" :: L.Html ())
+ L.body_ do
+ L.term "fig-backdrop" ""
+ L.term "fig-header" ""
+ L.term "fig-login" ""
+ L.term "fig-window" do
+ L.h1_ "hello"
+ , Tw.get "/api/check" $ authed cfg \auth -> do
+ Tw.send $ Tw.json @[Text] [auth.id, auth.name]
+ ]
diff --git a/fig-frontend/src/Fig/Frontend/Auth.hs b/fig-frontend/src/Fig/Frontend/Auth.hs
new file mode 100644
index 0000000..72eac0d
--- /dev/null
+++ b/fig-frontend/src/Fig/Frontend/Auth.hs
@@ -0,0 +1,78 @@
+module Fig.Frontend.Auth where
+
+import Fig.Prelude
+
+import GHC.Generics (Generic)
+
+import qualified Network.HTTP.Req as R
+
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Types as Aeson
+
+import qualified Web.Twain as Tw
+
+import qualified Jose.Jwk as Jwk
+import qualified Jose.Jwt as Jwt
+
+import Fig.Frontend.Utils
+
+data TokenContents = TokenContents
+ { aud :: Text
+ , exp :: Int
+ , iat :: Int
+ , iss :: Text
+ , sub :: Text
+ , azp :: Text
+ , nonce :: Text
+ , preferred_username :: Text
+ } deriving (Show, Eq, Generic)
+instance Aeson.FromJSON TokenContents
+
+fetchJwk :: MonadIO m => m (Maybe Jwk.Jwk)
+fetchJwk = do
+ resp <- R.responseBody <$> R.runReq R.defaultHttpConfig do
+ R.req R.GET (R.https "id.twitch.tv" R./: "oauth2" R./: "keys") R.NoReqBody R.jsonResponse mempty
+ let mkeys = Aeson.parseMaybe (Aeson..: "keys") resp
+ let mjwk = mkeys >>= headMay
+ log $ tshow mjwk
+ pure mjwk
+
+validateToken :: MonadIO m => ByteString -> m (Maybe TokenContents)
+validateToken encodedToken = fetchJwk >>= \case
+ Nothing -> pure Nothing
+ Just jwk -> liftIO (Jwt.decode [jwk] Nothing encodedToken) >>= \case
+ Left err -> do
+ log $ "Failed to decode token: " <> tshow err
+ pure Nothing
+ Right jwt -> do
+ let contents = case jwt of
+ Jwt.Unsecured bs -> bs
+ Jwt.Jws (_, bs) -> bs
+ Jwt.Jwe (_, bs) -> bs
+ log $ tshow contents
+ pure $ Aeson.decodeStrict contents
+
+data Auth = Auth { id :: Text, name :: Text } deriving Show
+checkAuth :: Config -> Tw.ResponderM (Maybe Auth)
+checkAuth cfg = (,)
+ <$> Tw.cookieParamMaybe "id_token"
+ <*> Tw.cookieParamMaybe "authnonce"
+ >>= \case
+ (Just token, Just nonce) -> do
+ validateToken token >>= \case
+ Just tc
+ | tc.aud == cfg.clientId
+ , tc.nonce == nonce
+ -> do
+ log $ tshow tc
+ pure . Just $ Auth
+ { name = tc.preferred_username
+ , id = tc.sub
+ }
+ _ -> pure Nothing
+ _ -> pure Nothing
+
+authed :: Config -> (Auth -> Tw.ResponderM a) -> Tw.ResponderM a
+authed cfg f = checkAuth cfg >>= \case
+ Nothing -> Tw.send . Tw.status Tw.status401 $ Tw.text "unauthorized"
+ Just auth -> f auth
diff --git a/fig-frontend/src/Fig/Frontend/Utils.hs b/fig-frontend/src/Fig/Frontend/Utils.hs
new file mode 100644
index 0000000..1ba1d5f
--- /dev/null
+++ b/fig-frontend/src/Fig/Frontend/Utils.hs
@@ -0,0 +1,37 @@
+{-# Language RecordWildCards #-}
+{-# Language ApplicativeDo #-}
+
+module Fig.Frontend.Utils
+ ( FigFrontendException(..)
+ , loadConfig
+ , Config(..)
+ , module Network.HTTP.Types.Status
+ ) where
+
+import Fig.Prelude
+
+import Network.HTTP.Types.Status
+
+import qualified Toml
+
+newtype FigFrontendException = FigFrontendException Text
+ deriving (Show, Eq, Ord)
+instance Exception FigFrontendException
+
+data Config = Config
+ { port :: Int
+ , assetPath :: FilePath
+ , clientId :: Text
+ } deriving (Show, Eq, Ord)
+
+configCodec :: Toml.TomlCodec Config
+configCodec = do
+ port <- Toml.int "port" Toml..= (\a -> a.port)
+ assetPath <- Toml.string "asset_path" Toml..= (\a -> a.assetPath)
+ clientId <- Toml.text "client_id" Toml..= (\a -> a.clientId)
+ pure $ Config{..}
+
+loadConfig :: FilePath -> IO Config
+loadConfig path = Toml.decodeFileEither configCodec path >>= \case
+ Left err -> throwM . FigFrontendException $ tshow err
+ Right config -> pure config
diff --git a/flake.nix b/flake.nix
index e24e0ff..5f1d610 100644
--- a/flake.nix
+++ b/flake.nix
@@ -17,6 +17,7 @@
fig-monitor-irc = self.callCabal2nix "fig-monitor-irc" ./fig-monitor-irc {};
fig-monitor-bullfrog = self.callCabal2nix "fig-monitor-bullfrog" ./fig-monitor-bullfrog {};
fig-bridge-irc-discord = self.callCabal2nix "fig-bridge-irc-discord" ./fig-bridge-irc-discord {};
+ fig-frontend = self.callCabal2nix "fig-frontend" ./fig-frontend {};
};
};
figBusModule = { config, lib, ... }:
@@ -187,9 +188,11 @@
fig-monitor-irc
fig-monitor-bullfrog
fig-bridge-irc-discord
+ fig-frontend
];
withHoogle = true;
buildInputs = [
+ haskellPackages.haskell-language-server
];
};
packages.x86_64-linux = {
@@ -199,6 +202,7 @@
figMonitorIRC = haskellPackages.fig-monitor-irc;
figMonitorBullfrog = haskellPackages.fig-monitor-bullfrog;
figBridgeIRCDiscord = haskellPackages.fig-bridge-irc-discord;
+ figFrontend = haskellPackages.fig-frontend;
};
apps.x86_64-linux.default = {
type = "app";
diff --git a/hie.yaml b/hie.yaml
index 2b4b7a3..45fa0c6 100644
--- a/hie.yaml
+++ b/hie.yaml
@@ -26,3 +26,7 @@ cradle:
config: { cradle: { cabal: { component: "fig-bridge-irc-discord:lib:fig-bridge-irc-discord" } } }
- path: "./fig-bridge-irc-discord/main/"
config: { cradle: { cabal: { component: "fig-bridge-irc-discord:exe:fig-bridge-irc-discord" } } }
+ - path: "./fig-frontend/src/"
+ config: { cradle: { cabal: { component: "fig-frontend:lib:fig-frontend" } } }
+ - path: "./fig-frontend/main/"
+ config: { cradle: { cabal: { component: "fig-frontend:exe:fig-frontend" } } }