From fbfbd6a6b466a7c9bca8663ae8a71cbd951cde84 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Thu, 4 Aug 2022 00:57:56 +0000 Subject: [PATCH] Exposed deleting users, mostly for my own testing, and used it to make a primitive admin app. Add a handful of apps I've been kicking around without version control, while I'm at it. git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3950 ed5197a5-7fde-0310-b194-c3ffbd925b24 --- apps/cory/admin.json | 1 + apps/cory/admin/app.js | 13 +++ apps/cory/admin/index.html | 10 +++ apps/cory/admin/lit.min.js | 13 +++ apps/cory/admin/script.js | 21 +++++ apps/cory/api.json | 1 + apps/cory/api/app.js | 11 +++ apps/cory/db.json | 1 + apps/cory/db/app.js | 70 ++++++++++++++++ apps/cory/follow.json | 1 + apps/cory/follow/app.js | 159 +++++++++++++++++++++++++++++++++++++ core/core.js | 21 +++++ src/main.c | 4 + 13 files changed, 326 insertions(+) create mode 100644 apps/cory/admin.json create mode 100644 apps/cory/admin/app.js create mode 100644 apps/cory/admin/index.html create mode 100644 apps/cory/admin/lit.min.js create mode 100644 apps/cory/admin/script.js create mode 100644 apps/cory/api.json create mode 100644 apps/cory/api/app.js create mode 100644 apps/cory/db.json create mode 100644 apps/cory/db/app.js create mode 100644 apps/cory/follow.json create mode 100644 apps/cory/follow/app.js 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?"":"",d=c;for(let i=0;i"===u[0]?(d=null!=h?h:c,p=-1):void 0===u[1]?p=-2:(p=d.lastIndex-u[2].length,o=u[1],d=void 0===u[3]?f:'"'===u[3]?g:_):d===g||d===_?d=f:d===v||d===a?d=c:(d=f,h=void 0);const y=d===f&&t[i+1].startsWith("/>")?" ":"";r+=d===c?s+n:p>=0?(l.push(o),s.slice(0,p)+"$lit$"+s.slice(p)+e+y):s+e+(-2===p?(l.push(void 0),i):y)}const u=r+(t[o]||"")+(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(`
        building... ${id}
        `); + tree += await buildTree(db, id, '', 2); + } + await app.setDocument('
        FOLLOWING:\n' + tree + '
        '); +} + +main(); \ No newline at end of file diff --git a/core/core.js b/core/core.js index d8404d0a..eb9ad7ca 100644 --- a/core/core.js +++ b/core/core.js @@ -224,6 +224,27 @@ async function getProcessBlob(blobId, key, options) { }, } }; + if (process.credentials?.permissions?.administration) { + imports.core.deleteUser = function(user) { + return imports.core.permissionTest('delete_user').then(function() { + let db = new Database('auth'); + + db.remove('user:' + user); + + let users = new Set(); + let users_original = db.get('users'); + try { + users = new Set(JSON.parse(users_original)); + } catch { + } + users.delete(user); + users = JSON.stringify([...users].sort()); + if (users !== users_original) { + db.set('users', users); + } + }); + } + } if (options.api) { imports.app = {}; for (let i in options.api) { diff --git a/src/main.c b/src/main.c index 2dcd004d..15d3a866 100644 --- a/src/main.c +++ b/src/main.c @@ -287,8 +287,12 @@ static int _tf_command_export(const char* file, int argc, char* argv[]) else { const char* k_export[] = { + "/~cory/api", + "/~cory/admin", "/~cory/apps", + "/~cory/db", "/~cory/docs", + "/~cory/follow", "/~cory/ssb", }; for (int i = 0; i < (int)_countof(k_export); i++)