forked from cory/tildefriends
		
	
		
			
				
	
	
		
			749 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			749 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * \file
 | |
|  * \defgroup tfcore Tilde Friends Core JS
 | |
|  * Tilde Friends process management, in JavaScript.
 | |
|  * @{
 | |
|  */
 | |
| 
 | |
| /** \cond */
 | |
| import * as app from './app.js';
 | |
| 
 | |
| export {invoke, getProcessBlob};
 | |
| /** \endcond */
 | |
| 
 | |
| /** All running processes. */
 | |
| let gProcesses = {};
 | |
| /** Whether stats are currently being sent. */
 | |
| let gStatsTimer = false;
 | |
| /** Effectively a process ID. */
 | |
| let g_handler_index = 0;
 | |
| /** Time between pings, in milliseconds. */
 | |
| const k_ping_interval = 60 * 1000;
 | |
| 
 | |
| /**
 | |
|  * Print an error.
 | |
|  * @param error The error.
 | |
|  */
 | |
| function printError(error) {
 | |
| 	if (error.stackTrace) {
 | |
| 		print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
 | |
| 		print(error.stackTrace);
 | |
| 	} else {
 | |
| 		for (let [k, v] of Object.entries(error)) {
 | |
| 			print(k, v);
 | |
| 		}
 | |
| 		print(error.toString());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Invoke a handler.
 | |
|  * @param handlers The handlers on which to invoke the callback.
 | |
|  * @param argv Arguments to pass to the handlers.
 | |
|  * @return A promise.
 | |
|  */
 | |
| function invoke(handlers, argv) {
 | |
| 	let promises = [];
 | |
| 	if (handlers) {
 | |
| 		for (let i = 0; i < handlers.length; ++i) {
 | |
| 			try {
 | |
| 				promises.push(handlers[i](...argv));
 | |
| 			} catch (error) {
 | |
| 				handlers.splice(i, 1);
 | |
| 				i--;
 | |
| 				promises.push(
 | |
| 					new Promise(function (resolve, reject) {
 | |
| 						reject(error);
 | |
| 					})
 | |
| 				);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return Promise.all(promises);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Broadcast a named event to all registered apps.
 | |
|  * @param eventName the name of the event.
 | |
|  * @param argv Arguments to pass to the handlers.
 | |
|  * @return A promise.
 | |
|  */
 | |
| function broadcastEvent(eventName, argv) {
 | |
| 	let promises = [];
 | |
| 	for (let process of Object.values(gProcesses)) {
 | |
| 		if (process.eventHandlers[eventName]) {
 | |
| 			promises.push(invoke(process.eventHandlers[eventName], argv));
 | |
| 		}
 | |
| 	}
 | |
| 	return Promise.all(promises);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Send a message to all other instances of the same app.
 | |
|  * @param message The message.
 | |
|  * @return A promise.
 | |
|  */
 | |
| function broadcast(message) {
 | |
| 	let sender = this;
 | |
| 	let promises = [];
 | |
| 	for (let process of Object.values(gProcesses)) {
 | |
| 		if (
 | |
| 			process != sender &&
 | |
| 			process.packageOwner == sender.packageOwner &&
 | |
| 			process.packageName == sender.packageName
 | |
| 		) {
 | |
| 			let from = getUser(process, sender);
 | |
| 			promises.push(postMessageInternal(from, process, message));
 | |
| 		}
 | |
| 	}
 | |
| 	return Promise.all(promises);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Send a message to all instances of the same app running as the same user.
 | |
|  * @param user The user.
 | |
|  * @param packageOwner The owner of the app.
 | |
|  * @param packageName The name of the app.
 | |
|  * @param eventName The name of the event.
 | |
|  * @param argv The arguments to pass.
 | |
|  * @return A promise.
 | |
|  */
 | |
| function broadcastAppEventToUser(
 | |
| 	user,
 | |
| 	packageOwner,
 | |
| 	packageName,
 | |
| 	eventName,
 | |
| 	argv
 | |
| ) {
 | |
| 	let promises = [];
 | |
| 	for (let process of Object.values(gProcesses)) {
 | |
| 		if (
 | |
| 			process.credentials?.session?.name === user &&
 | |
| 			process.packageOwner == packageOwner &&
 | |
| 			process.packageName == packageName
 | |
| 		) {
 | |
| 			if (process.eventHandlers[eventName]) {
 | |
| 				promises.push(invoke(process.eventHandlers[eventName], argv));
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return Promise.all(promises);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get user context information for a call.
 | |
|  * @param caller The calling process.
 | |
|  * @param process The receiving process.
 | |
|  */
 | |
| function getUser(caller, process) {
 | |
| 	return {
 | |
| 		key: process.key,
 | |
| 		packageOwner: process.packageOwner,
 | |
| 		packageName: process.packageName,
 | |
| 		credentials: process.credentials,
 | |
| 		postMessage: postMessageInternal.bind(caller, caller, process),
 | |
| 	};
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Send a message.
 | |
|  * @param from The calling process.
 | |
|  * @param to The receiving process.
 | |
|  * @param message The message.
 | |
|  * @return A promise.
 | |
|  */
 | |
| function postMessageInternal(from, to, message) {
 | |
| 	if (to.eventHandlers['message']) {
 | |
| 		return invoke(to.eventHandlers['message'], [getUser(from, from), message]);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get or create a process for an app blob.
 | |
|  * @param blobId The blob identifier.
 | |
|  * @param key A unique key for the invocation.
 | |
|  * @param options Other options.
 | |
|  * @return The process.
 | |
|  */
 | |
| async function getProcessBlob(blobId, key, options) {
 | |
| 	let process = gProcesses[key];
 | |
| 	if (!process && !(options && 'create' in options && !options.create)) {
 | |
| 		let resolveReady;
 | |
| 		let rejectReady;
 | |
| 		try {
 | |
| 			print('Creating task for ' + blobId + ' ' + key);
 | |
| 			process = {};
 | |
| 			process.key = key;
 | |
| 			process.credentials = options.credentials || {};
 | |
| 			process.task = new Task();
 | |
| 			process.packageOwner = options.packageOwner;
 | |
| 			process.packageName = options.packageName;
 | |
| 			process.url = options?.url;
 | |
| 			process.eventHandlers = {};
 | |
| 			if (!options?.script || options?.script === 'app.js') {
 | |
| 				process.app = new app.App();
 | |
| 			}
 | |
| 			process.lastActive = Date.now();
 | |
| 			process.lastPing = null;
 | |
| 			process.timeout = k_ping_interval;
 | |
| 			process.ready = new Promise(function (resolve, reject) {
 | |
| 				resolveReady = resolve;
 | |
| 				rejectReady = reject;
 | |
| 			});
 | |
| 			gProcesses[key] = process;
 | |
| 			process.task.onExit = function (exitCode, terminationSignal) {
 | |
| 				process.task = null;
 | |
| 				delete gProcesses[key];
 | |
| 			};
 | |
| 			let imports = {
 | |
| 				core: {
 | |
| 					broadcast: broadcast.bind(process),
 | |
| 					user: getUser(process, process),
 | |
| 					allPermissionsGranted: async function () {
 | |
| 						let user = process?.credentials?.session?.name;
 | |
| 						let settings = await loadSettings();
 | |
| 						if (
 | |
| 							user &&
 | |
| 							options?.packageOwner &&
 | |
| 							options?.packageName &&
 | |
| 							settings.userPermissions &&
 | |
| 							settings.userPermissions[user]
 | |
| 						) {
 | |
| 							return settings.userPermissions[user];
 | |
| 						}
 | |
| 					},
 | |
| 					permissionTest: async function (permission) {
 | |
| 						let user = process?.credentials?.session?.name;
 | |
| 						let settings = await loadSettings();
 | |
| 						if (!user || !options?.packageOwner || !options?.packageName) {
 | |
| 							return;
 | |
| 						} else if (
 | |
| 							settings.userPermissions &&
 | |
| 							settings.userPermissions[user] &&
 | |
| 							settings.userPermissions[user][options.packageOwner] &&
 | |
| 							settings.userPermissions[user][options.packageOwner][
 | |
| 								options.packageName
 | |
| 							] &&
 | |
| 							settings.userPermissions[user][options.packageOwner][
 | |
| 								options.packageName
 | |
| 							][permission] !== undefined
 | |
| 						) {
 | |
| 							if (
 | |
| 								settings.userPermissions[user][options.packageOwner][
 | |
| 									options.packageName
 | |
| 								][permission]
 | |
| 							) {
 | |
| 								return true;
 | |
| 							} else {
 | |
| 								throw Error(`Permission denied: ${permission}.`);
 | |
| 							}
 | |
| 						} else if (process.app) {
 | |
| 							return process.app
 | |
| 								.makeFunction(['requestPermission'])(permission)
 | |
| 								.then(async function (value) {
 | |
| 									if (value == 'allow') {
 | |
| 										await ssb.setUserPermission(
 | |
| 											user,
 | |
| 											options.packageOwner,
 | |
| 											options.packageName,
 | |
| 											permission,
 | |
| 											true
 | |
| 										);
 | |
| 										process.sendPermissions();
 | |
| 										return true;
 | |
| 									} else if (value == 'allow once') {
 | |
| 										return true;
 | |
| 									} else if (value == 'deny') {
 | |
| 										await ssb.setUserPermission(
 | |
| 											user,
 | |
| 											options.packageOwner,
 | |
| 											options.packageName,
 | |
| 											permission,
 | |
| 											false
 | |
| 										);
 | |
| 										process.sendPermissions();
 | |
| 										throw Error(`Permission denied: ${permission}.`);
 | |
| 									} else if (value == 'deny once') {
 | |
| 										throw Error(`Permission denied: ${permission}.`);
 | |
| 									}
 | |
| 									throw Error(`Permission denied: ${permission}.`);
 | |
| 								});
 | |
| 						} else {
 | |
| 							throw Error(`Permission denied: ${permission}.`);
 | |
| 						}
 | |
| 					},
 | |
| 				},
 | |
| 			};
 | |
| 			process.sendIdentities = async function () {
 | |
| 				process.app.send(
 | |
| 					Object.assign(
 | |
| 						{
 | |
| 							action: 'identities',
 | |
| 						},
 | |
| 						await ssb_internal.getIdentityInfo(
 | |
| 							process?.credentials?.session?.name,
 | |
| 							options?.packageOwner,
 | |
| 							options?.packageName
 | |
| 						)
 | |
| 					)
 | |
| 				);
 | |
| 			};
 | |
| 			process.setActiveIdentity = async function (identity) {
 | |
| 				if (
 | |
| 					process?.credentials?.session?.name &&
 | |
| 					options.packageOwner &&
 | |
| 					options.packageName
 | |
| 				) {
 | |
| 					await new Database(process?.credentials?.session?.name).set(
 | |
| 						`id:${options.packageOwner}:${options.packageName}`,
 | |
| 						identity
 | |
| 					);
 | |
| 				}
 | |
| 				process.sendIdentities();
 | |
| 				broadcastAppEventToUser(
 | |
| 					process?.credentials?.session?.name,
 | |
| 					options.packageOwner,
 | |
| 					options.packageName,
 | |
| 					'setActiveIdentity',
 | |
| 					[identity]
 | |
| 				);
 | |
| 			};
 | |
| 			process.createIdentity = async function () {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name &&
 | |
| 					process.credentials.session.name !== 'guest'
 | |
| 				) {
 | |
| 					let id = await ssb.createIdentity(process.credentials.session.name);
 | |
| 					await process.sendIdentities();
 | |
| 					broadcastAppEventToUser(
 | |
| 						process?.credentials?.session?.name,
 | |
| 						options.packageOwner,
 | |
| 						options.packageName,
 | |
| 						'setActiveIdentity',
 | |
| 						[
 | |
| 							await imports.ssb.getActiveIdentity(
 | |
| 								process.credentials?.session?.name,
 | |
| 								options.packageOwner,
 | |
| 								options.packageName
 | |
| 							),
 | |
| 						]
 | |
| 					);
 | |
| 					return id;
 | |
| 				} else {
 | |
| 					throw new Error('Must be signed-in to create an account.');
 | |
| 				}
 | |
| 			};
 | |
| 			if (process.credentials?.permissions?.administration) {
 | |
| 				imports.core.globalSettingsSet = async function (key, value) {
 | |
| 					await imports.core.permissionTest('set_global_setting');
 | |
| 					print('Setting', key, value);
 | |
| 					let settings = await loadSettings();
 | |
| 					settings[key] = value;
 | |
| 					await new Database('core').set('settings', JSON.stringify(settings));
 | |
| 					print('Done.');
 | |
| 				};
 | |
| 				imports.core.deleteUser = async function (user) {
 | |
| 					await imports.core.permissionTest('delete_user');
 | |
| 					let db = new Database('auth');
 | |
| 					db.remove('user:' + user);
 | |
| 					let users = new Set();
 | |
| 					let users_original = await db.get('users');
 | |
| 					try {
 | |
| 						users = new Set(JSON.parse(users_original));
 | |
| 					} catch {}
 | |
| 					users.delete(user);
 | |
| 					users = JSON.stringify([...users].sort());
 | |
| 					if (users !== users_original) {
 | |
| 						await db.set('users', users);
 | |
| 					}
 | |
| 				};
 | |
| 			}
 | |
| 			if (options.api) {
 | |
| 				imports.app = {};
 | |
| 				for (let i in options.api) {
 | |
| 					let api = options.api[i];
 | |
| 					imports.app[api[0]] = process.app.makeFunction(api);
 | |
| 				}
 | |
| 			}
 | |
| 			for (let [name, f] of Object.entries(options?.imports || {})) {
 | |
| 				imports[name] = f;
 | |
| 			}
 | |
| 			process.task.onPrint = function (args) {
 | |
| 				if (imports.app) {
 | |
| 					imports.app.print(...args);
 | |
| 				}
 | |
| 			};
 | |
| 			process.task.onError = function (error) {
 | |
| 				try {
 | |
| 					if (process.app) {
 | |
| 						process.app.makeFunction(['error'])(error);
 | |
| 					} else {
 | |
| 						printError(error);
 | |
| 					}
 | |
| 				} catch (e) {
 | |
| 					printError(error);
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb = Object.fromEntries(
 | |
| 				Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
 | |
| 			);
 | |
| 			imports.ssb.createIdentity = () => process.createIdentity();
 | |
| 			imports.ssb.addIdentity = function (id) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return Promise.resolve(
 | |
| 						imports.core.permissionTest('ssb_id_add')
 | |
| 					).then(function () {
 | |
| 						return ssb.addIdentity(process.credentials.session.name, id);
 | |
| 					});
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb.deleteIdentity = function (id) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return Promise.resolve(
 | |
| 						imports.core.permissionTest('ssb_id_delete')
 | |
| 					).then(function () {
 | |
| 						return ssb.deleteIdentity(process.credentials.session.name, id);
 | |
| 					});
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
 | |
| 			imports.ssb.getPrivateKey = function (id) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return Promise.resolve(
 | |
| 						imports.core.permissionTest('ssb_id_export')
 | |
| 					).then(function () {
 | |
| 						return ssb.getPrivateKey(process.credentials.session.name, id);
 | |
| 					});
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb.appendMessageWithIdentity = function (id, message) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return Promise.resolve(
 | |
| 						imports.core.permissionTest('ssb_append')
 | |
| 					).then(function () {
 | |
| 						return ssb.appendMessageWithIdentity(
 | |
| 							process.credentials.session.name,
 | |
| 							id,
 | |
| 							message
 | |
| 						);
 | |
| 					});
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb.privateMessageEncrypt = function (id, recipients, message) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return ssb.privateMessageEncrypt(
 | |
| 						process.credentials.session.name,
 | |
| 						id,
 | |
| 						recipients,
 | |
| 						message
 | |
| 					);
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb.privateMessageDecrypt = function (id, message) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return ssb.privateMessageDecrypt(
 | |
| 						process.credentials.session.name,
 | |
| 						id,
 | |
| 						message
 | |
| 					);
 | |
| 				}
 | |
| 			};
 | |
| 			imports.ssb.swapWithServerIdentity = function (id) {
 | |
| 				if (
 | |
| 					process.credentials &&
 | |
| 					process.credentials.session &&
 | |
| 					process.credentials.session.name
 | |
| 				) {
 | |
| 					return ssb.swapWithServerIdentity(
 | |
| 						process.credentials.session.name,
 | |
| 						id
 | |
| 					);
 | |
| 				}
 | |
| 			};
 | |
| 
 | |
| 			if (
 | |
| 				process.credentials &&
 | |
| 				process.credentials.session &&
 | |
| 				process.credentials.session.name
 | |
| 			) {
 | |
| 				imports.database = function (key) {
 | |
| 					let db = new Database(process.credentials.session.name + ':' + key);
 | |
| 					return Object.fromEntries(
 | |
| 						Object.keys(db).map((x) => [x, db[x].bind(db)])
 | |
| 					);
 | |
| 				};
 | |
| 				imports.my_shared_database = function (packageName, key) {
 | |
| 					let db = new Database(
 | |
| 						':shared:' +
 | |
| 							process.credentials.session.name +
 | |
| 							':' +
 | |
| 							packageName +
 | |
| 							':' +
 | |
| 							key
 | |
| 					);
 | |
| 					return Object.fromEntries(
 | |
| 						Object.keys(db).map((x) => [x, db[x].bind(db)])
 | |
| 					);
 | |
| 				};
 | |
| 				imports.databases = async function () {
 | |
| 					return [].concat(
 | |
| 						await databases.list(
 | |
| 							':shared:' + process.credentials.session.name + ':%'
 | |
| 						),
 | |
| 						await databases.list(process.credentials.session.name + ':%')
 | |
| 					);
 | |
| 				};
 | |
| 			}
 | |
| 			if (options.packageOwner && options.packageName) {
 | |
| 				imports.shared_database = function (key) {
 | |
| 					let db = new Database(
 | |
| 						':shared:' +
 | |
| 							options.packageOwner +
 | |
| 							':' +
 | |
| 							options.packageName +
 | |
| 							':' +
 | |
| 							key
 | |
| 					);
 | |
| 					return Object.fromEntries(
 | |
| 						Object.keys(db).map((x) => [x, db[x].bind(db)])
 | |
| 					);
 | |
| 				};
 | |
| 			}
 | |
| 			process.sendPermissions = async function sendPermissions() {
 | |
| 				process.app.send({
 | |
| 					action: 'permissions',
 | |
| 					permissions: await imports.core.permissionsGranted(),
 | |
| 				});
 | |
| 			};
 | |
| 			process.client_api = {
 | |
| 				createIdentity: function () {
 | |
| 					return process.createIdentity();
 | |
| 				},
 | |
| 				resetPermission: async function resetPermission(message) {
 | |
| 					let user = process?.credentials?.session?.name;
 | |
| 					await ssb.setUserPermission(
 | |
| 						user,
 | |
| 						options?.packageOwner,
 | |
| 						options?.packageName,
 | |
| 						message.permission,
 | |
| 						undefined
 | |
| 					);
 | |
| 					return process.sendPermissions();
 | |
| 				},
 | |
| 				setActiveIdentity: function setActiveIdentity(message) {
 | |
| 					return process.setActiveIdentity(message.identity);
 | |
| 				},
 | |
| 			};
 | |
| 			ssb.registerImports(imports, process);
 | |
| 			process.task.setImports(imports);
 | |
| 			process.task.activate();
 | |
| 			let source = await ssb.blobGet(blobId);
 | |
| 			let appSourceName = blobId;
 | |
| 			let appSource = utf8Decode(source);
 | |
| 			try {
 | |
| 				let appObject = JSON.parse(appSource);
 | |
| 				if (appObject.type == 'tildefriends-app') {
 | |
| 					appSourceName = options?.script ?? 'app.js';
 | |
| 					let id = appObject.files[appSourceName];
 | |
| 					let blob = await ssb.blobGet(id);
 | |
| 					appSource = utf8Decode(blob);
 | |
| 					await process.task.loadFile([
 | |
| 						'/tfrpc.js',
 | |
| 						await File.readFile('core/tfrpc.js'),
 | |
| 					]);
 | |
| 					await Promise.all(
 | |
| 						Object.keys(appObject.files).map(async function (f) {
 | |
| 							await process.task.loadFile([
 | |
| 								f,
 | |
| 								await ssb.blobGet(appObject.files[f]),
 | |
| 							]);
 | |
| 						})
 | |
| 					);
 | |
| 				}
 | |
| 			} catch (e) {
 | |
| 				printError(e);
 | |
| 			}
 | |
| 			if (process.app) {
 | |
| 				process.app.send({action: 'ready', version: version()});
 | |
| 				await process.sendPermissions();
 | |
| 			}
 | |
| 			await process.task.execute({name: appSourceName, source: appSource});
 | |
| 			resolveReady(process);
 | |
| 			if (!gStatsTimer) {
 | |
| 				gStatsTimer = true;
 | |
| 				sendStats();
 | |
| 			}
 | |
| 		} catch (error) {
 | |
| 			if (process?.app && process?.task?.onError) {
 | |
| 				process.task.onError(error);
 | |
| 			} else {
 | |
| 				printError(error);
 | |
| 			}
 | |
| 			rejectReady(error);
 | |
| 		}
 | |
| 	}
 | |
| 	return process;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * SSB message added callback.
 | |
|  */
 | |
| ssb_internal.addEventListener('message', function () {
 | |
| 	broadcastEvent('onMessage', [...arguments]);
 | |
| });
 | |
| 
 | |
| ssb_internal.addEventListener('blob', function () {
 | |
| 	broadcastEvent('onBlob', [...arguments]);
 | |
| });
 | |
| 
 | |
| ssb_internal.addEventListener('broadcasts', function () {
 | |
| 	broadcastEvent('onBroadcastsChanged', []);
 | |
| });
 | |
| 
 | |
| ssb_internal.addEventListener('connections', function () {
 | |
| 	broadcastEvent('onConnectionsChanged', []);
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Load settings from the database.
 | |
|  * @return The settings as a key value pairs object.
 | |
|  */
 | |
| async function loadSettings() {
 | |
| 	let data = {};
 | |
| 	try {
 | |
| 		let settings = await new Database('core').get('settings');
 | |
| 		if (settings) {
 | |
| 			data = JSON.parse(settings);
 | |
| 		}
 | |
| 	} catch (error) {
 | |
| 		print('Settings not found in database:', error);
 | |
| 	}
 | |
| 	for (let [key, value] of Object.entries(defaultGlobalSettings())) {
 | |
| 		if (data[key] === undefined) {
 | |
| 			data[key] = value.default_value;
 | |
| 		}
 | |
| 	}
 | |
| 	return data;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Send periodic stats to all clients.
 | |
|  */
 | |
| function sendStats() {
 | |
| 	let apps = Object.values(gProcesses)
 | |
| 		.filter((process) => process.app)
 | |
| 		.map((process) => process.app);
 | |
| 	if (apps.length) {
 | |
| 		let stats = getStats();
 | |
| 		for (let app of apps) {
 | |
| 			app.send({action: 'stats', stats: stats});
 | |
| 		}
 | |
| 		setTimeout(sendStats, 1000);
 | |
| 	} else {
 | |
| 		gStatsTimer = false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Invoke an app's handler.js.
 | |
|  * @param response The response object.
 | |
|  * @param app_blob_id The app's blob identifier.
 | |
|  * @param path The request path.
 | |
|  * @param query The request query string.
 | |
|  * @param headers The request headers.
 | |
|  * @param package_owner The app's owner.
 | |
|  * @param package_name The app's name.
 | |
|  */
 | |
| exports.callAppHandler = async function callAppHandler(
 | |
| 	response,
 | |
| 	app_blob_id,
 | |
| 	path,
 | |
| 	query,
 | |
| 	headers,
 | |
| 	package_owner,
 | |
| 	package_name
 | |
| ) {
 | |
| 	let answer;
 | |
| 	try {
 | |
| 		let do_resolve;
 | |
| 		let promise = new Promise(async function (resolve, reject) {
 | |
| 			do_resolve = resolve;
 | |
| 		});
 | |
| 		let process;
 | |
| 		try {
 | |
| 			process = await getProcessBlob(
 | |
| 				app_blob_id,
 | |
| 				'handler_' + g_handler_index++,
 | |
| 				{
 | |
| 					script: 'handler.js',
 | |
| 					imports: {
 | |
| 						request: {
 | |
| 							path: path,
 | |
| 							query: query,
 | |
| 						},
 | |
| 						respond: do_resolve,
 | |
| 					},
 | |
| 					credentials: await httpd.auth_query(headers),
 | |
| 					packageOwner: package_owner,
 | |
| 					packageName: package_name,
 | |
| 				}
 | |
| 			);
 | |
| 			await process.ready;
 | |
| 			answer = await promise;
 | |
| 		} finally {
 | |
| 			if (process?.task) {
 | |
| 				await process.task.kill();
 | |
| 			}
 | |
| 		}
 | |
| 	} catch (error) {
 | |
| 		let data = utf8Encode(
 | |
| 			`Internal Server Error\n\n${error?.message}\n${error?.stack}`
 | |
| 		);
 | |
| 		response.writeHead(500, {
 | |
| 			'Content-Type': 'text/plain; charset=utf-8',
 | |
| 			'Content-Length': data.length,
 | |
| 		});
 | |
| 		response.end(data);
 | |
| 		return;
 | |
| 	}
 | |
| 	if (typeof answer?.data == 'string') {
 | |
| 		answer.data = utf8Encode(answer.data);
 | |
| 	}
 | |
| 	response.writeHead(answer?.status_code, {
 | |
| 		'Content-Type': answer?.content_type,
 | |
| 		'Content-Length': answer?.data?.length,
 | |
| 		'Access-Control-Allow-Origin': '*',
 | |
| 		'Content-Security-Policy':
 | |
| 			'sandbox allow-downloads allow-top-navigation-by-user-activation',
 | |
| 	});
 | |
| 	response.end(answer?.data);
 | |
| };
 | |
| 
 | |
| /** @} */
 |