Angelo Gladding
lahacker.net

dlv5vbq7lzlthol5 4b942a3185b37d00

South Pasadena, California, United States
74°F

Home CodecanopyCommits

Kaleidoscopes and loveliness

by Angelo Gladding 09CEF88F29CC1A44

Changed Files

--- a/canopy/__init__.py

+++ b/canopy/__init__.py

 """-a decentralized social web platform+decentralized social web platform  """ 
 import textwrap import uuid -import mm import qrcode import sh from src import git import web+from web import config_templates as web_config from web import tx  from .content import ResourceNotFound, dump_json
 from .content.write import quick_draft from . import security from . import types  # noqa-from .util import get_root, get_path, get_db, get_kv, global_kv, enqueue+from .util import get_root, get_path, get_db, get_kv, global_kv+from .util import enqueue, prefix, config, config_servers -__all__ = ["setup_servers", "config_servers", "setup_host", "enqueue",-           "dump_json"]+__all__ = ["setup_servers", "setup_host", "enqueue", "dump_json"] -prefix = pathlib.Path("/home/gaea/detritus") TRUNCATION = 14  # XXX system_packages = ("canopy", "kv", "mf", "mkdn", "mkup", "mm", "pkg",
        f"canopy_{root_hash}:canopy_tor_{root_hash}")  -# TODO rename to reload_servers() ?-def config_servers() -> None:-    """-    reload servers--    """-    root = get_root()-    root_hash = web.get_host_hash(root)--    def config_web_server(nginx_conf_d, root_hash):-        for identity in (root / "var/identities").iterdir():-            su("ln", "-sf", (identity / "nginx/domain.conf").resolve(),-               nginx_conf_d / "01-{}-{}-domain.conf".format(root_hash,-                                                            identity.name))-            su("ln", "-sf", (identity / "nginx/hosting.conf").resolve(),-               nginx_conf_d / "01-{}-{}-hosting.conf".format(root_hash,-                                                             identity.name))-            su("ln", "-sf", (identity / "nginx/tor/nginx.conf").resolve(),-               nginx_conf_d / "01-{}-{}-tor.conf".format(root_hash,-                                                         identity.name))--    web.config_servers(root, config_web_server)-    su("rm", prefix / "var/tor/*.sock", "-f")--    su("supervisorctl", "restart",-       f"canopy_{root_hash}:canopy_nginx_{root_hash}")-- def setup_host(name, passphrase, keysize, domain=None) -> None:     """     initialize and configure an identity
     nginx_dir = home / "nginx"     tor_dir = nginx_dir / "tor"     tor_dir.mkdir(parents=True)-    web_config = web.config_templates-    canopy_config = mm.templates(__name__).config -    host_spec = canopy_config.nginx_host_body_spec(identity, root, root_hash)+    host_spec = config.nginx_host_body_spec(identity, root, root_hash)     host_body = web_config.nginx_host_body("canopy", identity, root,                                            src_dir, host_spec) 
             config_servers()          configure_domain(False)-        web.letsencrypt.generate_host_cert(root, nginx_dir / "domain", domain)+        ssl_dir = nginx_dir / "domain_ssl"+        web.letsencrypt.generate_host_cert(root, ssl_dir, domain)         configure_domain(True)-        with (nginx_dir / "ssl/hostname").open("w") as fp:+        with (ssl_dir / "hostname").open("w") as fp:             print(domain, file=fp)         global_kv["hosts"][domain] = identity         global_kv["identities", identity].append(domain)--    with (nginx_dir / "hosting.conf").open("w") as fp:-        fp.write("")     config_servers()      master_key, signing_key = security.initialize_gpg(gpgdir, onion, domain,

--- a/canopy/__web__/__init__.py

+++ b/canopy/__web__/__init__.py

 from ..content.read import get_resources from ..content.write import quick_draft from ..security import export_public_key-from ..util import get_onion, get_path, if_owner, enqueue+from ..util import get_onion, get_path, if_owner, enqueue, global_kv from ..util import content as content_util from .. import webmention from .util import app, view
 from . import chat from . import content from . import devices-from . import hosting from . import media+from . import orchard from . import security from . import system  -__all__ = ["chat", "content", "hosting", "news", "player", "security",+__all__ = ["chat", "content", "orchard", "news", "player", "security",            "system",            "Sitemap", "SitemapInclusion", "RobotsExclusion",            "Search", "OpenSearchDescription",
  app.mount(devices.app) app.mount(media.app)+app.mount(orchard.app) app.mount(security.app) app.mount(system.app) app.view = view
   @app.wrap+def serve_orchard(handle, app):+    """+    identity registration at public suffix++    """+    for admin_hostname, identity in global_kv["hosts"].items():+        if admin_hostname.endswith(".onion"):+            continue+    if tx.origin == global_kv["orchard:domains:primary"]:+        web.header("Content-Type", "text/html")+        raise web.OK(view.orchard.register(global_kv["orchard:name"],+                                           admin_hostname))+    yield+++@app.wrap def contextualize(handler, app):     """     update transaction context with current identity's data

--- a/canopy/__web__/devices.py

+++ b/canopy/__web__/devices.py

                                vals=[device["id"], device["ip"]])         if "identity" in capabilities:             identities = [i for i in get_resources("identities")-                          if i.get("-hosting-device") == device["id"]]+                          if i.get("-orchard-grove") == device["id"]]             templates["identity"] = view.identity(identities)         if "storage" in capabilities:             vpn = ps(ssh, "openvpn connection.ovpn")

rename from canopy/__web__/hosting.py

rename to canopy/__web__/orchard.py

  from ..content.read import get_resources from ..util import enqueue, send_email, send_text-from ..util import hosting, digitalocean, dynadot-from .util import app, view-view = view.hosting+from ..util import orchard, digitalocean, dynadot, setup_orchard, global_kv+from .util import view+view = view.orchard  +app = web.application("canopy-orchard", mount_prefix="orchard", factor=r"\w+") # google_recaptcha_sitekey = "6Lei8i4UAAAAANIMaYiadBU69XtjQ_VmzORC9WUL" # google_recaptcha_secretkey = "6Lei8i4UAAAAAHNWENrtsVXhGvHLgcchLt4aRnHb" # google_recaptcha_verify_ep = \ #   "https://www.google.com/recaptcha/api/siteverify"  -@app.route(r"hosting")-class Hosting:+@app.route(r"")+class Orchard:      def GET(self):         if tx.user.is_owner:             identities = [i for i in get_resources("identities")-                          if "-hosting-device" in i]-            return view.index(identities,-                              str(tx.kv["hosting:domains:primary"]),-                              str(tx.kv["hosting:domains:alias"]),-                              str(tx.kv["hosting:snapshot_id"]))+                          if "-orchard-grove" in i]+            return view.admin(identities, str(global_kv["orchard:name"]),+                              str(global_kv["orchard:domains:primary"]),+                              str(global_kv["orchard:domains:alias"]),+                              str(global_kv["orchard:snapshot_id"]))         # TODO elif tx.user is customer: show billing history         return view.register() -    def POST(self):-        form = web.form("name", "passphrase", subdomain=None)-        enqueue(hosting.spawn_identity, form.name, form.passphrase,-                form.subdomain, str(tx.kv["hosting:domains:primary"]))-        return "spawning identity.."- -@app.route(r"hosting/domains")+@app.route(r"domains") class Domains:      def GET(self):-        primary = str(tx.kv["hosting:domains:primary"])-        alias = str(tx.kv["hosting:domains:alias"])+        primary = str(global_kv["orchard:domains:primary"])+        alias = str(global_kv["orchard:domains:alias"])         dd = dynadot.Client(tx.kv["providers:dynadot"]["api_token"])         return view.domains(primary, alias, list(dd.list_domain()))      def POST(self):         form = web.form("action", "priority")         if form.action == "set":-            tx.kv[f"hosting:domains:{form.priority}"] = \+            global_kv[f"orchard:domains:{form.priority}"] = \                 web.form("domain").domain-            # TODO XXX schedule("*/30 * * * *", hosting.rebalance_grove)+            enqueue(setup_orchard)         elif form.action == "unset":-            tx.kv[f"hosting:domains:{form.priority}"] = ""+            global_kv[f"orchard:domains:{form.priority}"] = ""         return f"{form.priority} domain {form.action}"  -@app.route(r"hosting/snapshot")+@app.route(r"snapshot") class Snapshot:      def GET(self):         cli = digitalocean.get_client()         return view.snapshot(tx.kv["providers:digitalocean"]["api_token"],-                             tx.kv["hosting:snapshot_id"],+                             global_kv["orchard:snapshot_id"],                              cli.get_snapshots_of_droplets())      def POST(self):-        enqueue(hosting.prepare_clone)+        enqueue(orchard.prepare_clone)         return "preparing master identity clone.. will take ~20 minutes"  -@app.route(r"hosting/register")+@app.route(r"register") class Register:      def GET(self):         return view.register() +    # def POST(self):+    #     form = web.form("name", "passphrase", subdomain=None)+    #     enqueue(orchard.spawn_identity, form.name, form.passphrase,+    #             form.subdomain, str(tx.kv["orchard:domains:primary"]))+    #     return "spawning identity.."+     def POST(self):-        host_domain = tx.kv["hosting:domains:primary"]+        host_domain = str(global_kv["orchard:domains:primary"])         if not host_domain:-            raise web.InternalServerError("hosting unavailable: "+            raise web.InternalServerError("orchard unavailable: "                                           "no host domain configured")         form = web.form("name", "passphrase", "email", "phone", "service",                         domain="")  # , "g-recaptcha-response")
         #                           params=recaptcha_params).json()         # if not recaptcha["success"]:         #     return view.registration.error("captcha")+        # TODO ensure unique userid         userid = web.nbrandom(7)         name = form.name.strip()         email_confirm_code = ""
         if form.email:             send_email(form.email, "Your canopy registration",                        f"Please confirm your e-mail address:"-                       f" https://{tx.host.name}/hosting/confirm/email?"+                       f" https://{tx.host.name}/orchard/confirm/email?"                        f"user={userid}&code={email_confirm_code}")         if form.phone:             send_text(form.phone,                       f"Please confirm your phone number:"-                      f" https://{tx.host.name}/hosting/confirm/phone?"+                      f" https://{tx.host.name}/orchard/confirm/phone?"                       f"user={userid}&code={phone_confirm_code}")         domain = {}         if form.domain:
         # XXX email_confirm_code=email_confirm_code,         # XXX phone_confirm_code=phone_confirm_code,         # XXX domain=form.domain, service=form.service-        enqueue(hosting.spawn_identity, name, form.passphrase,+        print(orchard.spawn_identity, name, form.passphrase,+              form.domain, host_domain)+        return "done"+        enqueue(orchard.spawn_identity, name, form.passphrase,                 form.domain, host_domain)         tx.user.session = {"id": userid, "name": name, "uri": domain["domain"]}         return view.registration.completed()  -@app.route(r"hosting/register/{factor}")+@app.route(r"register/{factor}") class ConfirmSecondFactor:      def GET(self):

--- a/canopy/__web__/static/scripts/enliven.js

+++ b/canopy/__web__/static/scripts/enliven.js

 /*! js-cookie v2.2.0 | MIT */ !function(e){var n=!1;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var o=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=o,t}}}(function(){function e(){for(var e=0,n={};e<arguments.length;e++){var o=arguments[e];for(var t in o)n[t]=o[t]}return n}function n(o){function t(n,r,i){var c;if("undefined"!=typeof document){if(arguments.length>1){if("number"==typeof(i=e({path:"/"},t.defaults,i)).expires){var a=new Date;a.setMilliseconds(a.getMilliseconds()+864e5*i.expires),i.expires=a}i.expires=i.expires?i.expires.toUTCString():"";try{c=JSON.stringify(r),/^[\{\[]/.test(c)&&(r=c)}catch(e){}r=o.write?o.write(r,n):encodeURIComponent(r+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=(n=(n=encodeURIComponent(n+"")).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent)).replace(/[\(\)]/g,escape);var s="";for(var f in i)i[f]&&(s+="; "+f,!0!==i[f]&&(s+="="+i[f]));return document.cookie=n+"="+r+s}n||(c={});for(var p=document.cookie?document.cookie.split("; "):[],d=/(%[0-9A-Z]{2})+/g,u=0;u<p.length;u++){var l=p[u].split("="),C=l.slice(1).join("=");this.json||'"'!==C.charAt(0)||(C=C.slice(1,-1));try{var m=l[0].replace(d,decodeURIComponent);if(C=o.read?o.read(C,m):o(C,m)||C.replace(d,decodeURIComponent),this.json)try{C=JSON.parse(C)}catch(e){}if(n===m){c=C;break}n||(c[m]=C)}catch(e){}}return c}}return t.set=t,t.get=function(e){return t.call(t,e)},t.getJSON=function(){return t.apply({json:!0},[].slice.call(arguments))},t.defaults={},t.remove=function(n,o){t(n,"",e(o,{expires:-1}))},t.withConverter=n,t}return n(function(){})}); -window.$ = function(selector) {-    var node = document.querySelector(selector);-    return node-}--// window.$.load = function(callback) {-//     document.addEventListener("DOMContentLoaded", callback);-// }-// -// var unloadScripts = [];-// -// window.$.unload = function(callback) {-//     window.addEventListener("beforeunload", callback);-// }--var loadScripts = [];-var unloadScripts = [];-window.$.load = function(handler) {-    loadScripts.push(handler);-}-window.$.unload = function(handler) {-    unloadScripts.push(handler);-}--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;- function upgradeTimestamps() {     var pageLoad = moment.utc();     $$("time").each(function() {
 $.load(function() {     upgradeTimestamps(); -    window.debugSocket = new WebSocketClient.default(socket_origin + "debug",-                                                     null,-                                                     {backoff: "exponential"});-    window.onerror = function(message, url, lineNumber) {-        window.debugSocket.send({message: message, url: url,-                                 lineNumber: lineNumber});-    };+    // TODO window.debugSocket = new WebSocketClient.default(socket_origin + "debug",+    // TODO                                                  null,+    // TODO                                                  {backoff: "exponential"});+    // TODO window.onerror = function(message, url, lineNumber) {+    // TODO     window.debugSocket.send({message: message, url: url,+    // TODO                              lineNumber: lineNumber});+    // TODO };      if (window.navigator.standalone && $("#signout"))         $("#signout").style.display = "none";
     updateArticle(window.location, e.state.scroll); } -function executeLoadScripts() {-    loadScripts.forEach(function(handler) { handler(); });-    loadScripts = [];-}-function executeUnloadScripts() {-    unloadScripts.forEach(function(handler) { handler() });-    unloadScripts = [];-}- function updateArticle(url, scroll) {     $("#loading").style.display = "block";     var xhr = new XMLHttpRequest();
     xhr.send(); } +// TODO back button when coming back from different origin or same page hash+ function upgradeLink() {-    var url = this.href-    // TODO handle fragment identifier on *another page* at *this origin*-    if (url.indexOf(origin) == -1 || url.indexOf("#") > -1)-        return+    var url = this.href;+    if (url.indexOf(origin) == -1) {  // different origin+        return  // use native+    }+    if (url.indexOf("#") > -1) {  // same origin, contains fragment identifier+        var url_parts = url.split("#");+        var current_url_parts = window.location.href.split("#");+        if (url_parts[0] == current_url_parts[0])  // same page+            return  // use native+        console.log(url_parts, current_url_parts);  // different page+    }     this.addEventListener("click", (ev) => {         if (ev.ctrlKey)             return
                              window.location);         updateArticle(url, 0);         history.pushState({scroll: 0}, "title", url);-    })+    }); } -document.addEventListener("DOMContentLoaded", () => {-    loadScripts.forEach(function(handler) { handler(); });-    loadScripts = [];-    history.pushState({scroll: 0}, "title", window.location);-    $$("a:not(.breakout)").each(upgradeLink);--    /* var hammer = new Hammer($("body"));-    // hammer.on("panleft panright", function(e) {-    //     // right is back, left is forward-    //     // e.target.classList.toggle('expand');-    //     // if (e.type == "panleft")-    //     //     alert(history.popState());-    //     if (e.type == "panright") {-    //         history.back();-    //     }-    // }); */--});--window.addEventListener("beforeunload", function() {-    unloadScripts.forEach(function(handler) { handler(); });-});------ /*****************************************************************/  window.addEventListener('online',  updateOnlineStatus);

index 0000000..b9a72fb

--- /dev/null

+window.$ = function(selector) {+    var node = document.querySelector(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() {+    executeUnloadScripts();+});

index a8e064e..0000000

--- a/canopy/__web__/templates/devices/identity.html

-$def with (identities)--<ul>-$for identity in identities:-    <li>$identity["name"]<br><small><a-    href=https://$identity["alt-svc"]>$identity["alt-svc"]</a></small></li>-</ul>

index 23c3605..0000000

--- a/canopy/__web__/templates/hosting/register.html

-$def with ()-$var title: Hosting--<p>The Canopy is a decentralized social network of identity websites. You-can self-host by using the <a href=/code/canopy>canopy software</a> manually-or you can rely on me to host on your behalf.</p>--<h2>Hosting On Your Behalf</h2>--<form id=create method=post action=/hosting/register>--<p class=terms>The canopy software used is currently in beta.-<em>$tx.owner["name"]</em> accepts no responsibility for software defects-or malfunction and makes no guarantee of service uptime. You must be over-the age of 18 to register.</p>-<div style="text-align: right;"><label><input type=checkbox name=agree_to_terms-title="You must agree to the terms of service" required> I agree<br><small><em style=color:#dc322f;>required</em></small></label></div>--<fieldset id=services>-<legend>Service Plans</legend>-<dl>-    <label>-    <dt><input type=radio name=service value=shared checked required>-    shared server</dt>-    <dd>-        <em><small>$$3/month, 5 GB storage, &frac14; TB bandwidth</small></em>-    </dd>-    </label>-    <label>-    <dt><input type=radio name=service value=private required>-    private server</dt>-    <dd>-        <em><small>$$8/month, 20 GB storage, 1 TB bandwidth</small></em>-    </dd>-    </label>-</dl>-</fieldset>--<script>-$$.load(function() {-    $$$$("input[name=service]").each(function() {-        this.onclick = function() {-            $$$$("input[name=service]").each(function() {-                // this.parentNode.parentNode.style.backgroundColor = "#cdcdcd";-                this.parentNode.parentNode.style.backgroundColor = "#002b36";-            });-            // this.parentNode.parentNode.style.backgroundColor = "#ababab";-            this.parentNode.parentNode.style.backgroundColor = "#073642";-        };-    });-});-</script>--<label>Payment card <small><em style=color:#dc322f;>required</em></small><br>-<div class="bounding form-row"><div id=card-element></div></div></label>-<div id=card-errors role=alert></div>--<label>Passphrase <small><em style=color:#dc322f;>required</em></small><br>-<div class=bounding><input type=password name=passphrase required title="Passphrase must be provided"></div></label><br>--<label>Name<br>-<div class=bounding><input type=text name=name required-title="Name must be provided"></div></label><br>--<label>Domain name<br>-<div class="bounding code-wrapped"><input type=text-title="Domain name must be 2 to 30 characters in length" name=domain-maxlength=30 pattern="[a-z\d-]{2,30}"><code>.cnpy.gdn</code></div><p style="margin:0;text-align:right;"><small><em>increases accessibility, decreases privacy</em></small></p></label><br>--<label>Email address <small><em>increases security/accessibility, decreases privacy</em></small><br>-<div class=bounding><input type=email name=email></div></label><br>--<label>Phone number <small><em>increases security/accessibility, decreases privacy</em></small><br>-<div class=bounding><input type=tel name=phone></div></label>--$# <div class=g-recaptcha data-sitekey=$recaptcha_key data-callback=submitRegistration data-size=invisible></div>--<div class=buttons><button>create</button></div>--</form>--$def head():-    <style>-    header + p {-        -webkit-hyphens: auto;-        -moz-hyphens: auto;-        hyphens: auto;-        margin: 2em 0; }-    form label div code,-    input[name=domain], input[name=email], input[name=tel], p.terms {-        font-family: "Ubuntu Mono", monospace; }-    form label div code {-        background: none;-        color: #666;-        text-align: right; }-    input[name=domain] {-        font-size: 1.1em;-        text-align: center; }-    #hosting ul {-        list-style: none;-        padding-left: 0; }-    .code-wrapped {-        display: grid;-        grid-column-gap: 0;-        grid-template-columns: auto 6em; }-    pre code:before {-        content: "$$ "; }-    </style>-    $# <script src=//google.com/recaptcha/api.js></script>-    $# <script src=/static/scripts/stripe-3.js-    $#     integrity="sha256-$get_integrity('scripts/stripe-3.js')"></script>-    <script src=//js.stripe.com/v3></script>-    <script>-    var stripe = Stripe("pk_test_vny8kDMevOCMcPh8zjJiu4is");-    var elements = stripe.elements();--    // XXX Custom styling can be passed to options when creating an Element.-    // XXX var style = {-    // XXX     base: {-    // XXX         // Add your base input styles here. For example:-    // XXX         fontSize: '16px',-    // XXX         color: "#32325d",-    // XXX     }-    // XXX };--    // Create an instance of the card Element-    // XXX var card = elements.create('card', {style: style});-    var card = elements.create('card');--    function stripeTokenHandler(token) {-        // Insert the token ID into the form so it gets submitted to the server-        var form = document.getElementById('create');-        var hiddenInput = document.createElement('input');-        hiddenInput.setAttribute('type', 'hidden');-        hiddenInput.setAttribute('name', 'stripeToken');-        hiddenInput.setAttribute('value', token.id);-        form.appendChild(hiddenInput);-        form.submit();-    }--    $$.load(function() {-        $$("input[name=agree_to_terms]").required = false;-        $$("input[name=name]").required = false;-        $$("input[name=passphrase]").required = false;-        $$("input[name=passphrase]").addEventListener("keypress", function(event) {-            var score = 1;-            if (this.value.length > 15)-                score = score * 10;-            if (score < 10)-                this.parentNode.style.borderColor = "#c33";-            else-                this.parentNode.style.borderColor = "#eee";-        });--        // Add an instance of the card Element into the `card-element` <div>-        card.mount('#card-element');--        card.addEventListener('change', function(event) {-            var displayError = document.getElementById('card-errors');-            if (event.error) {-                displayError.textContent = event.error.message;-            } else {-                displayError.textContent = '';-            }-        });--        // Create a token or display an error when the form is submitted.-        var form = document.getElementById('create');-        form.addEventListener('submit', function(event) {-            event.preventDefault();--            if (form.reportValidity()) {-                /*if ($$("input[name=passphrase]").value !=-                    $$("input[name=repeated_passphrase]").value) {-                    alert("Passphrases don't match.");-                } else*/ if (!$$("input[name=agree_to_terms]").checked) {-                    alert("You must agree to the terms of service.");-                }-                // } else {-                //     grecaptcha.execute();-            }--            stripe.createToken(card).then(function(result) {-                if (result.error) {-                    // Inform the customer that there was an error-                    var errorElement = document.getElementById('card-errors');-                    errorElement.textContent = result.error.message;-                } else {-                    // Send the token to your server-                    stripeTokenHandler(result.token);-                }-            });-        });--    });-    </script>-$var head = head

rename from canopy/__web__/templates/hosting/index.html

rename to canopy/__web__/templates/orchard/admin.html

-$def with (identities, primary, alias, snapshot_id)-$var title: Hosting+$def with (identities, name, primary, alias, snapshot_id)+$var title: Orchard  <ul> $for identity in identities:
         <a href=https://$identity["alt-svc"]>$identity["alt-svc"]</a>\     $else:         <a href=https://$identity["uid"]>$identity["uid"]</a>\-    <br><code><a href=/system/devices/$identity["-hosting-device"]>$identity["-hosting-device"]</a></code>+    <br><code><a href=/system/devices/$identity["-orchard-grove"]>$identity["-orchard-grove"]</a></code>     </small></li> </ul>  $def aside():+    <p>Name:+    $if name:+        $name+    $else:+        none+    (<a href=/orchard/name>edit</a>)</p>     <p>Domain:     $if primary:         $primary
             , $alias     $else:         none-    (<a href=/hosting/domains>edit</a>)</p>+    (<a href=/orchard/domains>edit</a>)</p>     <p>Snapshot:     $if snapshot_id:         $snapshot_id     $else:         none-    (<a href=/hosting/snapshot>edit</a>)</p>+    (<a href=/orchard/snapshot>edit</a>)</p>     $if primary and snapshot_id:-        <form action=/hosting method=post>+        <form action=/orchard method=post>         <fieldset>         <legend>Identity Details</legend>         <label>Name

rename from canopy/__web__/templates/hosting/domains.html

rename to canopy/__web__/templates/orchard/domains.html

 $def with (primary, alias, domains)-$var breadcrumbs = ("hosting", "Hosting")+$var breadcrumbs = ("orchard", "Orchard") $var title: Domains -<form action=/hosting/domains method=post>+<form action=/orchard/domains method=post> <fieldset> <legend>Primary</legend> <input type=hidden name=priority value=primary>
 </form>  $if primary:-    <form action=/hosting/domains method=post>+    <form action=/orchard/domains method=post>     <fieldset>     <legend>Alias</legend>     <input type=hidden name=priority value=alias>

index 0000000..73cf5a7

--- /dev/null

+$def with (name, admin_hostname)+<html>+<head>+<title>$name</title>+<meta name=viewport+    content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">+<style>+html {+    background-color: ForestGreen; }+body {+    background-color: LightGreen;+    border-color: DarkCyan;+    border-style: solid;+    border-width: 0 2em;+    margin: 0 auto;+    max-width: 30em;+    padding: 2em; }+header + p {+    -webkit-hyphens: auto;+    -moz-hyphens: auto;+    hyphens: auto;+    margin: 2em 0; }+form label div code,+input[name=domain], input[name=email], input[name=tel], p.terms {+    font-family: "Ubuntu Mono", monospace; }+form label div code {+    background: none;+    color: #666;+    text-align: right; }+input[type=text] {+    width: 100%; }+input[name=domain] {+    font-size: 1.1em;+    text-align: center; }+.code-wrapped {+    display: grid;+    grid-column-gap: 0;+    grid-template-columns: auto 6em; }+pre code:before {+    content: "$$ "; }+</style>+<script src=//$admin_hostname/static/scripts/utils.js></script>+<script src=//google.com/recaptcha/api.js></script>+<script src=//js.stripe.com/v3></script>+<script>+var stripe = Stripe("pk_test_vny8kDMevOCMcPh8zjJiu4is");+var elements = stripe.elements();++// XXX Custom styling can be passed to options when creating an Element.+// XXX var style = {+// XXX     base: {+// XXX         // Add your base input styles here. For example:+// XXX         fontSize: '16px',+// XXX         color: "#32325d",+// XXX     }+// XXX };++// Create an instance of the card Element+// XXX var card = elements.create('card', {style: style});+var card = elements.create('card');++function stripeTokenHandler(token) {+    // Insert the token ID into the form so it gets submitted to the server+    var form = document.getElementById('create');+    var hiddenInput = document.createElement('input');+    hiddenInput.setAttribute('type', 'hidden');+    hiddenInput.setAttribute('name', 'stripeToken');+    hiddenInput.setAttribute('value', token.id);+    form.appendChild(hiddenInput);+    form.submit();+}++$$.load(function() {+    $$("input[name=agree_to_terms]").required = false;+    $$("input[name=name]").required = false;+    $$("input[name=passphrase]").required = false;+    $$("input[name=passphrase]").addEventListener("keypress", function(event) {+        var score = 1;+        if (this.value.length > 15)+            score = score * 10;+        if (score < 10)+            this.parentNode.style.borderColor = "#c33";+        else+            this.parentNode.style.borderColor = "#eee";+    });++    // Add an instance of the card Element into the `card-element` <div>+    card.mount('#card-element');++    card.addEventListener('change', function(event) {+        var displayError = document.getElementById('card-errors');+        if (event.error) {+            displayError.textContent = event.error.message;+        } else {+            displayError.textContent = '';+        }+    });++    // Create a token or display an error when the form is submitted.+    var form = document.getElementById('create');+    form.addEventListener('submit', function(event) {+        event.preventDefault();++        if (form.reportValidity()) {+            /*if ($$("input[name=passphrase]").value !=+                $$("input[name=repeated_passphrase]").value) {+                alert("Passphrases don't match.");+            } else*/ if (!$$("input[name=agree_to_terms]").checked) {+                alert("You must agree to the terms of service.");+            }+            // } else {+            //     grecaptcha.execute();+        }++        stripe.createToken(card).then(function(result) {+            if (result.error) {+                // Inform the customer that there was an error+                var errorElement = document.getElementById('card-errors');+                errorElement.textContent = result.error.message;+            } else {+                // Send the token to your server+                stripeTokenHandler(result.token);+            }+        });+    });+});+</script>+</head>+<body>+<h1>$name</h1>++<p>The <a href=//canopy.guide>canopy</a> is a decentralized social web platform. $name is an identity host.</p>++<form id=create method=post action=//$admin_hostname/orchard/register>++<fieldset id=services>+<legend>Service Plans</legend>+<dl>+    <label>+    <dt><input type=radio name=service value=shared checked required>+    shared server</dt>+    <dd>+        <em><small>$$3/month, 5 GB storage, &frac14; TB bandwidth, partial control</small></em>+    </dd>+    </label>+    <label>+    <dt><input type=radio name=service value=private required>+    private server</dt>+    <dd>+        <em><small>$$8/month, 20 GB storage, 1 TB bandwidth, full control</small></em>+    </dd>+    </label>+</dl>+</fieldset>++$# <script>+$# $$.load(function() {+$#     $$$$("input[name=service]").each(function() {+$#         this.onclick = function() {+$#             $$$$("input[name=service]").each(function() {+$#                 // this.parentNode.parentNode.style.backgroundColor = "#cdcdcd";+$#                 this.parentNode.parentNode.style.backgroundColor = "#002b36";+$#             });+$#             // this.parentNode.parentNode.style.backgroundColor = "#ababab";+$#             this.parentNode.parentNode.style.backgroundColor = "#073642";+$#         };+$#     });+$# });+$# </script>++<label>Payment card <small><em style=color:#dc322f;>required</em></small><br>+<div class="bounding form-row"><div id=card-element></div></div></label>+<div id=card-errors role=alert></div>++<label>Passphrase <small><em style=color:#dc322f;>required</em></small><br>+<div class=bounding><input type=password name=passphrase required title="Passphrase must be provided"></div></label><br>++<label>Name<br>+<div class=bounding><input type=text name=name required+title="Name must be provided"></div></label><br>++<label>Domain name<br>+<div class="bounding code-wrapped"><input type=text+title="Domain name must be 2 to 30 characters in length" name=domain+maxlength=30 pattern="[a-z\d-]{2,30}"><code>.cnpy.gdn</code></div><p style="margin:0;text-align:right;"><small><em>increases accessibility, decreases privacy</em></small></p></label><br>++<label>Email address <small><em>increases security/accessibility, decreases privacy</em></small><br>+<div class=bounding><input type=email name=email></div></label><br>++<label>Phone number <small><em>increases security/accessibility, decreases privacy</em></small><br>+<div class=bounding><input type=tel name=phone></div></label>++$# <div class=g-recaptcha data-sitekey=$recaptcha_key data-callback=submitRegistration data-size=invisible></div>++<p class=terms>You must be over the age of 18.</p>+<div style="text-align: right;"><label><input type=checkbox name=agree_to_terms+title="You must agree to the terms of service" required> I agree<br><small><em style=color:#dc322f;>required</em></small></label></div>++<div class=buttons><button>Create</button></div>++</form>+</body>+</html>

rename from canopy/__web__/templates/hosting/snapshot.html

rename to canopy/__web__/templates/orchard/snapshot.html

 $def with (api_token, snapshot_id, snapshots)-$var breadcrumbs = ("hosting", "Hosting")+$var breadcrumbs = ("orchard", "Orchard") $var title: Snapshot  $if api_token:-    <form action=/hosting/snapshot method=post>+    <form action=/orchard/snapshot method=post>     $if snapshot_id:         <button>Regenerate</button>     $else:

--- a/canopy/__web__/templates/system/domain.html

+++ b/canopy/__web__/templates/system/domain.html

 $var breadcrumbs = ("system", "System", "domains", "Domains") $var title: $domain -$if tx.kv["hosting:domain"] == domain:+$if domain in (global_kv["orchard:domains:primary"], global_kv["orchard:domains:alias"]):     <p>This domain is being used to host identities.</p>

--- a/canopy/__web__/templates/template.html

+++ b/canopy/__web__/templates/template.html

  <!-- TODO pack everything async-able --> <script src=/static/scripts/pouchdb-7.0.0.js></script>-<script src=/static/scripts/enliven.js?asd=123></script>+<script src=/static/scripts/utils.js></script>+<script src=/static/scripts/enliven.js></script> $# <script> $# if(navigator.serviceWorker) $#     navigator.serviceWorker.register("/static/scripts/serviceworker.js");
 const punct_re = /$:_punct_re/;  // used for permalink slugs const owner = "$name"; const origin = "$tx.origin";++$$.load(function() {+    $$$$("a:not(.breakout)").each(upgradeLink);+    history.pushState({scroll: 0}, "title", window.location);+    // $# var hammer = new Hammer($("body"));+    // $# hammer.on("panleft panright", function(e) {+    // $#     // right is back, left is forward+    // $#     // e.target.classList.toggle('expand');+    // $#     // if (e.type == "panleft")+    // $#     //     alert(history.popState());+    // $#     if (e.type == "panright") {+    // $#         history.back();+    // $#     }+    // $# });+}); </script> $if getattr(resource, "script", None):     $:str(resource.script()).strip()

--- a/canopy/__web__/util.py

+++ b/canopy/__web__/util.py

                        channel=r"[A-Za-z\d][A-Za-z\d-]+[A-Za-z\d]", url=r".*", -                      factor=r"\w+",-                       email_digest=r"[0-9a-f]{32}", email_payload=r"\d+") view = mm.templates("canopy.__web__", tx=web.tx) for _post_type in post_types:

index 0000000..69498ff

--- /dev/null

+$def with (domain, root_dir, nginx_dir, tls_on)+server {+    listen       80;+    server_name  $domain;++$if tls_on:+        location / {+            return  301  https://$domain$$request_uri;+        }+$else:+        location /.well-known/acme-challenge/ {+            alias      $root_dir/var/letsencrypt-challenges/;+            try_files  $$uri  =404;+        }+}++$if tls_on:+    server {+        listen       443 http2 ssl;+        server_name  $domain;++        ssl_certificate            $nginx_dir/overstory_ssl/chained.pem;+        ssl_certificate_key        $nginx_dir/overstory_ssl/domain.key;+        ssl_session_cache          shared:SSL:10m;+        ssl_session_timeout        30m;+        ssl_protocols              TLSv1.2;+        ssl_dhparam                /home/gaea/detritus/nginx/conf/dhparam.pem;+        ssl_prefer_server_ciphers  on;+        ssl_ciphers                ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;++        charset  utf-8;+        add_header  X-Powered-By  "canopy";+        add_header  X-Frame-Options  "SAMEORIGIN";+        add_header  X-Content-Type-Options  "nosniff";+        # add_header  Strict-Transport-Security  "max-age=15768000"  always;++        $# error_page  403 404  /error/40x.html;+        $# error_page  500 502 503 504  /error/50x.html;++        $# location /error/ {+        $#     internal;+        $#     alias  $src_dir/$pkg/$pkg/;+        $# }++        location / {+            uwsgi_param               Host  $$http_host;+            uwsgi_param               X-Real-IP  $$remote_addr;+            uwsgi_param               X-Forwarded-For  $$proxy_add_x_forwarded_for;+            uwsgi_max_temp_file_size  0;+            uwsgi_pass                unix:$root_dir/run/web.sock;+            include                   /home/gaea/detritus/nginx/conf/uwsgi_params;+        }+    }

--- a/canopy/types/identities/__web__.py

+++ b/canopy/types/identities/__web__.py

 from ...content.write import quick_draft from ...security import get_keyring, get_keys, gpg from ...security import export_public_key-from ...util import get_hostnames, hosting+from ...util import get_hostnames, orchard from ..resources import slugs, Index, Versions, Version, Resource # XXX from ..resources import ResourceData, ResourceSignature, Resource 
         return view.index(resource, get_keys(gpg, **kwargs))      def delete(self, resource):-        if "-hosting-device" in resource:+        if "-orchard-grove" in resource:             print("DELETING DATABASE ENTRY")             device = tx.db.select("identity_devices", join="devices",                                   where="id = ?",-                                  vals=[resource["-hosting-device"]])[0]+                                  vals=[resource["-orchard-grove"]])[0]             tx.db.update("identity_devices", what="occupancy = ?",                          where="id = ? and occupancy = ?",                          vals=[device["occupancy"] - 1, device["id"],                                device["occupancy"]])             print("DELETING TREE ON SERVER")-            ssh = hosting.do_get_ssh("gaea", device["ip"])+            ssh = orchard.do_get_ssh("gaea", device["ip"])             ssh("python3", "gaea.py", "destroy-tree", resource["-uuid"])  

--- a/canopy/util/__init__.py

+++ b/canopy/util/__init__.py

 import contextlib import email+import email.mime.text import hashlib import imaplib import json
  import kv import lxml.html.clean as clean+import mm import pendulum import phonenumbers as pn import secrets+from sh import sudo as su import sql from src import git import twilio
 cached_sqldbs = {} global_kv = kv.db("canopy", ":", {"hosts": "hash",                                   "identities:{identity}": "list",+                                  "orchard:name": "string",+                                  "orchard:domains:primary": "string",+                                  "orchard:domains:alias": "string",+                                  "orchard:snapshot_id": "string",                                   "jobqueue": "list"},                   identity=r"[\w-]+")+prefix = pathlib.Path("/home/gaea/detritus")+config = mm.templates("canopy").config   def get_kv():
                "providers:wasabi": "hash",                 "vpn:active": "string",-               "hosting:domains:primary": "string",-               "hosting:domains:alias": "string",-               "hosting:snapshot_id": "string",                "weather:apparent_temperature": "string",                "weather:air_quality": "string", 
     cfg = tx.kv["providers:email"]     if not cfg:         raise web.SeeOther("/system/providers/email")-    with SMTPClient(cfg["smtp_host"], cfg["smtp_user"], cfg["smtp_pass"]) as c:-        c.send(tx.kv["name"], to, subject, body)+    with SMTPClient(cfg["smtp_host"], cfg["smtp_username"],+                    cfg["smtp_password"]) as client:+        client.send(tx.owner["name"], to, subject, body)   class SMTPClient: -    # TODO support `with` to handle closing connection-     def __init__(self, host, username, password):-        self.smtp = smtplib.SMTP_SSL()+        self.smtp = smtplib.SMTP_SSL(host=host)  # TODO bug in 3.7 needs host         self.smtp.connect(host)         self.smtp.login(username, password)+        self.username = username++    def __enter__(self):+        return self++    def __exit__(self, exc_type, exc_val, exc_tb):+        self.disconnect()      def send(self, from_, to, subject, content):         if isinstance(to, str):
 def send_text(to, message):     cfg = tx.kv["providers:twilio"]     client = twilio.rest.Client(cfg["account_sid"], cfg["auth_token"])-    return client.messages.create(from_=cfg["endpoint"], to=to, body=message)+    return client.messages.create(from_=cfg["phone_number"],+                                  to=to, body=message)   def clean_html(html):
     message_dir.mkdir(exist_ok=True)     with (message_dir / digest).open("wb") as fp:         fp.write(payload)+++# TODO rename to reload_servers() ?+def config_servers() -> None:+    """+    reload servers++    """+    root = get_root()+    root_hash = web.get_host_hash(root)++    def config_web_server(nginx_conf_d, root_hash):+        for identity in (root / "var/identities").iterdir():+            su("ln", "-sf", (identity / "nginx/domain.conf").resolve(),+               nginx_conf_d / "01-{}-{}-domain.conf".format(root_hash,+                                                            identity.name))+            if (identity / "nginx/orchard.conf").exists():+                su("ln", "-sf", (identity / "nginx/orchard.conf").resolve(),+                   nginx_conf_d /+                   "01-{}-{}-orchard.conf".format(root_hash, identity.name))+            su("ln", "-sf", (identity / "nginx/tor/nginx.conf").resolve(),+               nginx_conf_d / "01-{}-{}-tor.conf".format(root_hash,+                                                         identity.name))++    web.config_servers(root, config_web_server)+    su("rm", prefix / "var/tor/*.sock", "-f")++    su("supervisorctl", "restart",  # TODO reload config only+       f"canopy_{root_hash}:canopy_nginx_{root_hash}")+++def setup_orchard():+    """+++    called from within the context of an identity++    """+    # TODO schedule("*/30 * * * *", orchard.balance_groves)+    root = get_root()+    nginx_dir = get_path() / "nginx"+    domain = global_kv["orchard:domains:primary"]+    with (nginx_dir / "orchard.conf").open("w") as fp:+        fp.write(str(config.nginx_orchard(domain, root, nginx_dir, False)))+    config_servers()+    web.letsencrypt.generate_host_cert(root, nginx_dir / "orchard_ssl", domain)+    with (nginx_dir / "orchard.conf").open("w") as fp:+        fp.write(str(config.nginx_orchard(domain, root, nginx_dir, True)))+    config_servers()

rename from canopy/util/hosting.py

rename to canopy/util/orchard.py

  from ..content.write import quick_draft from ..util import digitalocean as do-from ..util import random+from ..util import random, global_kv   def rebalance_grove():
     do.wait(cli, droplet["id"], "taking snapshot")     snapshot = cli.get_droplet_snapshots(droplet["id"])[0]     cli.delete_droplet(droplet["id"])-    tx.kv["hosting:snapshot_id"] = snapshot["id"]+    global_kv["orchard:snapshot_id"] = snapshot["id"]   def spawn_device():
     """     cli = do.get_client()     key = do.get_key(cli)-    snapshot_id = str(tx.kv["hosting:snapshot_id"])+    snapshot_id = str(global_kv["orchard:snapshot_id"])     print("spawning device.. will take ~4 minutes")     droplet_id = cli.create_droplet(str(uuid.uuid4()), size="2gb",                                     image=snapshot_id, tags=["Canopy"],
     # XXX tx.db.insert("identities", device_id=device["id"], uuid=uuid,     # XXX              onion=onion, domain=domain)     data = {"-audience": "private", "-uuid": uuid, "name": name, "uid": uid,-            "-hosting-service": "shared", "-hosting-device": device["id"]}+            "-orchard-tenancy": "multi", "-orchard-grove": device["id"]}     if domain:         data["alt-svc"] = domain     quick_draft("identity", data, publish=True)  # TODO email, phone, etc.

--- a/gaea.py

+++ b/gaea.py

-# [`gaea`][1]: manage the machines that power the canopy-# Copyright (c) 2018- @[Angelo Gladding][2]+# [`gaea`][1]: canopy installer+# Copyright (c) 2020- @[Angelo Gladding][2] # # This program is free software: it is distributed in the hope that it # will be useful, but *without any warranty*; without even the implied
 # [5]: https://fsf.org  """-manage the devices that power the canopy+canopy installer      $ python gaea.py --help 

--- a/kaleidoscope.py

+++ b/kaleidoscope.py

-# [`kaleidoscope`][1]: media support for the Canopy+# [`kaleidoscope`][1]: canopy device management # Copyright (c) 2018- @[Angelo Gladding][2] # # This program is free software: it is distributed in the hope that it
 # [5]: https://fsf.org  """-media support for the Canopy+canopy device management  """ 

rename from canopy_bot.py

rename to loveliness.py

+# [`loveliness`][1]: canopy job queue+# Copyright (c) 2020- @[Angelo Gladding][2]+#+# This program is free software: it is distributed in the hope that it+# will be useful, but *without any warranty*; without even the implied+# warranty of merchantability or fitness for a particular purpose. You+# can redistribute it and/or modify it under the terms of the @@[GNU's+# Not Unix][3] %[Affero General Public License][4] as published by the+# @@[Free Software Foundation][5], either version 3 of the License, or+# any later version.+#+# *[GNU]: GNU's Not Unix+#+# [1]: https://lahacker.net/code/canopy/files/loveliness.py+# [2]: https://lahacker.net+# [3]: https://gnu.org+# [4]: https://gnu.org/licenses/agpl+# [5]: https://fsf.org+ """ canopy job queue 
 from canopy.util import update_inbox, IMAPClient  # noqa  -main = term.application("canopy-bot", "Canopy bot")+main = term.application("loveliness", "canopy job queue") queue = gevent.queue.PriorityQueue() worker_count = 20 

--- a/setup.py

+++ b/setup.py

-# [`canopy`][1]: a decentralized social web platform-# Copyright (c) 2018- @[Angelo Gladding][2]+# [`canopy`][1]: decentralized social web platform+# Copyright (c) 2020- @[Angelo Gladding][2] # # This program is free software: it is distributed in the hope that it # will be useful, but *without any warranty*; without even the implied
                 "phonenumbers", "pillow", "qrcode", "sopel", "scrypt",                 "stripe", "tweepy", "twilio", "vobject", "web", "youtube_dl"],       provides={"term.apps": ["canopy = canopy.__main__:main",-                              "canopy-bot = canopy_bot:main",-                              "kaleidoscope = kaleidoscope:main"]},+                              "kaleidoscope = kaleidoscope:main",+                              "loveliness = loveliness:main"]},       discover=__file__)