diff options
author | 2022-06-23 18:10:39 +0200 | |
---|---|---|
committer | 2022-06-23 16:10:39 +0000 | |
commit | 6b2c2b8dfb86245a1747bf7977159f5129461863 (patch) | |
tree | 28944f62a962d8a658262ea902f8554d4419fa9e /src/models/Service.js | |
parent | chore: featureStore and GlobalErrorStore JS => TS (diff) | |
download | ferdium-app-6b2c2b8dfb86245a1747bf7977159f5129461863.tar.gz ferdium-app-6b2c2b8dfb86245a1747bf7977159f5129461863.tar.zst ferdium-app-6b2c2b8dfb86245a1747bf7977159f5129461863.zip |
chore: servicesStore + models into typescript (#344)
Diffstat (limited to 'src/models/Service.js')
-rw-r--r-- | src/models/Service.js | 478 |
1 files changed, 0 insertions, 478 deletions
diff --git a/src/models/Service.js b/src/models/Service.js deleted file mode 100644 index 53285e440..000000000 --- a/src/models/Service.js +++ /dev/null | |||
@@ -1,478 +0,0 @@ | |||
1 | import { autorun, computed, observable } from 'mobx'; | ||
2 | import { ipcRenderer } from 'electron'; | ||
3 | import { webContents } from '@electron/remote'; | ||
4 | import normalizeUrl from 'normalize-url'; | ||
5 | import { join } from 'path'; | ||
6 | |||
7 | import { todosStore } from '../features/todos'; | ||
8 | import { isValidExternalURL } from '../helpers/url-helpers'; | ||
9 | import UserAgent from './UserAgent'; | ||
10 | import { DEFAULT_SERVICE_ORDER } from '../config'; | ||
11 | import { | ||
12 | ifUndefinedString, | ||
13 | ifUndefinedBoolean, | ||
14 | ifUndefinedNumber, | ||
15 | } from '../jsUtils'; | ||
16 | |||
17 | const debug = require('../preload-safe-debug')('Ferdium:Service'); | ||
18 | |||
19 | // TODO: Shouldn't most of these values default to what's defined in DEFAULT_SERVICE_SETTINGS? | ||
20 | export default class Service { | ||
21 | id = ''; | ||
22 | |||
23 | recipe = null; | ||
24 | |||
25 | _webview = null; | ||
26 | |||
27 | timer = null; | ||
28 | |||
29 | events = {}; | ||
30 | |||
31 | @observable isAttached = false; | ||
32 | |||
33 | @observable isActive = false; // Is current webview active | ||
34 | |||
35 | @observable name = ''; | ||
36 | |||
37 | @observable unreadDirectMessageCount = 0; | ||
38 | |||
39 | @observable unreadIndirectMessageCount = 0; | ||
40 | |||
41 | @observable dialogTitle = ''; | ||
42 | |||
43 | @observable order = DEFAULT_SERVICE_ORDER; | ||
44 | |||
45 | @observable isEnabled = true; | ||
46 | |||
47 | @observable isMuted = false; | ||
48 | |||
49 | @observable team = ''; | ||
50 | |||
51 | @observable customUrl = ''; | ||
52 | |||
53 | @observable isNotificationEnabled = true; | ||
54 | |||
55 | @observable isBadgeEnabled = true; | ||
56 | |||
57 | @observable trapLinkClicks = false; | ||
58 | |||
59 | @observable isIndirectMessageBadgeEnabled = true; | ||
60 | |||
61 | @observable iconUrl = ''; | ||
62 | |||
63 | @observable hasCustomUploadedIcon = false; | ||
64 | |||
65 | @observable hasCrashed = false; | ||
66 | |||
67 | @observable isDarkModeEnabled = false; | ||
68 | |||
69 | @observable isProgressbarEnabled = true; | ||
70 | |||
71 | @observable darkReaderSettings = { brightness: 100, contrast: 90, sepia: 10 }; | ||
72 | |||
73 | @observable spellcheckerLanguage = null; | ||
74 | |||
75 | @observable isFirstLoad = true; | ||
76 | |||
77 | @observable isLoading = true; | ||
78 | |||
79 | @observable isLoadingPage = true; | ||
80 | |||
81 | @observable isError = false; | ||
82 | |||
83 | @observable errorMessage = ''; | ||
84 | |||
85 | @observable isUsingCustomUrl = false; | ||
86 | |||
87 | @observable isServiceAccessRestricted = false; | ||
88 | |||
89 | @observable restrictionType = null; | ||
90 | |||
91 | @observable isHibernationEnabled = false; | ||
92 | |||
93 | @observable isWakeUpEnabled = true; | ||
94 | |||
95 | @observable isHibernationRequested = false; | ||
96 | |||
97 | @observable onlyShowFavoritesInUnreadCount = false; | ||
98 | |||
99 | @observable lastUsed = Date.now(); // timestamp | ||
100 | |||
101 | @observable lastHibernated = null; // timestamp | ||
102 | |||
103 | @observable lastPoll = Date.now(); | ||
104 | |||
105 | @observable lastPollAnswer = Date.now(); | ||
106 | |||
107 | @observable lostRecipeConnection = false; | ||
108 | |||
109 | @observable lostRecipeReloadAttempt = 0; | ||
110 | |||
111 | @observable userAgentModel = null; | ||
112 | |||
113 | constructor(data, recipe) { | ||
114 | if (!data) { | ||
115 | throw new Error('Service config not valid'); | ||
116 | } | ||
117 | |||
118 | if (!recipe) { | ||
119 | throw new Error('Service recipe not valid'); | ||
120 | } | ||
121 | |||
122 | this.recipe = recipe; | ||
123 | |||
124 | this.userAgentModel = new UserAgent(recipe.overrideUserAgent); | ||
125 | |||
126 | this.id = ifUndefinedString(data.id, this.id); | ||
127 | this.name = ifUndefinedString(data.name, this.name); | ||
128 | this.team = ifUndefinedString(data.team, this.team); | ||
129 | this.customUrl = ifUndefinedString(data.customUrl, this.customUrl); | ||
130 | this.iconUrl = ifUndefinedString(data.iconUrl, this.iconUrl); | ||
131 | this.order = ifUndefinedNumber(data.order, this.order); | ||
132 | this.isEnabled = ifUndefinedBoolean(data.isEnabled, this.isEnabled); | ||
133 | this.isNotificationEnabled = ifUndefinedBoolean( | ||
134 | data.isNotificationEnabled, | ||
135 | this.isNotificationEnabled, | ||
136 | ); | ||
137 | this.isBadgeEnabled = ifUndefinedBoolean( | ||
138 | data.isBadgeEnabled, | ||
139 | this.isBadgeEnabled, | ||
140 | ); | ||
141 | this.trapLinkClicks = ifUndefinedBoolean( | ||
142 | data.trapLinkClicks, | ||
143 | this.trapLinkClicks, | ||
144 | ); | ||
145 | this.isIndirectMessageBadgeEnabled = ifUndefinedBoolean( | ||
146 | data.isIndirectMessageBadgeEnabled, | ||
147 | this.isIndirectMessageBadgeEnabled, | ||
148 | ); | ||
149 | this.isMuted = ifUndefinedBoolean(data.isMuted, this.isMuted); | ||
150 | this.isDarkModeEnabled = ifUndefinedBoolean( | ||
151 | data.isDarkModeEnabled, | ||
152 | this.isDarkModeEnabled, | ||
153 | ); | ||
154 | this.darkReaderSettings = ifUndefinedString( | ||
155 | data.darkReaderSettings, | ||
156 | this.darkReaderSettings, | ||
157 | ); | ||
158 | this.isProgressbarEnabled = ifUndefinedBoolean( | ||
159 | data.isProgressbarEnabled, | ||
160 | this.isProgressbarEnabled, | ||
161 | ); | ||
162 | this.hasCustomUploadedIcon = ifUndefinedBoolean( | ||
163 | data.iconId?.length > 0, | ||
164 | this.hasCustomUploadedIcon, | ||
165 | ); | ||
166 | this.onlyShowFavoritesInUnreadCount = ifUndefinedBoolean( | ||
167 | data.onlyShowFavoritesInUnreadCount, | ||
168 | this.onlyShowFavoritesInUnreadCount, | ||
169 | ); | ||
170 | this.proxy = ifUndefinedString(data.proxy, this.proxy); | ||
171 | this.spellcheckerLanguage = ifUndefinedString( | ||
172 | data.spellcheckerLanguage, | ||
173 | this.spellcheckerLanguage, | ||
174 | ); | ||
175 | this.userAgentPref = ifUndefinedString( | ||
176 | data.userAgentPref, | ||
177 | this.userAgentPref, | ||
178 | ); | ||
179 | this.isHibernationEnabled = ifUndefinedBoolean( | ||
180 | data.isHibernationEnabled, | ||
181 | this.isHibernationEnabled, | ||
182 | ); | ||
183 | this.isWakeUpEnabled = ifUndefinedBoolean( | ||
184 | data.isWakeUpEnabled, | ||
185 | this.isWakeUpEnabled, | ||
186 | ); | ||
187 | |||
188 | // Check if "Hibernate on Startup" is enabled and hibernate all services except active one | ||
189 | const { hibernateOnStartup } = window['ferdium'].stores.settings.app; | ||
190 | // The service store is probably not loaded yet so we need to use localStorage data to get active service | ||
191 | const isActive = | ||
192 | window.localStorage.service && | ||
193 | JSON.parse(window.localStorage.service).activeService === this.id; | ||
194 | if (hibernateOnStartup && !isActive) { | ||
195 | this.isHibernationRequested = true; | ||
196 | } | ||
197 | |||
198 | autorun(() => { | ||
199 | if (!this.isEnabled) { | ||
200 | this.webview = null; | ||
201 | this.isAttached = false; | ||
202 | this.unreadDirectMessageCount = 0; | ||
203 | this.unreadIndirectMessageCount = 0; | ||
204 | } | ||
205 | |||
206 | if (this.recipe.hasCustomUrl && this.customUrl) { | ||
207 | this.isUsingCustomUrl = true; | ||
208 | } | ||
209 | }); | ||
210 | } | ||
211 | |||
212 | @computed get shareWithWebview() { | ||
213 | return { | ||
214 | id: this.id, | ||
215 | spellcheckerLanguage: this.spellcheckerLanguage, | ||
216 | isDarkModeEnabled: this.isDarkModeEnabled, | ||
217 | isProgressbarEnabled: this.isProgressbarEnabled, | ||
218 | darkReaderSettings: this.darkReaderSettings, | ||
219 | team: this.team, | ||
220 | url: this.url, | ||
221 | hasCustomIcon: this.hasCustomIcon, | ||
222 | onlyShowFavoritesInUnreadCount: this.onlyShowFavoritesInUnreadCount, | ||
223 | trapLinkClicks: this.trapLinkClicks, | ||
224 | }; | ||
225 | } | ||
226 | |||
227 | @computed get isTodosService() { | ||
228 | return this.recipe.id === todosStore.todoRecipeId; | ||
229 | } | ||
230 | |||
231 | @computed get canHibernate() { | ||
232 | return this.isHibernationEnabled; | ||
233 | } | ||
234 | |||
235 | @computed get isHibernating() { | ||
236 | return this.canHibernate && this.isHibernationRequested; | ||
237 | } | ||
238 | |||
239 | get webview() { | ||
240 | if (this.isTodosService) { | ||
241 | return todosStore.webview; | ||
242 | } | ||
243 | |||
244 | return this._webview; | ||
245 | } | ||
246 | |||
247 | set webview(webview) { | ||
248 | this._webview = webview; | ||
249 | } | ||
250 | |||
251 | @computed get url() { | ||
252 | if (this.recipe.hasCustomUrl && this.customUrl) { | ||
253 | let url; | ||
254 | try { | ||
255 | url = normalizeUrl(this.customUrl, { | ||
256 | stripAuthentication: false, | ||
257 | stripWWW: false, | ||
258 | removeTrailingSlash: false, | ||
259 | }); | ||
260 | } catch { | ||
261 | console.error( | ||
262 | `Service (${this.recipe.name}): '${this.customUrl}' is not a valid Url.`, | ||
263 | ); | ||
264 | } | ||
265 | |||
266 | if (typeof this.recipe.buildUrl === 'function') { | ||
267 | url = this.recipe.buildUrl(url); | ||
268 | } | ||
269 | |||
270 | return url; | ||
271 | } | ||
272 | |||
273 | if (this.recipe.hasTeamId && this.team) { | ||
274 | return this.recipe.serviceURL.replace('{teamId}', this.team); | ||
275 | } | ||
276 | |||
277 | return this.recipe.serviceURL; | ||
278 | } | ||
279 | |||
280 | @computed get icon() { | ||
281 | if (this.iconUrl) { | ||
282 | return this.iconUrl; | ||
283 | } | ||
284 | |||
285 | return join(this.recipe.path, 'icon.svg'); | ||
286 | } | ||
287 | |||
288 | @computed get hasCustomIcon() { | ||
289 | return Boolean(this.iconUrl); | ||
290 | } | ||
291 | |||
292 | @computed get userAgent() { | ||
293 | return this.userAgentModel.userAgent; | ||
294 | } | ||
295 | |||
296 | @computed get userAgentPref() { | ||
297 | return this.userAgentModel.userAgentPref; | ||
298 | } | ||
299 | |||
300 | set userAgentPref(pref) { | ||
301 | this.userAgentModel.userAgentPref = pref; | ||
302 | } | ||
303 | |||
304 | @computed get defaultUserAgent() { | ||
305 | return this.userAgentModel.defaultUserAgent; | ||
306 | } | ||
307 | |||
308 | @computed get partition() { | ||
309 | return this.recipe.partition || `persist:service-${this.id}`; | ||
310 | } | ||
311 | |||
312 | initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) { | ||
313 | const webviewWebContents = webContents.fromId( | ||
314 | this.webview.getWebContentsId(), | ||
315 | ); | ||
316 | |||
317 | this.userAgentModel.setWebviewReference(this.webview); | ||
318 | |||
319 | // If the recipe has implemented 'modifyRequestHeaders', | ||
320 | // Send those headers to ipcMain so that it can be set in session | ||
321 | if (typeof this.recipe.modifyRequestHeaders === 'function') { | ||
322 | const modifiedRequestHeaders = this.recipe.modifyRequestHeaders(); | ||
323 | debug(this.name, 'modifiedRequestHeaders', modifiedRequestHeaders); | ||
324 | ipcRenderer.send('modifyRequestHeaders', { | ||
325 | modifiedRequestHeaders, | ||
326 | serviceId: this.id, | ||
327 | }); | ||
328 | } else { | ||
329 | debug(this.name, 'modifyRequestHeaders is not defined in the recipe'); | ||
330 | } | ||
331 | |||
332 | // if the recipe has implemented 'knownCertificateHosts' | ||
333 | if (typeof this.recipe.knownCertificateHosts === 'function') { | ||
334 | const knownHosts = this.recipe.knownCertificateHosts(); | ||
335 | debug(this.name, 'knownCertificateHosts', knownHosts); | ||
336 | ipcRenderer.send('knownCertificateHosts', { | ||
337 | knownHosts, | ||
338 | serviceId: this.id, | ||
339 | }); | ||
340 | } else { | ||
341 | debug(this.name, 'knownCertificateHosts is not defined in the recipe'); | ||
342 | } | ||
343 | |||
344 | this.webview.addEventListener('ipc-message', async e => { | ||
345 | if (e.channel === 'inject-js-unsafe') { | ||
346 | await Promise.all( | ||
347 | e.args.map(script => | ||
348 | this.webview.executeJavaScript( | ||
349 | `"use strict"; (() => { ${script} })();`, | ||
350 | ), | ||
351 | ), | ||
352 | ); | ||
353 | } else { | ||
354 | handleIPCMessage({ | ||
355 | serviceId: this.id, | ||
356 | channel: e.channel, | ||
357 | args: e.args, | ||
358 | }); | ||
359 | } | ||
360 | }); | ||
361 | |||
362 | this.webview.addEventListener( | ||
363 | 'new-window', | ||
364 | (event, url, frameName, options) => { | ||
365 | debug('new-window', event, url, frameName, options); | ||
366 | if (!isValidExternalURL(event.url)) { | ||
367 | return; | ||
368 | } | ||
369 | if ( | ||
370 | event.disposition === 'foreground-tab' || | ||
371 | event.disposition === 'background-tab' | ||
372 | ) { | ||
373 | openWindow({ | ||
374 | event, | ||
375 | url, | ||
376 | frameName, | ||
377 | options, | ||
378 | }); | ||
379 | } else { | ||
380 | ipcRenderer.send('open-browser-window', { | ||
381 | url: event.url, | ||
382 | serviceId: this.id, | ||
383 | }); | ||
384 | } | ||
385 | }, | ||
386 | ); | ||
387 | |||
388 | this.webview.addEventListener('did-start-loading', event => { | ||
389 | debug('Did start load', this.name, event); | ||
390 | |||
391 | this.hasCrashed = false; | ||
392 | this.isLoading = true; | ||
393 | this.isLoadingPage = true; | ||
394 | this.isError = false; | ||
395 | }); | ||
396 | |||
397 | this.webview.addEventListener('did-stop-loading', event => { | ||
398 | debug('Did stop load', this.name, event); | ||
399 | |||
400 | this.isLoading = false; | ||
401 | this.isLoadingPage = false; | ||
402 | }); | ||
403 | |||
404 | const didLoad = () => { | ||
405 | this.isLoading = false; | ||
406 | this.isLoadingPage = false; | ||
407 | |||
408 | if (!this.isError) { | ||
409 | this.isFirstLoad = false; | ||
410 | } | ||
411 | }; | ||
412 | |||
413 | this.webview.addEventListener('did-frame-finish-load', didLoad.bind(this)); | ||
414 | this.webview.addEventListener('did-navigate', didLoad.bind(this)); | ||
415 | |||
416 | this.webview.addEventListener('did-fail-load', event => { | ||
417 | debug('Service failed to load', this.name, event); | ||
418 | if ( | ||
419 | event.isMainFrame && | ||
420 | event.errorCode !== -21 && | ||
421 | event.errorCode !== -3 | ||
422 | ) { | ||
423 | this.isError = true; | ||
424 | this.errorMessage = event.errorDescription; | ||
425 | this.isLoading = false; | ||
426 | this.isLoadingPage = false; | ||
427 | } | ||
428 | }); | ||
429 | |||
430 | this.webview.addEventListener('crashed', () => { | ||
431 | debug('Service crashed', this.name); | ||
432 | this.hasCrashed = true; | ||
433 | }); | ||
434 | |||
435 | this.webview.addEventListener('found-in-page', ({ result }) => { | ||
436 | debug('Found in page', result); | ||
437 | this.webview.send('found-in-page', result); | ||
438 | }); | ||
439 | |||
440 | webviewWebContents.on('login', (event, request, authInfo, callback) => { | ||
441 | // const authCallback = callback; | ||
442 | debug('browser login event', authInfo); | ||
443 | event.preventDefault(); | ||
444 | |||
445 | if (authInfo.isProxy && authInfo.scheme === 'basic') { | ||
446 | debug('Sending service echo ping'); | ||
447 | webviewWebContents.send('get-service-id'); | ||
448 | |||
449 | debug('Received service id', this.id); | ||
450 | |||
451 | const ps = stores.settings.proxy[this.id]; | ||
452 | |||
453 | if (ps) { | ||
454 | debug('Sending proxy auth callback for service', this.id); | ||
455 | callback(ps.user, ps.password); | ||
456 | } else { | ||
457 | debug('No proxy auth config found for', this.id); | ||
458 | } | ||
459 | } | ||
460 | }); | ||
461 | } | ||
462 | |||
463 | initializeWebViewListener() { | ||
464 | if (this.webview && this.recipe.events) { | ||
465 | for (const eventName of Object.keys(this.recipe.events)) { | ||
466 | const eventHandler = this.recipe[this.recipe.events[eventName]]; | ||
467 | if (typeof eventHandler === 'function') { | ||
468 | this.webview.addEventListener(eventName, eventHandler); | ||
469 | } | ||
470 | } | ||
471 | } | ||
472 | } | ||
473 | |||
474 | resetMessageCount() { | ||
475 | this.unreadDirectMessageCount = 0; | ||
476 | this.unreadIndirectMessageCount = 0; | ||
477 | } | ||
478 | } | ||