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();
}
}
|