implement nhentai login

This commit is contained in:
Xymorot 2019-06-30 01:18:21 +02:00
parent 31e0b6d448
commit 35b778bd0b
25 changed files with 361 additions and 217 deletions

View File

@ -7,4 +7,4 @@ printWidth: 80
overrides: overrides:
- files: '*.svelte' - files: '*.svelte'
options: options:
parser: vue parser: html

49
package-lock.json generated
View File

@ -6318,6 +6318,24 @@
"tough-cookie": "~2.4.3", "tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
},
"dependencies": {
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"dev": true,
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
}
}
} }
}, },
"request-promise-core": { "request-promise-core": {
@ -6338,6 +6356,18 @@
"request-promise-core": "1.1.2", "request-promise-core": "1.1.2",
"stealthy-require": "^1.1.1", "stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3" "tough-cookie": "^2.3.3"
},
"dependencies": {
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
}
} }
}, },
"require-directory": { "require-directory": {
@ -7424,21 +7454,14 @@
} }
}, },
"tough-cookie": { "tough-cookie": {
"version": "2.4.3", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
"dev": true, "dev": true,
"requires": { "requires": {
"psl": "^1.1.24", "ip-regex": "^2.1.0",
"punycode": "^1.4.1" "psl": "^1.1.28",
}, "punycode": "^2.1.1"
"dependencies": {
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
}
} }
}, },
"tr46": { "tr46": {

View File

@ -22,6 +22,9 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@types/jsdom": "^12.2.3",
"@types/node-fetch": "^2.3.7",
"@types/tough-cookie": "^2.3.5",
"@types/webpack": "^4.4.32", "@types/webpack": "^4.4.32",
"electron": "^5.0.5", "electron": "^5.0.5",
"electron-rebuild": "^1.8.5", "electron-rebuild": "^1.8.5",
@ -32,10 +35,13 @@
"gulp-cli": "^2.2.0", "gulp-cli": "^2.2.0",
"gulp-sourcemaps": "^2.6.5", "gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^5.0.1", "gulp-typescript": "^5.0.1",
"jsdom": "^15.1.1",
"node-fetch": "^2.6.0",
"prettier": "^1.18.2", "prettier": "^1.18.2",
"sqlite3": "^4.0.9", "sqlite3": "^4.0.9",
"svelte": "^3.5.1", "svelte": "^3.5.1",
"svelte-loader": "^2.13.4", "svelte-loader": "^2.13.4",
"tough-cookie": "^3.0.1",
"ts-loader": "^6.0.3", "ts-loader": "^6.0.3",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",

8
src/declarations/electron.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import WebContents = Electron.WebContents;
declare type IpcEvent = {
frameId: number;
preventDefault: () => void;
reply: (channel: string, ...args: any) => void;
sender: WebContents;
};

View File

@ -6,7 +6,7 @@ import session from './main/services/session';
let mainWindow: Electron.BrowserWindow; let mainWindow: Electron.BrowserWindow;
async function createWindow() { async function createWindow(): Promise<void> {
session.init(); session.init();
// Create the browser window. // Create the browser window.

View File

@ -1,17 +1,13 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import WebContents = Electron.WebContents; import nhentai from './../services/nhentai-crawler';
ipcMain.on( ipcMain.on(IpcChannels.Credentials, (event: IpcEvent, args: ICredentials) => {
IpcChannels.Credentials, nhentai
( .login(args.name, args.password)
event: { .then(() => {
frameId: number; console.log('success');
preventDefault: () => void; })
reply: (channel: string, ...args: any) => void; .catch(() => {
sender: WebContents; console.log('fail');
}, });
...args: any });
) => {
event.reply(IpcChannels.Pong, args);
}
);

View File

@ -8,7 +8,7 @@ import { Tag } from './tag';
@Entity() @Entity()
export class Book extends MultiNamed { export class Book extends MultiNamed {
@OneToMany(() => Copy, copy => copy.original, { @OneToMany(() => Copy, (copy: Copy) => copy.original, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',

View File

@ -11,7 +11,7 @@ const enum CopyTypes {
@Entity() @Entity()
export class CopyType extends Base { export class CopyType extends Base {
@ManyToOne(() => Copy, copy => copy.types, { @ManyToOne(() => Copy, (copy: Copy) => copy.types, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',

View File

@ -15,7 +15,7 @@ import { Translator } from './translator';
@Entity() @Entity()
export class Copy extends Base { export class Copy extends Base {
@ManyToOne(() => Book, book => book.copies, { @ManyToOne(() => Book, (book: Book) => book.copies, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
@ -25,7 +25,7 @@ export class Copy extends Base {
@Column({ nullable: false, default: false }) @Column({ nullable: false, default: false })
public favorited: boolean; public favorited: boolean;
@OneToMany(() => CopyType, copyType => copyType.copy, { @OneToMany(() => CopyType, (copyType: CopyType) => copyType.copy, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',

View File

@ -4,7 +4,7 @@ import { Source } from './source';
@Entity() @Entity()
export class Site extends MultiNamed { export class Site extends MultiNamed {
@OneToMany(() => Source, source => source.site, { @OneToMany(() => Source, (source: Source) => source.site, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',

View File

@ -11,7 +11,7 @@ export class Source extends Base {
}) })
public uri: string; public uri: string;
@ManyToOne(() => Site, site => site.sources, { @ManyToOne(() => Site, (site: Site) => site.sources, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',

View File

@ -3,7 +3,7 @@ import { Connection, createConnection } from 'typeorm';
let connection: Connection; let connection: Connection;
function init() { function init(): void {
initConnection(); initConnection();
} }
@ -11,10 +11,10 @@ function initConnection(): void {
// createConnection method will automatically read connection options // createConnection method will automatically read connection options
// from your ormconfig file or environment variables // from your ormconfig file or environment variables
createConnection('library') createConnection('library')
.then(c => { .then((c: Connection) => {
connection = c; connection = c;
}) })
.catch(reason => { .catch((reason: any) => {
throw reason; throw reason;
}); });
} }

View File

@ -1,3 +1,8 @@
import { JSDOM } from 'jsdom';
import { RequestInit, Response } from 'node-fetch';
import RenaiError, { Errors } from '../../types/error';
import fetch from './web-crawler';
const url = 'https://nhentai.net/'; const url = 'https://nhentai.net/';
const paths = { const paths = {
@ -6,39 +11,88 @@ const paths = {
favorites: 'favorites/', favorites: 'favorites/',
}; };
// @ts-ignore const usernameInput = 'username_or_email';
let loginPassword: string; const passwordInput = 'password';
// @ts-ignore
let loginName: string;
function fetchNHentai(path: string): Promise<Document> { interface ILoginMeta {
return fetch(`${url}${path}`, { [key: string]: string;
credentials: 'include', }
})
.then(res => { interface ILoginAuth {
console.log(res); [usernameInput]: string;
[passwordInput]: string;
}
interface ILoginParams extends ILoginMeta, ILoginAuth {}
function login(name: string, password: string): Promise<void> {
return getLoginMeta()
.then((meta: ILoginMeta) => {
const loginParams: ILoginParams = {
...meta,
...{
// tslint:disable-next-line: object-literal-sort-keys
username_or_email: name,
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',
},
});
})
.then(() => {})
.catch(() => Promise.reject(new RenaiError(Errors.ELOGINFAIL)));
}
function getNHentai(path: string): Promise<Document> {
return fetch(`${url}${path}`)
.then((res: Response) => {
return res.text(); return res.text();
}) })
.then(text => { .then((text: string) => {
const parser = new DOMParser(); const { document } = new JSDOM(text).window;
return parser.parseFromString(text, 'text/html'); return document;
}); });
} }
function fetchLogin(): void { function postNHentai(path: string, init: RequestInit = {}): Promise<Response> {
fetchNHentai(paths.login) return fetch(`${url}${path}`, { ...init, ...{ method: 'post' } });
.then(() => true)
.catch(e => {
console.error(e);
});
} }
function setLoginCredentials(name: string, password: string): void { function getLoginMeta(): Promise<ILoginMeta> {
loginName = name; return getNHentai(paths.login).then((document: Document) => {
loginPassword = password; // tslint:disable-next-line: prefer-for-of
for (let i = 0; i < document.forms.length; i++) {
const form: HTMLFormElement = document.forms[i];
const valueStore: ILoginMeta = {};
let isLoginForm = false;
// tslint:disable-next-line: 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 default { export default {
setLoginCredentials, login,
fetchLogin,
}; };

View File

@ -0,0 +1,27 @@
import nodeFetch, { RequestInit, Response } from 'node-fetch';
import { Cookie, CookieJar } from 'tough-cookie';
const cookieJar: CookieJar = new CookieJar();
function fetch(url: string, init: RequestInit = {}): Promise<Response> {
const headers: HeadersInit = {};
cookieJar.getCookiesSync(url).forEach((cookie: Cookie) => {
headers[cookie.key] = cookie.value;
});
const cookiedInit = {
...init,
...{ headers: { ...init.headers, ...headers } },
};
return nodeFetch(url, cookiedInit).then((res: Response) => {
setCookies(res.headers.raw()['set-cookie'], url);
return res;
});
}
function setCookies(header: string[], url: string): void {
header.forEach((cookie: string) => {
cookieJar.setCookieSync(cookie, url);
});
}
export default fetch;

View File

@ -4,7 +4,7 @@
import App from './renderer/App.svelte'; import App from './renderer/App.svelte';
(() => ((): void =>
new App({ new App({
target: document.querySelector('#app'), target: document.querySelector('#app'),
props: { props: {

View File

@ -1,62 +1,74 @@
<script> <script>
import Button from 'atoms/Button.svelte'; import Bttn from 'atoms/Bttn.svelte';
import Divide from 'molecules/Divide.svelte'; import Divide from 'molecules/Divide.svelte';
import api from 'services/api'; import api from 'services/api';
let text = 'tach'; let form = {
name: '',
password: '',
};
function handleClick() { function handleClick() {
api.sendCredentials({ name: '1', password: '2' }); api.sendCredentials(form);
} }
</script> </script>
<style> <style>
:root { :root {
--color-white: #fff; --color-white: #fff;
--color-black: #000; --color-black: #000;
--color-background: #242424; --color-background: #242424;
--color-foreground: #3a3a3a; --color-foreground: #3a3a3a;
--color-foreground-light: #696969; --color-foreground-light: #696969;
--color-accent: #454585; --color-accent: #454585;
--color-accent-light: #6969ac; --color-accent-light: #6969ac;
--color-text: var(--color-white); --color-text: var(--color-white);
font-family: sans-serif; font-family: sans-serif;
} }
:global(*) { :global(*) {
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
} }
:global(html) { :global(html) {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
:global(body) { :global(body) {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
color: var(--color-text); color: var(--color-text);
background-color: var(--color-background); background-color: var(--color-background);
} }
main { main {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
</style> </style>
<main> <main>
<Divide> <Divide>
<div slot="1"> <div slot="1">
<h1>Hello World</h1> <h1>Login</h1>
<p>{ text }</p> <form>
<Button on:click="{handleClick}">test-inhalt</Button> <label>
<span>Username/Email</span>
<input bind:value="{form.name}" />
</label>
<label>
<span>Password</span>
<input bind:value="{form.password}" type="password" />
</label>
<Bttn on:click="{handleClick}">submit</Bttn>
</form>
</div> </div>
<div slot="2"> <div slot="2">
<Divide mode="v"> <Divide mode="v">

View File

@ -0,0 +1,15 @@
<style>
.button {
border: none;
color: var(--color-text);
background: var(--color-accent);
}
.button:focus {
outline-color: var(--color-accent-light);
}
</style>
<button class="button" on:click|preventDefault>
<slot></slot>
</button>

View File

@ -1,15 +0,0 @@
<button class="button" on:click>
<slot></slot>
</button>
<style>
.button {
border: none;
color: var(--color-text);
background: var(--color-accent);
}
.button:focus {
outline-color: var(--color-accent-light);
}
</style>

View File

@ -1,117 +1,117 @@
<script> <script>
import { onMount } from 'svelte/internal'; import { onMount } from 'svelte/internal';
import { c, s } from 'services/utils'; import { c, s } from 'services/utils';
export let mode = 'h'; export let mode = 'h';
export let basisFirst = 0.5; export let basisFirst = 0.5;
export let basisSecond = 0.5; export let basisSecond = 0.5;
export let minSize = 100; export let minSize = 100;
let divide; let divide;
let dragging = false; let dragging = false;
let size = 5; let size = 5;
let total = 0; let total = 0;
$: classes = c({ $: classes = c({
divide: true, divide: true,
'divide--vertical': mode === 'v', 'divide--vertical': mode === 'v',
}); });
$: classesDivider = c({ $: classesDivider = c({
divide__divider: true, divide__divider: true,
'divide__divider--vertical': mode === 'v', 'divide__divider--vertical': mode === 'v',
}); });
$: style = s({ $: style = s({
'--divide-size': `${size}px`, '--divide-size': `${size}px`,
'--divide-basis-first': `${basisFirst * 100}%`, '--divide-basis-first': `${basisFirst * 100}%`,
'--divide-basis-second': `${basisSecond * 100}%`, '--divide-basis-second': `${basisSecond * 100}%`,
'--divide-min-width': minSize > 0 && mode === 'h' ? `${minSize}px` : '0', '--divide-min-width': minSize > 0 && mode === 'h' ? `${minSize}px` : '0',
'--divide-min-height': minSize > 0 && mode === 'v' ? `${minSize}px` : '0', '--divide-min-height': minSize > 0 && mode === 'v' ? `${minSize}px` : '0',
}); });
function getTotal() { function getTotal() {
return mode === 'h' ? divide.clientWidth : divide.clientHeight; return mode === 'h' ? divide.clientWidth : divide.clientHeight;
}
function handleMousedown(event) {
if (event.button === 0) {
dragging = true;
total = getTotal();
} }
}
function handleMousemove(event) { function handleMousedown(event) {
if (dragging) { if (event.button === 0) {
const dragPos = dragging = true;
mode === 'h' total = getTotal();
? event.x - divide.getBoundingClientRect().x }
: event.y - divide.getBoundingClientRect().y;
basisFirst = dragPos / total;
basisSecond = 1 - basisFirst;
} }
}
function handleMouseup() { function handleMousemove(event) {
dragging = false; if (dragging) {
} const dragPos =
mode === 'h'
? event.x - divide.getBoundingClientRect().x
: event.y - divide.getBoundingClientRect().y;
basisFirst = dragPos / total;
basisSecond = 1 - basisFirst;
}
}
function handleMouseenter(event) { function handleMouseup() {
if (event.buttons !== 1) {
dragging = false; dragging = false;
} }
}
function handleMouseenter(event) {
if (event.buttons !== 1) {
dragging = false;
}
}
</script> </script>
<style> <style>
.divide { .divide {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.divide__divider { .divide__divider {
cursor: ew-resize; cursor: ew-resize;
width: var(--divide-size); width: var(--divide-size);
height: 100%; height: 100%;
background-color: var(--color-foreground); background-color: var(--color-foreground);
} }
.divide__divider:hover { .divide__divider:hover {
background-color: var(--color-foreground-light); background-color: var(--color-foreground-light);
} }
.divide__elem { .divide__elem {
flex: 0 0 50%; flex: 0 0 50%;
min-width: var(--divide-min-width); min-width: var(--divide-min-width);
max-width: calc(100% - var(--divide-size) - var(--divide-min-width)); max-width: calc(100% - var(--divide-size) - var(--divide-min-width));
min-height: var(--divide-min-height); min-height: var(--divide-min-height);
max-height: calc(100% - var(--divide-size) - var(--divide-min-height)); max-height: calc(100% - var(--divide-size) - var(--divide-min-height));
} }
.divide__elem > :global(*) { .divide__elem > :global(*) {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.divide__elem--first { .divide__elem--first {
flex-basis: calc(var(--divide-basis-first) - var(--divide-size) / 2); flex-basis: calc(var(--divide-basis-first) - var(--divide-size) / 2);
} }
.divide__elem--second { .divide__elem--second {
flex-basis: calc(var(--divide-basis-second) - var(--divide-size) / 2); flex-basis: calc(var(--divide-basis-second) - var(--divide-size) / 2);
} }
.divide--vertical { .divide--vertical {
flex-direction: column; flex-direction: column;
} }
.divide__divider--vertical { .divide__divider--vertical {
cursor: ns-resize; cursor: ns-resize;
width: 100%; width: 100%;
height: var(--divide-size); height: var(--divide-size);
} }
</style> </style>
<div <div

View File

@ -1,13 +1,9 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
function sendCredentials(credentials: ICredentials) { function sendCredentials(credentials: ICredentials): void {
ipcRenderer.send(IpcChannels.Credentials, credentials); ipcRenderer.send(IpcChannels.Credentials, credentials);
} }
ipcRenderer.on(IpcChannels.Pong, (event, ...args: any) => {
console.log(args);
});
export default { export default {
sendCredentials, sendCredentials,
}; };

17
src/types/error.ts Normal file
View File

@ -0,0 +1,17 @@
export const enum Errors {
ERROR = 'ERROR',
ENOLOGIN = 'ENOLOGIN',
ELOGINFAIL = 'ELOGINFAIL',
}
const messages = {
[Errors.ERROR]: 'error',
[Errors.ENOLOGIN]: 'no login form found',
[Errors.ELOGINFAIL]: 'login failed',
};
export default class RenaiError extends Error {
constructor(eno: Errors = Errors.ERROR, msg: string = '') {
super(`Error ${eno}: ${messages[eno]}.${msg ? ` ${msg}` : ''}`);
}
}

View File

@ -1,6 +1,5 @@
const enum IpcChannels { const enum IpcChannels {
Credentials = 'CREDENTIALS', Credentials = 'CREDENTIALS',
Pong = 'PONG',
} }
interface ICredentials { interface ICredentials {

View File

@ -20,11 +20,12 @@
"no-floating-promises": true, "no-floating-promises": true,
"no-unused-expression": true, "no-unused-expression": true,
"await-promise": true, "await-promise": true,
"no-inferrable-types": true, "no-inferrable-types": [true, "ignore-params", "ignore-properties"],
"prefer-for-of": true, "prefer-for-of": true,
"no-empty": [true, "allow-empty-functions"], "no-empty": [true, "allow-empty-functions"],
"no-magic-numbers": true, "no-magic-numbers": true,
"no-parameter-reassignment": true "no-parameter-reassignment": true,
"arrow-return-shorthand": true
}, },
"jsRules": true "jsRules": true
} }

View File

@ -1,6 +1,7 @@
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
mode: 'production',
entry: { entry: {
bundle: path.resolve(__dirname, 'src/renderer.ts'), bundle: path.resolve(__dirname, 'src/renderer.ts'),
}, },
@ -33,6 +34,10 @@ module.exports = {
alias: { alias: {
atoms: path.resolve(__dirname, 'src/renderer/components/1-atoms'), atoms: path.resolve(__dirname, 'src/renderer/components/1-atoms'),
molecules: path.resolve(__dirname, 'src/renderer/components/2-molecules'), molecules: path.resolve(__dirname, 'src/renderer/components/2-molecules'),
polymers: path.resolve(__dirname, 'src/renderer/components/3-polymers'),
cells: path.resolve(__dirname, 'src/renderer/components/4-cells'),
organisms: path.resolve(__dirname, 'src/renderer/components/5-organisms'),
templates: path.resolve(__dirname, 'src/renderer/components/6-templates'),
services: path.resolve(__dirname, 'src/renderer/services'), services: path.resolve(__dirname, 'src/renderer/services'),
}, },
}, },