Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Bifrost

livebooks/bifrost.livemd

Bifrost

Mix.install([
#  {:livebook_cluster, "0.1.1", git: "git@git.sr.ht:~minasmazar/livebook_cluster"},
  {:livebook_cluster, "0.1.1", path: Path.expand("~/workspace/livebook_cluster")},
  {:plug, "~> 1.16"},
  {:kino, "~> 0.14.1"},
  {:jason, "~> 1.4"},
  {:floki, "~> 0.36.2"}
],
config: [
  livebook_cluster: [slug: "bifrost"]
])

Purpose

We try to inject a /userscript/ (JS) code in and install a server executing Elixir code, and trying to do some coll stuff.

Client code (userscript)

// ==UserScript==
// @name         Bifrost (Livebook)
// @namespace    http://tampermonkey.net/
// @version      2024-07-02
// @description  Bifrost
// @author       MinasMazar
// @include      *
// @match        *
// @grant        GM_xmlhttpRequest
// @connect      localhost
// ==/UserScript==

window.addEventListener('load', function() {
    'use strict';

    // const BIFROST_BASE_ENDPOINT = "http://localhost/proxy/sessions/yuquf6wqnombih2qnklocwlvg6lmvztwnle65nmuf6lhqjuc";
    const BIFROST_BASE_ENDPOINT = "http://localhost/proxy/apps/bifrost";
    const BIFROST_PAGE_VISIT_ENDPOINT = `${BIFROST_BASE_ENDPOINT}/page`;
    const BIFROST_EVENT_ENDPOINT = `${BIFROST_BASE_ENDPOINT}/event`;

    function main() {
      console.log("Bifrost entrypoint");
      document.addEventListener("click", sendEvent);
      document.addEventListener("change", sendEvent);
      document.addEventListener("input", sendEvent);
      document.addEventListener("copy", sendEvent);
      sendToBifrost(BIFROST_PAGE_VISIT_ENDPOINT, buildSetupPayload());
    };

    function buildEventPayload(event) {
        return({
            "event": {
                "page": document.URL,
                "type": event.type,
                "tag": event.target.tagName,
                "class": event.target.className,
                "id": event.target.id,
                "text": event.target.innerText,
                "value": event.target.value,
                "html": event.target.innerHTML,
                "selection": document.getSelection().toString()
            }
        });
    }

    function buildSetupPayload() {
        return({
            "url": document.URL,
            "body": document.querySelector("body").innerHTML
        });
    }

    function sendEvent(event) {
        sendToBifrost(BIFROST_EVENT_ENDPOINT, buildEventPayload(event));
    }

    function sendToBifrost(endpoint, payload) {
        console.log("Sending to Bifrost");
        GM_xmlhttpRequest({
            method: "POST",
            url: endpoint,
            data: JSON.stringify(payload),
            headers: {
                "Content-Type": "application/json"
            },
            onload: function(response) {
                console.log(response);
            },
            ontimeout: function() {
                console.log("No response from Bifrost.");
            }
        });
    }

    setTimeout(main, 200);
});

Router

defmodule Bifrost.Router do
  @articles_css_selector "article, div[id&='content'], dic[class=*='content']"
  @links_css_selector "article a, div[id*='content'] a, div[class*='content'] a"

  use Plug.Router

  plug :match
  plug Plug.Parsers,
       parsers: [:json],
       pass:  ["application/json"],
       json_decoder: Jason
  plug :dispatch

  post "/page" do
    url = Map.get(conn.body_params, "url")
    body = Map.get(conn.body_params, "body")

    with true <- !!url, true <- !!body, {:ok, html} <- Floki.parse_fragment(body) do
      articles = html
      |> Floki.find(@articles_css_selector)
      |> Enum.map(&amp; Floki.text(&amp;1))

      links = html
      |> Floki.find(@links_css_selector)
      |> Enum.map(fn el ->
         case Floki.attribute(el, "href") do
           [""] -> nil
           [href] -> href
           _ -> nil
         end
      end)
      |> Enum.filter(&amp; &amp;1)

      page = %{}
      |> Map.put(:url, url)
      |> Map.put(:articles, articles)
      |> Map.put(:links, links)
      IO.puts("Page visit: #{url}, with #{length(articles)} articles and #{length(links)} links")

      LivebookCluster.broadcast({:page, page})
    end

    send_resp(conn, 200, "ok")
  end

  post "/event" do
    if event = Map.get(conn.body_params, "event", nil) do
      page = Map.get(event, "page")
      type = Map.get(event, "type")

      event = %{}
      |> Map.put(:html, Map.get(event, "html"))
      |> Map.put(:page, page)
      |> Map.put(:selection, Map.get(event, "selection"))
      |> Map.put(:tag, Map.get(event, "tag"))
      |> Map.put(:text, Map.get(event, "text"))
      |> Map.put(:type, type)
      IO.puts("Event #{type}: #{inspect event}")

      LivebookCluster.broadcast({:event, event})
    end

    send_resp(conn, 200, "ok")
  end
end

Kino.Proxy.listen(Bifrost.Router)
LivebookCluster.nodes