UNPKG

front-sdk

Version:

A JavaScript API for Front (https://frontapp.com)

231 lines (229 loc) 10.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FrontError = exports.Front = void 0; const Promise = require("bluebird"); const bodyParser = require("body-parser"); const crypto = require("crypto"); const express = require("express"); const _ = require("lodash"); const querystring = require("querystring"); const request = require("request-promise"); const typed_error_1 = require("typed-error"); const URL = 'https://api2.frontapp.com'; class Front { constructor(apiKey, apiSecret) { this.comment = { create: (params, callback) => this.httpCall({ method: 'POST', path: 'conversations/<conversation_id>/comments' }, params, callback), get: (params, callback) => this.httpCall({ method: 'GET', path: 'comments/<comment_id>' }, params, callback), listMentions: (params, callback) => this.httpCall({ method: 'GET', path: 'comments/<comment_id>/mentions' }, params, callback), }; this.contact = { create: (params, callback) => this.httpCall({ method: 'POST', path: 'contacts' }, params, callback), delete: (params, callback) => this.httpCall({ method: 'DELETE', path: 'contacts/<contact_id>' }, params, callback), get: (params, callback) => this.httpCall({ method: 'GET', path: 'contacts/<contact_id>' }, params, callback), update: (params, callback) => this.httpCall({ method: 'PATCH', path: 'contacts/<contact_id>' }, params, callback), }; this.conversation = { get: (params, callback) => this.httpCall({ method: 'GET', path: 'conversations/<conversation_id>' }, params, callback), list: (params, callback) => this.httpCall({ method: 'GET', path: 'conversations[q:page_token:limit]' }, params, callback), listComments: (params, callback) => this.httpCall({ method: 'GET', path: 'conversations/<conversation_id>/comments' }, params, callback), listFollowers: (params, callback) => this.httpCall({ method: 'GET', path: 'conversations/<conversation_id>/followers' }, params, callback), listInboxes: (params, callback) => this.httpCall({ method: 'GET', path: 'conversations/<conversation_id>/inboxes' }, params, callback), listMessages: (params, callback) => this.httpCall({ method: 'GET', path: 'conversations/<conversation_id>/messages[page_token:limit]', }, params, callback), listRecent: (callback) => this.httpCall({ method: 'GET', path: 'conversations' }, null, callback), update: (params, callback) => this.httpCall({ method: 'PATCH', path: `conversations/${params.conversation_id}` }, _.omit(params, ['conversation_id']), callback), }; this.inbox = { create: (params, callback) => this.httpCall({ method: 'POST', path: 'inboxes' }, params, callback), createChannel: (params, callback) => this.httpCall({ method: 'POST', path: 'inboxes/<inbox_id>/channels' }, params, callback), get: (params, callback) => this.httpCall({ method: 'GET', path: 'inboxes/<inbox_id>' }, params, callback), list: (callback) => this.httpCall({ method: 'GET', path: 'inboxes' }, null, callback), listChannels: (params, callback) => this.httpCall({ method: 'GET', path: 'inboxes/<inbox_id>/channels' }, params, callback), listConversations: (params, callback) => this.httpCall({ method: 'GET', path: 'inboxes/<inbox_id>/conversations[q:page_token:limit]', }, params, callback), listTeammates: (params, callback) => this.httpCall({ method: 'GET', path: 'inboxes/<inbox_id>/teammates' }, params, callback), }; this.message = { get: (params, callback) => this.httpCall({ method: 'GET', path: 'messages/<message_id>' }, params, callback), receiveCustom: (params, callback) => this.httpCall({ method: 'POST', path: 'channels/<channel_id>/incoming_messages' }, params, callback), reply: (params, callback) => this.httpCall({ method: 'POST', path: 'conversations/<conversation_id>/messages' }, params, callback), send: (params, callback) => this.httpCall({ method: 'POST', path: 'channels/<channel_id>/messages' }, params, callback), }; this.teammate = { get: (params, callback) => this.httpCall({ method: 'GET', path: 'teammates/<teammate_id>' }, params, callback), list: (callback) => this.httpCall({ method: 'GET', path: 'teammates' }, null, callback), update: (params, callback) => this.httpCall({ method: 'PATCH', path: 'teammates/<teammate_id>' }, params, callback), }; this.topic = { listConversations: (params, callback) => this.httpCall({ method: 'GET', path: 'topics/<topic_id>/conversations[q:page_token:limit]', }, params, callback), }; this.channel = { update: (params, callback) => this.httpCall({ method: 'PATCH', path: 'channels/<channel_id>' }, params, callback), }; this.apiKey = apiKey; if (apiSecret) { this.apiSecret = apiSecret; } } registerEvents(opts, callback) { let httpServer; let listener; const eventQueue = []; const requestEvent = () => { const eventId = eventQueue[0]; this.httpCall({ path: 'events/<event_id>', method: 'GET' }, { event_id: eventId, }) .asCallback(callback) .finally(() => { eventQueue.shift(); if (eventQueue.length > 0) { requestEvent(); } }); }; const addToEventQueue = (id) => { eventQueue.push(id); if (eventQueue.length === 1) { requestEvent(); } }; if (!this.apiSecret) { throw new Error('No secret key registered'); } if (!opts || (opts.server && opts.port) || (!opts.server && !opts.port)) { throw new Error('Pass either an Express instance or a port to listen on'); } if (opts.port && typeof opts.port !== 'number') { throw new Error('`port` must be a number'); } const hookPath = opts.hookPath || '/fronthook'; if (opts.server) { listener = opts.server; } else { listener = express(); listener.use(bodyParser.urlencoded({ extended: true })); listener.use(bodyParser.json()); httpServer = listener.listen(opts.port); } listener.post(hookPath, (req, res) => { const eventPreview = typeof req.body === 'string' ? JSON.parse(req.body) : req.body; const XFrontSignature = req.get('X-Front-Signature'); if (!XFrontSignature || !this.validateEventSignature(eventPreview, XFrontSignature)) { res.sendStatus(401); throw new Error('Event Signature does not match registered secret'); } res.sendStatus(200); addToEventQueue(eventPreview.id); }); return httpServer; } getFromLink(url, callback) { const path = url.replace(URL, '').replace(/^\//, ''); return this.httpCall({ method: 'GET', path }, null, callback); } httpCall(details, params, callback, retries = 0) { const url = 'url' in details ? details.url : `${URL}/${this.formatPath(details.path, params)}`; const body = params || {}; const requestOpts = { body, headers: { Authorization: `Bearer ${this.apiKey}`, }, json: true, method: details.method, url, }; return request(requestOpts) .promise() .catch((error) => { if (error.statusCode >= 500 && retries < 5) { return Promise.delay(300).then(() => { return this.httpCall(details, params, callback, retries + 1); }); } const frontError = new FrontError(error); frontError.message += ` at ${url} with body ${JSON.stringify(body)}`; throw frontError; }) .asCallback(callback); } formatPath(path, data = {}) { let newPath = path; const reSearch = (re, operation) => { let matches = path.match(re); if (matches) { operation(matches); } }; reSearch(/<(.*?)>/g, (mandatoryTags) => { _.map(mandatoryTags, (tag) => { const tagName = tag.substring(1, tag.length - 1); if (!data[tagName]) { throw new Error(`Tag ${tag} not found in parameter data`); } newPath = newPath.replace(tag, data[tagName]); }); }); reSearch(/\[(.*?)\]/g, (optionalTags) => { if (optionalTags.length > 1) { throw new Error(`Front endpoint ${path} is incorrectly defined`); } const trimmedTags = optionalTags[0]; const tags = trimmedTags.substring(1, trimmedTags.length - 1).split(':'); const queryTags = {}; newPath = newPath.replace(trimmedTags, ''); _.each(tags, (tag) => { if (tag !== 'q' && data[tag]) { queryTags[tag] = data[tag]; } }); newPath = `${newPath}?${querystring.stringify(queryTags)}`; if (_.includes(tags, 'q')) { newPath += `&${data.q}`; } }); return newPath; } validateEventSignature(data, signature) { let hash = ''; try { hash = crypto .createHmac('sha1', this.apiSecret) .update(JSON.stringify(data)) .digest('base64'); } catch (err) { return false; } return hash === signature; } } exports.Front = Front; class FrontError extends typed_error_1.TypedError { constructor(error) { var _a; super(error); const frontError = (_a = error === null || error === void 0 ? void 0 : error.error) === null || _a === void 0 ? void 0 : _a._error; if (frontError) { _.each(['status', 'title', 'message', 'details'], (key) => { if (frontError[key]) { this[key] = frontError[key]; } }); } } } exports.FrontError = FrontError; //# sourceMappingURL=index.js.map