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>