aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client')
-rw-r--r--packages/meshbay-client/src/cast-chromecast.js71
-rw-r--r--packages/meshbay-client/src/main.js30
-rw-r--r--packages/meshbay-client/src/preload.js3
-rw-r--r--packages/meshbay-client/src/transcripts.js16
4 files changed, 68 insertions, 52 deletions
diff --git a/packages/meshbay-client/src/cast-chromecast.js b/packages/meshbay-client/src/cast-chromecast.js
index 1355cdd..7fc8079 100644
--- a/packages/meshbay-client/src/cast-chromecast.js
+++ b/packages/meshbay-client/src/cast-chromecast.js
@@ -78,19 +78,25 @@ class CastChromecast {
constructor() {
this._bonjour = null;
this._browser = null;
+ this._scanTimer = null;
this._devices = new Map();
this._client = null;
this._player = null;
this._connectedDevice = null;
}
- async discover() {
+ /**
+ * Start a scan and return at once; `devices()` reads what it has found so far.
+ *
+ * The scan runs its full length because a receiver coming back from a reset
+ * can take several seconds to answer, but most answer within two, and a
+ * picker that waits the full length to show any of them is a picker that is
+ * slow every single time. So the page polls and lists each one as it lands.
+ * A scan started over an unfinished one replaces it, timer included.
+ */
+ startScan() {
this._devices.clear();
-
- if (this._browser) {
- this._browser.stop();
- this._browser = null;
- }
+ this._stopScan();
if (!this._bonjour) {
this._bonjour = new Bonjour();
@@ -98,30 +104,39 @@ class CastChromecast {
debug('[cast-chromecast] scanning for devices...');
- return new Promise((resolve) => {
- this._browser = this._bonjour.find({ type: 'googlecast' }, (service) => {
- const id = service.txt?.id || service.name;
- const name = service.txt?.fn || service.name;
- const host = service.addresses?.find((a) => /^\d+\.\d+\.\d+\.\d+$/.test(a))
- || (service.referer && service.referer.address);
- const port = service.port || 8009;
+ this._browser = this._bonjour.find({ type: 'googlecast' }, (service) => {
+ const id = service.txt?.id || service.name;
+ const name = service.txt?.fn || service.name;
+ const host = service.addresses?.find((a) => /^\d+\.\d+\.\d+\.\d+$/.test(a))
+ || (service.referer && service.referer.address);
+ const port = service.port || 8009;
- if (host && id) {
- this._devices.set(id, { id, name, host, port });
- debug(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`);
- }
- });
-
- setTimeout(() => {
- if (this._browser) {
- this._browser.stop();
- this._browser = null;
- }
- const devices = Array.from(this._devices.values());
- debug(`[cast-chromecast] scan complete: ${devices.length} device(s)`);
- resolve(devices);
- }, SCAN_DURATION_MS);
+ if (host && id) {
+ this._devices.set(id, { id, name, host, port });
+ debug(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`);
+ }
});
+
+ this._scanTimer = setTimeout(() => {
+ this._stopScan();
+ debug(`[cast-chromecast] scan complete: ${this._devices.size} device(s)`);
+ }, SCAN_DURATION_MS);
+ }
+
+ devices() {
+ return {
+ devices: Array.from(this._devices.values()),
+ scanning: this._browser !== null,
+ };
+ }
+
+ _stopScan() {
+ clearTimeout(this._scanTimer);
+ this._scanTimer = null;
+ if (this._browser) {
+ this._browser.stop();
+ this._browser = null;
+ }
}
async connect(deviceId, mediaUrl, subtitle) {
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index c1ff540..0ad7e71 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -1144,19 +1144,12 @@ function registerBridge() {
return true;
});
- // A folder chosen here is one the person pointed at in a dialog this process
- // drew, which is the consent that sharing it needs: the node is given it
- // without asking again. A folder the page names that was not chosen here is
- // confirmed natively before the node hears of it (`node:op`).
- const pickedFolders = new Set();
-
handle('root:choose', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory', 'createDirectory'],
});
if (result.canceled || !result.filePaths.length) return null;
const chosen = result.filePaths[0];
- pickedFolders.add(path.resolve(chosen));
return { path: chosen, name: path.basename(chosen) };
});
@@ -2185,38 +2178,28 @@ function registerBridge() {
const group = (a) => `/api/groups/${anId(a.groupId, 'the group')}`;
const root = (a) => `${group(a)}/roots/${encodeURIComponent(aText(a.rootName, 'the folder name'))}`;
- // Sharing a folder the person did not choose in this process's dialog.
- async function confirmFolder(folder) {
- if (!pickedFolders.has(path.resolve(folder))) {
- await confirmOrRefuse('native.folder_confirm', { path: folder });
- }
- }
-
const NODE_OPS = {
status: () => ['GET', '/api/status'],
groups: () => ['GET', '/api/groups'],
indexStatus: () => ['GET', '/api/index-status'],
groupIndexStatus: (a) => ['GET', `${group(a)}/index-status`],
reload: () => ['POST', '/api/reload'],
- attachGroup: async (a) => {
+ attachGroup: (a) => {
const body = { name: aText(a.name, 'the group name'),
shared_dir: aText(a.path, 'the folder', 4096),
writable: a.writable !== false,
// The person's choice on the creation form; the node never
// takes it from the hub.
join_policy: a.joinPolicy === 'open' ? 'open' : 'invite' };
- await confirmFolder(body.shared_dir);
return ['POST', '/api/groups/attach', body];
},
detachGroup: (a) => ['POST', '/api/groups/detach', { name: aText(a.name, 'the group name') }],
- addRoot: async (a) => {
+ addRoot: (a) => {
const body = { path: aText(a.path, 'the folder', 4096) };
if (a.name) body.name = aText(a.name, 'the folder name');
if (a.writable !== undefined) body.writable = Boolean(a.writable);
if (a.removable !== undefined) body.removable = Boolean(a.removable);
- const target = `${group(a)}/roots`;
- await confirmFolder(body.path);
- return ['POST', target, body];
+ return ['POST', `${group(a)}/roots`, body];
},
updateRoot: (a) => ['PATCH', root(a), anObject(a.updates)],
ejectRoot: (a) => ['PUT', `${root(a)}/eject`],
@@ -2330,10 +2313,13 @@ function registerBridge() {
// ── Chromecast discovery + control ──────────────────────────────────────
- handle('cast:discover', async () => {
- return castChromecast.discover();
+ handle('cast:scan', async () => {
+ castChromecast.startScan();
+ return true;
});
+ handle('cast:devices', async () => castChromecast.devices());
+
handle('cast:chromecast:connect', async (_e, { deviceId, mediaUrl, subtitle }) => {
return castChromecast.connect(deviceId, mediaUrl,
subtitle === undefined ? castRelay.subtitle : subtitle);
diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js
index e9c34d2..665ec1f 100644
--- a/packages/meshbay-client/src/preload.js
+++ b/packages/meshbay-client/src/preload.js
@@ -175,7 +175,8 @@ contextBridge.exposeInMainWorld('meshbay', {
subtitle: (sub) => ipcRenderer.invoke('cast:subtitle', sub),
finish: () => ipcRenderer.invoke('cast:finish'),
status: () => ipcRenderer.invoke('cast:status'),
- discover: () => ipcRenderer.invoke('cast:discover'),
+ scan: () => ipcRenderer.invoke('cast:scan'),
+ devices: () => ipcRenderer.invoke('cast:devices'),
chromecastConnect: (opts) => ipcRenderer.invoke('cast:chromecast:connect', opts),
chromecastReload: (opts) => ipcRenderer.invoke('cast:chromecast:reload', opts),
chromecastDisconnect: () => ipcRenderer.invoke('cast:chromecast:disconnect'),
diff --git a/packages/meshbay-client/src/transcripts.js b/packages/meshbay-client/src/transcripts.js
index 0b6d0c0..8b0f6ef 100644
--- a/packages/meshbay-client/src/transcripts.js
+++ b/packages/meshbay-client/src/transcripts.js
@@ -30,6 +30,20 @@ function lenPrefixed(prefix, parts) {
return Buffer.concat(chunks);
}
+// The signed operations this application asks a node to perform
+// (meshbay_common/adminop.py). A list, not a pattern: what widens a node's
+// sharing — `root_add`, `root_update`, `group_attach`, gone from MNP 6.0 — is
+// never signed here, so a node older than that cannot be driven into it by a
+// script in the page either.
+const ADMIN_OPS = new Set([
+ 'file_delete', 'dir_delete', 'invite_create', 'invite_link_create',
+ 'invite_cancel', 'member_revoke', 'apps_enabled', 'set_scan_settings',
+ 'tmdb_config', 'tmdb_enabled', 'tmdb_override', 'tmdb_rematch',
+ 'musicbrainz_enabled', 'root_remove', 'root_eject', 'root_plug',
+ 'app_directories', 'chat_directory', 'chat_link_preview', 'search_listed',
+ 'chat_epoch',
+]);
+
// ── Field checks ──────────────────────────────────────────────────────────
//
// Shapes, not trust: what is checked here is that a field is what its name
@@ -143,7 +157,7 @@ function transcriptFor(kind, f, ctx) {
}
case 'admin': {
const op = String(fields.op ?? '');
- if (!/^[a-z_]{1,32}$/.test(op)) refuse('not an operation');
+ if (!ADMIN_OPS.has(op)) refuse('not an operation');
return lenPrefixed(PREFIX.admin, [
enc(op), enc(sameNode()), enc(groupId(fields.groupId)),
enc(text(fields.subject, 'the subject', 16384)),