/**
* @module reading-data-instapaper
*/
const instapaper = require('instapaper')
const STRIP = require('strip')
const log = require('winston')
log.add(new log.transports.Console())
const ReadingDataInstapaper = (function () {
/**
* Return an array of bookmark IDs from an array of bookmarks.
* @memberof module:reading-data-instapaper
* @private
* @param {Array} bookmarks An array of Instapaper bookmarks.
* @return {Array} An array of Instapaper bookmark IDs.
*/
let getBookmarkIDs = (bookmarks) => {
let ids = []
bookmarks.map((bookmark) => {
ids.push(bookmark.bookmark_id)
})
return ids
}
/**
* Return a comma-delimited string of bookmark IDs.
* @memberof module:reading-data-instapaper
* @private
* @param {Array} ids An array of Instapaper bookmark IDs.
* @return {String} A comma-delimited string of bookmark IDs.
*/
let formatHaveString = (ids) => {
return ids.join()
}
/**
* Merge an Instapaper API response with pre-existing (or preloaded) data,
* and only overwrite new values, pushing new bookmarks to the existing
* `.bookmarks` array.
* @memberof module:reading-data-instapaper
* @private
* @param {Object} existingData An existing Instapaper API response object.
* @param {Object[]} existingData.bookmarks An array of bookmark objects.
* @param {Object} responseData A new Instapaper API response object.
* @param {Object[]} responseData.bookmarks An array of bookmark objects.
* @return {Object} A merged response object containing new and old bookmarks.
*/
let merge = (existingData = { bookmarks: [] }, responseData) => {
for (var key in responseData) {
if (responseData.hasOwnProperty(key)) {
if (existingData.hasOwnProperty(key)) {
// the key already exists, so merging should be gentler
if (Array.isArray(existingData[key]) && Array.isArray(responseData[key])) {
// Arrays should be merged by pushing response items to the existing array
responseData[key].map((item) => { existingData[key].push(item) })
} else if (typeof existingData[key] === 'object' && typeof responseData[key] === 'object') {
// Objects should be allowed to overwrite existing keys, add new
// keys, and preserve old keys
Object.assign(existingData[key], responseData[key])
} else {
// Types don’t match or are primitive, in which case overwrite in
// favour of the new value
existingData[key] = responseData[key]
}
} else {
// the key doesn’t exist, so create it
existingData[key] = responseData[key]
}
}
}
return existingData
}
return {
/**
* Configuration object providing a default configuration to be
* used by ReadingData#use()
* @type {Object}
* @property {String} scope='instapaper' - The scope this plugin’s data should be saved under on the ReadingData instance.
* @property {Number} limit=5 - The maximum number of bookmarks to request from the Instapaper API.
* @property {String} folder_id='archive' - The folder to request from the Instapaper API.
* @property {Number} apiVersion=1.1 - The version of the Instapaper API that should be queried.
* @property {Boolean} fetchText=false - Whether or not to try to retrieve a bookmark’s full text.
* @property {Boolean} useCache=false - Whether or not to use cached/preloaded data.
*/
config: {
scope: 'instapaper',
limit: 5,
folder_id: 'archive',
apiVersion: 1.1,
fetchText: false,
useCache: false
},
/**
* Create a client for the Instapaper API and request a collection of bookmarks.
* @param {Object} pluginContext Context variables specific to this plugin.
* @param {Object} pluginContext.config This plugin’s configuration.
* @param {Object} pluginContext.data Any data already stored by ReadingData under this plugin’s scope.
* @param {Object} context Contextual this passed from the ReadingData calling environment. Equivalent to the entire ReadingData instance.
* @param {Object} context.config Global configuration settings.
* @param {Object} context.data Data stored on the ReadingData instance.
* @return {Object} Data to be stored by ReadingData under this plugin’s scope.
*/
data: async function ({ config, data = { bookmarks: [] } } = {}) {
// Make sure the plugin’s data object has a useable bookmarks property
data.bookmarks = Array.isArray(data.bookmarks) ? data.bookmarks : []
// Set the URL for the configured API version
let apiUrl = 'https://www.instapaper.com/api/' + config.apiVersion
// Throw out any attempt to use fetch() without configuring API keys and
// user credentials
if (!config.hasOwnProperty('apiKey') || !config.hasOwnProperty('apiSecret')) {
throw new Error('ReadingDataInstapaper#fetch(): config must contain `apiKey` and `apiSecret`')
}
if (!config.hasOwnProperty('userKey') || !config.hasOwnProperty('userSecret')) {
throw new Error('ReadingDataInstapaper#fetch(): config must contain `userKey` and `userSecret`')
}
// Initialise Instapaper client with credentials
let client = instapaper(config.apiKey, config.apiSecret, { apiUrl: apiUrl })
client.setUserCredentials(config.userKey, config.userSecret)
let requestParameters = {
limit: config.limit,
folder_id: config.folder_id
}
// If useCache is true, pass IDs of already present bookmarks to the have
// parameter of the Instapaper API
if (config.useCache) {
requestParameters.have = formatHaveString(getBookmarkIDs(data.bookmarks))
}
let responseData
try {
let res = await client.bookmarks.list(requestParameters)
responseData = JSON.parse(res)
} catch (e) {
log.error('ReadingDataInstapaper#fetch(): error parsing Instapaper API response.\n', e)
}
// Fetch full text from the Instapaper API
if (config.fetchText && responseData.hasOwnProperty('bookmarks')) {
await Promise.all(responseData.bookmarks.map(async bookmark => {
try {
log.debug('ReadingDataInstapaper#fetch(): fetching full text for bookmark', bookmark.bookmark_id)
bookmark.html = await client.bookmarks.getText(bookmark.bookmark_id)
bookmark.text = STRIP(bookmark.html)
} catch (e) {
log.error('ReadingDataInstapaper#fetch(): error retrieving bookmark text for', bookmark.bookmark_id, '\n', e)
}
}))
}
if (config.useCache) {
return merge(data, responseData)
} else {
return responseData
}
}
}
}())
module.exports = ReadingDataInstapaper