diff options
| author | LLLL Colonq <llll@colonq> | 2024-11-05 02:58:33 -0500 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2024-11-05 02:58:33 -0500 |
| commit | cf0070ac5a78d8042fa74d407fb9cb65352e2066 (patch) | |
| tree | 85fb5cee6c7533b7ae6de846ed447772d7b67296 | |
Initial commit
| -rw-r--r-- | .envrc | 1 | ||||
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | Makefile | 54 | ||||
| -rw-r--r-- | assets/icon.png | bin | 0 -> 983 bytes | |||
| -rw-r--r-- | assets/iosevka-comfy-regular.ttf | bin | 0 -> 7811628 bytes | |||
| -rw-r--r-- | assets/mrblue.png | bin | 0 -> 771 bytes | |||
| -rw-r--r-- | assets/mrgreen.png | bin | 0 -> 714 bytes | |||
| -rw-r--r-- | assets/voice/0.wav | bin | 0 -> 26334 bytes | |||
| -rw-r--r-- | assets/voice/1.wav | bin | 0 -> 27102 bytes | |||
| -rw-r--r-- | assets/voice/2.wav | bin | 0 -> 27102 bytes | |||
| -rw-r--r-- | assets/voice/3.wav | bin | 0 -> 26334 bytes | |||
| -rw-r--r-- | assets/voice/4.wav | bin | 0 -> 24030 bytes | |||
| -rw-r--r-- | assets/voice/5.wav | bin | 0 -> 28642 bytes | |||
| -rw-r--r-- | assets/voice/6.wav | bin | 0 -> 27874 bytes | |||
| -rw-r--r-- | config/deploy.m4 | 4 | ||||
| -rw-r--r-- | config/extension.js | 2 | ||||
| -rw-r--r-- | config/test.m4 | 6 | ||||
| -rw-r--r-- | extension/background.js | 23 | ||||
| -rw-r--r-- | extension/main.css | 0 | ||||
| -rw-r--r-- | extension/manifest.dhall | 30 | ||||
| -rw-r--r-- | flake.lock | 293 | ||||
| -rw-r--r-- | flake.nix | 53 | ||||
| -rw-r--r-- | main.css | 115 | ||||
| -rw-r--r-- | src/Audio.js | 30 | ||||
| -rw-r--r-- | src/Audio.purs | 10 | ||||
| -rw-r--r-- | src/Auth.js | 45 | ||||
| -rw-r--r-- | src/Auth.purs | 29 | ||||
| -rw-r--r-- | src/Config.js | 4 | ||||
| -rw-r--r-- | src/Config.purs | 6 | ||||
| -rw-r--r-- | src/Main.purs | 157 | ||||
| -rw-r--r-- | src/Model.js | 108 | ||||
| -rw-r--r-- | src/Model.purs | 10 | ||||
| -rw-r--r-- | src/UI.js | 2 | ||||
| -rw-r--r-- | src/UI.purs | 13 | ||||
| -rw-r--r-- | templates/button.html | 17 | ||||
| -rw-r--r-- | templates/index.html | 40 | ||||
| -rw-r--r-- | templates/obs.html | 18 |
37 files changed, 1076 insertions, 0 deletions
@@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..335239a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.direnv +.psc-ide-port +main.js +node_modules +output/* +dist/*
\ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b178e8f --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +.PHONY: all dist deploy extension clean + +TEMPLATES=$(shell ls templates) + +all: dist $(addprefix dist/test/,$(TEMPLATES)) dist/test/assets dist/test/main.js dist/test/main.css + +deploy: dist $(addprefix dist/deploy/,$(TEMPLATES)) dist/deploy/assets dist/deploy/main.js dist/deploy/main.css + rsync -av dist/deploy/ "pub.colonq.computer:~/public_html/" + +dist: + mkdir -p dist/test + mkdir -p dist/deploy + +main.js: $(shell find src) + purs-nix bundle + +dist/%/assets: $(shell find assets) dist + rm -rf $@ + mkdir -p $@ + cp -r assets/* $@ + +dist/%/main.js: main.js dist + cp $< $@ + +dist/extension/main.css: extension/main.css dist + cp $< $@ + +dist/%/main.css: main.css dist + cp $< $@ + +dist/%/index.html: config/%.m4 dist index-template.html + sh -c "m4 $< >$@" + + +define GEN_RULE +dist/%/$(template): config/%.m4 templates/$(template) + sh -c "m4 $$^ >$$@" +endef +$(foreach template,$(TEMPLATES), $(eval $(GEN_RULE))) + +extension: dist dist/extension/assets dist/extension/manifest.json dist/extension/background.js dist/extension/main.js dist/extension/main.css dist/extension/config.js + +dist/extension/manifest.json: extension/manifest.dhall + dhall-to-json <$< >$@ + +dist/extension/config.js: config/extension.js + cp $< $@ + +dist/extension/%: extension/% + cp $< $@ + +clean: + rm main.js + rm -r dist/ diff --git a/assets/icon.png b/assets/icon.png Binary files differnew file mode 100644 index 0000000..0431e3f --- /dev/null +++ b/assets/icon.png diff --git a/assets/iosevka-comfy-regular.ttf b/assets/iosevka-comfy-regular.ttf Binary files differnew file mode 100644 index 0000000..b474adc --- /dev/null +++ b/assets/iosevka-comfy-regular.ttf diff --git a/assets/mrblue.png b/assets/mrblue.png Binary files differnew file mode 100644 index 0000000..8cf2a0b --- /dev/null +++ b/assets/mrblue.png diff --git a/assets/mrgreen.png b/assets/mrgreen.png Binary files differnew file mode 100644 index 0000000..1f1edca --- /dev/null +++ b/assets/mrgreen.png diff --git a/assets/voice/0.wav b/assets/voice/0.wav Binary files differnew file mode 100644 index 0000000..3b4884d --- /dev/null +++ b/assets/voice/0.wav diff --git a/assets/voice/1.wav b/assets/voice/1.wav Binary files differnew file mode 100644 index 0000000..7701e71 --- /dev/null +++ b/assets/voice/1.wav diff --git a/assets/voice/2.wav b/assets/voice/2.wav Binary files differnew file mode 100644 index 0000000..619c442 --- /dev/null +++ b/assets/voice/2.wav diff --git a/assets/voice/3.wav b/assets/voice/3.wav Binary files differnew file mode 100644 index 0000000..bccbcbf --- /dev/null +++ b/assets/voice/3.wav diff --git a/assets/voice/4.wav b/assets/voice/4.wav Binary files differnew file mode 100644 index 0000000..503c30c --- /dev/null +++ b/assets/voice/4.wav diff --git a/assets/voice/5.wav b/assets/voice/5.wav Binary files differnew file mode 100644 index 0000000..a0288ec --- /dev/null +++ b/assets/voice/5.wav diff --git a/assets/voice/6.wav b/assets/voice/6.wav Binary files differnew file mode 100644 index 0000000..11aceee --- /dev/null +++ b/assets/voice/6.wav diff --git a/config/deploy.m4 b/config/deploy.m4 new file mode 100644 index 0000000..97fc61f --- /dev/null +++ b/config/deploy.m4 @@ -0,0 +1,4 @@ +define(`CONFIG_SUBST', ` +globalThis.mode = 0; +globalThis.apiServer = "https://api.colonq.computer/api"; +') diff --git a/config/extension.js b/config/extension.js new file mode 100644 index 0000000..44b69d3 --- /dev/null +++ b/config/extension.js @@ -0,0 +1,2 @@ +globalThis.mode = 1; +globalThis.apiServer = "https://api.colonq.computer/api"; diff --git a/config/test.m4 b/config/test.m4 new file mode 100644 index 0000000..6d41963 --- /dev/null +++ b/config/test.m4 @@ -0,0 +1,6 @@ +define(`CONFIG_SUBST', ` +globalThis.mode = 0; +globalThis.apiServer = "http://localhost:8000/api"; +globalThis.clientID = "q486jugzn2my4iw6l181o006ugye4j" +globalThis.authRedirectURL = "http://localhost:8000"; +') diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..a0f4c91 --- /dev/null +++ b/extension/background.js @@ -0,0 +1,23 @@ +/* Retrieve any previously set cookie and send to content script */ + +function getActiveTab() { + return browser.tabs.query({active: true, currentWindow: true}); +} + +function cookieUpdate() { + getActiveTab().then((tabs) => { + // get any previously set cookie for the current tab + let gettingCookies = browser.cookies.get({ + url: tabs[0].url, + name: "name" + }); + gettingCookies.then((cookie) => { + browser.tabs.sendMessage(tabs[0].id, cookie.value); + }); + }); +} + +// update when the tab is updated +browser.tabs.onUpdated.addListener(cookieUpdate); +// update when the tab is activated +browser.tabs.onActivated.addListener(cookieUpdate); diff --git a/extension/main.css b/extension/main.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/extension/main.css diff --git a/extension/manifest.dhall b/extension/manifest.dhall new file mode 100644 index 0000000..cf9e7a1 --- /dev/null +++ b/extension/manifest.dhall @@ -0,0 +1,30 @@ +{ manifest_version = 2 +, name = "computerspotting" +, version = "1.0" +, description = "spot the computer" +, icons = + [ { mapKey = "48", mapValue = "assets/mrgreen.png" } + ] +, web_accessible_resources = + [ "mrgreen.png" + , "mrblue.png" + ] +, permissions = + [ "webRequest" + , "webRequestBlocking" + , "tabs" + , "cookies" + , "*://*.twitch.tv/*" + , "*://api.colonq.computer/*" + ] +, background = + { scripts = ["background.js"] + } +, content_scripts = + [ { matches = ["*://*.twitch.tv/*"] + , js = ["config.js", "main.js"] + , css = ["main.css"] + , run_at = "document_end" + } + ] +}
\ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a770068 --- /dev/null +++ b/flake.lock @@ -0,0 +1,293 @@ +{ + "nodes": { + "docs-search": { + "flake": false, + "locked": { + "lastModified": 1675992564, + "narHash": "sha256-Tk9VSogFHXtXe9O9vuCEfM/PV/S7plMIO0I++fCZn7U=", + "owner": "purs-nix", + "repo": "purescript-docs-search", + "rev": "35822b1d6ce65b1a07f80dd9e2caf15c3ee83e2c", + "type": "github" + }, + "original": { + "owner": "purs-nix", + "repo": "purescript-docs-search", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1618217525, + "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "locked": { + "lastModified": 1618217525, + "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "get-flake": { + "locked": { + "lastModified": 1644686428, + "narHash": "sha256-zkhYsURWFrvEZLkIoBeqFBzSu+cA2u5mo6M8vq9LN7M=", + "owner": "ursi", + "repo": "get-flake", + "rev": "703f15558daa56dfae19d1858bb3046afe68831a", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "get-flake", + "type": "github" + } + }, + "lint-utils": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "purs-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1707777931, + "narHash": "sha256-PsPb5xMBZ9dPDP04o9vqKEUIEG80Z84/74fPuOMs0ZI=", + "owner": "homotopic", + "repo": "lint-utils", + "rev": "5f11e3e51d8f1aa4ed62a89e90f05953931e105a", + "type": "github" + }, + "original": { + "owner": "homotopic", + "repo": "lint-utils", + "type": "github" + } + }, + "make-shell": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "ref": "1", + "repo": "nix-make-shell", + "type": "github" + } + }, + "make-shell_2": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "ref": "1", + "repo": "nix-make-shell", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1730643484, + "narHash": "sha256-FZN70acnL7V0gfmtLTdsi4J1o51bfaBYDTi3E7b7z3U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4ae2e647537bcdbb82265469442713d066675275", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1656549732, + "narHash": "sha256-eILutFZGjfk2bEzfim8S/qyYc//0S1KsCeO+OWbtoR0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d3248619647234b5dc74a6921bcdf6dd8323eb22", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "parsec": { + "locked": { + "lastModified": 1635533376, + "narHash": "sha256-/HrG0UPGnI5VdkhrNrpDiM2+nhdL6lD/bqyGtYv0QDE=", + "owner": "nprindle", + "repo": "nix-parsec", + "rev": "1bf25dd9c5de1257a1c67de3c81c96d05e8beb5e", + "type": "github" + }, + "original": { + "owner": "nprindle", + "repo": "nix-parsec", + "type": "github" + } + }, + "ps-tools": { + "inputs": { + "make-shell": "make-shell_2", + "nixpkgs": "nixpkgs_3", + "utils": "utils" + }, + "locked": { + "lastModified": 1704567308, + "narHash": "sha256-WbFPIkKLtyQOPBUjintckKIYnfs7MvIbmfVsLRSAPlc=", + "owner": "purs-nix", + "repo": "purescript-tools", + "rev": "ac626313141cbee78f06eb3c5e90359f695aef9b", + "type": "github" + }, + "original": { + "owner": "purs-nix", + "repo": "purescript-tools", + "type": "github" + } + }, + "purs-nix": { + "inputs": { + "docs-search": "docs-search", + "get-flake": "get-flake", + "lint-utils": "lint-utils", + "make-shell": "make-shell", + "nixpkgs": "nixpkgs_2", + "parsec": "parsec", + "ps-tools": "ps-tools", + "utils": "utils_2" + }, + "locked": { + "lastModified": 1725998053, + "narHash": "sha256-qS5eWGoM/gLkC5WnEXCLUIts/kc4zzPTw5GtlYIE5Eo=", + "owner": "purs-nix", + "repo": "purs-nix", + "rev": "7a4d5856d4381d0ff1919b2fca073bf261ca4daf", + "type": "github" + }, + "original": { + "owner": "purs-nix", + "ref": "ps-0.15", + "repo": "purs-nix", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "ps-tools": [ + "purs-nix", + "ps-tools" + ], + "purs-nix": "purs-nix" + } + }, + "utils": { + "inputs": { + "flake-utils": "flake-utils_2" + }, + "locked": { + "lastModified": 1656044990, + "narHash": "sha256-f01BB7CaOyntOab9XnpH9HD63rGcnu2iyL4M2ubs5F8=", + "owner": "ursi", + "repo": "flake-utils", + "rev": "f53b674a2c90f6202a2f4cd491aba121775490b5", + "type": "github" + }, + "original": { + "owner": "ursi", + "ref": "8", + "repo": "flake-utils", + "type": "github" + } + }, + "utils_2": { + "inputs": { + "flake-utils": "flake-utils_3" + }, + "locked": { + "lastModified": 1656044990, + "narHash": "sha256-f01BB7CaOyntOab9XnpH9HD63rGcnu2iyL4M2ubs5F8=", + "owner": "ursi", + "repo": "flake-utils", + "rev": "f53b674a2c90f6202a2f4cd491aba121775490b5", + "type": "github" + }, + "original": { + "owner": "ursi", + "ref": "8", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..44fd451 --- /dev/null +++ b/flake.nix @@ -0,0 +1,53 @@ +{ + description = "bundt - frontend for fig"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + ps-tools.follows = "purs-nix/ps-tools"; + purs-nix.url = "github:purs-nix/purs-nix/ps-0.15"; + }; + + outputs = { self, nixpkgs, ... }@inputs: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + ps-tools = inputs.ps-tools.legacyPackages.${system}; + purs-nix = inputs.purs-nix { inherit system; }; + + purescript = purs-nix.purs { + dependencies = [ + "console" + "effect" + "prelude" + "random" + "refs" + "web-html" + "web-dom" + "web-uievents" + "canvas" + "argonaut" + "fetch" + "fetch-argonaut" + ]; + dir = ./.; + srcs = [ "src" ]; + }; + bundt = purescript.bundle {}; + in { + devShells.x86_64-linux.default = pkgs.mkShell { + buildInputs = [ + pkgs.nodejs + (purescript.command {}) + ps-tools.for-0_15.purescript-language-server + purs-nix.esbuild + purs-nix.purescript + pkgs.m4 + pkgs.dhall + pkgs.dhall-json + ]; + }; + packages.x86_64-linux = { + default = bundt; + }; + }; +} diff --git a/main.css b/main.css new file mode 100644 index 0000000..7714273 --- /dev/null +++ b/main.css @@ -0,0 +1,115 @@ +@font-face { + font-family: "Iosevka Comfy"; + src: url("assets/iosevka-comfy-regular.ttf") format("truetype"); +} + +html { + font-family: "Iosevka Comfy"; + font-size: 16pt; +} + +html, body { + overflow: hidden; + margin: 0 !important; + padding: 0 !important; + height: 100%; + width: 100%; +} + +body.lcolonq-index { + font-family: "Iosevka Comfy"; + font-weight: bold; + color: black; + user-select: none; + image-rendering: pixelated; + background-color: #eeeeee; + background-size: 40px 40px; + background-image: + linear-gradient(to right, grey 1px, transparent 1px), + linear-gradient(to bottom, grey 1px, transparent 1px); +} + +body.lcolonq-obs { + background-color: rbga(0,0,0,0); +} + +#lcolonq-title { + position: absolute; + top: 2rem; + bottom: 0px; + left: 0px; + right: 0px; + text-align: center; + font-size: 20vw; +} + +.lcolonq-letter { + display: inline-block; +} + +.lcolonq-letter:hover { + color: #333333; +} + +#lcolonq-subtitle { + font-size: 5vw; + margin-top: -5vw; + margin-right: 12vw; + text-align: right; +} + +#lcolonq-header { + background-color: #cccccf; + position: absolute; + left: 0px; + right: 0px; + top: 0px; + height: 2rem; + border-bottom: 0.2rem ridge; +} + +#lcolonq-header a { + color: black; +} + +#lcolonq-header marquee { + color: black; + padding-top: 0.4rem; +} + +#lcolonq-footer { + background-color: #cccccf; + position: absolute; + left: 0px; + right: 0px; + bottom: 0px; + height: 2rem; + border-top: 0.2rem groove; +} + +#lcolonq-footer a { + display: inline-block; + color: black; + margin-top: 0.2rem; + margin-left: 0.1rem; + margin-right: 0.1rem; + padding: 0.1rem; + text-decoration: none; + border: 0.1rem outset; +} + +#lcolonq-footer a:active { + border: 0.1rem inset; +} + +#lcolonq-canvas { + position: absolute; + bottom: 2rem; + left: 0px; + width: 832px; + height: 832px; +} + +.right { + float: right; +} diff --git a/src/Audio.js b/src/Audio.js new file mode 100644 index 0000000..2a9ae54 --- /dev/null +++ b/src/Audio.js @@ -0,0 +1,30 @@ +let initialized = false; +let ctx = null; +let voiceTracks = null; + +function initializeCtx() { + if (!initialized) { + try { + initialized = true; + ctx = new window.AudioContext(); + voiceTracks = [ + document.getElementById("lcolonq-audio-voice-0"), + document.getElementById("lcolonq-audio-voice-1"), + document.getElementById("lcolonq-audio-voice-2"), + document.getElementById("lcolonq-audio-voice-3"), + document.getElementById("lcolonq-audio-voice-4"), + document.getElementById("lcolonq-audio-voice-5"), + document.getElementById("lcolonq-audio-voice-6"), + ]; + } catch (e) { + initialized = false; + } + } +} + +export const _playVoice = (b) => (i) => () => { + if (b) initializeCtx(); + try { + if (initialized) voiceTracks[i].play(); + } catch (e) {} +}; diff --git a/src/Audio.purs b/src/Audio.purs new file mode 100644 index 0000000..8f1f319 --- /dev/null +++ b/src/Audio.purs @@ -0,0 +1,10 @@ +module Audio where + +import Prelude + +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) + +foreign import _playVoice :: Boolean -> Int -> Effect Unit +playVoice :: forall m. MonadEffect m => Boolean -> Int -> m Unit +playVoice b i = liftEffect $ _playVoice b i diff --git a/src/Auth.js b/src/Auth.js new file mode 100644 index 0000000..7254c40 --- /dev/null +++ b/src/Auth.js @@ -0,0 +1,45 @@ +function generateNonce() { + var arr = new Uint8Array(20); + window.crypto.getRandomValues(arr); + return Array.from(arr, b => b.toString(16).padStart(2, "0")).join(""); +} + +export const _startTwitchAuth = (clientID) => (redirectURL) => () => { + const nonce = generateNonce(); + document.cookie = `authnonce=${nonce}; path=/; max-age=3000`; + window.location.href = + `https://id.twitch.tv/oauth2/authorize?response_type=id_token` + + `&client_id=${clientID}` + + `&redirect_uri=${redirectURL}` + + `&scope=openid` + + `&nonce=${nonce}` + + `&claims=${JSON.stringify({id_token: {preferred_username: null}})}` + ; +}; + +function getFragmentQuery() { + 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 const _getToken = (Just) => (Nothing) => (pair) => () => { + const frag = getFragmentQuery(); + const token = frag.get("id_token"); + if (token) { + document.cookie = `id_token=${token}; path=/; SameSite=Strict`; + } + let id_token = null; + let authnonce = null; + for (let c of document.cookie.split("; ")) { + const [k, v] = c.split("="); + if (k === "id_token") id_token = v; + else if (k === "authnonce") authnonce = v; + } + if (id_token && authnonce) return Just(pair(id_token)(authnonce)); + return Nothing; +}; diff --git a/src/Auth.purs b/src/Auth.purs new file mode 100644 index 0000000..2a53629 --- /dev/null +++ b/src/Auth.purs @@ -0,0 +1,29 @@ +module Auth where + +import Prelude + +import Config (authRedirectURL, clientID) +import Data.Array (fold) +import Data.Maybe (Maybe(..)) +import Data.Tuple (Tuple(..)) +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) + +foreign import _startTwitchAuth :: String -> String -> Effect Unit +startTwitchAuth :: forall m. MonadEffect m => m Unit +startTwitchAuth = liftEffect $ _startTwitchAuth clientID authRedirectURL + +type AuthInfo = Tuple String String +foreign import _getToken :: forall a. (a -> Maybe a) -> Maybe a -> (a -> a -> Tuple a a) -> Effect (Maybe (Tuple String String)) +getToken :: forall m. MonadEffect m => m (Maybe AuthInfo) +getToken = liftEffect $ _getToken Just Nothing Tuple + +authHeader :: AuthInfo -> String +authHeader (Tuple t n) = + fold + [ "FIG-TWITCH token=\"" + , t + , "\", nonce=\"" + , n + , "\"" + ] diff --git a/src/Config.js b/src/Config.js new file mode 100644 index 0000000..25259c7 --- /dev/null +++ b/src/Config.js @@ -0,0 +1,4 @@ +export const mode = globalThis.mode; +export const apiServer = globalThis.apiServer; +export const clientID = globalThis.clientID; +export const authRedirectURL = globalThis.authRedirectURL; diff --git a/src/Config.purs b/src/Config.purs new file mode 100644 index 0000000..536f163 --- /dev/null +++ b/src/Config.purs @@ -0,0 +1,6 @@ +module Config where + +foreign import mode :: Int +foreign import apiServer :: String +foreign import clientID :: String +foreign import authRedirectURL :: String diff --git a/src/Main.purs b/src/Main.purs new file mode 100644 index 0000000..bb5fa57 --- /dev/null +++ b/src/Main.purs @@ -0,0 +1,157 @@ +module Main where + +import Prelude + +import Audio as Audio +import Auth (AuthInfo, authHeader, getToken, startTwitchAuth) +import Config as Config +import Data.Array (head) +import Data.Array as Array +import Data.Foldable (fold) +import Data.Maybe (Maybe(..)) +import Data.Traversable (for, for_) +import Data.Tuple (Tuple(..)) +import Effect (Effect) +import Effect.Aff (Aff, launchAff_) +import Effect.Class (class MonadEffect, liftEffect) +import Effect.Console (log) +import Effect.Exception (throw) +import Fetch (fetch) +import Model (startModel) +import UI as UI +import Web.DOM as DOM +import Web.DOM.DOMTokenList as DOM.DTL +import Web.DOM.Document (doctype) +import Web.DOM.Document as DOM.Doc +import Web.DOM.Element as DOM.El +import Web.DOM.Node as DOM.Node +import Web.DOM.NodeList as DOM.NL +import Web.DOM.NonElementParentNode as DOM.NEP +import Web.DOM.ParentNode as DOM.P +import Web.DOM.Text as DOM.Text +import Web.Event.Event as Ev +import Web.Event.EventTarget as Ev.Tar +import Web.HTML as HTML +import Web.HTML.HTMLDocument as HTML.Doc +import Web.HTML.Window as HTML.Win + +maybeToArray :: forall a. Maybe a -> Array a +maybeToArray (Just x) = [x] +maybeToArray Nothing = [] + +byId :: forall m. MonadEffect m => String -> m DOM.Element +byId i = do + w <- liftEffect HTML.window + d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w + liftEffect (DOM.NEP.getElementById i (DOM.Doc.toNonElementParentNode d)) >>= case _ of + Nothing -> liftEffect $ throw $ "could not find element with id: " <> i + Just e -> pure e + +queryAll :: forall m. MonadEffect m => String -> m (Array DOM.Element) +queryAll q = do + w <- liftEffect HTML.window + d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w + nl <- liftEffect (DOM.P.querySelectorAll (DOM.P.QuerySelector q) (DOM.Doc.toParentNode d)) + ns <- liftEffect $ DOM.NL.toArray nl + pure $ fold $ (maybeToArray <<< DOM.El.fromNode) <$> ns + +query :: forall m . MonadEffect m => String -> m DOM.Element +query q = do + queryAll q >>= head >>> case _ of + Nothing -> liftEffect $ throw $ "could not find element matching query: " <> q + Just x -> pure x + +listen :: forall m. MonadEffect m => DOM.Element -> String -> (Ev.Event -> Effect Unit) -> m Unit +listen e ev f = do + l <- liftEffect $ Ev.Tar.eventListener f + liftEffect $ Ev.Tar.addEventListener (Ev.EventType ev) l false $ DOM.El.toEventTarget e + +create :: forall m. MonadEffect m => String -> Array String -> Array DOM.Element -> m DOM.Element +create tag classes children = do + w <- liftEffect HTML.window + d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w + el <- liftEffect $ DOM.Doc.createElement tag d + cl <- liftEffect $ DOM.El.classList el + for_ classes \c -> + liftEffect $ DOM.DTL.add cl c + for_ children \c -> + appendElement el c + pure el + +appendElement :: forall m. MonadEffect m => DOM.Element -> DOM.Element -> m Unit +appendElement parent child = liftEffect $ DOM.Node.appendChild (DOM.El.toNode child) (DOM.El.toNode parent) + +appendText :: forall m. MonadEffect m => DOM.Element -> String -> m Unit +appendText parent s = do + w <- liftEffect HTML.window + d <- liftEffect $ HTML.Doc.toDocument <$> HTML.Win.document w + n <- liftEffect $ DOM.Doc.createTextNode s d + liftEffect $ DOM.Node.appendChild (DOM.Text.toNode n) (DOM.El.toNode parent) + +setText :: forall m. MonadEffect m => DOM.Element -> String -> m Unit +setText e s = liftEffect $ DOM.Node.setTextContent s $ DOM.El.toNode e + +updateSubtitle :: Aff Unit +updateSubtitle = do + subtitle <- byId "lcolonq-subtitle" + { text: catchphrase } <- fetch (Config.apiServer <> "/catchphrase") {} + catchphrase >>= setText subtitle + +checkAuth :: AuthInfo -> Aff String +checkAuth auth = do + { text: resp } <- + fetch (Config.apiServer <> "/check") + { headers: + { "Authorization": authHeader auth + } + } + resp + +mainHomepage :: Effect Unit +mainHomepage = launchAff_ do + liftEffect $ log "hi" + startModel + marq <- byId "lcolonq-marquee" + { text: motd } <- fetch (Config.apiServer <> "/motd") {} + motd >>= setText marq + + getToken >>= case _ of + Just a@(Tuple t n) -> do + liftEffect $ log t + liftEffect $ log n + checkAuth a >>= log >>> liftEffect + _ -> pure unit + + updateSubtitle + subtitle <- byId "lcolonq-subtitle" + listen subtitle "click" \_ev -> do + startTwitchAuth + launchAff_ updateSubtitle + + for_ (Array.range 0 6) \i -> do + letter <- byId $ "lcolonq-letter-" <> show i + listen letter "click" \_ev -> do + Audio.playVoice true i + listen letter "mouseover" \_ev -> do + Audio.playVoice false i + +mainExtension :: Effect Unit +mainExtension = launchAff_ do + liftEffect $ log "hello from extension" + UI.setInterval 1000.0 do + e <- query ".chat-scrollable-area__message-container" + new <- create "div" [".chat-line__message"] [] + appendText new "test" + appendElement e new + +mainObs :: Effect Unit +mainObs = launchAff_ do + startModel + +main :: Effect Unit +main = case Config.mode of + 0 -> mainHomepage + 1 -> mainExtension + 2 -> mainObs + -- 3 -> mainButton + _ -> throw "unknown mode" diff --git a/src/Model.js b/src/Model.js new file mode 100644 index 0000000..797bae4 --- /dev/null +++ b/src/Model.js @@ -0,0 +1,108 @@ +let canvas = document.getElementById("lcolonq-canvas"); +let socket = null; +let currentFrame = null; + +async function decompress(blob) { + let ds = new DecompressionStream("gzip"); + let stream = blob.stream(); + let out = await new Response(stream.pipeThrough(ds)); + return out.arrayBuffer(); +} + +function readCell(dv, base) { + let cell = {}; + let o = base; + if (dv.getUint8(o) == 0) { + return [{type: "bg"}, o + 1]; + } else { + cell.type = "fg"; + cell.custom = dv.getUint8(o + 1); + cell.r = dv.getUint8(o + 2); + cell.g = dv.getUint8(o + 3); + cell.b = dv.getUint8(o + 4); + cell.g0 = dv.getUint32(o + 5); + if (dv.getUint8(o + 9) == 0) { + return [cell, o + 10]; + } else { + cell.g1 = dv.getUint32(o + 10); + return [cell, o + 14]; + } + } +} + +function readKeyframe(dv, base) { + let ret = []; + let o = base; + for (let idx = 0; idx < (64 * 64); ++idx) { + let res = readCell(dv, o); + ret.push(res[0]); + o = res[1]; + } + currentFrame = ret; +} + +function readDiff(dv, base) { + if (currentFrame) { + let len = dv.getUint32(base); + let o = base + 4; + for (let idx = 0; idx < len; ++idx) { + let x = dv.getUint8(o); + let y = dv.getUint8(o + 1); + let c = readCell(dv, o + 2); + currentFrame[x + (y * 64)] = c[0]; + o = c[1]; + } + } +} + +function readPacket(dv) { + if (dv.getUint8(0) == 0) { + readKeyframe(dv, 1); + } else { + readDiff(dv, 1); + } +} + +function renderCellCanvas(ctx, x, y, c) { + if (c && c.type === "fg") { + let msg = c.g1 ? String.fromCodePoint(c.g0, c.g1) : String.fromCodePoint(c.g0); + if (msg.trim().length) { + ctx.fillStyle = "black"; + ctx.fillRect(13 * y, 13 * x, 13, 13); + ctx.fillStyle = `rgba(${c.r}, ${c.g}, ${c.b}, 1.0)`; + ctx.fillText(msg, 13 * y, 13 * x + 10); + } + } +} + +function renderCanvas() { + if (canvas.width != canvas.clientWidth) { + canvas.width = canvas.clientWidth; + } + if (canvas.height != canvas.clientHeight) { + canvas.height = canvas.clientHeight; + } + if (currentFrame) { + let ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.font = "12px Iosevka Comfy"; + for (let y = 0; y < 64; ++y) { + for (let x = 0; x < 64; ++x) { + renderCellCanvas(ctx, x, y, currentFrame[(x * 64) + y]); + } + } + } +} + +export const _startModel = () => { + socket = new WebSocket("wss://colonq.computer/bullfrog/api/channel/listen/model"); + socket.addEventListener("open", (ev) => { + console.log("connected"); + }); + socket.addEventListener("message", async (ev) => { + let arr = await decompress(ev.data); + let view = new DataView(arr); + readPacket(view); + renderCanvas(); + }); +}; diff --git a/src/Model.purs b/src/Model.purs new file mode 100644 index 0000000..a9f2888 --- /dev/null +++ b/src/Model.purs @@ -0,0 +1,10 @@ +module Model where + +import Prelude + +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) + +foreign import _startModel :: Effect Unit +startModel :: forall m. MonadEffect m => m Unit +startModel = liftEffect _startModel diff --git a/src/UI.js b/src/UI.js new file mode 100644 index 0000000..0b00e5d --- /dev/null +++ b/src/UI.js @@ -0,0 +1,2 @@ +export const _cheatLog = (a) => () => console.log(a); +export const _setInterval = (delay) => (f) => () => setInterval(f, delay); diff --git a/src/UI.purs b/src/UI.purs new file mode 100644 index 0000000..49d797a --- /dev/null +++ b/src/UI.purs @@ -0,0 +1,13 @@ +module UI where + +import Prelude +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) + +foreign import _cheatLog :: forall a. a -> Effect Unit +cheatLog :: forall m a. MonadEffect m => a -> m Unit +cheatLog x = liftEffect $ _cheatLog x + +foreign import _setInterval :: Number -> Effect Unit -> Effect Unit +setInterval :: forall m. MonadEffect m => Number -> Effect Unit -> m Unit +setInterval d f = liftEffect $ _setInterval d f diff --git a/templates/button.html b/templates/button.html new file mode 100644 index 0000000..e2f9fb0 --- /dev/null +++ b/templates/button.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> + <title>LCOLONQ Browser Source</title> + <link rel="icon" href="./assets/mrgreen.png"> + <link rel="stylesheet" type="text/css" href="./main.css"> + <script type="module"> +CONFIG_SUBST + globalThis.mode = 3; + </script> + <script type="module" src="./main.js"></script> + </head> + <body class="lcolonq-button"> + </body> +</html> diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..8827694 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> + <title>LCOLONQ</title> + <link rel="icon" href="./assets/mrgreen.png"> + <link rel="stylesheet" type="text/css" href="./main.css"> + <script type="module"> +CONFIG_SUBST + </script> + <script type="module" src="./main.js"></script> + </head> + <body class="lcolonq-index"> + <!-- RINGBEARER --> + <canvas id="lcolonq-canvas"></canvas> + <div id="lcolonq-title"> + <span class="lcolonq-letter" id="lcolonq-letter-0">L</span><span class="lcolonq-letter" id="lcolonq-letter-1">C</span><span class="lcolonq-letter" id="lcolonq-letter-2">O</span><span class="lcolonq-letter" id="lcolonq-letter-3">L</span><span class="lcolonq-letter" id="lcolonq-letter-4">O</span><span class="lcolonq-letter" id="lcolonq-letter-5">N</span><span class="lcolonq-letter" id="lcolonq-letter-6">Q</span> + <div id="lcolonq-subtitle"> + </div> + </div> + <div id="lcolonq-header"> + <a href="https://twitch.tv/LCOLONQ"> + <marquee id="lcolonq-marquee"> + </marquee> + </a> + </div> + <div id="lcolonq-footer"> + <a href="https://pub.colonq.computer/~llll/cgi-bin/ring?me=llll&offset=-1">back</a> + <a class="right" href="https://pub.colonq.computer/~llll/cgi-bin/ring?me=llll&offset=1">next</a> + </div> + <audio id="lcolonq-audio-voice-0" src="assets/voice/0.wav"></audio> + <audio id="lcolonq-audio-voice-1" src="assets/voice/1.wav"></audio> + <audio id="lcolonq-audio-voice-2" src="assets/voice/2.wav"></audio> + <audio id="lcolonq-audio-voice-3" src="assets/voice/3.wav"></audio> + <audio id="lcolonq-audio-voice-4" src="assets/voice/4.wav"></audio> + <audio id="lcolonq-audio-voice-5" src="assets/voice/5.wav"></audio> + <audio id="lcolonq-audio-voice-6" src="assets/voice/6.wav"></audio> + </body> +</html> diff --git a/templates/obs.html b/templates/obs.html new file mode 100644 index 0000000..999035a --- /dev/null +++ b/templates/obs.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> + <title>LCOLONQ Browser Source</title> + <link rel="icon" href="./assets/mrgreen.png"> + <link rel="stylesheet" type="text/css" href="./main.css"> + <script type="module"> +CONFIG_SUBST + globalThis.mode = 2; + </script> + <script type="module" src="./main.js"></script> + </head> + <body class="lcolonq-obs"> + <canvas id="lcolonq-canvas"></canvas> + </body> +</html> |
