aboutsummaryrefslogtreecommitdiffstats
path: root/src/models/Service.js
diff options
context:
space:
mode:
authorLibravatar Ricardo Cino <ricardo@cino.io>2022-06-23 18:10:39 +0200
committerLibravatar GitHub <noreply@github.com>2022-06-23 16:10:39 +0000
commit6b2c2b8dfb86245a1747bf7977159f5129461863 (patch)
tree28944f62a962d8a658262ea902f8554d4419fa9e /src/models/Service.js
parentchore: featureStore and GlobalErrorStore JS => TS (diff)
downloadferdium-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.js478
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 @@
1import { autorun, computed, observable } from 'mobx';
2import { ipcRenderer } from 'electron';
3import { webContents } from '@electron/remote';
4import normalizeUrl from 'normalize-url';
5import { join } from 'path';
6
7import { todosStore } from '../features/todos';
8import { isValidExternalURL } from '../helpers/url-helpers';
9import UserAgent from './UserAgent';
10import { DEFAULT_SERVICE_ORDER } from '../config';
11import {
12 ifUndefinedString,
13 ifUndefinedBoolean,
14 ifUndefinedNumber,
15} from '../jsUtils';
16
17const 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?
20export 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}