From 1618ac552b465a2d041df5ca4cbe1d56836ff30e Mon Sep 17 00:00:00 2001 From: Xymorot Date: Mon, 9 Nov 2020 18:15:30 +0100 Subject: [PATCH] feat: remove web-crawler and use electron (chromium) itself as crawler, implementing a function to download nhentai favorite torrents --- .eslintrc.json | 1 + mocks/electron.ts | 3 + package-lock.json | 414 +----------------- package.json | 2 - src/main.ts | 24 +- src/main/core/container.ts | 14 +- src/main/modules/app-window/app-window.ts | 72 ++- .../modules/app-window/file-app-window.ts | 21 + src/main/modules/app-window/i-app-window.ts | 1 + .../modules/app-window/i-url-app-window.ts | 6 + .../modules/app-window/main-app-window.ts | 10 +- src/main/modules/app-window/url-app-window.ts | 57 +++ .../modules/app-window/window-closed-error.ts | 5 + src/main/modules/error/cookie-save-error.ts | 8 - .../modules/error/initialization-error.ts | 10 - src/main/modules/error/web-crawler-error.ts | 8 - .../modules/error/web-crawler-form-error.ts | 10 - .../modules/error/web-crawler-login-error.ts | 10 - src/main/modules/i18n/i-i18n-translator.ts | 3 + src/main/modules/i18n/i18n-translator.ts | 9 + src/main/modules/nhentai/i-nhentai-api.ts | 3 +- .../modules/nhentai/i-nhentai-app-window.ts | 5 + src/main/modules/nhentai/nhentai-api.ts | 124 +----- .../modules/nhentai/nhentai-app-window.ts | 223 ++++++++++ .../modules/nhentai/nhentai-ipc-controller.ts | 38 +- src/main/modules/nhentai/nhentai-util.ts | 26 ++ src/main/modules/nhentai/nhentai.d.ts | 4 + src/main/modules/session/i-session-helper.ts | 5 + src/main/modules/session/i-session.ts | 3 - src/main/modules/session/session-helper.ts | 43 ++ src/main/modules/session/session.d.ts | 23 + src/main/modules/session/session.ts | 27 -- src/main/modules/web-crawler/i-web-crawler.ts | 7 - .../modules/web-crawler/web-crawler.spec.ts | 64 --- src/main/modules/web-crawler/web-crawler.ts | 69 --- .../components/3-polymers/NhentaiLogin.svelte | 34 +- src/renderer/services/api.ts | 8 +- src/renderer/services/store.ts | 21 - types/ipc.d.ts | 8 +- 39 files changed, 567 insertions(+), 856 deletions(-) create mode 100644 src/main/modules/app-window/file-app-window.ts create mode 100644 src/main/modules/app-window/i-url-app-window.ts create mode 100644 src/main/modules/app-window/url-app-window.ts create mode 100644 src/main/modules/app-window/window-closed-error.ts delete mode 100644 src/main/modules/error/cookie-save-error.ts delete mode 100644 src/main/modules/error/initialization-error.ts delete mode 100644 src/main/modules/error/web-crawler-error.ts delete mode 100644 src/main/modules/error/web-crawler-form-error.ts delete mode 100644 src/main/modules/error/web-crawler-login-error.ts create mode 100644 src/main/modules/i18n/i-i18n-translator.ts create mode 100644 src/main/modules/i18n/i18n-translator.ts create mode 100644 src/main/modules/nhentai/i-nhentai-app-window.ts create mode 100644 src/main/modules/nhentai/nhentai-app-window.ts create mode 100644 src/main/modules/nhentai/nhentai-util.ts create mode 100644 src/main/modules/nhentai/nhentai.d.ts create mode 100644 src/main/modules/session/i-session-helper.ts delete mode 100644 src/main/modules/session/i-session.ts create mode 100644 src/main/modules/session/session-helper.ts create mode 100644 src/main/modules/session/session.d.ts delete mode 100644 src/main/modules/session/session.ts delete mode 100644 src/main/modules/web-crawler/i-web-crawler.ts delete mode 100644 src/main/modules/web-crawler/web-crawler.spec.ts delete mode 100644 src/main/modules/web-crawler/web-crawler.ts delete mode 100644 src/renderer/services/store.ts diff --git a/.eslintrc.json b/.eslintrc.json index e95f00f..034922c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -34,6 +34,7 @@ } ], "no-constant-condition": ["error", { "checkLoops": false }], + "no-throw-literal": "error", "import/no-extraneous-dependencies": [ "error", diff --git a/mocks/electron.ts b/mocks/electron.ts index b4e0f14..c67839a 100644 --- a/mocks/electron.ts +++ b/mocks/electron.ts @@ -9,6 +9,9 @@ const electronMock: DeepPartial = { return path.resolve('test-paths', name); }, quit(): void {}, + getAppPath(): string { + return path.resolve(__dirname, '..'); + }, }, BrowserWindow: class { public webContents: DeepPartial = { diff --git a/package-lock.json b/package-lock.json index f7bc905..b1380c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -894,17 +894,6 @@ "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", "dev": true }, - "@types/jsdom": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.3.tgz", - "integrity": "sha512-BREatezSn74rmLIDksuqGNFUTi9HNAWWQXYpFBFLK9U6wlMCO4M0QCa8CMpDsZQuqxSO9XifVLT5Q1P0vgKLqw==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/parse5": "*", - "@types/tough-cookie": "*" - } - }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", @@ -980,12 +969,6 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, - "@types/parse5": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", - "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==", - "dev": true - }, "@types/puppeteer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-3.0.1.tgz", @@ -1031,12 +1014,6 @@ "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==", "dev": true }, - "@types/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", - "dev": true - }, "@types/uglify-js": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.3.tgz", @@ -1515,41 +1492,17 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, - "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==" - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, - "acorn": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", - "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==" - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, "acorn-jsx": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" - }, "agent-base": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", @@ -2191,11 +2144,6 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -3294,26 +3242,6 @@ "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=", "dev": true }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - } - } - }, "cuint": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", @@ -3344,16 +3272,6 @@ "assert-plus": "^1.0.0" } }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - } - }, "date-fns": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz", @@ -3379,11 +3297,6 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, - "decimal.js": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz", - "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==" - }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -3416,7 +3329,8 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true }, "deepmerge": { "version": "4.2.2", @@ -3612,21 +3526,6 @@ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" - } - } - }, "dotenv": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", @@ -4236,18 +4135,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, "eslint": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.6.0.tgz", @@ -4661,11 +4548,6 @@ } } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, "esquery": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", @@ -4695,12 +4577,14 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true }, "events": { "version": "3.0.0", @@ -4975,7 +4859,8 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true }, "fd-slicer": { "version": "1.1.0", @@ -5802,14 +5687,6 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6223,11 +6100,6 @@ "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz", "integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==" }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" - }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -6417,11 +6289,6 @@ "isobject": "^3.0.1" } }, - "is-potential-custom-element-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", - "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=" - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -6690,84 +6557,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "jsdom": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.3.0.tgz", - "integrity": "sha512-zggeX5UuEknpdZzv15+MS1dPYG0J/TftiiNunOeNxSl3qr8Z6cIlQpN0IdJa44z9aFxZRIVqRncvEhQ7X5DtZg==", - "requires": { - "abab": "^2.0.3", - "acorn": "^7.1.1", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.2.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.0", - "domexception": "^2.0.1", - "escodegen": "^1.14.1", - "html-encoding-sniffer": "^2.0.1", - "is-potential-custom-element-name": "^1.0.0", - "nwsapi": "^2.2.0", - "parse5": "5.1.1", - "request": "^2.88.2", - "request-promise-native": "^1.0.8", - "saxes": "^5.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0", - "ws": "^7.2.3", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6880,15 +6669,6 @@ "readable-stream": "^2.0.5" } }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, "lighthouse-logger": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", @@ -7044,11 +6824,6 @@ "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", "dev": true }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, "lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -8148,11 +7923,6 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" - }, "nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", @@ -8469,19 +8239,6 @@ "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", "dev": true }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, "ora": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.5.tgz", @@ -8998,11 +8755,6 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -9526,42 +9278,6 @@ } } }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "requires": { - "lodash": "^4.17.19" - }, - "dependencies": { - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" - } - } - }, - "request-promise-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", - "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", - "requires": { - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9793,14 +9509,6 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "requires": { - "xmlchars": "^2.2.0" - } - }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -10133,7 +9841,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-resolve": { "version": "0.5.2", @@ -10410,11 +10119,6 @@ } } }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -10628,11 +10332,6 @@ "svelte-dev-helper": "^1.1.9" } }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -11039,24 +10738,6 @@ "repeat-string": "^1.6.1" } }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", - "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", - "requires": { - "punycode": "^2.1.1" - } - }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -11203,14 +10884,6 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -11522,22 +11195,6 @@ "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", "dev": true }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "requires": { - "xml-name-validator": "^3.0.0" - } - }, "watchpack": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", @@ -11834,11 +11491,6 @@ "webdriver": "6.3.5" } }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" - }, "webpack": { "version": "4.44.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.1.tgz", @@ -12070,36 +11722,6 @@ "source-map": "~0.6.1" } }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, - "whatwg-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz", - "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^2.0.2", - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" - } - } - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -12151,7 +11773,8 @@ "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true }, "wordwrap": { "version": "1.0.0", @@ -12243,12 +11866,8 @@ "ws": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true }, "xml2js": { "version": "0.4.23", @@ -12272,11 +11891,6 @@ "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", "dev": true }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, "xmldom": { "version": "0.1.31", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", diff --git a/package.json b/package.json index 8e8826c..5e00e78 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "electron-squirrel-startup": "^1.0.0", "fs-extra": "^9.0.1", "inversify": "^5.0.1", - "jsdom": "^16.2.2", "minimist": "^1.2.5", "node-fetch": "^2.6.0", "reflect-metadata": "^0.1.13", @@ -62,7 +61,6 @@ "@types/chai": "^4.2.12", "@types/chai-fs": "^2.0.2", "@types/fs-extra": "^9.0.1", - "@types/jsdom": "^16.2.3", "@types/minimist": "^1.2.0", "@types/mocha": "^8.0.1", "@types/node": "^12.12.54", diff --git a/src/main.ts b/src/main.ts index fc6d839..ae3d1a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,6 @@ import { app } from 'electron'; import { isDev } from './main/core/env'; import { IAppWindow } from './main/modules/app-window/i-app-window'; import { ILogger } from './main/modules/logger/i-logger'; -import { ISession } from './main/modules/session/i-session'; /** * have a read: https://github.com/nodejs/node/issues/20392, over 100 comments as of 2020-07-26 @@ -21,22 +20,19 @@ process.on('unhandledRejection', (reason) => { process.on('uncaughtException', (error) => { const logger: ILogger = container.get('logger'); void logger.exception(error); + if (isDev()) { + // eslint-disable-next-line no-console -- only for development purposes + console.error(error); + } }); async function createWindow(): Promise { - const session: ISession = container.get('session'); - session.setHeaders(); - const appWindowMain: IAppWindow = container.get('app-window-main'); - - // and load the index.html of the app. await appWindowMain.open(); - // Open the DevTools. - if (isDev()) { - // eslint-disable-next-line no-unused-expressions -- eslint can't handle optional chaining, yet - appWindowMain.window?.webContents.openDevTools(); - } + appWindowMain.window?.on('closed', () => { + app.quit(); + }); } // This method will be called when Electron has finished @@ -46,11 +42,7 @@ app.on('ready', createWindow); // Quit when all windows are closed. app.on('window-all-closed', () => { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } + app.quit(); }); app.on('activate', async () => { diff --git a/src/main/core/container.ts b/src/main/core/container.ts index f276b8a..af7cace 100644 --- a/src/main/core/container.ts +++ b/src/main/core/container.ts @@ -1,16 +1,17 @@ import 'reflect-metadata'; import { Container, interfaces } from 'inversify'; import { MainAppWindow } from '../modules/app-window/main-app-window'; +import { I18nTranslator } from '../modules/i18n/i18n-translator'; import { Logger } from '../modules/logger/logger'; import { NhentaiApi } from '../modules/nhentai/nhentai-api'; import '../modules/nhentai/nhentai-ipc-controller'; -import { Session } from '../modules/session/session'; +import { NhentaiAppWindow } from '../modules/nhentai/nhentai-app-window'; +import { SessionHelper } from '../modules/session/session-helper'; import { Store } from '../modules/store/store'; -import { WebCrawler } from '../modules/web-crawler/web-crawler'; import BindingToSyntax = interfaces.BindingToSyntax; export const container = { - original: new Container({ defaultScope: 'Singleton' }), + original: new Container({ defaultScope: 'Singleton', skipBaseClassChecks: true }), bind(key: string): BindingToSyntax { return this.original.bind(Symbol.for(key)); }, @@ -24,12 +25,13 @@ export const container = { container.bind('logger').to(Logger); +container.bind('i18n-translator').to(I18nTranslator); + container.bind('store').to(Store); -container.bind('web-crawler').to(WebCrawler); +container.bind('session-helper').to(SessionHelper); container.bind('nhentai-api').to(NhentaiApi); +container.bind('nhentai-app-window').to(NhentaiAppWindow); container.bind('app-window-main').to(MainAppWindow); - -container.bind('session').to(Session); diff --git a/src/main/modules/app-window/app-window.ts b/src/main/modules/app-window/app-window.ts index 4723b76..06ce667 100644 --- a/src/main/modules/app-window/app-window.ts +++ b/src/main/modules/app-window/app-window.ts @@ -1,14 +1,19 @@ -import { BrowserWindow } from 'electron'; +import { app, BrowserWindow, Event, LoadFileOptions, LoadURLOptions, NewWindowEvent } from 'electron'; import os from 'os'; -import { injectable } from 'inversify'; +import path from 'path'; +import { isDev } from '../../core/env'; +import { ISessionHelper } from '../session/i-session-helper'; import { IAppWindow } from './i-app-window'; import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions; -let defaultOptions = { +let defaultOptions: BrowserWindowConstructorOptions = { width: 1600, height: 900, webPreferences: { + enableRemoteModule: false, nodeIntegration: false, + contextIsolation: true, + devTools: isDev(), }, }; @@ -25,12 +30,23 @@ switch (os.platform()) { break; } -@injectable() export abstract class AppWindow implements IAppWindow { + protected static default = {}; + protected _window: BrowserWindow | null = null; - protected constructor(options: BrowserWindowConstructorOptions = {}) { - this.initialize(options); + protected readonly sessionHelper: ISessionHelper; + + protected options: BrowserWindowConstructorOptions; + + protected uri: string; + + protected abstract loadOptions: LoadFileOptions | LoadURLOptions; + + protected constructor(sessionHelper: ISessionHelper, uri: string, options: BrowserWindowConstructorOptions = {}) { + this.sessionHelper = sessionHelper; + this.options = { ...defaultOptions, ...options }; + this.uri = uri; } public get window(): BrowserWindow | null { @@ -38,14 +54,29 @@ export abstract class AppWindow implements IAppWindow { } public open(): Promise { - if (this.isClosed()) { - this.initialize(); + this._window = new BrowserWindow(this.options); + + this.sessionHelper.setCsp(this._window, this.getCsp()); + + this._window.on('closed', () => { + this._window = null; + }); + + if (isDev()) { + this._window.webContents.openDevTools(); } - if (this._window) { - return this._window.loadFile('frontend/index.html'); + this._window.webContents.on('will-navigate', this.onWillNavigate); + this._window.webContents.on('new-window', this.onNewWindow); + + return this.load(this._window); + } + + public close(force: boolean = false): void { + if (force) { + this._window?.destroy(); } else { - return Promise.reject(new Error('the window was not initialized')); + this._window?.close(); } } @@ -53,11 +84,18 @@ export abstract class AppWindow implements IAppWindow { return !this._window; } - private initialize(options: BrowserWindowConstructorOptions = {}): void { - this._window = new BrowserWindow({ ...defaultOptions, ...options }); - - this._window.on('closed', () => { - this._window = null; - }); + protected getCsp(): IContentSecurityPolicy { + return {}; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- it is used in child classes + protected onWillNavigate(event: Event, navigationUrl: string): void { + event.preventDefault(); + } + + protected onNewWindow(event: NewWindowEvent): void { + event.preventDefault(); + } + + protected abstract load(window: BrowserWindow): Promise; } diff --git a/src/main/modules/app-window/file-app-window.ts b/src/main/modules/app-window/file-app-window.ts new file mode 100644 index 0000000..14693fe --- /dev/null +++ b/src/main/modules/app-window/file-app-window.ts @@ -0,0 +1,21 @@ +import { BrowserWindow, BrowserWindowConstructorOptions, LoadFileOptions } from 'electron'; +import { ISessionHelper } from '../session/i-session-helper'; +import { AppWindow } from './app-window'; + +export abstract class FileAppWindow extends AppWindow { + protected loadOptions: LoadFileOptions; + + protected constructor( + sessionHelper: ISessionHelper, + uri: string, + options: BrowserWindowConstructorOptions = {}, + loadOptions: LoadFileOptions = {} + ) { + super(sessionHelper, uri, options); + this.loadOptions = loadOptions; + } + + protected load(window: BrowserWindow): Promise { + return window.loadFile(this.uri, this.loadOptions); + } +} diff --git a/src/main/modules/app-window/i-app-window.ts b/src/main/modules/app-window/i-app-window.ts index 307e0a7..00d6bc8 100644 --- a/src/main/modules/app-window/i-app-window.ts +++ b/src/main/modules/app-window/i-app-window.ts @@ -3,5 +3,6 @@ import BrowserWindow = Electron.BrowserWindow; export interface IAppWindow { window: BrowserWindow | null; open(): Promise; + close(force?: boolean): void; isClosed(): boolean; } diff --git a/src/main/modules/app-window/i-url-app-window.ts b/src/main/modules/app-window/i-url-app-window.ts new file mode 100644 index 0000000..8a2c7fb --- /dev/null +++ b/src/main/modules/app-window/i-url-app-window.ts @@ -0,0 +1,6 @@ +import { LoadURLOptions } from 'electron'; +import { IAppWindow } from './i-app-window'; + +export interface IUrlAppWindow extends IAppWindow { + loadUrlSafe(url: string, options?: LoadURLOptions): Promise; +} diff --git a/src/main/modules/app-window/main-app-window.ts b/src/main/modules/app-window/main-app-window.ts index b24259e..49e3561 100644 --- a/src/main/modules/app-window/main-app-window.ts +++ b/src/main/modules/app-window/main-app-window.ts @@ -1,10 +1,12 @@ import { injectable } from 'inversify'; -import { AppWindow } from './app-window'; +import { inject } from '../../core/inject'; +import { ISessionHelper } from '../session/i-session-helper'; +import { FileAppWindow } from './file-app-window'; @injectable() -export class MainAppWindow extends AppWindow { - public constructor() { - super({ +export class MainAppWindow extends FileAppWindow { + public constructor(@inject('session-helper') sessionHelper: ISessionHelper) { + super(sessionHelper, 'frontend/index.html', { webPreferences: { nodeIntegration: true, }, diff --git a/src/main/modules/app-window/url-app-window.ts b/src/main/modules/app-window/url-app-window.ts new file mode 100644 index 0000000..3d027c3 --- /dev/null +++ b/src/main/modules/app-window/url-app-window.ts @@ -0,0 +1,57 @@ +import { BrowserWindow, BrowserWindowConstructorOptions, LoadURLOptions } from 'electron'; +import { promisify } from 'util'; +import { ISessionHelper } from '../session/i-session-helper'; +import { AppWindow } from './app-window'; +import { IUrlAppWindow } from './i-url-app-window'; +import { WindowClosedError } from './window-closed-error'; + +export abstract class UrlAppWindow extends AppWindow implements IUrlAppWindow { + protected loadOptions: LoadURLOptions; + + protected constructor( + sessionHelper: ISessionHelper, + uri: string, + options: BrowserWindowConstructorOptions = {}, + loadOptions: LoadURLOptions = {} + ) { + const securedOptions: BrowserWindowConstructorOptions = { + ...options, + ...{ + webPreferences: { + enableRemoteModule: false, + nodeIntegration: false, + contextIsolation: true, + }, + }, + }; + super(sessionHelper, uri, securedOptions); + this.loadOptions = loadOptions; + } + + public async loadUrlSafe(url: string, options?: LoadURLOptions): Promise { + if (!this._window) { + throw new WindowClosedError(); + } + const waitInterval = 1000; + let failedLoad = true; + do { + await new Promise((resolve) => { + if (!this._window) { + throw new WindowClosedError(); + } + this._window.webContents.once('did-navigate', (event, navigationUrl, httpResponseCode) => { + failedLoad = HttpCode.BAD_REQUEST <= httpResponseCode; + resolve(); + }); + void this._window.loadURL(url, options); + }); + if (failedLoad) { + await promisify(setTimeout)(waitInterval); + } + } while (failedLoad); + } + + protected load(window: BrowserWindow): Promise { + return window.loadURL(this.uri, this.loadOptions); + } +} diff --git a/src/main/modules/app-window/window-closed-error.ts b/src/main/modules/app-window/window-closed-error.ts new file mode 100644 index 0000000..5e130f3 --- /dev/null +++ b/src/main/modules/app-window/window-closed-error.ts @@ -0,0 +1,5 @@ +export class WindowClosedError extends TypeError { + public constructor(message = 'window is closed') { + super(message); + } +} diff --git a/src/main/modules/error/cookie-save-error.ts b/src/main/modules/error/cookie-save-error.ts deleted file mode 100644 index 6829f20..0000000 --- a/src/main/modules/error/cookie-save-error.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Error thrown when cookies could not be saved. - */ -export class CookieSaveError extends Error { - public constructor(message: string = 'failed to save cookies') { - super(message); - } -} diff --git a/src/main/modules/error/initialization-error.ts b/src/main/modules/error/initialization-error.ts deleted file mode 100644 index ca33f0e..0000000 --- a/src/main/modules/error/initialization-error.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Error thrown when there is an error while initializing services or the app in general. - * - * You're pretty much fucked at this point, try to avoid unstable code on initialization - */ -export class InitializationError extends Error { - public constructor(message: string = 'initialization failed') { - super(message); - } -} diff --git a/src/main/modules/error/web-crawler-error.ts b/src/main/modules/error/web-crawler-error.ts deleted file mode 100644 index 03dbe84..0000000 --- a/src/main/modules/error/web-crawler-error.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * generic web crawler error - */ -export class WebCrawlerError extends Error { - public constructor(message: string = 'web crawler failed') { - super(message); - } -} diff --git a/src/main/modules/error/web-crawler-form-error.ts b/src/main/modules/error/web-crawler-form-error.ts deleted file mode 100644 index 5909a54..0000000 --- a/src/main/modules/error/web-crawler-form-error.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { WebCrawlerError } from './web-crawler-error'; - -/** - * Error thrown when the web crawler can't work with the provided form. - */ -export class WebCrawlerFormError extends WebCrawlerError { - public constructor(message: string = 'web crawler failed') { - super(message); - } -} diff --git a/src/main/modules/error/web-crawler-login-error.ts b/src/main/modules/error/web-crawler-login-error.ts deleted file mode 100644 index 8b01009..0000000 --- a/src/main/modules/error/web-crawler-login-error.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { WebCrawlerError } from './web-crawler-error'; - -/** - * Error thrown when the web crawler can't login - */ -export class WebCrawlerLoginError extends WebCrawlerError { - public constructor(message: string = 'login failed') { - super(message); - } -} diff --git a/src/main/modules/i18n/i-i18n-translator.ts b/src/main/modules/i18n/i-i18n-translator.ts new file mode 100644 index 0000000..d6b0311 --- /dev/null +++ b/src/main/modules/i18n/i-i18n-translator.ts @@ -0,0 +1,3 @@ +export interface II18nTranslator { + t(text: string): string; +} diff --git a/src/main/modules/i18n/i18n-translator.ts b/src/main/modules/i18n/i18n-translator.ts new file mode 100644 index 0000000..75b82bb --- /dev/null +++ b/src/main/modules/i18n/i18n-translator.ts @@ -0,0 +1,9 @@ +import { injectable } from 'inversify'; +import { II18nTranslator } from './i-i18n-translator'; + +@injectable() +export class I18nTranslator implements II18nTranslator { + public t(text: string): string { + return text; + } +} diff --git a/src/main/modules/nhentai/i-nhentai-api.ts b/src/main/modules/nhentai/i-nhentai-api.ts index 06af055..1680000 100644 --- a/src/main/modules/nhentai/i-nhentai-api.ts +++ b/src/main/modules/nhentai/i-nhentai-api.ts @@ -1,4 +1,3 @@ export interface INhentaiApi { - isLoggedIn(): Promise; - login(name: string, password: string): Promise; + getFavorites(): Promise; } diff --git a/src/main/modules/nhentai/i-nhentai-app-window.ts b/src/main/modules/nhentai/i-nhentai-app-window.ts new file mode 100644 index 0000000..4c40cc3 --- /dev/null +++ b/src/main/modules/nhentai/i-nhentai-app-window.ts @@ -0,0 +1,5 @@ +import { IUrlAppWindow } from '../app-window/i-url-app-window'; + +export interface INhentaiAppWindow extends IUrlAppWindow { + getFavorites(): Promise; +} diff --git a/src/main/modules/nhentai/nhentai-api.ts b/src/main/modules/nhentai/nhentai-api.ts index 336c50e..1c0d7f9 100644 --- a/src/main/modules/nhentai/nhentai-api.ts +++ b/src/main/modules/nhentai/nhentai-api.ts @@ -1,129 +1,17 @@ import { injectable } from 'inversify'; -import { JSDOM } from 'jsdom'; -import { RequestInit, Response } from 'node-fetch'; import { inject } from '../../core/inject'; -import { WebCrawlerFormError } from '../error/web-crawler-form-error'; -import { WebCrawlerLoginError } from '../error/web-crawler-login-error'; -import { IWebCrawler } from '../web-crawler/i-web-crawler'; import { INhentaiApi } from './i-nhentai-api'; - -const domain = 'nhentai.net'; -const url = `https://${domain}/`; - -const paths = { - books: 'g/', - login: 'login/', - favorites: 'favorites/', -}; - -const usernameInput = 'username_or_email'; -const passwordInput = 'password'; - -interface ILoginMeta { - [key: string]: string; -} - -interface ILoginAuth { - [usernameInput]: string; - [passwordInput]: string; -} - -interface ILoginParams extends ILoginMeta, ILoginAuth {} +import { INhentaiAppWindow } from './i-nhentai-app-window'; @injectable() export class NhentaiApi implements INhentaiApi { - private webCrawler: IWebCrawler; + private readonly appWindow: INhentaiAppWindow; - public constructor(@inject('web-crawler') webCrawler: IWebCrawler) { - this.webCrawler = webCrawler; + public constructor(@inject('nhentai-app-window') appWindow: INhentaiAppWindow) { + this.appWindow = appWindow; } - public isLoggedIn(): Promise { - return this.webCrawler - .fetch(`${url}${paths.favorites}`, { redirect: 'manual' }) - .then((res: Response) => res.status === HttpCode.OK); - } - - public login(name: string, password: string): Promise { - return this.getLoginMeta() - .then((meta: ILoginMeta) => { - const loginParams: ILoginParams = { - ...meta, - ...{ - [usernameInput]: name, - [passwordInput]: password, - }, - }; - - return this.postNHentai(paths.login, { - body: encodeURI( - Object.keys(loginParams) - .map((key: keyof ILoginParams) => `${key}=${loginParams[key]}`) - .join('&') - ), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - redirect: 'manual', - }); - }) - .then(() => {}) - .catch(() => Promise.reject(new WebCrawlerLoginError())); - } - - private getNHentai(path: string): Promise { - return this.webCrawler - .fetch(`${url}${path}`) - .then((res: Response) => res.text()) - .then((text: string) => { - const { document } = new JSDOM(text).window; - return document; - }); - } - - private postNHentai(path: string, requestInit: RequestInit = {}): Promise { - const postUrl = `${url}${path}`; - return this.webCrawler.fetch(postUrl, { - ...requestInit, - ...{ - headers: { - ...requestInit.headers, - ...{ - Host: domain, - Referer: postUrl, - }, - }, - }, - method: 'post', - }); - } - - private getLoginMeta(): Promise { - return this.getNHentai(paths.login).then((document: Document) => { - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < document.forms.length; i++) { - const form: HTMLFormElement = document.forms[i]; - const valueStore: ILoginMeta = {}; - let isLoginForm = false; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let j = 0; j < form.elements.length; j++) { - const input = form.elements[j]; - const name = input.getAttribute('name'); - - if (name === usernameInput || name === passwordInput) { - isLoginForm = true; - } else if (name) { - const value = input.getAttribute('value'); - if (value) { - valueStore[name] = value; - } - } - } - if (isLoginForm) { - return valueStore; - } - } - return Promise.reject(new WebCrawlerFormError()); - }); + public getFavorites(): Promise { + return this.appWindow.getFavorites(); } } diff --git a/src/main/modules/nhentai/nhentai-app-window.ts b/src/main/modules/nhentai/nhentai-app-window.ts new file mode 100644 index 0000000..83d3b7e --- /dev/null +++ b/src/main/modules/nhentai/nhentai-app-window.ts @@ -0,0 +1,223 @@ +import { WebContents } from 'electron'; +import os from 'os'; +import path from 'path'; +import { Readable } from 'stream'; +import { URL } from 'url'; +import { createReadStream, remove } from 'fs-extra'; +import { injectable } from 'inversify'; +import { inject } from '../../core/inject'; +import { UrlAppWindow } from '../app-window/url-app-window'; +import { WindowClosedError } from '../app-window/window-closed-error'; +import { ISessionHelper } from '../session/i-session-helper'; +import { INhentaiAppWindow } from './i-nhentai-app-window'; +import { IFavorite } from './nhentai'; +import { + url as nhentaiUrl, + hostname as nhentaiHostname, + paths as nhentaiPaths, + getFavoritePageUrl, + nextFavoritePageSelector, + coverLinkSelector, + downloadLinkId, + getGalleryId, +} from './nhentai-util'; + +const waitInterval = 2000; + +@injectable() +export class NhentaiAppWindow extends UrlAppWindow implements INhentaiAppWindow { + public constructor(@inject('session-helper') sessionHelper: ISessionHelper) { + super(sessionHelper, nhentaiUrl); + } + + /** + * @throws WindowClosedError when this._window is null + */ + public async getFavorites(): Promise { + const error = new WindowClosedError(); + + if (this.isClosed()) { + await this.open(); + } + + if (!(await this.isLoggedIn())) { + await this.login(); + } + + if (!this._window) { + throw error; + } + + const bookUrls: string[] = []; + for await (const wc of this.getFavoritePageWebContentsGenerator()) { + bookUrls.push( + ...((await wc.executeJavaScript( + `Array.from(document.querySelectorAll('${coverLinkSelector}')).map((el) => el.href)` + )) as string[]) + ); + } + + const readable = Readable.from( + (async function* (thisArg): AsyncGenerator { + for (const bookUrl of bookUrls) { + yield await thisArg.getBookTorrent(bookUrl); + } + })(this), + { + objectMode: true, + } + ); + readable.once('close', this.close); + + return readable; + } + + protected getCsp(): IContentSecurityPolicy { + return { + 'default-src': ['nhentai.net'], + 'script-src': ['nhentai.net', "'unsafe-eval'"], + 'script-src-elem': ['*.nhentai.net', "'unsafe-inline'", '*.google.com', '*.gstatic.com'], + 'style-src': [ + '*.nhentai.net', + "'unsafe-inline'", + 'fonts.googleapis.com', + 'cdnjs.cloudflare.com', + 'maxcdn.bootstrapcdn.com', + '*.gstatic.com', + ], + 'img-src': ['*.nhentai.net', 'data:', '*.gstatic.com', '*.google.com'], + 'font-src': ['fonts.gstatic.com', 'cdnjs.cloudflare.com', 'maxcdn.bootstrapcdn.com'], + 'frame-src': ['*.google.com'], + 'connect-src': ['nhentai.net', '*.google.com'], + 'worker-src': ['*.google.com'], + }; + } + + protected onWillNavigate(event: Electron.Event, navigationUrl: string): void { + const url = new URL(navigationUrl); + + if (url.hostname !== nhentaiHostname) { + event.preventDefault(); + } + } + + /** + * @throws WindowClosedError when this._window is null + */ + private async isLoggedIn(): Promise { + if (!this._window) { + throw new WindowClosedError(); + } + return this._window.webContents + .executeJavaScript( + `fetch('${ + nhentaiUrl + nhentaiPaths.favorites + }', {credentials: 'include', redirect: 'manual'}).then((res) => res.status)` + ) + .then((status: number) => status === HttpCode.OK); + } + + /** + * @throws WindowClosedError when this._window is null + */ + private async login(): Promise { + if (!this._window) { + throw new WindowClosedError(); + } + await this.loadUrlSafe(nhentaiUrl + nhentaiPaths.login); + + return new Promise((resolve, reject) => { + const timeout = setInterval(() => { + this.isLoggedIn() + .then((loggedIn) => { + if (loggedIn) { + clearTimeout(timeout); + resolve(); + } + }) + .catch((reason) => reject(reason)); + }, waitInterval); + }); + } + + private async *getFavoritePageWebContentsGenerator(): AsyncGenerator { + const error = new WindowClosedError(); + if (!this._window) { + throw error; + } + await this.loadUrlSafe(getFavoritePageUrl(1)); + yield this._window.webContents; + do { + try { + const hasNextPage = (await this._window.webContents.executeJavaScript( + `!!document.querySelector('${nextFavoritePageSelector}')` + )) as boolean; + if (hasNextPage) { + yield new Promise((resolve) => { + if (this._window) { + this._window.webContents.once('did-finish-load', () => { + if (this._window) { + resolve(this._window.webContents); + } else { + throw error; + } + }); + void this._window.webContents.executeJavaScript( + `document.querySelector('${nextFavoritePageSelector}').click()` + ); + } else { + throw error; + } + }); + } else { + break; + } + } catch { + break; + } + } while (true); + return undefined; + } + + private async getBookTorrent(bookUrl: string): Promise { + const galleryId = getGalleryId(bookUrl); + const fileName = `${galleryId}.torrent`; + const filePath = path.resolve(os.tmpdir(), fileName); + await this.loadUrlSafe(bookUrl); + await new Promise((resolve, reject) => { + if (!this._window) { + throw new WindowClosedError(); + } + this._window.webContents.session.once('will-download', (event, item) => { + item.setSavePath(filePath); + item.once('done', (doneEvent, state) => { + switch (state) { + case 'completed': + resolve(); + break; + case 'cancelled': + case 'interrupted': + default: + reject(new Error(state)); + break; + } + }); + item.on('updated', () => { + item.resume(); + }); + }); + void this._window.webContents.executeJavaScript(`document.getElementById('${downloadLinkId}').click()`); + }); + + const readable = createReadStream(filePath, { emitClose: true }); + + readable.once('close', () => { + void remove(filePath); + }); + + return { + name: fileName, + torrentFile: readable, + }; + } +} diff --git a/src/main/modules/nhentai/nhentai-ipc-controller.ts b/src/main/modules/nhentai/nhentai-ipc-controller.ts index b6d6553..1fcd220 100644 --- a/src/main/modules/nhentai/nhentai-ipc-controller.ts +++ b/src/main/modules/nhentai/nhentai-ipc-controller.ts @@ -1,26 +1,44 @@ +import { dialog } from 'electron'; +import path from 'path'; +import { createWriteStream } from 'fs-extra'; import { container } from '../../core/container'; +import { II18nTranslator } from '../i18n/i-i18n-translator'; import { answer } from '../ipc/annotations/answer'; import { INhentaiApi } from './i-nhentai-api'; +import { IFavorite } from './nhentai'; export class NhentaiIpcController implements IIpcController { - private nhentaiApi: INhentaiApi; + private readonly nhentaiApi: INhentaiApi; - public constructor(nhentaiApi: INhentaiApi) { + private readonly translator: II18nTranslator; + + private constructor(nhentaiApi: INhentaiApi, translator: II18nTranslator) { this.nhentaiApi = nhentaiApi; + this.translator = translator; } - @answer(IpcChannel.LOGIN) - public login(credentials: ICredentials): Promise { - return this.nhentaiApi.login(credentials.name, credentials.password); - } + @answer(IpcChannel.NHENTAI_SAVE_FAVORITES) + public async nhentaiSaveFavorites(): Promise { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory'], + title: this.translator.t('Select torrent file save location'), + }); - @answer(IpcChannel.LOGGED_IN) - public loggedIn(): Promise { - return this.nhentaiApi.isLoggedIn(); + const favoritesStream = await this.nhentaiApi.getFavorites(); + + return new Promise((resolve) => { + favoritesStream.on('data', (favorite: IFavorite) => { + const writable = createWriteStream(path.resolve(result.filePaths[0], favorite.name)); + favorite.torrentFile.pipe(writable); + }); + + favoritesStream.once('close', resolve); + }); } public get(): NhentaiIpcController { const nhentaiApi: INhentaiApi = container.get('nhentai-api'); - return new NhentaiIpcController(nhentaiApi); + const translator: II18nTranslator = container.get('i18n-translator'); + return new NhentaiIpcController(nhentaiApi, translator); } } diff --git a/src/main/modules/nhentai/nhentai-util.ts b/src/main/modules/nhentai/nhentai-util.ts new file mode 100644 index 0000000..5970354 --- /dev/null +++ b/src/main/modules/nhentai/nhentai-util.ts @@ -0,0 +1,26 @@ +export const hostname = 'nhentai.net'; +export const url = `https://${hostname}/`; + +export const paths = { + books: 'g/', + login: 'login/', + favorites: 'favorites/', +}; + +export const downloadLinkId = 'download'; + +export const nextFavoritePageSelector = 'a.next'; +export const coverLinkSelector = 'a.cover'; + +export function getFavoritePageUrl(page: number): string { + return `${url + paths.favorites}?page=${page}`; +} + +export function getGalleryId(bookUrl: string): string { + const regExpExecArray = /https:\/\/nhentai\.net\/g\/(\d+)/.exec(bookUrl); + if (regExpExecArray && regExpExecArray[1]) { + return regExpExecArray[1]; + } else { + throw new TypeError(`could not get nhentai gallery if from the book url ${bookUrl}`); + } +} diff --git a/src/main/modules/nhentai/nhentai.d.ts b/src/main/modules/nhentai/nhentai.d.ts new file mode 100644 index 0000000..9f49f49 --- /dev/null +++ b/src/main/modules/nhentai/nhentai.d.ts @@ -0,0 +1,4 @@ +export interface IFavorite { + name: string; + torrentFile: NodeJS.ReadableStream; +} diff --git a/src/main/modules/session/i-session-helper.ts b/src/main/modules/session/i-session-helper.ts new file mode 100644 index 0000000..3fc070a --- /dev/null +++ b/src/main/modules/session/i-session-helper.ts @@ -0,0 +1,5 @@ +import { BrowserWindow } from 'electron'; + +export interface ISessionHelper { + setCsp(window: BrowserWindow, csp: IContentSecurityPolicy): void; +} diff --git a/src/main/modules/session/i-session.ts b/src/main/modules/session/i-session.ts deleted file mode 100644 index 32d84d0..0000000 --- a/src/main/modules/session/i-session.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ISession { - setHeaders(): void; -} diff --git a/src/main/modules/session/session-helper.ts b/src/main/modules/session/session-helper.ts new file mode 100644 index 0000000..97c7190 --- /dev/null +++ b/src/main/modules/session/session-helper.ts @@ -0,0 +1,43 @@ +import { injectable } from 'inversify'; +import { isDev } from '../../core/env'; +import { ISessionHelper } from './i-session-helper'; + +const defaultCsp: IContentSecurityPolicy = { + 'default-src': ["'self'"], + 'style-src': ["'unsafe-inline'"], + 'object-src': ["'none'"], +}; + +@injectable() +export class SessionHelper implements ISessionHelper { + private static stringifyCspHeader(csp: IContentSecurityPolicy): string { + return Object.entries(csp) + .map((directive: [string, CspValue[]]) => `${directive[0]} ${directive[1]?.join(' ')}`) + .join('; '); + } + + public setCsp(window: Electron.BrowserWindow, csp: IContentSecurityPolicy): void { + const mergedCsp: IContentSecurityPolicy = { ...defaultCsp, ...csp }; + + if (isDev()) { + mergedCsp['default-src'] = ['devtools:'].concat(mergedCsp['default-src'] ?? []); + mergedCsp['script-src'] = ["'unsafe-eval'"].concat(mergedCsp['script-src'] ?? []); + mergedCsp['script-src-elem'] = ['file:', 'devtools:', "'unsafe-inline'"].concat( + mergedCsp['script-src-elem'] ?? [] + ); + mergedCsp['style-src'] = ['devtools:', "'unsafe-inline'"].concat(mergedCsp['style-src'] ?? []); + mergedCsp['img-src'] = ['devtools:'].concat(mergedCsp['img-src'] ?? []); + mergedCsp['connect-src'] = ['devtools:', 'data:'].concat(mergedCsp['connect-src'] ?? []); + mergedCsp['worker-src'] = ['devtools:'].concat(mergedCsp['worker-src'] ?? []); + } + + window.webContents.session.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': SessionHelper.stringifyCspHeader(mergedCsp), + }, + }); + }); + } +} diff --git a/src/main/modules/session/session.d.ts b/src/main/modules/session/session.d.ts new file mode 100644 index 0000000..fb1350c --- /dev/null +++ b/src/main/modules/session/session.d.ts @@ -0,0 +1,23 @@ +type CspValue = '*' | "'none'" | "'self'" | "'unsafe-inline'" | "'unsafe-eval'" | string; + +/** + * This interface represents a content security policy. + * + * @see https://www.w3.org/TR/CSP/ + * @see https://content-security-policy.com/ + */ +interface IContentSecurityPolicy { + 'child-src'?: CspValue[]; + 'connect-src'?: CspValue[]; + 'default-src'?: CspValue[]; + 'font-src'?: CspValue[]; + 'frame-src'?: CspValue[]; + 'img-src'?: CspValue[]; + 'media-src'?: CspValue[]; + 'object-src'?: ["'none'"]; + 'script-src'?: CspValue[]; + 'script-src-elem'?: CspValue[]; + 'script-src-attr'?: CspValue[]; + 'style-src'?: CspValue[]; + 'worker-src'?: CspValue[]; +} diff --git a/src/main/modules/session/session.ts b/src/main/modules/session/session.ts deleted file mode 100644 index 727667e..0000000 --- a/src/main/modules/session/session.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { session } from 'electron'; -import { injectable } from 'inversify'; -import { isDev } from '../../core/env'; -import { ISession } from './i-session'; - -@injectable() -export class Session implements ISession { - public setHeaders(): void { - // these headers only work on web requests, file:// protocol is handled via meta tags in the html - session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: { - ...details.responseHeaders, - 'Content-Security-Policy': isDev() - ? [ - 'default-src devtools:;' + - "script-src 'unsafe-eval';" + - "script-src-elem file: devtools: 'sha256-hl04hLzKBpmsfWF2wIA/0Vs6ZNV5T9ZNFY//3uXrgSk=';" + - "style-src devtools: 'unsafe-inline';" + - 'connect-src devtools: data:', - ] - : ["default-src 'self';" + "style-src 'unsafe-inline'"], - }, - }); - }); - } -} diff --git a/src/main/modules/web-crawler/i-web-crawler.ts b/src/main/modules/web-crawler/i-web-crawler.ts deleted file mode 100644 index 9e64c80..0000000 --- a/src/main/modules/web-crawler/i-web-crawler.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CookieJar } from 'jsdom'; -import { RequestInit, Response } from 'node-fetch'; - -export interface IWebCrawler { - cookieJar: CookieJar; - fetch(url: string, requestInit?: RequestInit): Promise; -} diff --git a/src/main/modules/web-crawler/web-crawler.spec.ts b/src/main/modules/web-crawler/web-crawler.spec.ts deleted file mode 100644 index 4d85f6a..0000000 --- a/src/main/modules/web-crawler/web-crawler.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import rewiremock from 'rewiremock'; -import '../../../../mocks/electron'; - -import { expect } from 'chai'; -import 'mocha'; -import nock from 'nock'; -import { Response } from 'node-fetch'; -import sinon from 'sinon'; -import { container } from '../../core/container'; -import { Store } from '../store/store'; -import { StoreMock } from '../store/store.mock'; -import { IWebCrawler } from './i-web-crawler'; - -describe('Web Crawler', function () { - this.timeout(2000); - - before(() => { - rewiremock.enable(); - - container.unbind('store'); - container.bind('store').to(StoreMock); - }); - - beforeEach(() => { - if (!nock.isActive()) { - nock.activate(); - } - }); - - afterEach(() => { - nock.cleanAll(); - }); - - after(() => { - rewiremock.disable(); - - container.unbind('store'); - container.bind('store').to(Store); - }); - - it('fetches websites', async () => { - const callback = sinon.spy(); - - const testUrl = 'https://example.com'; - nock(testUrl) - .get(/.*/) - .reply( - HttpCode.OK, - () => { - callback(); - return JSON.stringify([{ id: 12, comment: 'Hey there' }]); - }, - { 'Content-Type': 'application/json' } - ) - .persist(); - - const webCrawler: IWebCrawler = container.get('web-crawler'); - - const res: Response = await webCrawler.fetch(testUrl); - expect(callback.callCount).to.equal(1, 'multiple requests (or none) are sent when only one should be'); - const json = (await res.json()) as unknown; - expect(json).to.deep.equal([{ id: 12, comment: 'Hey there' }], 'response body is incorrect'); - }); -}); diff --git a/src/main/modules/web-crawler/web-crawler.ts b/src/main/modules/web-crawler/web-crawler.ts deleted file mode 100644 index e9aafb7..0000000 --- a/src/main/modules/web-crawler/web-crawler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { injectable } from 'inversify'; -import { CookieJar } from 'jsdom'; -import nodeFetch, { RequestInit, Response } from 'node-fetch'; -import { inject } from '../../core/inject'; -import { CookieSaveError } from '../error/cookie-save-error'; -import { IStore } from '../store/i-store'; -import { IWebCrawler } from './i-web-crawler'; - -@injectable() -export class WebCrawler implements IWebCrawler { - public cookieJar: CookieJar; - - private initialized: boolean; - - private store: IStore; - - public constructor(@inject('store') store: IStore) { - this.initialized = false; - this.cookieJar = new CookieJar(); - this.store = store; - } - - public fetch(url: string, requestInit: RequestInit = {}): Promise { - return this.init().then(() => { - const cookiedInit = { - ...requestInit, - ...{ - headers: { - ...requestInit.headers, - ...{ - Cookie: this.cookieJar.getCookieStringSync(url), - }, - }, - }, - }; - return nodeFetch(url, cookiedInit).then((res: Response) => { - this.setCookies(res.headers.raw()['set-cookie'], url).catch((reason: Error) => { - throw new CookieSaveError(reason.message); - }); - return res; - }); - }); - } - - private init(): Promise { - if (!this.initialized) { - return this.store.load(StoreKey.COOKIES).then((cookies: unknown) => { - if (cookies !== undefined) { - this.cookieJar = CookieJar.deserializeSync(cookies as string); - } - this.initialized = true; - }); - } else { - return Promise.resolve(); - } - } - - private setCookies(header: string[], url: string): Promise { - if (header) { - header.forEach((cookie: string) => { - this.cookieJar.setCookieSync(cookie, url); - }); - return this.store.save(StoreKey.COOKIES, this.cookieJar.serializeSync()).catch((reason: Error) => { - throw new CookieSaveError(reason.message); - }); - } - return Promise.resolve(); - } -} diff --git a/src/renderer/components/3-polymers/NhentaiLogin.svelte b/src/renderer/components/3-polymers/NhentaiLogin.svelte index 431e552..c4b05eb 100644 --- a/src/renderer/components/3-polymers/NhentaiLogin.svelte +++ b/src/renderer/components/3-polymers/NhentaiLogin.svelte @@ -1,43 +1,15 @@ diff --git a/src/renderer/services/api.ts b/src/renderer/services/api.ts index a6221c1..af65dd2 100644 --- a/src/renderer/services/api.ts +++ b/src/renderer/services/api.ts @@ -27,10 +27,6 @@ const ipcClient: IIpcClient = { }, }; -export function login(credentials: ICredentials): Promise { - return ipcClient.ask(IpcChannel.LOGIN, credentials) as Promise; -} - -export function isLoggedIn(): Promise { - return ipcClient.ask(IpcChannel.LOGGED_IN) as Promise; +export function nhentaiSaveFavorites(): Promise { + return ipcClient.ask(IpcChannel.NHENTAI_SAVE_FAVORITES) as Promise; } diff --git a/src/renderer/services/store.ts b/src/renderer/services/store.ts deleted file mode 100644 index ada4959..0000000 --- a/src/renderer/services/store.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { writable, Readable } from 'svelte/store'; -import * as api from './api'; - -const { subscribe, set } = writable(false); - -interface ILoggedIn extends Readable { - fetchIsLoggedIn(): Promise; - fetchLogin(credentials: ICredentials): Promise; -} - -export const loggedIn: ILoggedIn = { - subscribe, - fetchIsLoggedIn(): Promise { - return api.isLoggedIn().then((isLoggedIn: boolean) => { - set(isLoggedIn); - }); - }, - fetchLogin(this: ILoggedIn, credentials: ICredentials): Promise { - return api.login(credentials).then(this.fetchIsLoggedIn); - }, -}; diff --git a/types/ipc.d.ts b/types/ipc.d.ts index ed5a03b..1bd83a4 100644 --- a/types/ipc.d.ts +++ b/types/ipc.d.ts @@ -1,6 +1,5 @@ declare const enum IpcChannel { - LOGIN = 'LOGIN', - LOGGED_IN = 'LOGGED_IN', + NHENTAI_SAVE_FAVORITES = 'NHENTAI_SAVE_FAVORITES', } interface IIpcPayload { @@ -16,11 +15,6 @@ interface IIpcResponse { error?: string; } -interface ICredentials { - name: string; - password: string; -} - interface IIpcClient { ask: (channel: IpcChannel, data?: unknown) => Promise; }