refactor: re-do source structure with InversifyJS (dependency injection) and adjust meta processes
This commit is contained in:
parent
19c11312c5
commit
00ebd0e5c8
|
@ -9,10 +9,7 @@
|
||||||
node_modules
|
node_modules
|
||||||
.nyc_output
|
.nyc_output
|
||||||
/src/**/*.js
|
/src/**/*.js
|
||||||
/tests/**/*.js
|
|
||||||
/mocks/**/*.js
|
/mocks/**/*.js
|
||||||
/frontend
|
/frontend
|
||||||
/store-backup
|
|
||||||
/test-paths
|
/test-paths
|
||||||
/store
|
|
||||||
/out
|
/out
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
|
"plugins": ["@typescript-eslint", "import"],
|
||||||
"extends": ["eslint:recommended", "prettier"],
|
"extends": ["eslint:recommended", "prettier"],
|
||||||
"plugins": ["import"],
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2019
|
"ecmaVersion": 2019
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/core-modules": ["electron"]
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true
|
"node": true
|
||||||
|
@ -26,7 +29,7 @@
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"devDependencies": [
|
"devDependencies": [
|
||||||
"tests/**/*",
|
"src/**/*.spec.*",
|
||||||
"mocks/**/*",
|
"mocks/**/*",
|
||||||
"src/renderer/**/*",
|
"src/renderer/**/*",
|
||||||
"templates/**/*",
|
"templates/**/*",
|
||||||
|
@ -35,7 +38,16 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/no-default-export": "error"
|
"import/no-default-export": "error",
|
||||||
|
"import/first": "error",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
@ -51,7 +63,10 @@
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"import/core-modules": ["electron"]
|
"import/extensions": [".ts", "d.ts", ".js", ".json"],
|
||||||
|
"import/parsers": {
|
||||||
|
"@typescript-eslint/parser": [".ts", "d.ts"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
|
@ -93,7 +108,21 @@
|
||||||
"@typescript-eslint/unbound-method": "off",
|
"@typescript-eslint/unbound-method": "off",
|
||||||
"@typescript-eslint/ban-ts-ignore": "off",
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }]
|
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
|
||||||
|
"@typescript-eslint/member-ordering": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["**/*.{spec,mock}.*"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": "off",
|
||||||
|
|
||||||
|
"import/order": "off",
|
||||||
|
|
||||||
|
"@typescript-eslint/no-magic-numbers": "off",
|
||||||
|
"@typescript-eslint/typedef": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -101,12 +130,6 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/no-default-export": "off"
|
"import/no-default-export": "off"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["src/renderer/**/*.*"],
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,12 @@ node_modules
|
||||||
# generated code
|
# generated code
|
||||||
/src/**/*.js
|
/src/**/*.js
|
||||||
/src/**/*.js.map
|
/src/**/*.js.map
|
||||||
/tests/**/*.js
|
|
||||||
/tests/**/*.js.map
|
|
||||||
/mocks/**/*.js
|
/mocks/**/*.js
|
||||||
/mocks/**/*.js.map
|
/mocks/**/*.js.map
|
||||||
/frontend
|
/frontend
|
||||||
|
|
||||||
# created by testing
|
# created by testing
|
||||||
/store-backup
|
|
||||||
/test-paths
|
/test-paths
|
||||||
|
|
||||||
# managed by application
|
|
||||||
/store
|
|
||||||
|
|
||||||
# built app
|
# built app
|
||||||
/out
|
/out
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
# https://github.com/mochajs/mocha/blob/master/example/config
|
# https://github.com/mochajs/mocha/blob/master/example/config
|
||||||
spec: 'tests/**/*spec.js'
|
spec: 'src/**/*.spec.js'
|
||||||
|
|
|
@ -11,6 +11,7 @@ report-dir: './.nyc_output/coverage'
|
||||||
include:
|
include:
|
||||||
- 'src/**'
|
- 'src/**'
|
||||||
exclude:
|
exclude:
|
||||||
|
- 'src/**/*.spec.*'
|
||||||
- 'src/main/entities/**'
|
- 'src/main/entities/**'
|
||||||
watermarks:
|
watermarks:
|
||||||
statements: [80, 95]
|
statements: [80, 95]
|
||||||
|
|
|
@ -15,10 +15,7 @@
|
||||||
node_modules
|
node_modules
|
||||||
.nyc_output
|
.nyc_output
|
||||||
/src/**/*.js
|
/src/**/*.js
|
||||||
/tests/**/*.js
|
|
||||||
/mocks/**/*.js
|
/mocks/**/*.js
|
||||||
/frontend
|
/frontend
|
||||||
/store-backup
|
|
||||||
/test-paths
|
/test-paths
|
||||||
/store
|
|
||||||
/out
|
/out
|
||||||
|
|
|
@ -113,7 +113,7 @@ The application uses [SQLite3](https://www.npmjs.com/package/sqlite3) as a datab
|
||||||
|
|
||||||
#### Database Migrations
|
#### Database Migrations
|
||||||
|
|
||||||
Migrations are stored in [src/main/migrations](src/main/migrations) and handled by typeorm. Migrations are run on app start inside [database.ts](src/main/services/database.ts).
|
Migrations are stored in [src/main/migrations](src/main/migrations) and handled by typeorm. Migrations are run on app start inside [database.ts](src/main/core/database.ts).
|
||||||
|
|
||||||
To auto-generate a migration:
|
To auto-generate a migration:
|
||||||
`node_modules/.bin/typeorm migration:generate -n <migration name> -c <connection name>`
|
`node_modules/.bin/typeorm migration:generate -n <migration name> -c <connection name>`
|
||||||
|
@ -140,6 +140,8 @@ The testing framework of choice is [Mocha](https://mochajs.org/). Call `npm run
|
||||||
- HTTP server mocking is done by [nock](https://github.com/nock/nock)
|
- HTTP server mocking is done by [nock](https://github.com/nock/nock)
|
||||||
- property based testing is made possible by [fast-check](https://github.com/dubzzz/fast-check)
|
- property based testing is made possible by [fast-check](https://github.com/dubzzz/fast-check)
|
||||||
|
|
||||||
|
For the creation of test files look at existing ones, they are named `*.spec.ts`.
|
||||||
|
|
||||||
#### Mocks
|
#### Mocks
|
||||||
|
|
||||||
There are 2 ways in which mocks are defined/used:
|
There are 2 ways in which mocks are defined/used:
|
||||||
|
@ -147,7 +149,7 @@ There are 2 ways in which mocks are defined/used:
|
||||||
0. for external modules, in [mocks](mocks)
|
0. for external modules, in [mocks](mocks)
|
||||||
- uses the [rewiremock](https://github.com/theKashey/rewiremock) package
|
- uses the [rewiremock](https://github.com/theKashey/rewiremock) package
|
||||||
- use this only when there is some magic happening like for electron which normally runs in its own node process
|
- use this only when there is some magic happening like for electron which normally runs in its own node process
|
||||||
1. for own modules, just beside their test file in [tests](tests)
|
1. for own modules, just beside their file
|
||||||
- name the file `*.mock.ts` and use existing mock files for orientation on how to build them
|
- name the file `*.mock.ts` and use existing mock files for orientation on how to build them
|
||||||
- use sparingly and only when not having a mock makes it more complex e.g. for modules which interact with the file system
|
- use sparingly and only when not having a mock makes it more complex e.g. for modules which interact with the file system
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const minimist = require('minimist');
|
|
||||||
const webpackConfig = require('./webpack.config');
|
|
||||||
const templating = require('./templates');
|
|
||||||
const { watch } = require('chokidar');
|
const { watch } = require('chokidar');
|
||||||
const { debounce } = require('lodash');
|
const { debounce } = require('lodash');
|
||||||
|
const minimist = require('minimist');
|
||||||
|
const templating = require('./templates');
|
||||||
|
const webpackConfig = require('./webpack.config');
|
||||||
|
|
||||||
/** @type {Object} */
|
/** @type {Object} */
|
||||||
const argv = minimist(process.argv);
|
const argv = minimist(process.argv);
|
||||||
|
|
|
@ -7,11 +7,9 @@ const ignoreList = [
|
||||||
/^\/\.nyc_output($|\/)/,
|
/^\/\.nyc_output($|\/)/,
|
||||||
/^\/declarations($|\/)/,
|
/^\/declarations($|\/)/,
|
||||||
/^\/mocks($|\/)/,
|
/^\/mocks($|\/)/,
|
||||||
/^\/store($|\/)/,
|
|
||||||
/^\/store-backup($|\/)/,
|
|
||||||
/^\/templates($|\/)/,
|
/^\/templates($|\/)/,
|
||||||
/^\/test-paths($|\/)/,
|
/^\/test-paths($|\/)/,
|
||||||
/^\/tests($|\/)/,
|
/^\/types($|\/)/,
|
||||||
/^\/workspace($|\/)/,
|
/^\/workspace($|\/)/,
|
||||||
|
|
||||||
/^\/\.editorconfig/,
|
/^\/\.editorconfig/,
|
||||||
|
@ -29,6 +27,9 @@ const ignoreList = [
|
||||||
/^\/webpack\.config\.js/,
|
/^\/webpack\.config\.js/,
|
||||||
|
|
||||||
/^\/node_modules\/\.cache($|\/)/,
|
/^\/node_modules\/\.cache($|\/)/,
|
||||||
|
// test and mock files:
|
||||||
|
/^\/src\/.*\.(spec|mock)\.(ts|js(\.map)?)/,
|
||||||
|
// original typescript source and generated source map files:
|
||||||
/^\/src\/.*\.(ts|js\.map)/,
|
/^\/src\/.*\.(ts|js\.map)/,
|
||||||
/^\/src\/.*\.eslintrc\.json/,
|
/^\/src\/.*\.eslintrc\.json/,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import WebContents = Electron.WebContents;
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { rewiremock } from './rewiremock';
|
import { rewiremock } from './rewiremock';
|
||||||
|
import WebContents = Electron.WebContents;
|
||||||
|
|
||||||
const electronMock: DeepPartial<typeof Electron> = {
|
const electronMock: DeepPartial<typeof Electron> = {
|
||||||
app: {
|
app: {
|
||||||
|
|
|
@ -5446,6 +5446,11 @@
|
||||||
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
|
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"inversify": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ=="
|
||||||
|
},
|
||||||
"invert-kv": {
|
"invert-kv": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
|
||||||
|
|
10
package.json
10
package.json
|
@ -25,14 +25,8 @@
|
||||||
"watch:ts": "tsc -w --pretty --preserveWatchOutput",
|
"watch:ts": "tsc -w --pretty --preserveWatchOutput",
|
||||||
"build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"",
|
"build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"",
|
||||||
"watch": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run watch:webpack\" \"npm run watch:index\" \"npm run watch:ts\"",
|
"watch": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run watch:webpack\" \"npm run watch:index\" \"npm run watch:ts\"",
|
||||||
"test:before": "node tests/setup/before.js",
|
|
||||||
"test:after": "node tests/setup/after.js",
|
|
||||||
"pretest:fast": "npm run test:before",
|
|
||||||
"test:fast": "mocha --grep @slow --invert",
|
"test:fast": "mocha --grep @slow --invert",
|
||||||
"posttest:fast": "npm run test:after",
|
|
||||||
"pretest": "npm run test:before",
|
|
||||||
"test": "mocha",
|
"test": "mocha",
|
||||||
"posttest": "npm run test:after",
|
|
||||||
"coverage:fast": "nyc npm run test:fast",
|
"coverage:fast": "nyc npm run test:fast",
|
||||||
"coverage": "nyc npm run test",
|
"coverage": "nyc npm run test",
|
||||||
"prelint": "eslint --print-config forge.config.js | eslint-config-prettier-check",
|
"prelint": "eslint --print-config forge.config.js | eslint-config-prettier-check",
|
||||||
|
@ -40,7 +34,7 @@
|
||||||
"lint:fix": "eslint ./**/*.* --fix",
|
"lint:fix": "eslint ./**/*.* --fix",
|
||||||
"prettier": "prettier -c **/*.*",
|
"prettier": "prettier -c **/*.*",
|
||||||
"prettier:fix": "prettier --write **/*.*",
|
"prettier:fix": "prettier --write **/*.*",
|
||||||
"fix": "npm run lint:check && npm run lint:fix && npm run prettier:fix",
|
"fix": "npm run lint:fix && npm run prettier:fix",
|
||||||
"forge:make": "electron-forge --platform win32 --arch x64 make",
|
"forge:make": "electron-forge --platform win32 --arch x64 make",
|
||||||
"forge": "npm audit && npm run build && npm run forge:make",
|
"forge": "npm audit && npm run build && npm run forge:make",
|
||||||
"precommit": "npm run build && npm run prettier && npm run lint && npm run coverage:fast",
|
"precommit": "npm run build && npm run prettier && npm run lint && npm run coverage:fast",
|
||||||
|
@ -48,9 +42,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
|
"inversify": "^5.0.1",
|
||||||
"jsdom": "^15.2.1",
|
"jsdom": "^15.2.1",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
"sqlite3": "^4.1.1",
|
"sqlite3": "^4.1.1",
|
||||||
"typeorm": "^0.2.21",
|
"typeorm": "^0.2.21",
|
||||||
"uuid": "^3.3.3"
|
"uuid": "^3.3.3"
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import rewiremock from 'rewiremock';
|
|
||||||
rewiremock.disable();
|
|
||||||
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import rewiremock from 'rewiremock';
|
||||||
|
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { Application } from 'spectron';
|
import { Application } from 'spectron';
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
|
rewiremock.disable();
|
||||||
|
|
||||||
describe('Application @slow', function() {
|
describe('Application @slow', function() {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
|
|
59
src/main.ts
59
src/main.ts
|
@ -1,60 +1,22 @@
|
||||||
import { app, BrowserWindow } from 'electron';
|
import { app } from 'electron';
|
||||||
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
|
import { container } from './main/core/container';
|
||||||
import os from 'os';
|
import { isDev } from './main/core/dev';
|
||||||
import path from 'path';
|
import { IAppWindow } from './main/modules/app-window/i-app-window';
|
||||||
import packageJson from '../package.json';
|
import { ISession } from './main/modules/session/i-session';
|
||||||
import './main/controllers/api';
|
|
||||||
import { isDev } from './main/services/dev';
|
|
||||||
import * as session from './main/services/session';
|
|
||||||
|
|
||||||
export let mainWindow: Electron.BrowserWindow;
|
|
||||||
|
|
||||||
export const appPath = path.resolve(app.getPath('userData'), `${packageJson.version}${isDev() ? '-dev' : ''}`);
|
|
||||||
|
|
||||||
async function createWindow(): Promise<void> {
|
async function createWindow(): Promise<void> {
|
||||||
|
const session: ISession = container.get(Symbol.for('session'));
|
||||||
session.setHeaders();
|
session.setHeaders();
|
||||||
|
|
||||||
// universal options
|
const appWindowMain: IAppWindow = container.get(Symbol.for('app-window-main'));
|
||||||
let options: BrowserWindowConstructorOptions = {
|
|
||||||
width: 1600,
|
|
||||||
height: 900,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// platform specifics
|
|
||||||
switch (os.platform()) {
|
|
||||||
case 'win32':
|
|
||||||
options = {
|
|
||||||
...options,
|
|
||||||
...{
|
|
||||||
icon: 'resources/icon.ico',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the browser window.
|
|
||||||
mainWindow = new BrowserWindow(options);
|
|
||||||
|
|
||||||
// and load the index.html of the app.
|
// and load the index.html of the app.
|
||||||
await mainWindow.loadFile('frontend/index.html');
|
await appWindowMain.open();
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
mainWindow.webContents.openDevTools();
|
appWindowMain.window.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emitted when the window is closed.
|
|
||||||
mainWindow.on('closed', () => {
|
|
||||||
// Dereference the window object, usually you would store windows
|
|
||||||
// in an array if your app supports multi windows, this is the time
|
|
||||||
// when you should delete the corresponding element.
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
|
@ -74,7 +36,8 @@ app.on('window-all-closed', () => {
|
||||||
app.on('activate', async () => {
|
app.on('activate', async () => {
|
||||||
// On OS X it"s common to re-create a window in the app when the
|
// On OS X it"s common to re-create a window in the app when the
|
||||||
// dock icon is clicked and there are no other windows open.
|
// dock icon is clicked and there are no other windows open.
|
||||||
if (mainWindow === null) {
|
const appWindowMain: IAppWindow = container.get(Symbol.for('app-window-main'));
|
||||||
|
if (appWindowMain.isClosed()) {
|
||||||
await createWindow();
|
await createWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { app } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
import packageJson from '../../../package.json';
|
||||||
|
import { isDev } from './dev';
|
||||||
|
|
||||||
|
export const appPath = path.resolve(app.getPath('userData'), `${packageJson.version}${isDev() ? '-dev' : ''}`);
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'reflect-metadata';
|
||||||
|
import { Container } from 'inversify';
|
||||||
|
import { MainAppWindow } from '../modules/app-window/main-app-window';
|
||||||
|
import { NhentaiApi } from '../modules/nhentai/nhentai-api';
|
||||||
|
import { NhentaiIpcServer } from '../modules/nhentai/nhentai-ipc-server';
|
||||||
|
import { Session } from '../modules/session/session';
|
||||||
|
import { WebCrawler } from '../modules/web-crawler/web-crawler';
|
||||||
|
|
||||||
|
export const container = new Container({ defaultScope: 'Singleton' });
|
||||||
|
|
||||||
|
container.bind(Symbol.for('web-crawler')).to(WebCrawler);
|
||||||
|
|
||||||
|
container.bind(Symbol.for('nhentai-api')).to(NhentaiApi);
|
||||||
|
container.bind(Symbol.for('nhentai-ipc-server')).to(NhentaiIpcServer);
|
||||||
|
container.get(Symbol.for('nhentai-ipc-server'));
|
||||||
|
|
||||||
|
container.bind(Symbol.for('app-window-main')).to(MainAppWindow);
|
||||||
|
|
||||||
|
container.bind(Symbol.for('session')).to(Session);
|
|
@ -3,7 +3,7 @@ import '../../../mocks/electron';
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { Databases, getConnection } from '../../../src/main/services/database';
|
import { Databases, getConnection } from './database';
|
||||||
|
|
||||||
describe('Database Service', () => {
|
describe('Database Service', () => {
|
||||||
before(() => {
|
before(() => {
|
|
@ -1,7 +1,7 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Connection, createConnection as ormCreateConnection } from 'typeorm';
|
import { Connection, createConnection as ormCreateConnection } from 'typeorm';
|
||||||
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
|
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
|
||||||
import { appPath } from '../../main';
|
import { appPath } from './app-path';
|
||||||
|
|
||||||
export enum Databases {
|
export enum Databases {
|
||||||
LIBRARY = 'library',
|
LIBRARY = 'library',
|
|
@ -3,7 +3,7 @@ import '../../../mocks/electron';
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { isDev } from '../../../src/main/services/dev';
|
import { isDev } from './dev';
|
||||||
|
|
||||||
describe('Development Mode Service', () => {
|
describe('Development Mode Service', () => {
|
||||||
before(() => {
|
before(() => {
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
import os from 'os';
|
||||||
|
import { injectable } from 'inversify';
|
||||||
|
import { IAppWindow } from './i-app-window';
|
||||||
|
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
|
||||||
|
|
||||||
|
let defaultOptions = {
|
||||||
|
width: 1600,
|
||||||
|
height: 900,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (os.platform()) {
|
||||||
|
case 'win32':
|
||||||
|
defaultOptions = {
|
||||||
|
...defaultOptions,
|
||||||
|
...{
|
||||||
|
icon: 'resources/icon.ico',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export abstract class AppWindow implements IAppWindow {
|
||||||
|
protected _window: BrowserWindow | null;
|
||||||
|
|
||||||
|
protected constructor(options: BrowserWindowConstructorOptions = {}) {
|
||||||
|
this.initialize(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get window(): BrowserWindow {
|
||||||
|
return this._window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public open(): Promise<void> {
|
||||||
|
if (this.isClosed()) {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._window.loadFile('frontend/index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
public isClosed(): boolean {
|
||||||
|
return !this._window;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(options: BrowserWindowConstructorOptions = {}): void {
|
||||||
|
this._window = new BrowserWindow({ ...defaultOptions, ...options });
|
||||||
|
|
||||||
|
this._window.on('closed', () => {
|
||||||
|
this._window = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import BrowserWindow = Electron.BrowserWindow;
|
||||||
|
|
||||||
|
export interface IAppWindow {
|
||||||
|
window: BrowserWindow;
|
||||||
|
open(): Promise<void>;
|
||||||
|
isClosed(): boolean;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { injectable } from 'inversify';
|
||||||
|
import { AppWindow } from './app-window';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class MainAppWindow extends AppWindow {
|
||||||
|
public constructor() {
|
||||||
|
super({
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
import { injectable } from 'inversify';
|
||||||
import IpcMainEvent = Electron.IpcMainEvent;
|
import IpcMainEvent = Electron.IpcMainEvent;
|
||||||
import { mainWindow } from '../../main';
|
import BrowserWindow = Electron.BrowserWindow;
|
||||||
import { isLoggedIn, login } from '../services/nhentai-crawler';
|
|
||||||
|
|
||||||
export const ipcServer: IIpcServer = {
|
@injectable()
|
||||||
answer: (channel: IpcChannels, handler: (data?: any) => Promise<any>): void => {
|
export abstract class IpcServer {
|
||||||
|
protected answer(channel: IpcChannels, handler: (data?: any) => Promise<any>): void {
|
||||||
ipcMain.on(channel, (event: IpcMainEvent, payload: IIpcPayload) => {
|
ipcMain.on(channel, (event: IpcMainEvent, payload: IIpcPayload) => {
|
||||||
handler(payload.data)
|
handler(payload.data)
|
||||||
.then((result: any) => {
|
.then((result: any) => {
|
||||||
|
@ -24,12 +25,9 @@ export const ipcServer: IIpcServer = {
|
||||||
event.reply(channel, response);
|
event.reply(channel, response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
send: (channel: IpcChannels, data: any): void => {
|
|
||||||
mainWindow.webContents.send(channel, data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
ipcServer.answer(IpcChannels.LOGIN, (credentials: ICredentials) => login(credentials.name, credentials.password));
|
protected send(window: BrowserWindow, channel: IpcChannels, data: any): void {
|
||||||
|
window.webContents.send(channel, data);
|
||||||
ipcServer.answer(IpcChannels.LOGGED_IN, isLoggedIn);
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface INhentaiApi {
|
||||||
|
isLoggedIn(): Promise<boolean>;
|
||||||
|
login(name: string, password: string): Promise<void>;
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { inject, injectable } from 'inversify';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import { RequestInit, Response } from 'node-fetch';
|
||||||
|
import { Errors, RenaiError } from '../../core/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 {}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class NhentaiApi implements INhentaiApi {
|
||||||
|
private webCrawler: IWebCrawler;
|
||||||
|
|
||||||
|
public constructor(@inject(Symbol.for('web-crawler')) webCrawler: IWebCrawler) {
|
||||||
|
this.webCrawler = webCrawler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isLoggedIn(): Promise<boolean> {
|
||||||
|
return this.webCrawler
|
||||||
|
.fetch(`${url}${paths.favorites}`, { redirect: 'manual' })
|
||||||
|
.then((res: Response) => res.status === HttpCode.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public login(name: string, password: string): Promise<void> {
|
||||||
|
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 RenaiError(Errors.ELOGINFAIL)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNHentai(path: string): Promise<Document> {
|
||||||
|
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<Response> {
|
||||||
|
const postUrl = `${url}${path}`;
|
||||||
|
return this.webCrawler.fetch(postUrl, {
|
||||||
|
...requestInit,
|
||||||
|
...{
|
||||||
|
headers: {
|
||||||
|
...requestInit.headers,
|
||||||
|
...{
|
||||||
|
Host: domain,
|
||||||
|
Referer: postUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLoginMeta(): Promise<ILoginMeta> {
|
||||||
|
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) {
|
||||||
|
valueStore[name] = input.getAttribute('value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isLoginForm) {
|
||||||
|
return valueStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(new RenaiError(Errors.ENOLOGIN));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { inject, injectable } from 'inversify';
|
||||||
|
import { IpcServer } from '../ipc/ipc-server';
|
||||||
|
import { INhentaiApi } from './i-nhentai-api';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class NhentaiIpcServer extends IpcServer {
|
||||||
|
private nhentaiApi: INhentaiApi;
|
||||||
|
|
||||||
|
public constructor(@inject(Symbol.for('nhentai-api')) nhentaiApi: INhentaiApi) {
|
||||||
|
super();
|
||||||
|
this.nhentaiApi = nhentaiApi;
|
||||||
|
|
||||||
|
this.answer(IpcChannels.LOGIN, (credentials: ICredentials) =>
|
||||||
|
this.nhentaiApi.login(credentials.name, credentials.password)
|
||||||
|
);
|
||||||
|
this.answer(IpcChannels.LOGGED_IN, () => this.nhentaiApi.isLoggedIn());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ISession {
|
||||||
|
setHeaders(): void;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { session } from 'electron';
|
||||||
|
import { injectable } from 'inversify';
|
||||||
|
import { ISession } from './i-session';
|
||||||
|
import OnHeadersReceivedDetails = Electron.OnHeadersReceivedDetails;
|
||||||
|
|
||||||
|
@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: OnHeadersReceivedDetails, callback: (response: {}) => void) => {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
'Content-Security-Policy': ["default-src 'none'"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
const store = require('../../../src/main/services/store');
|
import { load, save } from './store';
|
||||||
import { load, save } from '../../../src/main/services/store';
|
const store = require('./store');
|
||||||
|
|
||||||
interface IStoreMock extends IMock {
|
interface IStoreMock extends IMock {
|
||||||
original: {
|
original: {
|
|
@ -1,13 +1,14 @@
|
||||||
import rewiremock from 'rewiremock';
|
import rewiremock from 'rewiremock';
|
||||||
import '../../../mocks/electron';
|
import '../../../../mocks/electron';
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { load, save, StoreKeys } from '../../../src/main/services/store';
|
import { appPath } from '../../core/app-path';
|
||||||
|
import { load, save, StoreKeys } from './store';
|
||||||
|
|
||||||
const storeDirectory = path.resolve('store');
|
const storeDirectory = path.resolve(appPath, 'store');
|
||||||
|
|
||||||
describe('Store Service', function() {
|
describe('Store Service', function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
|
@ -1,5 +1,6 @@
|
||||||
import fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { appPath } from '../../core/app-path';
|
||||||
|
|
||||||
export const enum StoreKeys {
|
export const enum StoreKeys {
|
||||||
'COOKIES' = 'cookies',
|
'COOKIES' = 'cookies',
|
||||||
|
@ -17,7 +18,7 @@ let store: Store = {};
|
||||||
let synced = false;
|
let synced = false;
|
||||||
|
|
||||||
const options: IStoreOptions = {
|
const options: IStoreOptions = {
|
||||||
path: path.resolve('store', 'store.json'),
|
path: path.resolve(appPath, 'store', 'store.json'),
|
||||||
};
|
};
|
||||||
const folder = path.dirname(options.path);
|
const folder = path.dirname(options.path);
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { CookieJar } from 'jsdom';
|
||||||
|
import { RequestInit, Response } from 'node-fetch';
|
||||||
|
|
||||||
|
export interface IWebCrawler {
|
||||||
|
cookieJar: CookieJar;
|
||||||
|
fetch(url: string, requestInit?: RequestInit): Promise<Response>;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import rewiremock from 'rewiremock';
|
import rewiremock from 'rewiremock';
|
||||||
import '../../../mocks/electron';
|
import '../../../../mocks/electron';
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { CookieJar } from 'jsdom';
|
import { CookieJar } from 'jsdom';
|
||||||
|
@ -7,8 +7,8 @@ import 'mocha';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
import { Response } from 'node-fetch';
|
import { Response } from 'node-fetch';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { fetch } from '../../../src/main/services/web-crawler';
|
import { WebCrawler } from './web-crawler';
|
||||||
import { storeMock } from './store.mock';
|
import { storeMock } from '../store/store.mock';
|
||||||
|
|
||||||
describe('Web Crawler', function() {
|
describe('Web Crawler', function() {
|
||||||
this.timeout(2000);
|
this.timeout(2000);
|
||||||
|
@ -52,7 +52,9 @@ describe('Web Crawler', function() {
|
||||||
)
|
)
|
||||||
.persist();
|
.persist();
|
||||||
|
|
||||||
const res: Response = await fetch(testUrl);
|
const webCrawler = new WebCrawler();
|
||||||
|
|
||||||
|
const res: Response = await webCrawler.fetch(testUrl);
|
||||||
expect(callback.callCount).to.equal(1, 'multiple requests (or none) are sent when only one should be');
|
expect(callback.callCount).to.equal(1, 'multiple requests (or none) are sent when only one should be');
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
expect(json).to.deep.equal([{ id: 12, comment: 'Hey there' }], 'response body is incorrect');
|
expect(json).to.deep.equal([{ id: 12, comment: 'Hey there' }], 'response body is incorrect');
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { injectable } from 'inversify';
|
||||||
|
import { CookieJar } from 'jsdom';
|
||||||
|
import nodeFetch, { RequestInit, Response } from 'node-fetch';
|
||||||
|
import { Errors, RenaiError } from '../../core/error';
|
||||||
|
import { load, save, StoreKeys } from '../store/store';
|
||||||
|
import { IWebCrawler } from './i-web-crawler';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class WebCrawler implements IWebCrawler {
|
||||||
|
public cookieJar: CookieJar;
|
||||||
|
|
||||||
|
private initialized: boolean;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.initialized = false;
|
||||||
|
this.cookieJar = new CookieJar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetch(url: string, requestInit: RequestInit = {}): Promise<Response> {
|
||||||
|
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: any) => {
|
||||||
|
throw new RenaiError(Errors.ECOOKIESAVEFAIL, reason);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(): Promise<void> {
|
||||||
|
if (!this.initialized) {
|
||||||
|
return load(StoreKeys.COOKIES).then((cookies: any) => {
|
||||||
|
if (cookies !== undefined) {
|
||||||
|
this.cookieJar = CookieJar.deserializeSync(cookies);
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCookies(header: string[], url: string): Promise<void> {
|
||||||
|
if (header) {
|
||||||
|
header.forEach((cookie: string) => {
|
||||||
|
this.cookieJar.setCookieSync(cookie, url);
|
||||||
|
});
|
||||||
|
return save(StoreKeys.COOKIES, this.cookieJar.serializeSync()).catch((reason: any) => {
|
||||||
|
throw new RenaiError(Errors.ECOOKIESAVEFAIL, reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
import { ipcServer } from '../controllers/api';
|
|
||||||
|
|
||||||
export function throwError(error: any, isFatal: boolean = false): void {
|
|
||||||
let errorInstance = error;
|
|
||||||
if (!(errorInstance instanceof Error)) {
|
|
||||||
errorInstance = new Error(error);
|
|
||||||
}
|
|
||||||
if (isFatal) {
|
|
||||||
throw errorInstance;
|
|
||||||
}
|
|
||||||
ipcServer.send(IpcChannels.ERROR, errorInstance);
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { JSDOM } from 'jsdom';
|
|
||||||
import { RequestInit, Response } from 'node-fetch';
|
|
||||||
import { Errors, RenaiError } from '../../types/error';
|
|
||||||
import { fetch } from './web-crawler';
|
|
||||||
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
function getNHentai(path: string): Promise<Document> {
|
|
||||||
return fetch(`${url}${path}`)
|
|
||||||
.then((res: Response) => res.text())
|
|
||||||
.then((text: string) => {
|
|
||||||
const { document } = new JSDOM(text).window;
|
|
||||||
return document;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function postNHentai(path: string, requestInit: RequestInit = {}): Promise<Response> {
|
|
||||||
const postUrl = `${url}${path}`;
|
|
||||||
return fetch(postUrl, {
|
|
||||||
...requestInit,
|
|
||||||
...{
|
|
||||||
headers: {
|
|
||||||
...requestInit.headers,
|
|
||||||
...{
|
|
||||||
Host: domain,
|
|
||||||
Referer: postUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
method: 'post',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLoginMeta(): Promise<ILoginMeta> {
|
|
||||||
return 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) {
|
|
||||||
valueStore[name] = input.getAttribute('value');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isLoginForm) {
|
|
||||||
return valueStore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.reject(new RenaiError(Errors.ENOLOGIN));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLoggedIn(): Promise<boolean> {
|
|
||||||
return fetch(`${url}${paths.favorites}`, { redirect: 'manual' }).then((res: Response) => res.status === HttpCode.OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function login(name: string, password: string): Promise<void> {
|
|
||||||
return getLoginMeta()
|
|
||||||
.then((meta: ILoginMeta) => {
|
|
||||||
const loginParams: ILoginParams = {
|
|
||||||
...meta,
|
|
||||||
...{
|
|
||||||
[usernameInput]: name,
|
|
||||||
[passwordInput]: password,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return 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 RenaiError(Errors.ELOGINFAIL)));
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { session } from 'electron';
|
|
||||||
import OnHeadersReceivedDetails = Electron.OnHeadersReceivedDetails;
|
|
||||||
|
|
||||||
export function setHeaders(): void {
|
|
||||||
// these headers only work on web requests, file:// protocol is handled via meta tags in the html
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived(
|
|
||||||
(details: OnHeadersReceivedDetails, callback: (response: {}) => void) => {
|
|
||||||
callback({
|
|
||||||
responseHeaders: {
|
|
||||||
...details.responseHeaders,
|
|
||||||
'Content-Security-Policy': ["default-src 'none'"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { CookieJar } from 'jsdom';
|
|
||||||
import nodeFetch, { RequestInit, Response } from 'node-fetch';
|
|
||||||
import { Errors, RenaiError } from '../../types/error';
|
|
||||||
import { throwError } from './error';
|
|
||||||
import { load, save, StoreKeys } from './store';
|
|
||||||
|
|
||||||
export let cookieJar: CookieJar = new CookieJar();
|
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
function init(): Promise<void> {
|
|
||||||
if (!initialized) {
|
|
||||||
return load(StoreKeys.COOKIES).then((cookies: any) => {
|
|
||||||
if (cookies !== undefined) {
|
|
||||||
cookieJar = CookieJar.deserializeSync(cookies);
|
|
||||||
}
|
|
||||||
initialized = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetch(url: string, requestInit: RequestInit = {}): Promise<Response> {
|
|
||||||
return init().then(() => {
|
|
||||||
const cookiedInit = {
|
|
||||||
...requestInit,
|
|
||||||
...{
|
|
||||||
headers: {
|
|
||||||
...requestInit.headers,
|
|
||||||
...{
|
|
||||||
Cookie: cookieJar.getCookieStringSync(url),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return nodeFetch(url, cookiedInit).then((res: Response) => {
|
|
||||||
setCookies(res.headers.raw()['set-cookie'], url).catch((reason: any) => {
|
|
||||||
throwError(new RenaiError(Errors.ECOOKIESAVEFAIL, reason));
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCookies(header: string[], url: string): Promise<void> {
|
|
||||||
if (header) {
|
|
||||||
header.forEach((cookie: string) => {
|
|
||||||
cookieJar.setCookieSync(cookie, url);
|
|
||||||
});
|
|
||||||
return save(StoreKeys.COOKIES, cookieJar.serializeSync()).catch((reason: any) => {
|
|
||||||
throwError(new RenaiError(Errors.ECOOKIESAVEFAIL, reason));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": ["../../.eslintrc.json"],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import fc from 'fast-check';
|
import fc from 'fast-check';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { c, s, t } from '../../../src/renderer/services/utils';
|
import { c, s, t } from './utils';
|
||||||
|
|
||||||
describe('Frontend Utils', function() {
|
describe('Frontend Utils', function() {
|
||||||
this.timeout(1000);
|
this.timeout(1000);
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { uuid } from '../../src/services/uuid';
|
import { uuid } from './uuid';
|
||||||
|
|
||||||
describe('UUID Service', function() {
|
describe('UUID Service', function() {
|
||||||
this.timeout(1000);
|
this.timeout(1000);
|
|
@ -1,6 +1,6 @@
|
||||||
const handlebars = require('handlebars');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const handlebars = require('handlebars');
|
||||||
const packageJson = require('../package');
|
const packageJson = require('../package');
|
||||||
|
|
||||||
function compile(isDevMode = false) {
|
function compile(isDevMode = false) {
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"extends": ["../.eslintrc.json"],
|
|
||||||
"rules": {
|
|
||||||
"no-unused-expressions": "off",
|
|
||||||
|
|
||||||
"@typescript-eslint/no-magic-numbers": "off",
|
|
||||||
"@typescript-eslint/typedef": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
import 'mocha';
|
|
||||||
import { moveDir, storeBackupDirectory, storeDirectory } from './before';
|
|
||||||
|
|
||||||
moveDir(storeBackupDirectory, storeDirectory);
|
|
|
@ -1,17 +0,0 @@
|
||||||
import fs from 'fs-extra';
|
|
||||||
import 'mocha';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export const storeDirectory = path.resolve('store');
|
|
||||||
export const storeBackupDirectory = path.resolve('store-backup');
|
|
||||||
|
|
||||||
export function moveDir(fromDir: string, toDir: string) {
|
|
||||||
if (fs.existsSync(fromDir)) {
|
|
||||||
if (fs.existsSync(toDir)) {
|
|
||||||
fs.removeSync(toDir);
|
|
||||||
}
|
|
||||||
fs.moveSync(fromDir, toDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
moveDir(storeDirectory, storeBackupDirectory);
|
|
|
@ -1,5 +1,8 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "es2019",
|
||||||
|
"lib": ["es2019", "dom"],
|
||||||
|
"types": ["reflect-metadata"],
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
@ -9,8 +12,7 @@
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"preserveConstEnums": false,
|
"preserveConstEnums": false,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true
|
||||||
"lib": ["es2018", "dom"]
|
|
||||||
},
|
},
|
||||||
"include": ["declarations/**/*.ts", "src/**/*.ts", "tests/**/*.ts", "mocks/**/*.ts"]
|
"include": ["declarations/**/*.ts", "types/**/*.ts", "src/**/*.ts", "mocks/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const enum HttpCode {
|
declare const enum HttpCode {
|
||||||
// 100
|
// 100
|
||||||
'CONTINUE' = 100,
|
'CONTINUE' = 100,
|
||||||
'SWITCHING_PROTOCOLS' = 101,
|
'SWITCHING_PROTOCOLS' = 101,
|
|
@ -1,4 +1,4 @@
|
||||||
const enum IpcChannels {
|
declare const enum IpcChannels {
|
||||||
ERROR = 'ERROR',
|
ERROR = 'ERROR',
|
||||||
LOGIN = 'LOGIN',
|
LOGIN = 'LOGIN',
|
||||||
LOGGED_IN = 'LOGGED_IN',
|
LOGGED_IN = 'LOGGED_IN',
|
Loading…
Reference in New Issue