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
|
||||
.nyc_output
|
||||
/src/**/*.js
|
||||
/tests/**/*.js
|
||||
/mocks/**/*.js
|
||||
/frontend
|
||||
/store-backup
|
||||
/test-paths
|
||||
/store
|
||||
/out
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
{
|
||||
"root": true,
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"extends": ["eslint:recommended", "prettier"],
|
||||
"plugins": ["import"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2019
|
||||
},
|
||||
"settings": {
|
||||
"import/core-modules": ["electron"]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
|
@ -26,7 +29,7 @@
|
|||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"tests/**/*",
|
||||
"src/**/*.spec.*",
|
||||
"mocks/**/*",
|
||||
"src/renderer/**/*",
|
||||
"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": [
|
||||
{
|
||||
|
@ -51,7 +63,10 @@
|
|||
"project": "./tsconfig.json"
|
||||
},
|
||||
"settings": {
|
||||
"import/core-modules": ["electron"]
|
||||
"import/extensions": [".ts", "d.ts", ".js", ".json"],
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", "d.ts"]
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "error",
|
||||
|
@ -93,7 +108,21 @@
|
|||
"@typescript-eslint/unbound-method": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "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": {
|
||||
"import/no-default-export": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/renderer/**/*.*"],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,18 +7,12 @@ node_modules
|
|||
# generated code
|
||||
/src/**/*.js
|
||||
/src/**/*.js.map
|
||||
/tests/**/*.js
|
||||
/tests/**/*.js.map
|
||||
/mocks/**/*.js
|
||||
/mocks/**/*.js.map
|
||||
/frontend
|
||||
|
||||
# created by testing
|
||||
/store-backup
|
||||
/test-paths
|
||||
|
||||
# managed by application
|
||||
/store
|
||||
|
||||
# built app
|
||||
/out
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
# 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:
|
||||
- 'src/**'
|
||||
exclude:
|
||||
- 'src/**/*.spec.*'
|
||||
- 'src/main/entities/**'
|
||||
watermarks:
|
||||
statements: [80, 95]
|
||||
|
|
|
@ -15,10 +15,7 @@
|
|||
node_modules
|
||||
.nyc_output
|
||||
/src/**/*.js
|
||||
/tests/**/*.js
|
||||
/mocks/**/*.js
|
||||
/frontend
|
||||
/store-backup
|
||||
/test-paths
|
||||
/store
|
||||
/out
|
||||
|
|
|
@ -113,7 +113,7 @@ The application uses [SQLite3](https://www.npmjs.com/package/sqlite3) as a datab
|
|||
|
||||
#### 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:
|
||||
`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)
|
||||
- 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
|
||||
|
||||
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)
|
||||
- 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
|
||||
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
|
||||
- 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 path = require('path');
|
||||
const minimist = require('minimist');
|
||||
const webpackConfig = require('./webpack.config');
|
||||
const templating = require('./templates');
|
||||
const { watch } = require('chokidar');
|
||||
const { debounce } = require('lodash');
|
||||
const minimist = require('minimist');
|
||||
const templating = require('./templates');
|
||||
const webpackConfig = require('./webpack.config');
|
||||
|
||||
/** @type {Object} */
|
||||
const argv = minimist(process.argv);
|
||||
|
|
|
@ -7,11 +7,9 @@ const ignoreList = [
|
|||
/^\/\.nyc_output($|\/)/,
|
||||
/^\/declarations($|\/)/,
|
||||
/^\/mocks($|\/)/,
|
||||
/^\/store($|\/)/,
|
||||
/^\/store-backup($|\/)/,
|
||||
/^\/templates($|\/)/,
|
||||
/^\/test-paths($|\/)/,
|
||||
/^\/tests($|\/)/,
|
||||
/^\/types($|\/)/,
|
||||
/^\/workspace($|\/)/,
|
||||
|
||||
/^\/\.editorconfig/,
|
||||
|
@ -29,6 +27,9 @@ const ignoreList = [
|
|||
/^\/webpack\.config\.js/,
|
||||
|
||||
/^\/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\/.*\.eslintrc\.json/,
|
||||
];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import WebContents = Electron.WebContents;
|
||||
import path from 'path';
|
||||
import { rewiremock } from './rewiremock';
|
||||
import WebContents = Electron.WebContents;
|
||||
|
||||
const electronMock: DeepPartial<typeof Electron> = {
|
||||
app: {
|
||||
|
|
|
@ -5446,6 +5446,11 @@
|
|||
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
"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\"",
|
||||
"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",
|
||||
"posttest:fast": "npm run test:after",
|
||||
"pretest": "npm run test:before",
|
||||
"test": "mocha",
|
||||
"posttest": "npm run test:after",
|
||||
"coverage:fast": "nyc npm run test:fast",
|
||||
"coverage": "nyc npm run test",
|
||||
"prelint": "eslint --print-config forge.config.js | eslint-config-prettier-check",
|
||||
|
@ -40,7 +34,7 @@
|
|||
"lint:fix": "eslint ./**/*.* --fix",
|
||||
"prettier": "prettier -c **/*.*",
|
||||
"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": "npm audit && npm run build && npm run forge:make",
|
||||
"precommit": "npm run build && npm run prettier && npm run lint && npm run coverage:fast",
|
||||
|
@ -48,9 +42,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^8.1.0",
|
||||
"inversify": "^5.0.1",
|
||||
"jsdom": "^15.2.1",
|
||||
"minimist": "^1.2.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sqlite3": "^4.1.1",
|
||||
"typeorm": "^0.2.21",
|
||||
"uuid": "^3.3.3"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import rewiremock from 'rewiremock';
|
||||
rewiremock.disable();
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as electron from 'electron';
|
||||
import { expect } from 'chai';
|
||||
import rewiremock from 'rewiremock';
|
||||
|
||||
import 'mocha';
|
||||
import { Application } from 'spectron';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
rewiremock.disable();
|
||||
|
||||
describe('Application @slow', function() {
|
||||
this.timeout(20000);
|
||||
|
59
src/main.ts
59
src/main.ts
|
@ -1,60 +1,22 @@
|
|||
import { app, BrowserWindow } from 'electron';
|
||||
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import packageJson from '../package.json';
|
||||
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' : ''}`);
|
||||
import { app } from 'electron';
|
||||
import { container } from './main/core/container';
|
||||
import { isDev } from './main/core/dev';
|
||||
import { IAppWindow } from './main/modules/app-window/i-app-window';
|
||||
import { ISession } from './main/modules/session/i-session';
|
||||
|
||||
async function createWindow(): Promise<void> {
|
||||
const session: ISession = container.get(Symbol.for('session'));
|
||||
session.setHeaders();
|
||||
|
||||
// universal options
|
||||
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);
|
||||
const appWindowMain: IAppWindow = container.get(Symbol.for('app-window-main'));
|
||||
|
||||
// and load the index.html of the app.
|
||||
await mainWindow.loadFile('frontend/index.html');
|
||||
await appWindowMain.open();
|
||||
|
||||
// Open the DevTools.
|
||||
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
|
||||
|
@ -74,7 +36,8 @@ app.on('window-all-closed', () => {
|
|||
app.on('activate', async () => {
|
||||
// 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.
|
||||
if (mainWindow === null) {
|
||||
const appWindowMain: IAppWindow = container.get(Symbol.for('app-window-main'));
|
||||
if (appWindowMain.isClosed()) {
|
||||
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 'mocha';
|
||||
import { Databases, getConnection } from '../../../src/main/services/database';
|
||||
import { Databases, getConnection } from './database';
|
||||
|
||||
describe('Database Service', () => {
|
||||
before(() => {
|
|
@ -1,7 +1,7 @@
|
|||
import path from 'path';
|
||||
import { Connection, createConnection as ormCreateConnection } from 'typeorm';
|
||||
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
|
||||
import { appPath } from '../../main';
|
||||
import { appPath } from './app-path';
|
||||
|
||||
export enum Databases {
|
||||
LIBRARY = 'library',
|
|
@ -3,7 +3,7 @@ import '../../../mocks/electron';
|
|||
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
import { isDev } from '../../../src/main/services/dev';
|
||||
import { isDev } from './dev';
|
||||
|
||||
describe('Development Mode Service', () => {
|
||||
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 { injectable } from 'inversify';
|
||||
import IpcMainEvent = Electron.IpcMainEvent;
|
||||
import { mainWindow } from '../../main';
|
||||
import { isLoggedIn, login } from '../services/nhentai-crawler';
|
||||
import BrowserWindow = Electron.BrowserWindow;
|
||||
|
||||
export const ipcServer: IIpcServer = {
|
||||
answer: (channel: IpcChannels, handler: (data?: any) => Promise<any>): void => {
|
||||
@injectable()
|
||||
export abstract class IpcServer {
|
||||
protected answer(channel: IpcChannels, handler: (data?: any) => Promise<any>): void {
|
||||
ipcMain.on(channel, (event: IpcMainEvent, payload: IIpcPayload) => {
|
||||
handler(payload.data)
|
||||
.then((result: any) => {
|
||||
|
@ -24,12 +25,9 @@ export const ipcServer: IIpcServer = {
|
|||
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));
|
||||
|
||||
ipcServer.answer(IpcChannels.LOGGED_IN, isLoggedIn);
|
||||
protected send(window: BrowserWindow, channel: IpcChannels, data: any): void {
|
||||
window.webContents.send(channel, data);
|
||||
}
|
||||
}
|
|
@ -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 '../../../src/main/services/store';
|
||||
import { load, save } from './store';
|
||||
const store = require('./store');
|
||||
|
||||
interface IStoreMock extends IMock {
|
||||
original: {
|
|
@ -1,13 +1,14 @@
|
|||
import rewiremock from 'rewiremock';
|
||||
import '../../../mocks/electron';
|
||||
import '../../../../mocks/electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import fs from 'fs-extra';
|
||||
import 'mocha';
|
||||
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() {
|
||||
this.timeout(10000);
|
|
@ -1,5 +1,6 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { appPath } from '../../core/app-path';
|
||||
|
||||
export const enum StoreKeys {
|
||||
'COOKIES' = 'cookies',
|
||||
|
@ -17,7 +18,7 @@ let store: Store = {};
|
|||
let synced = false;
|
||||
|
||||
const options: IStoreOptions = {
|
||||
path: path.resolve('store', 'store.json'),
|
||||
path: path.resolve(appPath, 'store', 'store.json'),
|
||||
};
|
||||
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 '../../../mocks/electron';
|
||||
import '../../../../mocks/electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { CookieJar } from 'jsdom';
|
||||
|
@ -7,8 +7,8 @@ import 'mocha';
|
|||
import nock from 'nock';
|
||||
import { Response } from 'node-fetch';
|
||||
import sinon from 'sinon';
|
||||
import { fetch } from '../../../src/main/services/web-crawler';
|
||||
import { storeMock } from './store.mock';
|
||||
import { WebCrawler } from './web-crawler';
|
||||
import { storeMock } from '../store/store.mock';
|
||||
|
||||
describe('Web Crawler', function() {
|
||||
this.timeout(2000);
|
||||
|
@ -52,7 +52,9 @@ describe('Web Crawler', function() {
|
|||
)
|
||||
.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');
|
||||
const json = await res.json();
|
||||
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 fc from 'fast-check';
|
||||
import 'mocha';
|
||||
import { c, s, t } from '../../../src/renderer/services/utils';
|
||||
import { c, s, t } from './utils';
|
||||
|
||||
describe('Frontend Utils', function() {
|
||||
this.timeout(1000);
|
|
@ -1,6 +1,6 @@
|
|||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
import { uuid } from '../../src/services/uuid';
|
||||
import { uuid } from './uuid';
|
||||
|
||||
describe('UUID Service', function() {
|
||||
this.timeout(1000);
|
|
@ -1,6 +1,6 @@
|
|||
const handlebars = require('handlebars');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const handlebars = require('handlebars');
|
||||
const packageJson = require('../package');
|
||||
|
||||
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": {
|
||||
"target": "es2019",
|
||||
"lib": ["es2019", "dom"],
|
||||
"types": ["reflect-metadata"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
|
@ -9,8 +12,7 @@
|
|||
"sourceMap": true,
|
||||
"preserveConstEnums": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"lib": ["es2018", "dom"]
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"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
|
||||
'CONTINUE' = 100,
|
||||
'SWITCHING_PROTOCOLS' = 101,
|
|
@ -1,4 +1,4 @@
|
|||
const enum IpcChannels {
|
||||
declare const enum IpcChannels {
|
||||
ERROR = 'ERROR',
|
||||
LOGIN = 'LOGIN',
|
||||
LOGGED_IN = 'LOGGED_IN',
|
Loading…
Reference in New Issue