my eye

Bootstrap

Committed d8101b

index 0000000..b816639
--- /dev/null

+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "understory"
+version = "0.22.2"
+description = "a decentralized social web host"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "AGPL-3.0-or-later"
+
+[tool.poetry.plugins."websites"]
+understory = "understory.__web__:app"
+
+[[tool.poetry.source]]
+name = "main"
+url = "https://ragt.ag/code/pypi"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+python-whois = "^0.8.0"
+phonenumbers = "^8.12.55"
+stripe = "^5.2.0"
+webint = "^0.1"
+webint-data = "^0.0"
+webint-system = "^0.0"
+
+[tool.poetry.group.dev.dependencies]
+gmpg = {path="../gmpg", develop=true}
+bgq = {path="../bgq", develop=true}
+webint = {path="../webint", develop=true}
+webint-data = {path="../webint-data", develop=true}
+webint-system = {path="../webint-system", develop=true}

index 0000000..edc039c
--- /dev/null

+"""A host for canopy-based personal websites."""
+
+import stripe
+import web
+
+app = web.application(__name__, automount=True, autotemplate=True)
+host_name = app.cfg["host_name"]
+stripe.api_key = app.cfg["stripe_sk"]
+
+
+@app.control("")
+class Home:
+    """Homepage."""
+
+    def get(self):
+        return app.view.home(host_name)
+
+    def post(self):
+        return "coming soon"
+
+
+@app.control("secret")
+class StripeSecret:
+    """Provide a Stripe secret for the registration client."""
+
+    def get(self):
+        intent = stripe.PaymentIntent.create(
+            amount=100,
+            currency="usd",
+            metadata={"integration_check": "accept_a_payment"},
+        )
+        web.header("Content-Type", "application/json")
+        return {"client_secret": intent.client_secret}
+
+
+@app.control("register")
+class Register:
+    """Registration."""
+
+    def post(self):
+        username = web.tx.request.body._data["username"]
+        passphrase = f"TODO-PW-FOR-{username}"  # canopy.initialize(username)
+        # TODO insert into database
+        web.header("Content-Type", "application/json")
+        return {"passphrase": passphrase}

index 0000000..fb54cdc
--- /dev/null

+window.$ = function(selector) {
+    if (typeof selector == "string")
+        var node = document.querySelector(selector);
+    else
+        var node = selector;
+    return node
+}
+
+window.$$ = function(selector) {
+    var nodes = document.querySelectorAll(selector);
+    var results = Array.prototype.slice.call(nodes);
+    var items = {};
+    for (var i = 0; i < results.length; i++)
+        items[i] = results[i];
+    items.length = results.length;
+    items.splice = [].splice();  // simulates an array
+    items.each = function(callback) {
+        for (var i = 0; i < results.length; i++)
+            callback.call(items[i]);
+    }
+    return items;
+}
+
+Element.prototype.appendAfter = function (element) {
+    element.parentNode.insertBefore(this, element.nextSibling);
+}, false;
+
+var loadScripts = [];
+var unloadScripts = [];
+window.$.load = function(handler) {
+    loadScripts.push(handler);
+}
+window.$.unload = function(handler) {
+    unloadScripts.push(handler);
+}
+
+function executeLoadScripts() {
+    loadScripts.forEach(function(handler) { handler(); });
+    loadScripts = [];
+}
+function executeUnloadScripts() {
+    unloadScripts.forEach(function(handler) { handler() });
+    unloadScripts = [];
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+    executeLoadScripts();
+});
+
+window.addEventListener("beforeunload", function() {
+    if (WEBACTION == false)
+        executeUnloadScripts();
+});
+
+function upgradeTimestamps() {
+    var pageLoad = moment.utc();
+    $$("time").each(function() {
+        var value = this.attributes["datetime"].value;
+        this.setAttribute("title", value);
+        this.innerHTML = moment(value).from(pageLoad);
+    });
+}

index 0000000..dc3decd
--- /dev/null

+import web
+from web import tx
+
+__all__ = ["tx", "admin_name", "admin_domain", "stripe_pk"]
+
+
+understory = web.application("understory")
+
+admin_name = understory.cfg["admin_name"]
+admin_domain = understory.cfg["admin_domain"]
+stripe_pk = understory.cfg["stripe_pk"]

index 0000000..3224519
--- /dev/null

+$def with (host_name)
+$var title = host_name
+<body class=h-app>
+<h1><a class="p-name u-url" href=/>$host_name</a></h1>
+<p>Host a <a href=//ragt.ag/code/projects/canopy>canopy</a> website for $$5/mo.</p>
+<form method=post>
+    $# <label><span>https://</span><input type=text name=domain maxlength=30
+    $# title="domain name must be 2 to 30 characters in length" required
+    $# pattern="[a-zA-Z\d-]{2,30}"><span>.cnpy.gdn</span></label>
+    $# <div id=card></div>
+    <button>Try Free</button>
+    $# <div id=error role=alert></div>
+</form>
+<p style="color:#586e75;font-size:.8em">This host is administered by
+<a href=//$admin_domain>$admin_name</a>.</p>
+<script>
+window.$$ = function(selector) {
+    if (typeof selector == "string")
+        var node = document.querySelector(selector);
+    else
+        var node = selector;
+    return node
+}
+var stripe = Stripe("$stripe_pk");
+var elements = stripe.elements();
+var style = {base: {backgroundColor: "#073642",
+                    color: "#839496",
+                    fontFamily: "Inconsolata, monospace",
+                    fontSmoothing: "antialiased",
+                    fontSize: "1em",
+                    "::placeholder": {color: "#586e75"}},
+             invalid: {color: "#dc322f",
+                       iconColor: "#dc322f"}};
+document.addEventListener("DOMContentLoaded", function() {
+    // var domain = $$("input[name=domain]");
+    // domain.addEventListener("keydown", function(event) {
+    //     if (!event.key.match(/[a-zA-Z\d-]/))
+    //         event.preventDefault();
+    // });
+    var card = elements.create('card', {style: style});
+    card.mount('#card');
+    var errorDisplay = $$("#error");
+    card.on('change', function(event) {
+        if (event.error) {
+            errorDisplay.textContent = event.error.message;
+        } else {
+            errorDisplay.textContent = '';
+        }
+    });
+    var form = $$("form");
+    form.addEventListener('submit', function(event) {
+        event.preventDefault();
+        form.reportValidity();
+        var response = fetch("/secret").then(function(response) {
+            return response.json();
+        }).then(function(responseJson) {
+            var username = "foobar"; // domain.value;
+            var clientSecret = responseJson.client_secret;
+            stripe.confirmCardPayment(clientSecret, {
+                payment_method: {card: card,
+                                 billing_details: {name: username}}
+            }).then(function(result) {
+                if (result.error) {
+                    errorDisplay.textContent = result.error.message;
+                } else {
+                    if (result.paymentIntent.status === 'succeeded') {
+                        fetch("/register",
+                              {method: "POST",
+                               body: JSON.stringify({username: username}),
+                              headers: {"Content-type":
+                                        "application/json; charset=UTF-8"}
+                              })
+                        .then(response => response.json())
+                        .then(json => {
+                            errorDisplay.textContent = json["passphrase"];
+                        });
+                    }
+                }
+            });
+        });
+    });
+});
+</script>
+</body>

index 0000000..e348618
--- /dev/null

+$def with (resource)
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<meta name=viewport content=initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width>
+<title>$resource.title</title>
+<script src=//js.stripe.com/v3></script>
+<style>
+@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700');
+@import url('https://fonts.googleapis.com/css2?family=Raleway');
+@import url('https://fonts.googleapis.com/css2?family=Inconsolata');
+* {
+    line-height: 1; }
+body {
+    background-color: #002b36;
+    color: #839496;
+    font: 18px "Raleway", sans-serif;
+    margin: 3em auto;
+    max-width: 25em;
+    text-align: center; }
+a:link {
+    color: #268bd2; }
+a:visited {
+    color: #6c71c4; }
+a:active {
+    color: #dc322f; }
+h1 {
+    font-family: "Dancing Script", cursive;
+    font-size: 3em;
+    margin: .5em 0; }
+h1 a {
+    text-decoration: none; }
+h1 a:link, h1 a:visited, h1 a:active {
+    color: #839496; }
+p {
+    line-height: 1.5;
+    margin: 1em 0; }
+code {
+    font-family: Inconsolata, monospace;
+    font-weight: bold; }
+label {
+    background-color: #073642;
+    color: #586e75;
+    display: grid;
+    font-family: Inconsolata, monospace;
+    font-size: 1em;
+    grid-column-gap: 0;
+    grid-template-columns: 4em auto 4.5em;
+    margin: 1em 0;
+    padding: .3em .3em .2em .3em; }
+input[type=text] {
+    background-color: #073642;
+    border: 0;
+    color: #839496;
+    font-family: "Inconsolata", monospace;
+    font-size: 1em;
+    padding: 0;
+    text-align: center;
+    text-transform: lowercase;
+    width: 100%; }
+input[type=text]::placeholder {
+    color: #586e75;
+    opacity: 1; }
+div#card {
+    background-color: #073642;
+    margin: 0 0 1em 0;
+    padding: .3em; }
+div#error {
+    line-height: 1.5;
+    margin: 3em 0; }
+button {
+    background: #2aa198;
+    border: 0;
+    color: #002b36;
+    font-family: Inconsolata, monospace;
+    font-weight: bold;
+    padding: .5em;
+    text-transform: uppercase; }
+</style>
+</head>
+$:resource
+</html>