'use strict'
var _ = require('lodash')
, errors = require('./errors')
, EventEmitter = require('events').EventEmitter
, request = require('request')
, validate = require('./validator')
, VERSION = require('../package.json').version
, APIError = errors.APIError
, ReportError = errors.ReportError
, defaultClient = null
var defaults = {
reauth: false,
sessionCookie: 'toggl_api_session',
apiUrl: 'https://www.toggl.com',
reportsUrl: 'https://toggl.com/reports'
}
function noop() {
}
/**
* Validate client options
*/
function validateOptions(options) {
if (!options.apiToken && !(options.username && options.password)) {
throw new Error('You should either specify apiToken or username and password')
}
// if we use apiToken we do not need a session cookie
if (options.apiToken) {
options.reauth = true
}
if (!options.apiUrl) {
throw new Error('Toggl API base URL is not specified')
}
}
/**
* Expose client
*/
module.exports = TogglClient
/**
* Toggl API client
*
* @constructor
* @param [options] Client options
*/
function TogglClient(options) {
/**
* @private
*/
this.options = {}
_.assign(this.options, defaults)
if (options !== undefined) {
_.assign(this.options, options)
validateOptions(this.options)
}
/**
* For internal needs
*
* @private
* @type {EventEmitter}
*/
this.emitter = new EventEmitter()
this.emitter.setMaxListeners(0)
/**
* Used to store and set cookies for API requests
*
* @private
* @type {CookieJar}
*/
this.cookieJar = request.jar()
/**
* Result of authentication call
*
* @public
*/
this.authData = null
/**
* Re-authentication timeout ID
*
* @private
*/
this.authTimeout = null
/**
* If we're authenticating
*
* @private
*/
this.authenticating = false
}
/**
* User agent string
*
* @public
* @static
*/
TogglClient.USER_AGENT = 'node-toggl v' + VERSION
/**
* Return default API client
*
* @public
* @static
* @returns {TogglClient}
*/
TogglClient.defaultClient = function() {
if (!defaultClient) {
defaultClient = new TogglClient()
}
return defaultClient
}
/**
* Set default settings for client
*
* @public
* @static
* @param {object} newDefaults
*/
TogglClient.setDefaults = function(newDefaults) {
_.assign(defaults, newDefaults)
}
/**
* Request to Toggl API v8
*
* @private
* @param {string} path API path
* @param {object} opts Request options
* @param {function} callback Accepts arguments: (err, data)
*/
TogglClient.prototype.apiRequest = function(path, opts, callback) {
var self = this
, options = this.options
if (this.authenticating) {
this.emitter.on('authenticate', function(err) {
if (err) {
return callback(err)
}
self.apiRequest(path, opts, callback)
})
return
}
else if (!opts.noauth && !options.apiToken && !this.authData) {
return callback(new Error('Authenticate first'))
}
if (!opts.noauth) {
if (options.apiToken) {
opts.auth = {
user: options.apiToken,
pass: 'api_token'
}
}
else {
opts.jar = this.cookieJar
}
}
opts.url = options.apiUrl + path
opts.json = true
request(opts, onresponse)
function onresponse(err, response, data) {
var statusCode = response.statusCode
if (err) {
callback(err)
}
else if (statusCode >= 200 && statusCode < 300) {
callback(null, data)
}
else {
return callback(new APIError(statusCode, data))
}
}
}
/**
* Make authentication call only if you use username & password
*
* @see https://github.com/toggl/toggl_api_docs/blob/master/chapters/authentication.md
* @public
* @param {function} [callback] Accepts arguments: (err, userData)
*/
TogglClient.prototype.authenticate = function(callback) {
var self = this
, options = this.options
, auth
, req = {}
callback = callback || noop
if (options.username && options.password) {
auth = {
user: options.username,
pass: options.password
}
}
else {
return callback(new Error('No need to authenticate thus you use apiToken'))
}
req.auth = auth
req.method = 'GET'
this.apiRequest('/api/v8/me', req, done)
this.authenticating = true
function done(err, data) {
self.emitter.emit('authenticate', err, data)
if (err) {
return error(err)
}
self.authData = data
if (options.reauth) {
self.cookieJar._jar.getCookies(options.apiUrl, oncookies)
}
else {
success()
}
}
function oncookies(err, cookies) {
if (err) {
error(err)
}
var sessionCookie = _.find(cookies, {key: options.sessionCookie})
, ttl = sessionCookie.ttl()
if (ttl) {
self.setAuthTimer(ttl)
}
success()
}
function success() {
self.authenticating = false
self.emitter.emit('authenticate', null, self.authData)
callback(null, self.authData)
}
function error(err) {
self.authenticating = false
self.emitter.emit('authenticate', err)
callback(err)
}
}
/**
* Call when client is no longer needed
*
* @public
*/
TogglClient.prototype.destroy = function() {
if (this.authTimeout) {
clearTimeout(this.authTimeout)
}
}
/**
* Request to Toggl API v8
*
* @private
* @param {string} path API path
* @param {object} opts Request options
* @param {function} callback Accepts arguments: (err, data)
*/
TogglClient.prototype.reportsRequest = function(path, opts, callback) {
var options = this.options
if (!options.apiToken) {
return callback(new Error('API token is not specified. Reports API can\'t be used.'))
}
opts.auth = {
user: options.apiToken,
pass: 'api_token'
}
opts.url = options.reportsUrl + path
opts.json = true
opts.qs = opts.qs || {}
opts.qs.user_agent = TogglClient.USER_AGENT
request(opts, onresponse)
function onresponse(err, response, data) {
var statusCode = response.statusCode
if (err) {
callback(err)
}
else if (statusCode >= 200 && statusCode < 300) {
callback(null, data)
}
else if (data.error) {
return callback(new ReportError(data.error))
}
else {
return callback(new ReportError('Unknown Reports API error', statusCode, data))
}
}
}
/**
* Set timer for re-authentication
*
* @private
* @param {number} duration
*/
TogglClient.prototype.setAuthTimer = function(duration) {
var self = this
// run re-auth before current session actually expires
duration -= 5000
this.authTimeout = setTimeout(reauth, duration)
function reauth() {
self.authTimeout = null
self.authenticate()
}
}
/**
* Validate request options
*
* @private
* @param {object|string} schema Validation schema
* @param {object} options Request options
* @param {function} callback Request callback. If validation error is raised it will be called with error.
* @returns {boolean}
*/
TogglClient.prototype.validateOptions = function(schema, options, callback) {
var error = validate(schema, options)
if (error) {
callback(error)
return false
}
return true
}
/**
* Extend TogglClient prototype
*/
require('./api/reports')
require('./api/user')
require('./api/clients')
require('./api/projects')
require('./api/project_users')
require('./api/tags')
require('./api/tasks')
require('./api/time_entries')
require('./api/workspaces')
require('./api/workspace_users')