aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js
blob: 20930987ec371dfd79b1d0dafef5e4cfc8212c89 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// Where the Android application's backups go (docs/MESHBAY_DESIGN.md §9.12):
// one folder of one group the account owns and is the only member of, shared
// by every kind of data. Each kind goes into its own folder under it,
// `<folder>/<account>-<kind>`, named by the phone.
//
// What a group's folder holds every member can read; photos, an address book
// or a conversation are not the group's. So the group is checked when it is
// chosen and again at every run, before anything is read: somebody who joins
// later stops the backups rather than reading them from then on.

import { ConnectionPool } from './connection-pool.js';
import * as platform from './platform.js';

let ctx = null;          // { hub, hubFetch, getUser, getBundleKey }

/** Set by whichever runner starts first; both are handed the same context. */
export function setSyncContext(context) { ctx = context; }

export const destination = {
  get: () => platform.phoneSync.destination(),
  set: (d) => platform.phoneSync.setDestination(d),
};

/** True when this account owns the group, is its one member, and nobody is invited. */
export async function isPrivate(groupId, user) {
  const r = await ctx.hubFetch(`/v1/groups/${groupId}/members`, { token: user.token });
  const members = r.members || [];
  return members.length === 1 && members[0].user_id === user.userId
    && r.admin_id === user.userId && !(r.invited || []).length;
}

/**
 * Before a run reads anything: null when the destination may be written to,
 * otherwise `not_private` or `not_a_member` (lasting) or `offline`.
 */
export async function checkDestination(dest, user) {
  try {
    return await isPrivate(dest.groupId, user) ? null : 'not_private';
  } catch (e) {
    return /not a member|not found/i.test(e.message || '') ? 'not_a_member' : 'offline';
  }
}

/** The groups among `groups` this account owns and is alone in; a failed look leaves one out. */
export async function privateGroups(groups) {
  const user = ctx && ctx.getUser();
  if (!user) return [];
  const alone = await Promise.all(groups.map((g) => isPrivate(g.id, user).catch(() => false)));
  return groups.filter((_, i) => alone[i]);
}

/** Every folder a member can add to: on a writable root whose drive is there. */
export function writableFolders(index) {
  const roots = new Set((index.roots || [])
    .filter((r) => r.writable && r.available !== false).map((r) => r.name));
  const all = new Set([...(index.dirs || []), ...roots]);
  return [...all].filter((d) => roots.has(d.split('/')[0])).sort((a, b) => a.localeCompare(b));
}

/** True when the group's Photos tab shows `dir`. */
export function photosTabShows(dir, photoDirectories) {
  return (photoDirectories || []).some((p) => dir === p || dir.startsWith(`${p}/`));
}

/**
 * A one-off look at a group, for choosing a folder: connected, read, closed.
 * `photoDirectories` is what its Photos tab shows, for saying when photos sent
 * there would show nowhere.
 */
export async function probeGroup(groupId) {
  const user = ctx && ctx.getUser();
  if (!user) throw new Error('Not signed in');
  const pool = new ConnectionPool(ctx.hub);
  try {
    const bundleKey = await ctx.getBundleKey();
    const conn = await pool.connect(groupId, user.token, bundleKey, user.username, user.userId);
    const index = await conn.transport.fetchIndex();
    return {
      folders: writableFolders(index),
      photoDirectories: (conn.ack && conn.ack.photo_directories) || [],
    };
  } finally {
    pool.closeAll();
  }
}