/* jshint esversion: 8 */
/**
* This callback type is called `requestCallback` and is displayed as a global symbol.
*
* @callback requestCallback
* @param {object} data the data returned from the endpoint
*/
const { URL } = require('url')
const axios = require('axios')
const schedule = require('node-schedule')
const Campaign = require('./lib/campaign')
const TeamCampaign = require('./lib/teamCampaign')
const Cause = require('./lib/cause')
const FundraisingEvents = require('./lib/fundraisingEvents')
const Team = require('./lib/team')
const User = require('./lib/user')
const Webhook = require('./lib/webhook')
class TiltifyClient {
#clientID
#clientSecret
#schedule
apiKey
// Self-referential to split the subclass this and the client this... since it's not an extended class cant use 'super'
parent = this
/**
* A TiltifyClient contains all of the sub-types that exist on the Tiltify API
* @param {string} clientID The Client ID that you got from Tiltify.
* @param {string} clientSecret The Client Secret that you got from Tiltify.
* @constructor
*/
constructor (clientID, clientSecret) {
this.#clientID = clientID
this.#clientSecret = clientSecret
/**
* this.Campaigns is used to get info about campaigns
* @type Campaign
*/
this.Campaigns = new Campaign(this)
/**
* this.TeamCampaign is used to get info about team campaigns
* @type TeamCampaign
*/
this.TeamCampaigns = new TeamCampaign(this)
/**
* this.Causes is used to get info about causes
* @type Cause
*/
this.Causes = new Cause(this)
/**
* this.FundraisingEvents is used to get info about fundraising events
* @type FundraisingEvents
*/
this.FundraisingEvents = new FundraisingEvents(this)
/**
* this.Team is used to get info about a team
* @type Team
*/
this.Team = new Team(this)
/**
* this.User is used to get info about a user
* @type User
*/
this.User = new User(this)
/**
* this.Webhook is used to get info, subscribe, and manage webhooks
* @type User
*/
this.Webhook = new Webhook(this)
}
/**
* Generate access key and fully initialize the client
*/
async initialize () {
await this.generateKey()
}
/**
* Set the API key manually, this also disables the refresh checker.
* Primarily used for testing
* @param {string} key API key
*/
setKey (key) {
this.apiKey = key
this.#schedule.cancel()
}
/**
* Generate an access token to call the api, recursively calls itself when regenerating keys
* @param {int} attempt Attempt counter, for spacing out retries
*/
async generateKey (attempt = 1) {
const url = `https://v5api.tiltify.com/oauth/token?client_id=${this.#clientID}&client_secret=${this.#clientSecret}&grant_type=client_credentials&scope=public webhooks:write`
const options = {
url,
method: 'POST'
}
try {
const payload = await axios(options)
if (payload.status === 200) {
this.apiKey = payload.data?.access_token
const expDate = new Date(new Date(payload.data?.created_at).getTime() + (payload.data?.expires_in * 1000)) // Date token will have to be regenerated at, based on supplied expired time
// Schedule renew job, recursively call this function
this.#schedule = schedule.scheduleJob(expDate, function () {
this.generateKey()
}.bind(this))
return this.apiKey
} else {
// Schedule renew job to try again, recursively call this function
const currentDate = new Date()
const retryDate = new Date(currentDate.getTime() + (5000 * attempt)) // Add 5000 milliseconds (5 seconds) * attempt count
this.#schedule = schedule.scheduleJob(retryDate, function () {
this.generateKey(attempt + 1)
}.bind(this))
}
} catch (error) {
return Promise.reject(error)
}
}
/**
* _doRequest does a single request and returns the response.
* Normally this is wrapped in _sendRequest, but for some
* endpoints like Campaigns.getRecentDonations(id) need to send
* only a single request. This function is not actually called in
* the TiltifyClient, and is passed down to each of the types.
* @param {string} path The path, without /api/.
* @param {string} method HTTP method to make calls with, default to GET
* @param {Object} payload JSON payload to send
*/
async _doRequest (path, method = 'GET', payload) {
if (!this.parent.apiKey) {
console.error('tiltify-api-client ERROR Client has not been initalized or apiKey is missing')
return
}
const url = `https://v5api.tiltify.com/api/${path}`
const options = {
url,
headers: {
Authorization: `Bearer ${this.parent.apiKey}`,
'Content-Type': 'application/json'
},
method
}
if (payload) {
options.data = JSON.stringify(payload)
}
try {
const payload = await axios(options)
return payload
} catch (error) {
return Promise.reject(error)
}
}
/**
* _sendRequest is used for all endpoints, but only has a recursive
* effect when called againt an endpoint that contains a `metadata.after` string
* @param {string} path The path, without /api/public/
* @param {function} callback A function to call when we're done processing.
*/
async _sendRequest (path, callback) {
let results = []
let keepGoing = true
while (keepGoing) {
const response = (await this.parent._doRequest(path)).data
if (
response.data !== undefined &&
response.metadata !== undefined &&
response.metadata.after !== undefined &&
response.metadata.after !== null
) {
const url = 'https://temp.com/' + path // Combine the base URL and path
const urlObj = new URL(url) // Create a URL object
urlObj.searchParams.set('after', response.metadata.after) // Set the 'after' query parameter
const updatedPath = urlObj.pathname.replace('/', '') + urlObj.search // Get the updated path with query parameters. Remove first /
path = updatedPath
} else {
keepGoing = false
}
results = results.concat(response.data)
if (response.data.length === 0 || response.metadata?.after == null) {
keepGoing = false
callback(results)
}
}
}
}
module.exports = TiltifyClient