summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Audio.js30
-rw-r--r--src/Audio.purs10
-rw-r--r--src/Auth.js45
-rw-r--r--src/Auth.purs29
-rw-r--r--src/Config.js4
-rw-r--r--src/Config.purs6
-rw-r--r--src/Main.purs157
-rw-r--r--src/Model.js108
-rw-r--r--src/Model.purs10
-rw-r--r--src/UI.js2
-rw-r--r--src/UI.purs13
11 files changed, 414 insertions, 0 deletions
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