diff --git a/apps/cory/admin.json b/apps/cory/admin.json
new file mode 100644
index 00000000..4daf78b7
--- /dev/null
+++ b/apps/cory/admin.json
@@ -0,0 +1 @@
+{"type":"tildefriends-app","files":{"app.js":"&xotWQ8M3xgnWAPM/1TdrLmkcCyxGPiXqg9CsBm2ngcc=.sha256","index.html":"&PrdNng+/SYCFSEbx+E7tMKxs4/ypPDxbRlak4tGN/SM=.sha256","lit.min.js":"&3FfrVflmGr0n4lvN0GriN1Qz1lEw31SbZxRSJrcXR28=.sha256","script.js":"&hW7AyNMgC+paQBFDcggxmhwNWmEY+5HofubRalcz6u8=.sha256"}}
\ No newline at end of file
diff --git a/apps/cory/admin/app.js b/apps/cory/admin/app.js
new file mode 100644
index 00000000..8a6d4a64
--- /dev/null
+++ b/apps/cory/admin/app.js
@@ -0,0 +1,13 @@
+import * as tfrpc from '/tfrpc.js';
+
+tfrpc.register(function delete_user(user) {
+ return core.deleteUser(user);
+});
+
+async function main() {
+ let data = {
+ users: await core.users(),
+ };
+ await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
+}
+main();
\ No newline at end of file
diff --git a/apps/cory/admin/index.html b/apps/cory/admin/index.html
new file mode 100644
index 00000000..bb94399e
--- /dev/null
+++ b/apps/cory/admin/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
Test
+
+
+
\ No newline at end of file
diff --git a/apps/cory/admin/lit.min.js b/apps/cory/admin/lit.min.js
new file mode 100644
index 00000000..d0eb9ca9
--- /dev/null
+++ b/apps/cory/admin/lit.min.js
@@ -0,0 +1,13 @@
+/**
+ * Skipped minification because the original files appears to be already minified.
+ * Original file: /npm/lit-html@2.2.7/lit-html.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+var t;const i=globalThis.trustedTypes,s=i?i.createPolicy("lit-html",{createHTML:t=>t}):void 0,e=`lit$${(Math.random()+"").slice(9)}$`,o="?"+e,n=`<${o}>`,l=document,h=(t="")=>l.createComment(t),r=t=>null===t||"object"!=typeof t&&"function"!=typeof t,d=Array.isArray,u=t=>d(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),c=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,a=/>/g,f=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),_=/'/g,g=/"/g,m=/^(?:script|style|textarea|title)$/i,p=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),$=p(1),y=p(2),b=Symbol.for("lit-noChange"),w=Symbol.for("lit-nothing"),x=new WeakMap,T=(t,i,s)=>{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new N(i.insertBefore(h(),t),t,void 0,null!=s?s:{})}return l._$AI(t),l},A=l.createTreeWalker(l,129,null,!1),E=(t,i)=>{const o=t.length-1,l=[];let h,r=2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==s?s.createHTML(u):u,l]};class C{constructor({strings:t,_$litType$:s},n){let l;this.parts=[];let r=0,d=0;const u=t.length-1,c=this.parts,[v,a]=E(t,s);if(this.el=C.createElement(v,n),A.currentNode=this.el.content,2===s){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes)}for(;null!==(l=A.nextNode())&&c.length0){l.textContent=i?i.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=w}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=P(this,t,i,0),n=!r(t)||t!==this._$AH&&t!==b,n&&(this._$AH=t);else{const e=t;let l,h;for(t=o[0],l=0;l html`
${user}
`;
+ const users_template = (users) =>
+ html`
+ ${users.map(u => user_template(u))}
+
`;
+ render(users_template(g_data.users), document.body);
+});
\ No newline at end of file
diff --git a/apps/cory/api.json b/apps/cory/api.json
new file mode 100644
index 00000000..febd470f
--- /dev/null
+++ b/apps/cory/api.json
@@ -0,0 +1 @@
+{"type":"tildefriends-app","files":{"app.js":"&p35JmopfHf8hFh3Y9x6LrIxiUwaJZ5Nabzi2sVXpKoo=.sha256"}}
\ No newline at end of file
diff --git a/apps/cory/api/app.js b/apps/cory/api/app.js
new file mode 100644
index 00000000..533febec
--- /dev/null
+++ b/apps/cory/api/app.js
@@ -0,0 +1,11 @@
+var global = Function('return this')();
+function treeify(o) {
+ if (typeof(o) == 'object') {
+ return Object.fromEntries(Object.keys(o).map(x => [x, treeify(o[x])]));
+ } else if (typeof(o) == 'function') {
+ return 'function';
+ } else if (typeof(o) == 'string' || typeof(o) == 'number') {
+ return o;
+ }
+}
+app.setDocument(`
${JSON.stringify(treeify(global), null, 2)}
`);
\ No newline at end of file
diff --git a/apps/cory/db.json b/apps/cory/db.json
new file mode 100644
index 00000000..c475e39d
--- /dev/null
+++ b/apps/cory/db.json
@@ -0,0 +1 @@
+{"type":"tildefriends-app","files":{"app.js":"&V5o5IM9/OUyIsVkjkMW/X0i/tflQOSVJuJBmHdMT9aM=.sha256"}}
\ No newline at end of file
diff --git a/apps/cory/db/app.js b/apps/cory/db/app.js
new file mode 100644
index 00000000..b18c9b31
--- /dev/null
+++ b/apps/cory/db/app.js
@@ -0,0 +1,70 @@
+async function database_list() {
+ var dbs = await databases();
+ var doc = `
+
+
+
Databases
+
+
+
+`
+ app.setDocument(doc);
+}
+
+async function key_list(db) {
+ let keys = await db.getAll();
+ let object = {};
+ for (let key of keys) {
+ object[key] = await db.get(key);
+ }
+ let doc = `
+
+
+back
+
Keys
+
+
+
+`
+ app.setDocument(doc);
+}
+
+core.register('message', async function(message) {
+ if (message.event == 'hashChange') {
+ let hash = message.hash.substring(1);
+ if (hash.startsWith(':shared:')) {
+ let parts = hash.split(':');
+ let packageName = parts[3];
+ let key = parts.slice(4).join(':');
+ key_list(await my_shared_database(packageName, key));
+ } else if (hash.length) {
+ key_list(await database(hash.split(':').slice(1).join(':')));
+ } else {
+ database_list();
+ }
+ }
+});
+
+database_list();
\ No newline at end of file
diff --git a/apps/cory/follow.json b/apps/cory/follow.json
new file mode 100644
index 00000000..03e538bc
--- /dev/null
+++ b/apps/cory/follow.json
@@ -0,0 +1 @@
+{"type":"tildefriends-app","files":{"app.js":"&+LbIl429+UZeS9Nh8zO6n7pzRfWOfFF2K/Hg7Kq2HQo=.sha256"}}
\ No newline at end of file
diff --git a/apps/cory/follow/app.js b/apps/cory/follow/app.js
new file mode 100644
index 00000000..7e0db3f0
--- /dev/null
+++ b/apps/cory/follow/app.js
@@ -0,0 +1,159 @@
+"use strict";
+
+var g_following_cache = {};
+var g_following_deep_cache = {};
+var g_about_cache = {};
+
+async function following(db, id) {
+ if (g_following_cache[id]) {
+ return g_following_cache[id];
+ }
+ var o = await db.get(id + ":following");
+ const k_version = 5;
+ var f = o ? JSON.parse(o) : o;
+ if (!f || f.version != k_version) {
+ f = {users: [], sequence: 0, version: k_version};
+ }
+ f.users = new Set(f.users);
+ await ssb.sqlStream(
+ "SELECT "+
+ " sequence, "+
+ " json_extract(content, '$.contact') AS contact, "+
+ " json_extract(content, '$.following') AS following "+
+ "FROM messages "+
+ "WHERE "+
+ " author = ?1 AND "+
+ " sequence > ?2 AND "+
+ " json_extract(content, '$.type') = 'contact' "+
+ "UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
+ "ORDER BY sequence",
+ [id, f.sequence],
+ function(row) {
+ if (row.following) {
+ f.users.add(row.contact);
+ } else {
+ f.users.delete(row.contact);
+ }
+ f.sequence = row.sequence;
+ });
+ var as_set = f.users;
+ f.users = Array.from(f.users).sort();
+ var j = JSON.stringify(f);
+ if (o != j) {
+ await db.set(id + ":following", j);
+ }
+ f.users = as_set;
+ g_following_cache[id] = f.users;
+ return f.users;
+}
+
+async function followingDeep(db, seed_ids, depth) {
+ if (depth <= 0) {
+ return seed_ids;
+ }
+ var key = JSON.stringify([seed_ids, depth]);
+ if (g_following_deep_cache[key]) {
+ return g_following_deep_cache[key];
+ }
+ var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
+ var ids = [].concat(...f);
+ var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
+ x = [...new Set([].concat(...x, ...seed_ids))].sort();
+ g_following_deep_cache[key] = x;
+ return x;
+}
+
+async function getAbout(db, id) {
+ if (g_about_cache[id]) {
+ return g_about_cache[id];
+ }
+ var o = await db.get(id + ":about");
+ const k_version = 4;
+ var f = o ? JSON.parse(o) : o;
+ if (!f || f.version != k_version) {
+ f = {about: {}, sequence: 0, version: k_version};
+ }
+ await ssb.sqlStream(
+ "SELECT "+
+ " sequence, "+
+ " content "+
+ "FROM messages "+
+ "WHERE "+
+ " author = ?1 AND "+
+ " sequence > ?2 AND "+
+ " json_extract(content, '$.type') = 'about' AND "+
+ " json_extract(content, '$.about') = ?1 "+
+ "UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
+ "ORDER BY sequence",
+ [id, f.sequence],
+ function(row) {
+ f.sequence = row.sequence;
+ if (row.content) {
+ var about = {};
+ try {
+ about = JSON.parse(row.content);
+ } catch {
+ }
+ delete about.about;
+ delete about.type;
+ f.about = Object.assign(f.about, about);
+ }
+ });
+ var j = JSON.stringify(f);
+ if (o != j) {
+ await db.set(id + ":about", j);
+ }
+ g_about_cache[id] = f.about;
+ return f.about;
+}
+
+async function getSize(db, id) {
+ let size = 0;
+ await ssb.sqlStream(
+ "SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
+ [id],
+ function (row) {
+ size += row.size;
+ });
+ return size;
+}
+
+function niceSize(bytes) {
+ let value = bytes;
+ let unit = 'B';
+ const k_units = ['kB', 'MB', 'GB', 'TB'];
+ for (let u of k_units) {
+ if (value >= 1024) {
+ value /= 1024;
+ unit = u;
+ } else {
+ break;
+ }
+ }
+ return Math.round(value * 10) / 10 + ' ' + unit;
+}
+
+async function buildTree(db, root, indent, depth) {
+ var f = await following(db, root);
+ var result = indent + '[' + f.size + '] ' + '' + ((await getAbout(db, root)).name || root) + ' ' + niceSize(await getSize(db, root)) + '\n';
+ if (depth > 0) {
+ for (let next of f) {
+ result += await buildTree(db, next, indent + ' ', depth - 1);
+ }
+ }
+ return result;
+}
+
+async function main() {
+ await app.setDocument('
building...
');
+ var db = await database('ssb');
+ var whoami = await ssb.getIdentities();
+ var tree = '';
+ for (let id of whoami) {
+ await app.setDocument(`