git ssb

2+

mixmix / ticktack



Tree: dc5f4dc6989f09141fae9235f7690c01ebdc9e74

Files: dc5f4dc6989f09141fae9235f7690c01ebdc9e74 / ftu / app.js

9238 bytesRaw
1const { h, Value, when, resolve, computed, Struct, watch, throttle } = require('mutant')
2const nest = require('depnest')
3const path = require('path')
4const fs = require('fs')
5const { remote } = require('electron')
6const insertCss = require('insert-css')
7const values = require('lodash/values')
8const get = require('lodash/get')
9const electron = require('electron')
10const { dialog } = require('electron').remote
11const os = require('os')
12const progress = require('progress-string')
13
14const appName = process.env.SSB_APPNAME || 'ssb'
15const configFolder = path.join(os.homedir(), `.${appName}`)
16
17var isBusy = Value(false)
18var isPresentingOptions = Value(true)
19var checkerTimeout
20
21// these initial values are overwritten by the identity file.
22var state = Struct({
23 latestSequence: 0,
24 confirmedRemotely: false,
25 currentSequence: -1
26})
27
28exports.gives = nest('ftu.app')
29
30exports.needs = nest({
31 'styles.css': 'reduce',
32 'translations.sync.strings': 'first'
33})
34
35exports.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
115electron.ipcRenderer.on('import-started', function (ev, c) {
116 console.log('background process is running, begin observing')
117
118 observeSequence()
119})
120
121function 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
133function 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
179function 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
206function assetPath (name) {
207 return path.join(__dirname, '../assets', name)
208}
209
210function 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
220function setImportData (data) {
221 var importFile = path.join(configFolder, 'importing.json')
222 fs.writeFileSync(importFile, JSON.stringify(data))
223}
224
225function 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