Quellcode durchsuchen

refactor: share endpoint

KernelDeimos vor 11 Monaten
Ursprung
Commit
b78c83a4ab

+ 7 - 1
doc/devmeta/track-comments.md

@@ -50,4 +50,10 @@ Comments beginning with `// track:`. See
   This code manually creates a new "client-safe" version of
   This code manually creates a new "client-safe" version of
   some object that's in scope. This could be either to pass
   some object that's in scope. This could be either to pass
   onto the browser or to pass to something like the
   onto the browser or to pass to something like the
-  notification service.
+  notification service.
+- `track: common operations on multiple items`
+  A patterm which emerges when multiple variables have
+  common operations done upon them in sequence.
+  It may be applicable to write an iterator in the
+  future, or something will come up that require
+  these to be handled with a modular approach instead.

+ 24 - 6
packages/backend/src/codex/Sequence.js

@@ -109,7 +109,7 @@ class Sequence {
         async run (values) {
         async run (values) {
             // Initialize scope
             // Initialize scope
             values = values || this.thisArg?.values || {};
             values = values || this.thisArg?.values || {};
-            Object.assign(this.scope_, values); // TODO: can this be __proto__?
+            this.scope_.__proto__ = values;
 
 
             // Run sequence
             // Run sequence
             for ( ; this.i < this.steps.length ; this.i++ ) {
             for ( ; this.i < this.steps.length ; this.i++ ) {
@@ -138,7 +138,14 @@ class Sequence {
                     await this.sequence_.options_.before_each(this, step);
                     await this.sequence_.options_.before_each(this, step);
                 }
                 }
 
 
-                this.last_return_ = await step.fn(this);
+                this.last_return_ = await step.fn.call(
+                    this.thisArg, this,
+                );
+                
+                if ( this.sequence_.options_.after_each ) {
+                    await this.sequence_.options_.after_each(this, step);
+                }
+                
                 if ( this.stopped_ ) {
                 if ( this.stopped_ ) {
                     break;
                     break;
                 }
                 }
@@ -186,7 +193,13 @@ class Sequence {
             this.scope_[k] = v;
             this.scope_[k] = v;
         }
         }
 
 
-        values () {
+        values (opt_itemsToSet) {
+            if ( opt_itemsToSet ) {
+                for ( const k in opt_itemsToSet ) {
+                    this.set(k, opt_itemsToSet[k]);
+                }
+            }
+
             return new Proxy(this.scope_, {
             return new Proxy(this.scope_, {
                 get: (target, property) => {
                 get: (target, property) => {
                     if (property in target) {
                     if (property in target) {
@@ -231,16 +244,21 @@ class Sequence {
             }
             }
         }
         }
 
 
-        const fn = async function () {
+        const fn = async function (opt_values) {
+            if ( opt_values && opt_values instanceof Sequence.SequenceState ) {
+                opt_values = opt_values.scope_;
+            }
             const state = new Sequence.SequenceState(sequence, this);
             const state = new Sequence.SequenceState(sequence, this);
-            await state.run();
+            await state.run(opt_values ?? undefined);
             return state.last_return_;
             return state.last_return_;
         }
         }
 
 
         this.steps_ = steps;
         this.steps_ = steps;
         this.options_ = options || {};
         this.options_ = options || {};
 
 
-        Object.defineProperty(fn, 'name', { value: 'Sequence' });
+        Object.defineProperty(fn, 'name', {
+            value: options.name || 'Sequence'
+        });
         Object.defineProperty(fn, 'sequence', { value: this });
         Object.defineProperty(fn, 'sequence', { value: this });
 
 
         return fn;
         return fn;

+ 0 - 636
packages/backend/src/routers/share.js

@@ -1,636 +0,0 @@
-const express = require('express');
-const { Endpoint } = require('../util/expressutil');
-
-const validator = require('validator');
-const APIError = require('../api/APIError');
-const { get_user, get_app } = require('../helpers');
-const { Context } = require('../util/context');
-const config = require('../config');
-const FSNodeParam = require('../api/filesystem/FSNodeParam');
-const { TYPE_DIRECTORY } = require('../filesystem/FSNodeContext');
-
-const { PermissionUtil } = require('../services/auth/PermissionService');
-const configurable_auth = require('../middleware/configurable_auth');
-const { UsernameNotifSelector } = require('../services/NotificationService');
-const { quot } = require('../util/strutil');
-const { UtilFn } = require('../util/fnutil');
-const { WorkList } = require('../util/workutil');
-const { DB_WRITE } = require('../services/database/consts');
-
-const router = express.Router();
-
-const v0_2 = async (req, res) => {
-    const svc_token = req.services.get('token');
-    const svc_email = req.services.get('email');
-    const svc_permission = req.services.get('permission');
-    const svc_notification = req.services.get('notification');
-    const svc_share = req.services.get('share');
-
-    const lib_typeTagged = req.services.get('lib-type-tagged');
-
-    const actor = Context.get('actor');
-    
-    const db = req.services.get('database').get('share', DB_WRITE);
-    
-    // === Request Validators ===
-    
-    const validate_mode = UtilFn(mode => {
-        if ( mode === 'strict' ) return true;
-        if ( ! mode || mode === 'best-effort' ) return false;
-        throw APIError.create('field_invalid', null, {
-            key: 'mode',
-            expected: '`strict`, `best-effort`, or undefined',
-        });
-    })
-    
-    // Expect: an array of usernames and/or emails
-    const validate_recipients = UtilFn(recipients => {
-        // A string can be adapted to an array of one string
-        if ( typeof recipients === 'string' ) {
-            recipients = [recipients];
-        }
-        // Must be an array
-        if ( ! Array.isArray(recipients) ) {
-            throw APIError.create('field_invalid', null, {
-                key: 'recipients',
-                expected: 'array or string',
-                got: typeof recipients,
-            })
-        }
-        return recipients;
-    });
-    
-    const validate_shares = UtilFn(shares => {
-        // Single-values get adapted into an array
-        if ( ! Array.isArray(shares) ) {
-            shares = [shares];
-        }
-        return shares;
-    })
-    
-    // === Request Values ===
-
-    const strict_mode =
-        validate_mode.if(req.body.mode) ?? false;
-    const req_recipients =
-        validate_recipients.if(req.body.recipients) ?? [];
-    const req_shares =
-        validate_shares.if(req.body.shares) ?? [];
-        
-    // === State Values ===
-
-    const recipients = [];
-    const result = {
-        // Metadata
-        $: 'api:share',
-        $version: 'v0.0.0',
-        
-        // Results
-        status: null,
-        recipients: Array(req_recipients.length).fill(null),
-        shares: Array(req_shares.length).fill(null),
-    }
-    const recipients_work = new WorkList();
-    const shares_work = new WorkList();
-    
-    // const assert_work_item = (wut, item) => {
-    //     if ( item.$ !== wut ) {
-    //         // This should never happen, so 500 is acceptable here
-    //         throw new Error('work item assertion failed');
-    //     }
-    // }
-    
-    // === Request Preprocessing ===
-    
-    // --- Function that returns early in strict mode ---
-    const serialize_result = () => {
-        for ( let i=0 ; i < result.recipients.length ; i++ ) {
-            if ( ! result.recipients[i] ) continue;
-            if ( result.recipients[i] instanceof APIError ) {
-                result.status = 'mixed';
-                result.recipients[i] = result.recipients[i].serialize();
-            }
-        }
-        for ( let i=0 ; i < result.shares.length ; i++ ) {
-            if ( ! result.shares[i] ) continue;
-            if ( result.shares[i] instanceof APIError ) {
-                result.status = 'mixed';
-                result.shares[i] = result.shares[i].serialize();
-            }
-        }
-    };
-    const strict_check = () =>{
-        if ( ! strict_mode ) return;
-        console.log('OK');
-        if (
-            result.recipients.some(v => v !== null) ||
-            result.shares.some(v => v !== null)
-        ) {
-            console.log('DOESNT THIS??')
-            serialize_result();
-            result.status = 'aborted';
-            res.status(218).send(result);
-            console.log('HOWW???');
-            return true;
-        }
-    }
-    
-    // --- Process Recipients ---
-    
-    // Expect: at least one recipient
-    if ( req_recipients.length < 1 ) {
-        throw APIError.create('field_invalid', null, {
-            key: 'recipients',
-            expected: 'at least one',
-            got: 'none',
-        })
-    }
-    
-    for ( let i=0 ; i < req_recipients.length ; i++ ) {
-        const value = req_recipients[i];
-        recipients_work.push({ i, value })
-    }
-    recipients_work.lockin();
-    
-    // track: good candidate for sequence
-    
-    // Expect: each value should be a valid username or email
-    for ( const item of recipients_work.list() ) {
-        const { value, i } = item;
-        
-        if ( typeof value !== 'string' ) {
-            item.invalid = true;
-            result.recipients[i] =
-                APIError.create('invalid_username_or_email', null, {
-                    value,
-                });
-            continue;
-        }
-
-        if ( value.match(config.username_regex) ) {
-            item.type = 'username';
-            continue;
-        }
-        if ( validator.isEmail(value) ) {
-            item.type = 'email';
-            continue;
-        }
-        
-        item.invalid = true;
-        result.recipients[i] =
-            APIError.create('invalid_username_or_email', null, {
-                value,
-            });
-    }
-    
-    // Return: if there are invalid values in strict mode
-    recipients_work.clear_invalid();
-    
-    // Expect: no emails specified yet
-    //    AND  usernames exist
-    for ( const item of recipients_work.list() ) {
-        const allowed_types = ['email', 'username'];
-        if ( ! allowed_types.includes(item.type) ) {
-            item.invalid = true;
-            result.recipients[item.i] =
-                APIError.create('disallowed_value', null, {
-                    key: `recipients[${item.i}].type`,
-                    allowed: allowed_types,
-                });
-            continue;
-        }
-    }
-
-    // Return: if there are invalid values in strict mode
-    recipients_work.clear_invalid();
-
-    for ( const item of recipients_work.list() ) {
-        if ( item.type !== 'email' ) continue;
-    
-        const errors = [];
-        if ( ! validator.isEmail(item.value) ) {
-            errors.push('`email` is not valid');
-        }
-        
-        if ( errors.length ) {
-            item.invalid = true;
-            result.recipients[item.i] =
-                APIError.create('field_errors', null, {
-                    key: `recipients[${item.i}]`,
-                    errors,
-                });
-            continue;
-        }
-    }
-
-    recipients_work.clear_invalid();
-
-    // CHECK EXISTING USERS FOR EMAIL SHARES
-    for ( const recipient_item of recipients_work.list() ) {
-        if ( recipient_item.type !== 'email' ) continue;
-        const user = await get_user({
-            email: recipient_item.value,
-        });
-        if ( ! user ) continue;
-        recipient_item.type = 'username';
-        recipient_item.value = user.username;
-    }
-
-    recipients_work.clear_invalid();
-    
-    // Check: users specified by username exist
-    for ( const item of recipients_work.list() ) {
-        if ( item.type !== 'username' ) continue;
-
-        const user = await get_user({ username: item.value });
-        if ( ! user ) {
-            item.invalid = true;
-            result.recipients[item.i] =
-                APIError.create('user_does_not_exist', null, {
-                    username: item.value,
-                });
-            continue;
-        }
-        item.user = user;
-    }
-
-    // Return: if there are invalid values in strict mode
-    recipients_work.clear_invalid();
-    
-    // --- Process Paths ---
-    
-    // Expect: at least one path
-    if ( req_shares.length < 1 ) {
-        throw APIError.create('field_invalid', null, {
-            key: 'shares',
-            expected: 'at least one',
-            got: 'none',
-        })
-    }
-    
-    for ( let i=0 ; i < req_shares.length ; i++ ) {
-        const value = req_shares[i];
-        shares_work.push({ i, value });
-    }
-    shares_work.lockin();
-    
-    // Check: all share items are a type-tagged-object
-    //        with one of these types: fs-share, app-share.
-    for ( const item of shares_work.list() ) {
-        const { i } = item;
-        let { value } = item;
-        
-        const thing = lib_typeTagged.process(value);
-        if ( thing.$ === 'error' ) {
-            item.invalid = true;
-            result.shares[i] =
-                APIError.create('format_error', null, {
-                    message: thing.message
-                });
-            continue;
-        }
-        
-        const allowed_things = ['fs-share', 'app-share'];
-        if ( ! allowed_things.includes(thing.$) ) {
-            item.invalid = true;
-            result.shares[i] =
-                APIError.create('disallowed_thing', null, {
-                    thing: thing.$,
-                    accepted: allowed_things,
-                });
-            continue;
-        }
-        
-        item.thing = thing;
-    }
-
-    shares_work.clear_invalid();
-    
-    // Process: create $share-intent:file for file items
-    for ( const item of shares_work.list() ) {
-        const { thing } = item;
-        if ( thing.$ !== 'fs-share' ) continue;
-
-        item.type = 'fs';
-        const errors = [];
-        if ( ! thing.path ) {
-            errors.push('`path` is required');
-        }
-        let access = thing.access;
-        if ( access ) {
-            if ( ! ['read','write'].includes(access) ) {
-                errors.push('`access` should be `read` or `write`');
-            }
-        } else access = 'read';
-
-        if ( errors.length ) {
-            item.invalid = true;
-            result.shares[item.i] =
-                APIError.create('field_errors', null, {
-                    key: `shares[${item.i}]`,
-                    errors
-                });
-            continue;
-        }
-        
-        item.path = thing.path;
-        item.share_intent = {
-            $: 'share-intent:file',
-            permissions: [PermissionUtil.join('fs', thing.path, access)],
-        };
-    }
-
-    shares_work.clear_invalid();
-
-    // Process: create $share-intent:app for app items
-    for ( const item of shares_work.list() ) {
-        const { thing } = item;
-        if ( thing.$ !== 'app-share' ) continue;
-
-        item.type = 'app';
-        const errors = [];
-        if ( ! thing.uid && ! thing.name ) {
-            errors.push('`uid` or `name` is required');
-        }
-
-        if ( errors.length ) {
-            item.invalid = true;
-            result.shares[item.i] =
-                APIError.create('field_errors', null, {
-                    key: `shares[${item.i}]`,
-                    errors
-                });
-            continue;
-        }
-        
-        const app_selector = thing.uid
-            ? `uid#${thing.uid}` : thing.name;
-        
-        item.share_intent = {
-            $: 'share-intent:app',
-            permissions: [
-                PermissionUtil.join('app', app_selector, 'access')
-            ]
-        }
-        continue;
-    }
-    
-    shares_work.clear_invalid();
-    
-    for ( const item of shares_work.list() ) {
-        if ( item.type !== 'fs' ) continue;
-        const node = await (new FSNodeParam('path')).consolidate({
-            req, getParam: () => item.path
-        });
-        
-        if ( ! await node.exists() ) {
-            item.invalid = true;
-            result.shares[item.i] = APIError.create('subject_does_not_exist', {
-                path: item.path,
-            })
-            continue;
-        }
-        
-        item.node = node;
-        let email_path = item.path;
-        let is_dir = true;
-        if ( await node.get('type') !== TYPE_DIRECTORY ) {
-            is_dir = false;
-            // remove last component
-            email_path = email_path.slice(0, item.path.lastIndexOf('/')+1);
-        }
-
-        if ( email_path.startsWith('/') ) email_path = email_path.slice(1);
-        const email_link = `${config.origin}/show/${email_path}`;
-        item.is_dir = is_dir;
-        item.email_link = email_link;
-    }
-    
-    shares_work.clear_invalid();
-    
-    // Fetch app info for app shares
-    for ( const item of shares_work.list() ) {
-        if ( item.type !== 'app' ) continue;
-        const { thing } = item;
-
-        const app = await get_app(thing.uid ?
-            { uid: thing.uid } : { name: thing.name });
-        if ( ! app ) {
-            item.invalid = true;
-            result.shares[item.i] =
-                // note: since we're reporting `entity_not_found`
-                // we will report the id as an entity-storage-compatible
-                // identifier.
-                APIError.create('entity_not_found', null, {
-                    identifier: thing.uid
-                        ? { uid: thing.uid }
-                        : { id: { name: thing.name } }
-                });
-        }
-        
-        app.metadata = db.case({
-            mysql: () => app.metadata,
-            otherwise: () => JSON.parse(app.metadata ?? '{}')
-        })();
-        
-        item.app = app;
-    }
-    
-    shares_work.clear_invalid();
-    
-    // Process: conditionally add permission for subdomain
-    for ( const item of shares_work.list() ) {
-        if ( item.type !== 'app' ) continue;
-        const [subdomain] = await db.read(
-            `SELECT * FROM subdomains WHERE associated_app_id = ? ` +
-            `AND user_id = ? LIMIT 1`,
-            [item.app.id, actor.type.user.id]
-        );
-        if ( ! subdomain ) continue;
-        
-        // The subdomain is also owned by this user, so we'll
-        // add a permission for that as well
-        
-        const site_selector = `uid#${subdomain.uuid}`;
-        item.share_intent.permissions.push(
-            PermissionUtil.join('site', site_selector, 'access')
-        )
-    }
-
-    // Process: conditionally add permission for AppData
-    for ( const item of shares_work.list() ) {
-        if ( item.type !== 'app' ) continue;
-        if ( ! item.app.metadata?.shared_appdata ) continue;
-        
-        const app_owner = await get_user({ id: item.app.owner_user_id });
-        
-        const appdatadir =
-            `/${app_owner.username}/AppData/${item.app.uid}`;
-        const appdatadir_perm =
-            PermissionUtil.join('fs', appdatadir, 'write');
-
-        item.share_intent.permissions.push(appdatadir_perm);
-    }
-
-    shares_work.clear_invalid();
-
-    // Mark files as successful; further errors will be
-    // reported on recipients instead.
-    for ( const item of shares_work.list() ) {
-        result.shares[item.i] =
-            {
-                $: 'api:status-report',
-                status: 'success',
-                fields: {
-                    permission: item.permission,
-                }
-            };
-    }
-    
-    if ( strict_check() ) return;
-    if ( req.body.dry_run ) {
-        // Mark everything left as successful
-        for ( const item of recipients_work.list() ) {
-            result.recipients[item.i] =
-                { $: 'api:status-report', status: 'success' };
-        }
-        
-        result.status = 'success';
-        result.dry_run = true;
-        serialize_result();
-        res.send(result);
-        return;
-    }
-    
-    for ( const recipient_item of recipients_work.list() ) {
-        if ( recipient_item.type !== 'username' ) continue;
-        
-        const username = recipient_item.user.username;
-
-        for ( const share_item of shares_work.list() ) {
-            const permissions = share_item.share_intent.permissions;
-            for ( const perm of permissions ) {
-                await svc_permission.grant_user_user_permission(
-                    actor,
-                    username,
-                    perm,
-                );
-            }
-        }
-        
-        // TODO: Need to re-work this for multiple files
-        /*
-        const email_values = {
-            link: recipient_item.email_link,
-            susername: req.user.username,
-            rusername: username,
-        };
-
-        const email_tmpl = 'share_existing_user';
-
-        await svc_email.send_email(
-            { email: recipient_item.user.email },
-            email_tmpl,
-            email_values,
-        );
-        */
-       
-        const files = []; {
-            for ( const item of shares_work.list() ) {
-                if ( item.type !== 'file' ) continue;
-                files.push(
-                    await item.node.getSafeEntry(),
-                );
-            }
-        }
-
-        const apps = []; {
-            for ( const item of shares_work.list() ) {
-                if ( item.type !== 'app' ) continue;
-                // TODO: is there a general way to create a
-                //       client-safe app right now without
-                //       going through entity storage?
-                // track: manual safe object
-                apps.push(item.name
-                    ? item.name : await get_app({
-                        uid: item.uid,
-                    }));
-            }
-        }
-        
-        svc_notification.notify(UsernameNotifSelector(username), {
-            source: 'sharing',
-            icon: 'shared.svg',
-            title: 'Files were shared with you!',
-            template: 'file-shared-with-you',
-            fields: {
-                username: actor.type.user.username,
-                files,
-            },
-            text: `The user ${quot(req.user.username)} shared ` +
-                `${files.length} ` +
-                (files.length === 1 ? 'file' : 'files') + ' ' +
-                'with you.',
-        });
-        
-        result.recipients[recipient_item.i] =
-            { $: 'api:status-report', status: 'success' };
-    }
-
-    for ( const recipient_item of recipients_work.list() ) {
-        if ( recipient_item.type !== 'email' ) continue;
-        
-        const email = recipient_item.value;
-        
-        // data that gets stored in the `data` column of the share
-        const data = {
-            $: 'internal:share',
-            $v: 'v0.0.0',
-            permissions: [],
-        };
-        
-        for ( const share_item of shares_work.list() ) {
-            const permissions = share_item.share_intent.permissions;
-            data.permissions.push(...permissions);
-        }
-        
-        // track: scoping iife
-        const share_token = await (async () => {
-            const share_uid = await svc_share.create_share({
-                issuer: actor,
-                email,
-                data,
-            });
-            return svc_token.sign('share', {
-                $: 'token:share',
-                $v: '0.0.0',
-                uid: share_uid,
-            }, {
-                expiresIn: '14d'
-            });
-        })();
-        
-        const email_link =
-            `${config.origin}?share_token=${share_token}`;
-        
-        await svc_email.send_email({ email }, 'share_by_email', {
-            link: email_link,
-        });
-    }
-    
-    result.status = 'success';
-    serialize_result(); // might change result.status to 'mixed'
-    res.send(result);
-};
-
-Endpoint({
-    // "item" here means a filesystem node
-    route: '/',
-    mw: [configurable_auth()],
-    methods: ['POST'],
-    handler: v0_2,
-}).attach(router);
-
-module.exports = app => {
-    app.use('/share', router);
-};

+ 0 - 1
packages/backend/src/services/PuterAPIService.js

@@ -76,7 +76,6 @@ class PuterAPIService extends BaseService {
         app.use(require('../routers/healthcheck'))
         app.use(require('../routers/healthcheck'))
         app.use(require('../routers/test'))
         app.use(require('../routers/test'))
         app.use(require('../routers/update-taskbar-items'))
         app.use(require('../routers/update-taskbar-items'))
-        require('../routers/share')(app);
         require('../routers/whoami')(app);
         require('../routers/whoami')(app);
 
 
     }
     }

+ 30 - 0
packages/backend/src/services/ShareService.js

@@ -1,6 +1,7 @@
 const APIError = require("../api/APIError");
 const APIError = require("../api/APIError");
 const { get_user } = require("../helpers");
 const { get_user } = require("../helpers");
 const configurable_auth = require("../middleware/configurable_auth");
 const configurable_auth = require("../middleware/configurable_auth");
+const { Context } = require("../util/context");
 const { Endpoint } = require("../util/expressutil");
 const { Endpoint } = require("../util/expressutil");
 const { whatis } = require("../util/langutil");
 const { whatis } = require("../util/langutil");
 const { Actor, UserActorType } = require("./auth/Actor");
 const { Actor, UserActorType } = require("./auth/Actor");
@@ -20,6 +21,11 @@ class ShareService extends BaseService {
     }
     }
     
     
     ['__on_install.routes'] (_, { app }) {
     ['__on_install.routes'] (_, { app }) {
+        this.install_sharelink_endpoints({ app });
+        this.install_share_endpoint({ app });
+    }
+    
+    install_sharelink_endpoints ({ app }) {
         // track: scoping iife
         // track: scoping iife
         const router = (() => {
         const router = (() => {
             const require = this.require;
             const require = this.require;
@@ -208,6 +214,30 @@ class ShareService extends BaseService {
         }).attach(router);
         }).attach(router);
     }
     }
     
     
+    install_share_endpoint ({ app }) {
+        // track: scoping iife
+        const router = (() => {
+            const require = this.require;
+            const express = require('express');
+            return express.Router();
+        })();
+        
+        app.use('/share', router);
+        
+        const share_sequence = require('../structured/sequence/share.js');
+        Endpoint({
+            route: '/',
+            methods: ['POST'],
+            mw: [configurable_auth()],
+            handler: async (req, res) => {
+                const actor = Actor.adapt(req.user);
+                return await share_sequence.call(this, {
+                    actor, req, res,
+                });
+            }
+        }).attach(router);
+    }
+    
     async get_share ({ uid }) {
     async get_share ({ uid }) {
         const [share] = await this.db.read(
         const [share] = await this.db.read(
             'SELECT * FROM share WHERE uid = ?',
             'SELECT * FROM share WHERE uid = ?',

+ 6 - 0
packages/backend/src/structured/README.md

@@ -0,0 +1,6 @@
+# Structured Code
+
+Each directory in this directory represents some type of
+structured code. For example, everything in the directory
+`./sequence` (relative to this file's location) is a
+cjs module that exports an instance of [Sequence](../codex/Sequence.js).

+ 584 - 0
packages/backend/src/structured/sequence/share.js

@@ -0,0 +1,584 @@
+const APIError = require("../../api/APIError");
+const { Sequence } = require("../../codex/Sequence");
+const config = require("../../config");
+const { WorkList } = require("../../util/workutil");
+
+const validator = require('validator');
+const { get_user, get_app } = require("../../helpers");
+const { PermissionUtil } = require("../../services/auth/PermissionService");
+const FSNodeParam = require("../../api/filesystem/FSNodeParam");
+const { TYPE_DIRECTORY } = require("../../filesystem/FSNodeContext");
+const { UsernameNotifSelector } = require("../../services/NotificationService");
+const { quot } = require("../../util/strutil");
+
+/*
+    This code is optimized for editors supporting folding.
+    Fold at Level 2 to conveniently browse sequence steps.
+    Fold at Level 3 after opening an inner-sequence.
+    
+    If you're using VSCode {
+        typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
+        to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
+        https://stackoverflow.com/questions/30067767
+    }
+*/
+
+
+module.exports = new Sequence([
+    function validate_mode (a) {
+        const req = a.get('req');
+        const mode = req.body.mode;
+        
+        if ( mode === 'strict' ) {
+            a.set('strict_mode', true);
+            return;
+        }
+        if ( ! mode || mode === 'best-effort' ) {
+            a.set('strict_mode', false);
+            return;
+        }
+        throw APIError.create('field_invalid', null, {
+            key: 'mode',
+            expected: '`strict`, `best-effort`, or undefined',
+        });
+    },
+    function validate_recipients (a) {
+        const req = a.get('req');
+        let recipients = req.body.recipients;
+
+        // A string can be adapted to an array of one string
+        if ( typeof recipients === 'string' ) {
+            recipients = [recipients];
+        }
+        // Must be an array
+        if ( ! Array.isArray(recipients) ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'recipients',
+                expected: 'array or string',
+                got: typeof recipients,
+            })
+        }
+        // At least one recipient
+        if ( recipients.length < 1 ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'recipients',
+                expected: 'at least one',
+                got: 'none',
+            });
+        }
+        a.set('req_recipients', recipients);
+    },
+    function validate_shares (a) {
+        const req = a.get('req');
+        let shares = req.body.shares;
+
+        if ( ! Array.isArray(shares) ) {
+            shares = [shares];
+        }
+        
+        // At least one share
+        if ( shares.length < 1 ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'shares',
+                expected: 'at least one',
+                got: 'none',
+            });
+        }
+        
+        a.set('req_shares', shares);
+    },
+    function initialize_result_object (a) {
+        a.set('result', {
+            $: 'api:share',
+            $version: 'v0.0.0',
+            status: null,
+            recipients:
+                Array(a.get('req_recipients').length).fill(null),
+            shares:
+                Array(a.get('req_shares').length).fill(null),
+            serialize () {
+                const result = this;
+                for ( let i=0 ; i < result.recipients.length ; i++ ) {
+                    if ( ! result.recipients[i] ) continue;
+                    if ( result.recipients[i] instanceof APIError ) {
+                        result.status = 'mixed';
+                        result.recipients[i] = result.recipients[i].serialize();
+                    }
+                }
+                for ( let i=0 ; i < result.shares.length ; i++ ) {
+                    if ( ! result.shares[i] ) continue;
+                    if ( result.shares[i] instanceof APIError ) {
+                        result.status = 'mixed';
+                        result.shares[i] = result.shares[i].serialize();
+                    }
+                }
+                delete result.serialize;
+                return result;
+            }
+        });
+    },
+    function initialize_worklists (a) {
+        const recipients_work = new WorkList();
+        const shares_work = new WorkList();
+        
+        const { req_recipients, req_shares } = a.values();
+        
+        // track: common operations on multiple items
+        
+        for ( let i=0 ; i < req_recipients.length ; i++ ) {
+            const value = req_recipients[i];
+            recipients_work.push({ i, value });
+        }
+        
+        for ( let i=0 ; i < req_shares.length ; i++ ) {
+            const value = req_shares[i];
+            shares_work.push({ i, value });
+        }
+        
+        recipients_work.lockin();
+        shares_work.lockin();
+        
+        a.values({ recipients_work, shares_work });
+    },
+    new Sequence({ name: 'process recipients',
+        after_each (a) {
+            const { recipients_work } = a.values();
+            recipients_work.clear_invalid();
+        }
+    }, [
+        function valid_username_or_email (a) {
+            const { result, recipients_work } = a.values();
+            for ( const item of recipients_work.list() ) {
+                const { value, i } = item;
+                
+                if ( typeof value !== 'string' ) {
+                    item.invalid = true;
+                    result.recipients[i] =
+                        APIError.create('invalid_username_or_email', null, {
+                            value,
+                        });
+                    continue;
+                }
+
+                if ( value.match(config.username_regex) ) {
+                    item.type = 'username';
+                    continue;
+                }
+                if ( validator.isEmail(value) ) {
+                    item.type = 'email';
+                    continue;
+                }
+                
+                item.invalid = true;
+                result.recipients[i] =
+                    APIError.create('invalid_username_or_email', null, {
+                        value,
+                    });
+            }
+        },
+        async function check_existing_users_for_email_shares (a) {
+            const { recipients_work } = a.values();
+            for ( const recipient_item of recipients_work.list() ) {
+                if ( recipient_item.type !== 'email' ) continue;
+                const user = await get_user({
+                    email: recipient_item.value,
+                });
+                if ( ! user ) continue;
+                recipient_item.type = 'username';
+                recipient_item.value = user.username;
+            }
+        },
+        async function check_username_specified_users_exist (a) {
+            const { result, recipients_work } = a.values();
+            for ( const item of recipients_work.list() ) {
+                if ( item.type !== 'username' ) continue;
+
+                const user = await get_user({ username: item.value });
+                if ( ! user ) {
+                    item.invalid = true;
+                    result.recipients[item.i] =
+                        APIError.create('user_does_not_exist', null, {
+                            username: item.value,
+                        });
+                    continue;
+                }
+                item.user = user;
+            }
+        }
+    ]),
+    new Sequence({ name: 'process shares',
+        beforeEach (a) {
+            const { shares_work } = a.values();
+            shares_work.clear_invalid();
+        }
+    }, [
+        function validate_share_types (a) {
+            const { result, shares_work } = a.values();
+            
+            const lib_typeTagged = a.iget('services').get('lib-type-tagged');
+            
+            for ( const item of shares_work.list() ) {
+                const { i } = item;
+                let { value } = item;
+                
+                const thing = lib_typeTagged.process(value);
+                if ( thing.$ === 'error' ) {
+                    item.invalid = true;
+                    result.shares[i] =
+                        APIError.create('format_error', null, {
+                            message: thing.message
+                        });
+                    continue;
+                }
+                
+                const allowed_things = ['fs-share', 'app-share'];
+                if ( ! allowed_things.includes(thing.$) ) {
+                    item.invalid = true;
+                    result.shares[i] =
+                        APIError.create('disallowed_thing', null, {
+                            thing: thing.$,
+                            accepted: allowed_things,
+                        });
+                    continue;
+                }
+                
+                item.thing = thing;
+            }
+        },
+        function create_file_share_intents (a) {
+            const { result, shares_work } = a.values();
+            for ( const item of shares_work.list() ) {
+                const { thing } = item;
+                if ( thing.$ !== 'fs-share' ) continue;
+
+                item.type = 'fs';
+                const errors = [];
+                if ( ! thing.path ) {
+                    errors.push('`path` is required');
+                }
+                let access = thing.access;
+                if ( access ) {
+                    if ( ! ['read','write'].includes(access) ) {
+                        errors.push('`access` should be `read` or `write`');
+                    }
+                } else access = 'read';
+
+                if ( errors.length ) {
+                    item.invalid = true;
+                    result.shares[item.i] =
+                        APIError.create('field_errors', null, {
+                            key: `shares[${item.i}]`,
+                            errors
+                        });
+                    continue;
+                }
+                
+                item.path = thing.path;
+                item.share_intent = {
+                    $: 'share-intent:file',
+                    permissions: [PermissionUtil.join('fs', thing.path, access)],
+                };
+            }
+        },
+        function create_app_share_intents (a) {
+            const { result, shares_work } = a.values();
+            for ( const item of shares_work.list() ) {
+                const { thing } = item;
+                if ( thing.$ !== 'app-share' ) continue;
+
+                item.type = 'app';
+                const errors = [];
+                if ( ! thing.uid && ! thing.name ) {
+                    errors.push('`uid` or `name` is required');
+                }
+
+                if ( errors.length ) {
+                    item.invalid = true;
+                    result.shares[item.i] =
+                        APIError.create('field_errors', null, {
+                            key: `shares[${item.i}]`,
+                            errors
+                        });
+                    continue;
+                }
+                
+                const app_selector = thing.uid
+                    ? `uid#${thing.uid}` : thing.name;
+                
+                item.share_intent = {
+                    $: 'share-intent:app',
+                    permissions: [
+                        PermissionUtil.join('app', app_selector, 'access')
+                    ]
+                }
+                continue;
+            }
+        },
+        async function fetch_nodes_for_file_shares (a) {
+            const { req, result, shares_work } = a.values();
+            for ( const item of shares_work.list() ) {
+                if ( item.type !== 'fs' ) continue;
+                const node = await (new FSNodeParam('path')).consolidate({
+                    req, getParam: () => item.path
+                });
+                
+                if ( ! await node.exists() ) {
+                    item.invalid = true;
+                    result.shares[item.i] = APIError.create('subject_does_not_exist', {
+                        path: item.path,
+                    })
+                    continue;
+                }
+                
+                item.node = node;
+                let email_path = item.path;
+                let is_dir = true;
+                if ( await node.get('type') !== TYPE_DIRECTORY ) {
+                    is_dir = false;
+                    // remove last component
+                    email_path = email_path.slice(0, item.path.lastIndexOf('/')+1);
+                }
+
+                if ( email_path.startsWith('/') ) email_path = email_path.slice(1);
+                const email_link = `${config.origin}/show/${email_path}`;
+                item.is_dir = is_dir;
+                item.email_link = email_link;
+            }
+        },
+        async function fetch_apps_for_app_shares (a) {
+            const { result, shares_work } = a.values();
+            const db = a.iget('db');
+            
+            for ( const item of shares_work.list() ) {
+                if ( item.type !== 'app' ) continue;
+                const { thing } = item;
+
+                const app = await get_app(thing.uid ?
+                    { uid: thing.uid } : { name: thing.name });
+                if ( ! app ) {
+                    item.invalid = true;
+                    result.shares[item.i] =
+                        // note: since we're reporting `entity_not_found`
+                        // we will report the id as an entity-storage-compatible
+                        // identifier.
+                        APIError.create('entity_not_found', null, {
+                            identifier: thing.uid
+                                ? { uid: thing.uid }
+                                : { id: { name: thing.name } }
+                        });
+                }
+                
+                app.metadata = db.case({
+                    mysql: () => app.metadata,
+                    otherwise: () => JSON.parse(app.metadata ?? '{}')
+                })();
+                
+                item.app = app;
+            }
+        },
+        async function add_subdomain_permissions (a) {
+            const { shares_work } = a.values();
+            const actor = a.get('actor');
+            const db = a.iget('db');
+
+            for ( const item of shares_work.list() ) {
+                if ( item.type !== 'app' ) continue;
+                const [subdomain] = await db.read(
+                    `SELECT * FROM subdomains WHERE associated_app_id = ? ` +
+                    `AND user_id = ? LIMIT 1`,
+                    [item.app.id, actor.type.user.id]
+                );
+                if ( ! subdomain ) continue;
+                
+                // The subdomain is also owned by this user, so we'll
+                // add a permission for that as well
+                
+                const site_selector = `uid#${subdomain.uuid}`;
+                item.share_intent.permissions.push(
+                    PermissionUtil.join('site', site_selector, 'access')
+                )
+            }
+        },
+        async function add_appdata_permissions (a) {
+            const { result, shares_work } = a.values();
+            for ( const item of shares_work.list() ) {
+                if ( item.type !== 'app' ) continue;
+                if ( ! item.app.metadata?.shared_appdata ) continue;
+                
+                const app_owner = await get_user({ id: item.app.owner_user_id });
+                
+                const appdatadir =
+                    `/${app_owner.username}/AppData/${item.app.uid}`;
+                const appdatadir_perm =
+                    PermissionUtil.join('fs', appdatadir, 'write');
+
+                item.share_intent.permissions.push(appdatadir_perm);
+            }
+        },
+        function apply_success_status_to_shares (a) {
+            const { result, shares_work } = a.values();
+            for ( const item of shares_work.list() ) {
+                result.shares[item.i] =
+                    {
+                        $: 'api:status-report',
+                        status: 'success',
+                        fields: {
+                            permission: item.permission,
+                        }
+                    };
+            }
+        },
+    ]),
+    function abort_on_error_if_mode_is_strict (a) {
+        const strict_mode = a.get('strict_mode');
+        if ( ! strict_mode ) return;
+        
+        const result = a.get('result');
+        if (
+            result.recipients.some(v => v !== null) ||
+            result.shares.some(v => v !== null)
+        ) {
+            result.serialize();
+            result.status = 'aborted';
+            const res = a.get('res');
+            res.status(218).send(result);
+            a.stop();
+        }
+    },
+    function early_return_on_dry_run (a) {
+        if ( ! a.get('req').body.dry_run ) return;
+            
+        const { res, result, recipients_work } = a.values();
+        for ( const item of recipients_work.list() ) {
+            result.recipients[item.i] =
+                { $: 'api:status-report', status: 'success' };
+        }
+        
+        result.serialize();
+        result.status = 'success';
+        result.dry_run = true;
+        res.send(result);
+        a.stop();
+    },
+    async function grant_permissions_to_existing_users (a) {
+        const {
+            req, result, recipients_work, shares_work
+        } = a.values();
+        
+        const svc_permission = a.iget('services').get('permission');
+        const svc_notification = a.iget('services').get('notification');
+        
+        const actor = a.get('actor');
+
+        for ( const recipient_item of recipients_work.list() ) {
+            if ( recipient_item.type !== 'username' ) continue;
+            
+            const username = recipient_item.user.username;
+
+            for ( const share_item of shares_work.list() ) {
+                const permissions = share_item.share_intent.permissions;
+                for ( const perm of permissions ) {
+                    await svc_permission.grant_user_user_permission(
+                        actor,
+                        username,
+                        perm,
+                    );
+                }
+            }
+        
+            const files = []; {
+                for ( const item of shares_work.list() ) {
+                    if ( item.type !== 'file' ) continue;
+                    files.push(
+                        await item.node.getSafeEntry(),
+                    );
+                }
+            }
+
+            const apps = []; {
+                for ( const item of shares_work.list() ) {
+                    if ( item.type !== 'app' ) continue;
+                    // TODO: is there a general way to create a
+                    //       client-safe app right now without
+                    //       going through entity storage?
+                    // track: manual safe object
+                    apps.push(item.name
+                        ? item.name : await get_app({
+                            uid: item.uid,
+                        }));
+                }
+            }
+            
+            svc_notification.notify(UsernameNotifSelector(username), {
+                source: 'sharing',
+                icon: 'shared.svg',
+                title: 'Files were shared with you!',
+                template: 'file-shared-with-you',
+                fields: {
+                    username: actor.type.user.username,
+                    files,
+                },
+                text: `The user ${quot(req.user.username)} shared ` +
+                    `${files.length} ` +
+                    (files.length === 1 ? 'file' : 'files') + ' ' +
+                    'with you.',
+            });
+            
+            result.recipients[recipient_item.i] =
+                { $: 'api:status-report', status: 'success' };
+        }
+    },
+    async function email_the_email_recipients (a) {
+        const { actor, recipients_work, shares_work } = a.values();
+        
+        const svc_share = a.iget('services').get('share');
+        const svc_token = a.iget('services').get('token');
+        const svc_email = a.iget('services').get('token');
+        
+        for ( const recipient_item of recipients_work.list() ) {
+            if ( recipient_item.type !== 'email' ) continue;
+            
+            const email = recipient_item.value;
+            
+            // data that gets stored in the `data` column of the share
+            const data = {
+                $: 'internal:share',
+                $v: 'v0.0.0',
+                permissions: [],
+            };
+            
+            for ( const share_item of shares_work.list() ) {
+                const permissions = share_item.share_intent.permissions;
+                data.permissions.push(...permissions);
+            }
+            
+            // track: scoping iife
+            const share_token = await (async () => {
+                const share_uid = await svc_share.create_share({
+                    issuer: actor,
+                    email,
+                    data,
+                });
+                return svc_token.sign('share', {
+                    $: 'token:share',
+                    $v: '0.0.0',
+                    uid: share_uid,
+                }, {
+                    expiresIn: '14d'
+                });
+            })();
+            
+            const email_link =
+                `${config.origin}?share_token=${share_token}`;
+            
+            await svc_email.send_email({ email }, 'share_by_email', {
+                link: email_link,
+            });
+        }
+    },
+    function send_result (a) {
+        const { res, result } = a.values();
+        result.serialize();
+        res.send(result);
+    }
+]);