refactor: re-do source structure with InversifyJS (dependency injection) and adjust meta processes

This commit is contained in:
Xymorot 2020-02-08 23:26:57 +01:00
parent 19c11312c5
commit 00ebd0e5c8
53 changed files with 463 additions and 353 deletions

View File

@ -9,10 +9,7 @@
node_modules
.nyc_output
/src/**/*.js
/tests/**/*.js
/mocks/**/*.js
/frontend
/store-backup
/test-paths
/store
/out

View File

@ -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"
}
}
]
}

6
.gitignore vendored
View File

@ -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

View File

@ -1,2 +1,2 @@
# https://github.com/mochajs/mocha/blob/master/example/config
spec: 'tests/**/*spec.js'
spec: 'src/**/*.spec.js'

View File

@ -11,6 +11,7 @@ report-dir: './.nyc_output/coverage'
include:
- 'src/**'
exclude:
- 'src/**/*.spec.*'
- 'src/main/entities/**'
watermarks:
statements: [80, 95]

View File

@ -15,10 +15,7 @@
node_modules
.nyc_output
/src/**/*.js
/tests/**/*.js
/mocks/**/*.js
/frontend
/store-backup
/test-paths
/store
/out

View File

@ -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

View File

@ -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);

View File

@ -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/,
];

View File

@ -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: {

5
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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);

View File

@ -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();
}
});

View File

@ -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' : ''}`);

View File

@ -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);

View File

@ -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(() => {

View File

@ -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',

View File

@ -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(() => {

View File

@ -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;
});
}
}

View File

@ -0,0 +1,7 @@
import BrowserWindow = Electron.BrowserWindow;
export interface IAppWindow {
window: BrowserWindow;
open(): Promise<void>;
isClosed(): boolean;
}

View File

@ -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,
},
});
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
export interface INhentaiApi {
isLoggedIn(): Promise<boolean>;
login(name: string, password: string): Promise<void>;
}

View File

@ -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));
});
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,3 @@
export interface ISession {
setHeaders(): void;
}

View File

@ -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'"],
},
});
}
);
}
}

View File

@ -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: {

View File

@ -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);

View File

@ -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);

View File

@ -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>;
}

View File

@ -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');

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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)));
}

View File

@ -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'"],
},
});
}
);
}

View File

@ -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();
}

View File

@ -0,0 +1,6 @@
{
"extends": ["../../.eslintrc.json"],
"parserOptions": {
"sourceType": "module"
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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"
}
}

View File

@ -1,4 +0,0 @@
import 'mocha';
import { moveDir, storeBackupDirectory, storeDirectory } from './before';
moveDir(storeBackupDirectory, storeDirectory);

View File

@ -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);

View File

@ -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"]
}

View File

@ -1,4 +1,4 @@
const enum HttpCode {
declare const enum HttpCode {
// 100
'CONTINUE' = 100,
'SWITCHING_PROTOCOLS' = 101,

View File

@ -1,4 +1,4 @@
const enum IpcChannels {
declare const enum IpcChannels {
ERROR = 'ERROR',
LOGIN = 'LOGIN',
LOGGED_IN = 'LOGGED_IN',