Skip to content

kuwoyuki/qi-re

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

44 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Webnovel/Qidian API client

npm bundle size NPM npm

Node.js client for Qidian APIs build by reverse engineering their native application. Uses native app endpoints.

Note: All endpoints default to webnovel.com's.

If you just want to use this as a reference the useful files are:

The rest is just traffic sniffing.

Usage

yarn add webnovel.js
const { Client: WNClient } = require("webnovel.js");

(async () => {
  const username = "some@mail.com";

  const client = new WNClient({
    username,
    password: "supersekret",
    uuid: "000000003ede1bf9000000003ede1bf9" // UUID
  });

  const res = await client.login(true);

  // Since we set emailVer = true we need to manually check for "encry" (email verification token)
  // you can of course catch and call client.sendEmail() on your own
  const {
    data: { encry }
  } = res;

  // if you want raw tokens (ticket, autologin)
  let user;

  if (encry) {
    const code = await getCodeUsingIMAP(email); // somehow get the emailed code
    user = await client.confirmCode(encry, code); // cookies now set
  } else {
    user = res;
  }

  const {
    body: {
      Data: { Email }
    }
  } = await client.apiClient("/user/get");

  console.log(Email === username); // true

  // Ze Tian Ji 😍
  const bookId = "8205217405006105";

  // destructure the first chapter
  // *note*: check com.qidian.QDReader.components.book.al.QDChapterManager
  // you probably need to send some other requests, I'm getting some incorrect
  // chapter IDs.. maybe /book-case/report-operation-time
  const {
    body: {
      Data: {
        Chapters: [, { Id: secChptID }]
      }
    }
  } = await client.apiClient("/book/get-chapters", {
    query: {
      bookId,
      maxUpdateTime: 0,
      maxIndex: 0,
      sign: ""
    }
  });

  const chapter = await client.getChapter(bookId, secChptID);
  // ...
})();

The client class only implements complex/encrypted/signed requests, so for the most part you need to manually find the endpoint you need and use the Client.apiClient Got instance to request it.
Most API endpoints are declared in the com.qidian.QDReader.components.api package, in the Urls class.

Maybe have a look at examples too.

Classes

Client

Webnovel client, instantiate, login then call the API endpoints using this.apiClient

Typedefs

SessionInfo : Object

Auth methods session info

RegisterPayload : Object

Client

Webnovel client, instantiate, login then call the API endpoints using this.apiClient

Kind: global class

new Client()

Webnovel client

client.ctx

Kind: instance property of Client Properties

Name Type Description
credentials Object Auth credentials
cookieJar CookieJar CookieJar instance
apiURL string API base URL
authURL string Auth API base URL
uuid string IMEI/UUID,
session Object Session properties, can be used to manually resume sessions
session.id number User session ID
session.key string User session key
session.autoLoginKey string User session autologin key
session.autoLoginExpires string Autologin expiration time

client.authClient : got.GotInstance.<got.GotJSONFn>

Got auth (passport endpoint) client instance

Kind: instance property of Client Access: public

client.apiClient : got.GotInstance.<got.GotJSONFn>

Got API (idroid) client instance

Kind: instance property of Client Access: public

client.register() β‡’ Promise.<RegisterPayload>

Register an account, gets credentials from Client context

Kind: instance method of Client

client.confirmRegistration(emailkey, code) β‡’ Promise.<SessionInfo>

Confirm registration and get session creds

Kind: instance method of Client

Param Type
emailkey string
code string

client.confirmCode(encry, code) β‡’ Promise.<SessionInfo>

Login using email verification code (if login method returned code 11318)

Kind: instance method of Client Throws:

  • AuthError
Param Type Description
encry string encry property from the failed login response
code string email verif. code

client.login(emailVer) β‡’ Promise.<SessionInfo>

Login into Webnovel

Kind: instance method of Client Throws:

  • AuthError
Param Type Default Description
emailVer boolean false Set to true if you want it to pass and send an email with a ver. code

client.resumeSession() β‡’ Promise.<SessionInfo>

Resume current session

Kind: instance method of Client

client.getChapter(bookId, chapterId) β‡’ Promise.<Object>

gets and decrypts a chapter, unauthenticated requests probably won't work

Kind: instance method of Client

Param Type
bookId string
chapterId string

Client.Client

Kind: static class of Client

new Client(obj)

Creates an instance of Webnovel Client.

Param Type Description
obj Object
obj.username string Webnovel username
obj.password string Webnovel password
obj.apiURL string override Webnovel API endpoint (Qidian should work)
obj.authURL string override Webnovel auth API endoint (Qidian should work)
obj.uuid string UUID is auto generated if not passed, which will trigger mail verification

SessionInfo : Object

Auth methods session info

Kind: global typedef Properties

Name Type Description
code number status code.
data Object Session data
data.ticket string Session validation ticket
data.ukey string Currently logged in user's key (used in jwkey cookie)
data.autoLoginFlag number Whether we logged in with an autologin flag
data.autoLoginSessionKey string AL session key
data.autoLoginKeepTime number AL session lifetime
data.autoLoginExpiredTime number expiration unix timestamp
data.userid number user ID
msg string ok.

RegisterPayload : Object

Kind: global typedef Properties

Name Type Description
code number status code
data Object data payload
email string email used for the registration
emailkey string email confirmation code
msg string status message

Web login

Here's the web login version: note: csrf token is static

const NodeRSA = require("node-rsa");

// Webnovel's auth RSA key
// https://passport.webnovel.com/login.html
const keyData = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOf5B7Sg/EsfK+29BhFn1SUgoX
gcLP9Dl1Sf3g3PgwRTEkqMwhFVpIYoNVo1TV1q6Y6dRYZ1BExt/tqQqJcLvQhCKc
b4JuINKdftwG5le+Q2n6S/Ioyx7euYZgkmm3LSQ5VW7JmWV9VJFOIm4mpHmom9kE
CwVP/wBG9hmUs+USSwIDAQAB
-----END PUBLIC KEY-----`;

/**
 * web passport aes encryption
 * @param {string} str
 * @returns {string} aes -> base64 encoded string
 */
function rsaEncrypt(str) {
  const key = new NodeRSA();

  key.importKey(keyData, "public");
  return key.encrypt(str, "base64");
}

/**
 * @param {Object} q
 * @param {string} q.username - webnovel username
 * @param {string} q.password - webnovel password
 * @param {string} q._csrfToken - CSRF token from cookie
 * @param {CookieJar} cookieJar
 * @returns {GotPromise<string>}
 */
function web(q, cookieJar) {
  const query = {
    appId: 900,
    areaId: 1,
    source: "",
    returnurl: "http://www.webnovel.com",
    version: "",
    imei: "",
    qimei: "",
    target: "",
    format: "",
    ticket: "",
    autotime: "",
    auto: 1, // adds autologin tokens
    fromuid: 0,
    method: "LoginV1.checkCodeCallback",
    logintype: 22,
    username: "",
    password: rsaEncrypt(q.password),
    _csrfToken: q._csrfToken
  };

  return got("https://ptlogin.webnovel.com/login/checkcode", {
    query,
    headers: {
      "user-agent":
        "Mozilla/5.0 (X11; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0"
    },
    cookieJar,
    json: true
  });
}