-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathindex.js
323 lines (281 loc) · 9.61 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/*
Author @ Rocco Musolino
DB Structure Example:
{
"shortname1": [url1, url2, ...],
"shortname2": [url3, url4, ...],
...
...
"shortnameX": [urlZ, ...]
}
*/
var debug = require('debug')('node-webhooks')
var Promise = require('bluebird') // for backward compatibility
var _ = require('lodash')
var jsonfile = require('jsonfile')
var fs = require('fs')
var crypto = require('crypto')
var request = require('request')
var events = require('eventemitter2')
// will contain all the functions. We need to store them to be able to remove the listener callbacks
var _functions = {}
// WebHooks Class
function WebHooks (options) {
if (typeof options !== 'object') throw new TypeError('Expected an Object')
if (typeof options.db !== 'string' && typeof options.db !== 'object') {
throw new TypeError('db Must be a String path or an object')
}
this.db = options.db
// If webhooks data is kept in memory, we skip all disk operations
this.isMemDb = typeof options.db === 'object'
if (options.hasOwnProperty('httpSuccessCodes')) {
if (!(options.httpSuccessCodes instanceof Array)) throw new TypeError('httpSuccessCodes must be an array')
if (options.httpSuccessCodes.length <= 0) throw new TypeError('httpSuccessCodes must contain at least one http status code')
this.httpSuccessCodes = options.httpSuccessCodes
} else {
this.httpSuccessCodes = [200]
}
this.emitter = new events.EventEmitter2({ wildcard: true })
var self = this
if (this.isMemDb) {
debug('setting listeners based on provided configuration object...')
_setListeners(self)
} else {
// sync loading:
try {
fs.accessSync(this.db, fs.R_OK | fs.W_OK)
// DB already exists, set listeners for every URL.
debug('webHook DB loaded, setting listeners...')
_setListeners(self)
} catch (e) {
// DB file not found, initialize it
if (e.hasOwnProperty('code')) {
if (e.code === 'ENOENT') {
// file not found, init DB:
debug('webHook DB init')
_initDB(self.db)
} else console.error(e)
} else console.error(e)
}
}
}
function _initDB (file) {
// init DB.
var db = {} // init empty db
jsonfile.writeFileSync(file, db, {spaces: 2})
}
function _setListeners (self) {
// set Listeners - sync method
try {
var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db)
if (!obj) throw Error('can\'t read webHook DB content')
for (var key in obj) {
// skip loop if the property is from prototype
if (!obj.hasOwnProperty(key)) continue
var urls = obj[key]
urls.forEach(function (url) {
var encUrl = crypto.createHash('md5').update(url).digest('hex')
_functions[encUrl] = _getRequestFunction(self, url)
self.emitter.on(key, _functions[encUrl])
})
}
} catch (e) {
throw Error(e)
}
// console.log(_functions[0] == _functions[1]);
// console.log(_functions[1] == _functions[2]);
// console.log(_functions[0] == _functions[2]);
}
function _getRequestFunction (self, url) {
// return the function then called by the event listener.
var func = function (shortname, jsonData, headersData) { // argument required when eventEmitter.emit()
var obj = {'Content-Type': 'application/json'}
var headers = headersData ? _.merge(obj, headersData) : obj
debug('POST request to:', url)
// POST request to the instantiated URL with custom headers if provided
request({
method: 'POST',
uri: url,
strictSSL: false,
headers: headers,
body: JSON.stringify(jsonData)
},
function (error, response, body) {
var statusCode = response ? response.statusCode : null
body = body || null
debug('Request sent - Server responded with:', statusCode, body)
if ((error || self.httpSuccessCodes.indexOf(statusCode) === -1)) {
self.emitter.emit(shortname + '.failure', shortname, statusCode, body)
return debug('HTTP failed: ' + error)
}
self.emitter.emit(shortname + '.success', shortname, statusCode, body)
}
)
}
return func
}
// 'prototype' has improved performances, let's declare the methods
WebHooks.prototype.trigger = function (shortname, jsonData, headersData) {
// trigger a webHook
this.emitter.emit(shortname, shortname, jsonData, headersData)
}
WebHooks.prototype.add = function (shortname, url) { // url is required
// add a new webHook.
if (typeof shortname !== 'string') throw new TypeError('shortname required!')
if (typeof url !== 'string') throw new TypeError('Url must be a string')
var self = this
return new Promise(function (resolve, reject) {
try {
var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db)
if (!obj) throw Error('can\'t read webHook DB content')
var modified = false
var encUrl
if (obj[shortname]) {
// shortname already exists
if (obj[shortname].indexOf(url) === -1) {
// url doesn't exists for given shortname
debug('url added to an existing shortname!')
obj[shortname].push(url)
encUrl = crypto.createHash('md5').update(url).digest('hex')
_functions[encUrl] = _getRequestFunction(self, url)
self.emitter.on(shortname, _functions[encUrl])
modified = true
}
} else {
// new shortname
debug('new shortname!')
obj[shortname] = [url]
encUrl = crypto.createHash('md5').update(url).digest('hex')
_functions[encUrl] = _getRequestFunction(self, url)
self.emitter.on(shortname, _functions[encUrl])
modified = true
}
// actualize DB
if (modified) {
if (!self.isMemDb) jsonfile.writeFileSync(self.db, obj)
resolve(true)
} else resolve(false)
} catch (e) {
reject(e)
}
})
}
WebHooks.prototype.remove = function (shortname, url) { // url is optional
// if url exists remove only the url attached to the selected webHook.
// else remove the webHook and all the attached URLs.
if (typeof shortname !== 'string') {
throw new TypeError('shortname required!')
}
var self = this
return new Promise(function (resolve, reject) {
// Basically removeListener will look up the given function by reference, if it found that function it will remove it from the event hander.
try {
if (typeof url !== 'undefined') {
// save in db
_removeUrlFromShortname(self, shortname, url, function (err, done) {
if (err) return reject(err)
if (done) {
// remove only the specified url
var urlKey = crypto.createHash('md5').update(url).digest('hex')
self.emitter.removeListener(shortname, _functions[urlKey])
delete _functions[urlKey]
resolve(true)
} else resolve(false)
})
} else {
// remove every event listener attached to the webHook shortname.
self.emitter.removeAllListeners(shortname)
// delete all the callbacks in _functions for the specified shortname. Let's loop over the url taken from the DB.
var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db)
if (obj.hasOwnProperty(shortname)) {
var urls = obj[shortname]
urls.forEach(function (url) {
var urlKey = crypto.createHash('md5').update(url).digest('hex')
delete _functions[urlKey]
})
// save it back to the DB
_removeShortname(self, shortname, function (err) {
if (err) return reject(err)
resolve(true)
})
} else {
debug('webHook doesn\'t exist')
resolve(false)
}
}
} catch (e) {
reject(e)
}
})
}
function _removeUrlFromShortname (self, shortname, url, callback) {
try {
var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db)
var deleted = false
var len = obj[shortname].length
if (obj[shortname].indexOf(url) !== -1) {
obj[shortname].splice(obj[shortname].indexOf(url), 1)
}
if (obj[shortname].length !== len) deleted = true
// save it back to the DB
if (deleted) {
if (!self.isMemDb) jsonfile.writeFileSync(self.db, obj)
debug('url removed from existing shortname')
callback(null, deleted)
} else callback(null, deleted)
} catch (e) {
callback(e, null)
}
}
function _removeShortname (self, shortname, callback) {
try {
var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db)
delete obj[shortname]
// save it back to the DB
if (!self.isMemDb) jsonfile.writeFileSync(self.db, obj)
debug('whole shortname urls removed')
callback(null)
} catch (e) {
callback(e)
}
}
// async method
WebHooks.prototype.getDB = function () {
// return the whole JSON DB file.
var self = this
return new Promise(function (resolve, reject) {
if (self.isMemDb) resolve(self.db)
jsonfile.readFile(self.db, function (err, obj) {
if (err) {
reject(err) // file not found
} else {
resolve(obj) // file exists
}
})
})
}
// async method
WebHooks.prototype.getWebHook = function (shortname) {
// return the selected WebHook.
var self = this
return new Promise(function (resolve, reject) {
if (self.isMemDb) {
resolve(self.db[shortname] || {})
} else {
jsonfile.readFile(self.db, function (err, obj) {
if (err) {
reject(err) // file not found
} else {
resolve(obj[shortname] || {}) // file exists
}
})
}
})
}
WebHooks.prototype.getListeners = function () {
return _functions
}
WebHooks.prototype.getEmitter = function () {
return this.emitter
}
module.exports = WebHooks