Files: dc5f4dc6989f09141fae9235f7690c01ebdc9e74 / ftu / app.js
9238 bytesRaw
1 | const { h, Value, when, resolve, computed, Struct, watch, throttle } = require('mutant') |
2 | const nest = require('depnest') |
3 | const path = require('path') |
4 | const fs = require('fs') |
5 | const { remote } = require('electron') |
6 | const insertCss = require('insert-css') |
7 | const values = require('lodash/values') |
8 | const get = require('lodash/get') |
9 | const electron = require('electron') |
10 | const { dialog } = require('electron').remote |
11 | const os = require('os') |
12 | const progress = require('progress-string') |
13 | |
14 | const appName = process.env.SSB_APPNAME || 'ssb' |
15 | const configFolder = path.join(os.homedir(), `.${appName}`) |
16 | |
17 | var isBusy = Value(false) |
18 | var isPresentingOptions = Value(true) |
19 | var checkerTimeout |
20 | |
21 | // these initial values are overwritten by the identity file. |
22 | var state = Struct({ |
23 | latestSequence: 0, |
24 | confirmedRemotely: false, |
25 | currentSequence: -1 |
26 | }) |
27 | |
28 | exports.gives = nest('ftu.app') |
29 | |
30 | exports.needs = nest({ |
31 | 'styles.css': 'reduce', |
32 | 'translations.sync.strings': 'first' |
33 | }) |
34 | |
35 | exports.create = (api) => { |
36 | return nest({ |
37 | 'ftu.app': function app () { |
38 | const strings = api.translations.sync.strings() |
39 | |
40 | const css = values(api.styles.css()).join('\n') |
41 | insertCss(css) |
42 | |
43 | var actionButtons = h('section', [ |
44 | h('div.left', h('Button', { 'ev-click': () => actionImportIdentity(strings) }, strings.backup.ftu.importAction)), |
45 | h('div.right', h('Button', { 'ev-click': () => actionCreateNewOne() }, strings.backup.ftu.createAction)) |
46 | ]) |
47 | |
48 | var busyMessage = h('p', strings.backup.ftu.busyMessage) |
49 | |
50 | var initialOptions = h('Page -ftu', [ |
51 | h('div.content', [ |
52 | h('h1', strings.backup.ftu.welcomeHeader), |
53 | h('p', strings.backup.ftu.welcomeMessage), |
54 | when(isBusy, busyMessage, actionButtons) |
55 | ]) |
56 | ]) |
57 | |
58 | var importProgress = h('Page -ftu', [ |
59 | h('div.content', [ |
60 | h('h1', strings.backup.import.header), |
61 | h('p', [strings.backup.import.synchronizeMessage]), |
62 | h('pre', computed(state, s => { |
63 | return progress({ |
64 | width: 42, |
65 | total: s.latestSequence, |
66 | style: function (complete, incomplete) { |
67 | // add an arrow at the head of the completed part |
68 | return `${complete}>${incomplete} (${s.currentSequence}/ ${s.latestSequence})` |
69 | } |
70 | })(s.currentSequence) |
71 | })) |
72 | ]) |
73 | ]) |
74 | |
75 | // This watcher is responsible for switching from FTU to Ticktack main app |
76 | watch(throttle(state, 500), s => { |
77 | if (s.currentSequence >= s.latestSequence && s.confirmedRemotely) { |
78 | console.log('all imported') |
79 | clearTimeout(checkerTimeout) |
80 | electron.ipcRenderer.send('import-completed') |
81 | } |
82 | }) |
83 | |
84 | if (fs.existsSync(path.join(configFolder, 'secret'))) { |
85 | // somehow the FTU started but the identity is already in place. |
86 | // treat it as a failed import and start importing... |
87 | console.log('resuming import') |
88 | let previousData = getImportData() |
89 | if (previousData === false) { |
90 | // there is a secret but there is no previous import data. |
91 | // so, we proceed as normal because we can't do anything else, |
92 | // it looks like a normal standard installation... |
93 | setImportData({ importing: false }) |
94 | electron.ipcRenderer.send('import-completed') |
95 | } else { |
96 | state.latestSequence.set(previousData.latestSequence) |
97 | state.currentSequence.set(previousData.currentSequence) |
98 | isPresentingOptions.set(false) |
99 | observeSequence() |
100 | } |
101 | } |
102 | |
103 | var app = h('App', [ |
104 | h('Header', [ |
105 | windowControls() |
106 | ]), |
107 | when(isPresentingOptions, initialOptions, importProgress) |
108 | ]) |
109 | |
110 | return app |
111 | } |
112 | }) |
113 | } |
114 | |
115 | electron.ipcRenderer.on('import-started', function (ev, c) { |
116 | console.log('background process is running, begin observing') |
117 | |
118 | observeSequence() |
119 | }) |
120 | |
121 | function actionCreateNewOne () { |
122 | isBusy.set(true) |
123 | const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../manifest.json'))) |
124 | const manifestFile = path.join(configFolder, 'manifest.json') |
125 | if (!fs.existsSync(configFolder)) { |
126 | fs.mkdirSync(configFolder) |
127 | } |
128 | fs.writeFileSync(manifestFile, JSON.stringify(manifest)) |
129 | |
130 | electron.ipcRenderer.send('create-new-identity') |
131 | } |
132 | |
133 | function actionImportIdentity (strings) { |
134 | const peersFile = path.join(configFolder, 'gossip.json') |
135 | const secretFile = path.join(configFolder, 'secret') |
136 | const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../manifest.json'))) |
137 | const manifestFile = path.join(configFolder, 'manifest.json') |
138 | |
139 | // place the other files first |
140 | dialog.showOpenDialog( |
141 | { |
142 | title: strings.backup.import.dialog.title, |
143 | butttonLabel: strings.backup.import.dialog.label, |
144 | defaultPath: 'ticktack-identity.backup', |
145 | properties: ['openFile'] |
146 | }, |
147 | (filenames) => { |
148 | if (typeof filenames !== 'undefined') { |
149 | let filename = filenames[0] |
150 | let data = JSON.parse(fs.readFileSync(filename)) |
151 | if (data.hasOwnProperty('secret') && data.hasOwnProperty('peers') && data.hasOwnProperty('latestSequence')) { |
152 | if (!fs.existsSync(configFolder)) { |
153 | fs.mkdirSync(configFolder) |
154 | } |
155 | |
156 | fs.writeFileSync(manifestFile, JSON.stringify(manifest)) |
157 | fs.writeFileSync(peersFile, JSON.stringify(data.peers), 'utf8') |
158 | fs.writeFileSync(secretFile, data.secret, 'utf8') |
159 | state.latestSequence.set(data.latestSequence) |
160 | state.currentSequence.set(0) |
161 | isPresentingOptions.set(false) |
162 | |
163 | data.importing = true |
164 | data.currentSequence = 0 |
165 | |
166 | setImportData(data) |
167 | |
168 | electron.ipcRenderer.send('import-identity') |
169 | } else { |
170 | console.log('> bad export file') |
171 | console.log(data) |
172 | alert('Bad Export File') |
173 | } |
174 | } |
175 | } |
176 | ) |
177 | } |
178 | |
179 | function windowControls () { |
180 | if (process.platform === 'darwin') return |
181 | |
182 | const window = remote.getCurrentWindow() |
183 | const minimize = () => window.minimize() |
184 | const maximize = () => { |
185 | if (!window.isMaximized()) window.maximize() |
186 | else window.unmaximize() |
187 | } |
188 | const close = () => window.close() |
189 | |
190 | return h('div.window-controls', [ |
191 | h('img.min', { |
192 | src: assetPath('minimize.png'), |
193 | 'ev-click': minimize |
194 | }), |
195 | h('img.max', { |
196 | src: assetPath('maximize.png'), |
197 | 'ev-click': maximize |
198 | }), |
199 | h('img.close', { |
200 | src: assetPath('close.png'), |
201 | 'ev-click': close |
202 | }) |
203 | ]) |
204 | } |
205 | |
206 | function assetPath (name) { |
207 | return path.join(__dirname, '../assets', name) |
208 | } |
209 | |
210 | function getImportData () { |
211 | var importFile = path.join(configFolder, 'importing.json') |
212 | if (fs.existsSync(importFile)) { |
213 | let data = JSON.parse(fs.readFileSync(importFile)) |
214 | return data || false |
215 | } else { |
216 | return false |
217 | } |
218 | } |
219 | |
220 | function setImportData (data) { |
221 | var importFile = path.join(configFolder, 'importing.json') |
222 | fs.writeFileSync(importFile, JSON.stringify(data)) |
223 | } |
224 | |
225 | function observeSequence () { |
226 | const pull = require('pull-stream') |
227 | const Client = require('ssb-client') |
228 | const config = require('../config').create().config.sync.load() |
229 | |
230 | Client(config.keys, config, (err, ssbServer) => { |
231 | if (err) return console.error('problem starting client', err) |
232 | |
233 | console.log('> sbot running!!!!') |
234 | |
235 | ssbServer.gossip.peers((err, peers) => { |
236 | if (err) return console.error(err) |
237 | |
238 | connectToPeers(peers) |
239 | checkPeers() |
240 | }) |
241 | |
242 | // start listening to the my seq, and update the state |
243 | pull( |
244 | ssbServer.createUserStream({ live: true, id: ssbServer.id }), |
245 | pull.drain((msg) => { |
246 | let seq = get(msg, 'value.sequence', false) |
247 | if (seq) { |
248 | state.currentSequence.set(seq) |
249 | } |
250 | }) |
251 | ) |
252 | |
253 | function connectToPeers (peers) { |
254 | if (peers.length > 10) { |
255 | const lessPeers = peers.filter(p => !p.error) |
256 | if (lessPeers.length > 10) peers = lessPeers |
257 | } |
258 | |
259 | peers.forEach(({ host, port, key }) => { |
260 | if (host && port && key) { |
261 | ssbServer.gossip.connect({ host, port, key }, (err, v) => { |
262 | if (err) console.log('error connecting to ', host, err) |
263 | else console.log('connected to ', host) |
264 | }) |
265 | } |
266 | }) |
267 | } |
268 | function checkPeers () { |
269 | ssbServer.ebt.peerStatus(ssbServer.id, (err, data) => { |
270 | if (err) { |
271 | checkerTimeout = setTimeout(checkPeers, 5000) |
272 | return |
273 | } |
274 | |
275 | const latest = resolve(state.latestSequence) |
276 | |
277 | const remoteSeqs = Object.keys(data.peers) |
278 | .map(p => data.peers[p].seq) // get my seq reported by each peer |
279 | .filter(s => s >= latest) // only keep remote seq that confirm or update backup seq |
280 | .sort((a, b) => a > b ? -1 : 1) // order them |
281 | |
282 | console.log(remoteSeqs) |
283 | |
284 | const newLatest = remoteSeqs[0] |
285 | if (newLatest) { |
286 | state.latestSequence.set(newLatest) |
287 | |
288 | // if this value is confirmed remotely twice, assume safe |
289 | if (remoteSeqs.filter(s => s === newLatest).length >= 2) { |
290 | state.confirmedRemotely.set(true) |
291 | } |
292 | } |
293 | |
294 | checkerTimeout = setTimeout(checkPeers, 5000) |
295 | }) |
296 | } |
297 | }) |
298 | } |
299 |
Built with git-ssb-web