danbooru
Version:
danbooru api wrapper
272 lines (239 loc) • 7.55 kB
JavaScript
const { http, https, URL, btoa, fetch } = require('./requires.node')
const constants = require('./constants')
const util = require('./util')
const defaultUrl = Object.freeze(new URL('https://danbooru.donmai.us/'))
module.exports = class {
/**
* Create a new Danbooru API wrapper
*
* Optionally specify an alternate url or login details:
* - `login:api_key`
* - `http://safebooru.donmai.us`
* - `https://login:api_key@sonohara.donmai.us`
*
* @param {string} [conn] Connection customization string
*/
constructor(conn = false) {
this.auth(conn)
}
/**
* Look up current username while optionally changing connection details
*
* Note that changing urls does not log out. To log out, pass a falsy value
* and ensure that this function returns undefined.
*
* @param {string | *} conn Connection string, like in constructor
* @returns {string} Current username
*/
auth(conn) {
const store = storage(this)
if (typeof conn !== 'undefined') {
let username, password
if (conn && /^https?:\/\//.test(conn)) {
const url = new URL(conn)
;({ username, password } = url)
Object.assign(url, {
username: '',
password: '',
hash: '',
search: '',
pathname: (url.pathname || '').replace(/\/*$/, '/')
})
if (url.href !== store.url.href) {
if (url.protocol === 'http:') store.module = http
else store.module = https
if (url.href !== defaultUrl.href) store.url = url
else store.url = defaultUrl
}
} else {
const match = /^(.*?):(.*)$/.exec(conn)
if (match) [, username, password] = match
}
if (username && password) {
// store.authorization = `Basic ${btoa(`${username}:${password}`)}`
store.authParams = { login: username, api_key: password }
store.username = username
} else if (!conn) {
// delete store.authorization
delete store.authParams
delete store.username
}
}
return store.username
}
/**
* Get base url, or resolve a path
*
* @param {URL | string} [path] Path to resolve
* @returns {URL} Base url or resolved absolute url
*/
url(path = '') {
return new URL(path.toString().replace(/^\/*/, ''), storage(this).url)
}
/**
* Perform a web request
*
* May use 'request' or 'fetch', indicated by the first element in array
*
* @param {Object} [options] Request options
* @param {string} [options.method] HTTP request method
* @param {string} [options.path] Path to be appended to base path
* @param {Object} [options.headers] Request headers
* @param {*} [options.body] Request body
* @returns {Array} Function used, followed by return value
*/
request(options = {}) {
const { module } = storage(this)
const { method, path, body } = options
const url = this.url(path)
let { headers = {} } = options
if (module) {
const request = module.request({
protocol: url.protocol,
hostname: url.hostname,
port: url.port,
method,
path: url.pathname + url.search,
headers
})
if (body) request.end(body)
else request.end()
return ['request', request]
} else {
return ['fetch', fetch(url.href, { method, headers, body })]
}
}
/**
* Perform a request and receive an object
*
* Only works with `application/json` data, and automatically appends `.json`
* to paths.
*
* Rejects on connection errors and resolves data object or parsing error.
* Returned objects have extra symbol key values:
* * `Danbooru.status` Status code from server
* * `Danbooru.data` Original string data
* * `Danbooru.headers` Response headers
*
* @param {string} path Path to be appended to base path
* @param {Object} [options] Request options
* @param {string} [options.method] HTTP request method
* @param {Object} [options.headers] Request headers
* @param {Object | Array} [options.query] Data to be added to path
* @param {Object | Array} [options.body] Data to be sent as request body
* @returns {Promise} Resolves to a json object or a parsing error
*/
json(path, options = {}) {
return new Promise((resolve, reject) => {
const { method } = options
let { query, body } = options
let { headers = {} } = options
const { authParams } = storage(this)
if (authParams) {
if (query) {
query = { ...authParams, ...query }
} else if (!method || method === 'GET') {
query = authParams
} else if (body) {
body = { ...authParams, ...body }
} else {
body = authParams
}
}
const reqBody = body && JSON.stringify(body)
if (reqBody) {
headers = {
...headers,
'content-type': 'application/json'
}
}
const reqOptions = {
method,
headers,
path: `${path}.json${util.queryString(query)}`,
body: reqBody
}
const [reqType, reqValue] = this.request(reqOptions)
switch (reqType) {
case 'request':
reqValue.on('error', reject).on('response', response => {
let data = ''
const { statusCode: status, headers } = response
response
.setEncoding('utf8')
.on('data', chunk => (data += chunk))
.on('end', () => resolve({ data, status, headers }))
})
break
case 'fetch':
reqValue
.then(response => {
const { status, headers } = response
return response.text().then(data => ({ data, status, headers }))
})
.then(resolve, reject)
break
}
}).then(response => {
let data
try {
data = JSON.parse(response.data)
} catch (error) {
data = error
}
Object.assign(data, {
[constants.status]: response.status,
[constants.data]: response.data,
[constants.headers]: response.headers
})
return data
})
}
/**
* Perform a json GET request
*
* @param {string} path Resource path
* @param {Object | Array} [query] Query string data
* @returns {Promise} Resolves to server response data
*/
get(path, query) {
return this.json(path, { query })
}
/**
* Perform a json POST request
*
* @param {string} path Resource path
* @param {Object | Array} [body] Request body data
* @returns {Promise} Resolves to server response data
*/
post(path, body) {
return this.json(path, { method: 'POST', body })
}
/**
* Perform a json PUT request
*
* @param {string} path Resource path
* @param {Object | Array} [body] Request body data
* @returns {Promise} Resolves to server response data
*/
put(path, body) {
return this.json(path, { method: 'PUT', body })
}
/**
* Perform a json DELETE request
*
* @param {string} path Resource path
* @param {Object | Array} [body] Request body data
* @returns {Promise} Resolves to server response data
*/
delete(path, body) {
return this.json(path, { method: 'DELETE', body })
}
}
function storage(key) {
const map = storage.map || (storage.map = new WeakMap())
if (map.has(key)) return map.get(key)
const value = { url: defaultUrl, module: https }
map.set(key, value)
return value
}