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:
- files: '*.svelte'
options:
parser: vue
parser: html

49
package-lock.json generated
View File

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

View File

@ -22,6 +22,9 @@
},
"dependencies": {},
"devDependencies": {
"@types/jsdom": "^12.2.3",
"@types/node-fetch": "^2.3.7",
"@types/tough-cookie": "^2.3.5",
"@types/webpack": "^4.4.32",
"electron": "^5.0.5",
"electron-rebuild": "^1.8.5",
@ -32,10 +35,13 @@
"gulp-cli": "^2.2.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^5.0.1",
"jsdom": "^15.1.1",
"node-fetch": "^2.6.0",
"prettier": "^1.18.2",
"sqlite3": "^4.0.9",
"svelte": "^3.5.1",
"svelte-loader": "^2.13.4",
"tough-cookie": "^3.0.1",
"ts-loader": "^6.0.3",
"tslint": "^5.17.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;
async function createWindow() {
async function createWindow(): Promise<void> {
session.init();
// Create the browser window.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { Connection, createConnection } from 'typeorm';
let connection: Connection;
function init() {
function init(): void {
initConnection();
}
@ -11,10 +11,10 @@ function initConnection(): void {
// createConnection method will automatically read connection options
// from your ormconfig file or environment variables
createConnection('library')
.then(c => {
.then((c: Connection) => {
connection = c;
})
.catch(reason => {
.catch((reason: any) => {
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 paths = {
@ -6,39 +11,88 @@ const paths = {
favorites: 'favorites/',
};
// @ts-ignore
let loginPassword: string;
// @ts-ignore
let loginName: string;
const usernameInput = 'username_or_email';
const passwordInput = 'password';
function fetchNHentai(path: string): Promise<Document> {
return fetch(`${url}${path}`, {
credentials: 'include',
})
.then(res => {
console.log(res);
interface ILoginMeta {
[key: string]: string;
}
interface ILoginAuth {
[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();
})
.then(text => {
const parser = new DOMParser();
return parser.parseFromString(text, 'text/html');
.then((text: string) => {
const { document } = new JSDOM(text).window;
return document;
});
}
function fetchLogin(): void {
fetchNHentai(paths.login)
.then(() => true)
.catch(e => {
console.error(e);
});
function postNHentai(path: string, init: RequestInit = {}): Promise<Response> {
return fetch(`${url}${path}`, { ...init, ...{ method: 'post' } });
}
function setLoginCredentials(name: string, password: string): void {
loginName = name;
loginPassword = password;
function getLoginMeta(): Promise<ILoginMeta> {
return getNHentai(paths.login).then((document: Document) => {
// 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 {
setLoginCredentials,
fetchLogin,
login,
};

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';
(() =>
((): void =>
new App({
target: document.querySelector('#app'),
props: {

View File

@ -1,62 +1,74 @@
<script>
import Button from 'atoms/Button.svelte';
import Divide from 'molecules/Divide.svelte';
import api from 'services/api';
import Bttn from 'atoms/Bttn.svelte';
import Divide from 'molecules/Divide.svelte';
import api from 'services/api';
let text = 'tach';
let form = {
name: '',
password: '',
};
function handleClick() {
api.sendCredentials({ name: '1', password: '2' });
}
function handleClick() {
api.sendCredentials(form);
}
</script>
<style>
:root {
--color-white: #fff;
--color-black: #000;
:root {
--color-white: #fff;
--color-black: #000;
--color-background: #242424;
--color-foreground: #3a3a3a;
--color-foreground-light: #696969;
--color-accent: #454585;
--color-accent-light: #6969ac;
--color-background: #242424;
--color-foreground: #3a3a3a;
--color-foreground-light: #696969;
--color-accent: #454585;
--color-accent-light: #6969ac;
--color-text: var(--color-white);
--color-text: var(--color-white);
font-family: sans-serif;
}
font-family: sans-serif;
}
:global(*) {
box-sizing: border-box;
overflow: hidden;
}
:global(*) {
box-sizing: border-box;
overflow: hidden;
}
:global(html) {
width: 100vw;
height: 100vh;
}
:global(html) {
width: 100vw;
height: 100vh;
}
:global(body) {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
color: var(--color-text);
background-color: var(--color-background);
}
:global(body) {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
color: var(--color-text);
background-color: var(--color-background);
}
main {
width: 100%;
height: 100%;
}
main {
width: 100%;
height: 100%;
}
</style>
<main>
<Divide>
<div slot="1">
<h1>Hello World</h1>
<p>{ text }</p>
<Button on:click="{handleClick}">test-inhalt</Button>
<h1>Login</h1>
<form>
<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 slot="2">
<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>
import { onMount } from 'svelte/internal';
import { c, s } from 'services/utils';
import { onMount } from 'svelte/internal';
import { c, s } from 'services/utils';
export let mode = 'h';
export let basisFirst = 0.5;
export let basisSecond = 0.5;
export let minSize = 100;
export let mode = 'h';
export let basisFirst = 0.5;
export let basisSecond = 0.5;
export let minSize = 100;
let divide;
let dragging = false;
let size = 5;
let total = 0;
let divide;
let dragging = false;
let size = 5;
let total = 0;
$: classes = c({
divide: true,
'divide--vertical': mode === 'v',
});
$: classes = c({
divide: true,
'divide--vertical': mode === 'v',
});
$: classesDivider = c({
divide__divider: true,
'divide__divider--vertical': mode === 'v',
});
$: classesDivider = c({
divide__divider: true,
'divide__divider--vertical': mode === 'v',
});
$: style = s({
'--divide-size': `${size}px`,
'--divide-basis-first': `${basisFirst * 100}%`,
'--divide-basis-second': `${basisSecond * 100}%`,
'--divide-min-width': minSize > 0 && mode === 'h' ? `${minSize}px` : '0',
'--divide-min-height': minSize > 0 && mode === 'v' ? `${minSize}px` : '0',
});
$: style = s({
'--divide-size': `${size}px`,
'--divide-basis-first': `${basisFirst * 100}%`,
'--divide-basis-second': `${basisSecond * 100}%`,
'--divide-min-width': minSize > 0 && mode === 'h' ? `${minSize}px` : '0',
'--divide-min-height': minSize > 0 && mode === 'v' ? `${minSize}px` : '0',
});
function getTotal() {
return mode === 'h' ? divide.clientWidth : divide.clientHeight;
}
function handleMousedown(event) {
if (event.button === 0) {
dragging = true;
total = getTotal();
function getTotal() {
return mode === 'h' ? divide.clientWidth : divide.clientHeight;
}
}
function handleMousemove(event) {
if (dragging) {
const dragPos =
mode === 'h'
? event.x - divide.getBoundingClientRect().x
: event.y - divide.getBoundingClientRect().y;
basisFirst = dragPos / total;
basisSecond = 1 - basisFirst;
function handleMousedown(event) {
if (event.button === 0) {
dragging = true;
total = getTotal();
}
}
}
function handleMouseup() {
dragging = false;
}
function handleMousemove(event) {
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) {
if (event.buttons !== 1) {
function handleMouseup() {
dragging = false;
}
}
function handleMouseenter(event) {
if (event.buttons !== 1) {
dragging = false;
}
}
</script>
<style>
.divide {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.divide {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.divide__divider {
cursor: ew-resize;
width: var(--divide-size);
height: 100%;
background-color: var(--color-foreground);
}
.divide__divider {
cursor: ew-resize;
width: var(--divide-size);
height: 100%;
background-color: var(--color-foreground);
}
.divide__divider:hover {
background-color: var(--color-foreground-light);
}
.divide__divider:hover {
background-color: var(--color-foreground-light);
}
.divide__elem {
flex: 0 0 50%;
min-width: var(--divide-min-width);
max-width: calc(100% - var(--divide-size) - var(--divide-min-width));
min-height: var(--divide-min-height);
max-height: calc(100% - var(--divide-size) - var(--divide-min-height));
}
.divide__elem {
flex: 0 0 50%;
min-width: var(--divide-min-width);
max-width: calc(100% - var(--divide-size) - var(--divide-min-width));
min-height: var(--divide-min-height);
max-height: calc(100% - var(--divide-size) - var(--divide-min-height));
}
.divide__elem > :global(*) {
width: 100%;
height: 100%;
}
.divide__elem > :global(*) {
width: 100%;
height: 100%;
}
.divide__elem--first {
flex-basis: calc(var(--divide-basis-first) - var(--divide-size) / 2);
}
.divide__elem--first {
flex-basis: calc(var(--divide-basis-first) - var(--divide-size) / 2);
}
.divide__elem--second {
flex-basis: calc(var(--divide-basis-second) - var(--divide-size) / 2);
}
.divide__elem--second {
flex-basis: calc(var(--divide-basis-second) - var(--divide-size) / 2);
}
.divide--vertical {
flex-direction: column;
}
.divide--vertical {
flex-direction: column;
}
.divide__divider--vertical {
cursor: ns-resize;
width: 100%;
height: var(--divide-size);
}
.divide__divider--vertical {
cursor: ns-resize;
width: 100%;
height: var(--divide-size);
}
</style>
<div

View File

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

View File

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

View File

@ -1,6 +1,7 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: {
bundle: path.resolve(__dirname, 'src/renderer.ts'),
},
@ -33,6 +34,10 @@ module.exports = {
alias: {
atoms: path.resolve(__dirname, 'src/renderer/components/1-atoms'),
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'),
},
},