| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | /** | 
					
						
							|  |  |  |  * \file | 
					
						
							|  |  |  |  * \defgroup tfapp Tilde Friends App JS | 
					
						
							|  |  |  |  * Tilde Friends server-side app wrapper. | 
					
						
							|  |  |  |  * @{ | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** \cond */ | 
					
						
							| 
									
										
										
										
											2022-06-18 17:39:08 +00:00
										 |  |  | import * as core from './core.js'; | 
					
						
							| 
									
										
										
										
											2022-03-18 01:24:29 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | export {App}; | 
					
						
							|  |  |  | /** \endcond */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** A sequence number of apps. */ | 
					
						
							|  |  |  | let g_session_index = 0; | 
					
						
							| 
									
										
										
										
											2022-03-18 01:24:29 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | /** | 
					
						
							|  |  |  |  ** App constructor. | 
					
						
							|  |  |  |  ** @return An app instance. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | function App() { | 
					
						
							|  |  |  | 	this._send_queue = []; | 
					
						
							| 
									
										
										
										
											2025-02-27 13:52:24 -05:00
										 |  |  | 	this.calls = {}; | 
					
						
							|  |  |  | 	this._next_call_id = 1; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 	return this; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | /** | 
					
						
							|  |  |  |  ** Create a function wrapper that when called invokes a function on the app | 
					
						
							|  |  |  |  ** itself. | 
					
						
							|  |  |  |  ** @param api The function and argument names. | 
					
						
							|  |  |  |  ** @return A function. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | App.prototype.makeFunction = function (api) { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 	let self = this; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 	let result = function () { | 
					
						
							| 
									
										
										
										
											2025-02-27 13:52:24 -05:00
										 |  |  | 		let id = self._next_call_id++; | 
					
						
							|  |  |  | 		while (!id || self.calls[id]) { | 
					
						
							|  |  |  | 			id = self._next_call_id++; | 
					
						
							| 
									
										
										
										
											2023-01-21 00:16:18 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 		let promise = new Promise(function (resolve, reject) { | 
					
						
							| 
									
										
										
										
											2025-02-27 13:52:24 -05:00
										 |  |  | 			self.calls[id] = {resolve: resolve, reject: reject}; | 
					
						
							| 
									
										
										
										
											2023-01-21 00:16:18 +00:00
										 |  |  | 		}); | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 		let message = { | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  | 			action: 'tfrpc', | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 			method: api[0], | 
					
						
							|  |  |  | 			params: [...arguments], | 
					
						
							|  |  |  | 			id: id, | 
					
						
							|  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 		self.send(message); | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 		return promise; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 	}; | 
					
						
							|  |  |  | 	Object.defineProperty(result, 'name', {value: api[0], writable: false}); | 
					
						
							|  |  |  | 	return result; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | /** | 
					
						
							|  |  |  |  ** Send a message to the app. | 
					
						
							|  |  |  |  ** @param message The message to send. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | App.prototype.send = function (message) { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 	if (this._send_queue) { | 
					
						
							|  |  |  | 		if (this._on_output) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 			this._send_queue.forEach((x) => this._on_output(x)); | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 			this._send_queue = null; | 
					
						
							|  |  |  | 		} else if (message) { | 
					
						
							|  |  |  | 			this._send_queue.push(message); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 	if (message && this._on_output) { | 
					
						
							|  |  |  | 		this._on_output(message); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | /** | 
					
						
							|  |  |  |  ** App socket handler. | 
					
						
							|  |  |  |  ** @param request The HTTP request of the WebSocket connection. | 
					
						
							|  |  |  |  ** @param response The HTTP response. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2025-02-02 12:27:06 -05:00
										 |  |  | exports.app_socket = async function socket(request, response) { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 	let process; | 
					
						
							|  |  |  | 	let options = {}; | 
					
						
							| 
									
										
										
										
											2024-06-10 20:22:28 -04:00
										 |  |  | 	let credentials = await httpd.auth_query(request.headers); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 	response.onClose = async function () { | 
					
						
							| 
									
										
										
										
											2022-01-21 00:49:03 +00:00
										 |  |  | 		if (process && process.task) { | 
					
						
							|  |  |  | 			process.task.kill(); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-01-27 15:45:51 +00:00
										 |  |  | 		if (process) { | 
					
						
							|  |  |  | 			process.timeout = 0; | 
					
						
							| 
									
										
										
										
											2022-01-21 00:49:03 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 	}; | 
					
						
							| 
									
										
										
										
											2022-01-21 00:49:03 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 	response.onMessage = async function (event) { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 		if (event.opCode == 0x1 || event.opCode == 0x2) { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 			let message; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 			try { | 
					
						
							|  |  |  | 				message = JSON.parse(event.data); | 
					
						
							|  |  |  | 			} catch (error) { | 
					
						
							| 
									
										
										
										
											2025-02-27 15:00:37 -05:00
										 |  |  | 				print( | 
					
						
							|  |  |  | 					'WebSocket error:', | 
					
						
							|  |  |  | 					error, | 
					
						
							|  |  |  | 					event.data, | 
					
						
							|  |  |  | 					event.data.length, | 
					
						
							|  |  |  | 					event.opCode | 
					
						
							|  |  |  | 				); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 				return; | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  | 			if (!process && message.action == 'hello') { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 				let packageOwner; | 
					
						
							|  |  |  | 				let packageName; | 
					
						
							|  |  |  | 				let blobId; | 
					
						
							|  |  |  | 				let match; | 
					
						
							|  |  |  | 				let parentApp; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				if ( | 
					
						
							|  |  |  | 					(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path)) | 
					
						
							|  |  |  | 				) { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 					blobId = match[1]; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				} else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) { | 
					
						
							| 
									
										
										
										
											2022-03-16 00:23:14 +00:00
										 |  |  | 					packageOwner = match[1]; | 
					
						
							|  |  |  | 					packageName = match[2]; | 
					
						
							|  |  |  | 					blobId = await new Database(packageOwner).get('path:' + packageName); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 					if (!blobId) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 						response.send( | 
					
						
							|  |  |  | 							JSON.stringify({ | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  | 								action: 'tfrpc', | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 								method: 'error', | 
					
						
							|  |  |  | 								params: [message.path + ' not found'], | 
					
						
							|  |  |  | 								id: -1, | 
					
						
							|  |  |  | 							}), | 
					
						
							|  |  |  | 							0x1 | 
					
						
							|  |  |  | 						); | 
					
						
							| 
									
										
										
										
											2022-01-18 02:50:46 +00:00
										 |  |  | 						return; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2022-03-16 00:23:14 +00:00
										 |  |  | 					if (packageOwner != 'core') { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 						let coreId = await new Database('core').get('path:' + packageName); | 
					
						
							| 
									
										
										
										
											2022-01-30 14:51:09 +00:00
										 |  |  | 						parentApp = { | 
					
						
							| 
									
										
										
										
											2022-03-16 00:23:14 +00:00
										 |  |  | 							path: '/~core/' + packageName + '/', | 
					
						
							| 
									
										
										
										
											2022-01-30 14:51:09 +00:00
										 |  |  | 							id: coreId, | 
					
						
							|  |  |  | 						}; | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				response.send( | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  | 					JSON.stringify( | 
					
						
							|  |  |  | 						Object.assign( | 
					
						
							|  |  |  | 							{ | 
					
						
							|  |  |  | 								action: 'session', | 
					
						
							|  |  |  | 								credentials: credentials, | 
					
						
							|  |  |  | 								parentApp: parentApp, | 
					
						
							|  |  |  | 								id: blobId, | 
					
						
							|  |  |  | 							}, | 
					
						
							| 
									
										
										
										
											2025-09-28 17:19:58 -04:00
										 |  |  | 							await ssb_internal.getIdentityInfo( | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  | 								credentials?.session?.name, | 
					
						
							|  |  |  | 								packageOwner, | 
					
						
							|  |  |  | 								packageName | 
					
						
							|  |  |  | 							) | 
					
						
							|  |  |  | 						) | 
					
						
							|  |  |  | 					), | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 					0x1 | 
					
						
							|  |  |  | 				); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				options.api = message.api || []; | 
					
						
							|  |  |  | 				options.credentials = credentials; | 
					
						
							| 
									
										
										
										
											2022-03-16 00:23:14 +00:00
										 |  |  | 				options.packageOwner = packageOwner; | 
					
						
							|  |  |  | 				options.packageName = packageName; | 
					
						
							| 
									
										
										
										
											2023-07-31 00:26:09 +00:00
										 |  |  | 				options.url = message.url; | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | 				let sessionId = 'session_' + (g_session_index++).toString(); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 				if (blobId) { | 
					
						
							| 
									
										
										
										
											2023-08-17 00:49:02 +00:00
										 |  |  | 					if (message.edit_only) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 						response.send( | 
					
						
							|  |  |  | 							JSON.stringify({action: 'ready', edit_only: true}), | 
					
						
							|  |  |  | 							0x1 | 
					
						
							|  |  |  | 						); | 
					
						
							| 
									
										
										
										
											2023-08-17 00:49:02 +00:00
										 |  |  | 					} else { | 
					
						
							| 
									
										
										
										
											2024-10-23 15:38:49 -04:00
										 |  |  | 						process = await core.getProcessBlob(blobId, sessionId, options); | 
					
						
							| 
									
										
										
										
											2023-08-17 00:49:02 +00:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 				} | 
					
						
							|  |  |  | 				if (process) { | 
					
						
							| 
									
										
										
										
											2025-02-27 15:00:37 -05:00
										 |  |  | 					process.client_api.tfrpc = function (message) { | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  | 						if (message.id) { | 
					
						
							|  |  |  | 							let calls = process?.app?.calls; | 
					
						
							|  |  |  | 							if (calls) { | 
					
						
							|  |  |  | 								let call = calls[message.id]; | 
					
						
							|  |  |  | 								if (call) { | 
					
						
							|  |  |  | 									if (message.error !== undefined) { | 
					
						
							|  |  |  | 										call.reject(message.error); | 
					
						
							|  |  |  | 									} else { | 
					
						
							|  |  |  | 										call.resolve(message.result); | 
					
						
							|  |  |  | 									} | 
					
						
							|  |  |  | 									delete calls[message.id]; | 
					
						
							|  |  |  | 								} | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					}; | 
					
						
							| 
									
										
										
										
											2025-02-27 15:00:37 -05:00
										 |  |  | 					process.app._on_output = (message) => | 
					
						
							|  |  |  | 						response.send(JSON.stringify(message), 0x1); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 					process.app.send(); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				let ping = function () { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 					let now = Date.now(); | 
					
						
							|  |  |  | 					let again = true; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 					if (now - process.lastActive < process.timeout) { | 
					
						
							|  |  |  | 						// Active.
 | 
					
						
							|  |  |  | 					} else if (process.lastPing > process.lastActive) { | 
					
						
							|  |  |  | 						// We lost them.
 | 
					
						
							|  |  |  | 						if (process.task) { | 
					
						
							|  |  |  | 							process.task.kill(); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 						again = false; | 
					
						
							|  |  |  | 					} else { | 
					
						
							|  |  |  | 						// Idle.  Ping them.
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 						response.send('', 0x9); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 						process.lastPing = now; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-27 15:45:51 +00:00
										 |  |  | 					if (again && process.timeout) { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 						setTimeout(ping, process.timeout); | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 				}; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				if (process && process.timeout > 0) { | 
					
						
							|  |  |  | 					setTimeout(ping, process.timeout); | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  | 			} else { | 
					
						
							| 
									
										
										
										
											2022-08-14 18:24:41 +00:00
										 |  |  | 				if (process) { | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  | 					if (process.client_api[message.action]) { | 
					
						
							|  |  |  | 						process.client_api[message.action](message); | 
					
						
							|  |  |  | 					} else if (process.eventHandlers['message']) { | 
					
						
							|  |  |  | 						await core.invoke(process.eventHandlers['message'], [message]); | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} else if (event.opCode == 0x8) { | 
					
						
							|  |  |  | 			// Close.
 | 
					
						
							| 
									
										
										
										
											2022-01-21 00:49:03 +00:00
										 |  |  | 			if (process && process.task) { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 				process.task.kill(); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			response.send(event.data, 0x8); | 
					
						
							|  |  |  | 		} else if (event.opCode == 0xa) { | 
					
						
							|  |  |  | 			// PONG
 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (process) { | 
					
						
							|  |  |  | 			process.lastActive = Date.now(); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  | 	}; | 
					
						
							| 
									
										
										
										
											2022-10-05 01:20:47 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-02 12:42:31 -04:00
										 |  |  | 	response.upgrade(100, {}); | 
					
						
							| 
									
										
										
										
											2025-02-02 12:27:06 -05:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:25:20 -04:00
										 |  |  | /** @} */ |