Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 42 additions & 41 deletions pd-notifier/background/pd-notifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ function PagerDutyNotifier()
self.openOnAck = false; // Whether to open the incident in a new tab when ack-ing.
self.notifSound = false; // Whether to play a notification sound.
self.requireInteraction = false; // Whether the notification will require user interaction to dismiss.
self.filterServices = null; // ServiceID's of services to only show alerts for.
self.filterUsers = null; // UserID's of users to only show alerts for.
self.basicFilters = {}
for (var filter in basicFilters)
self.basicFilters[filter] = null;
self.advancedFilter = false; // Compiled advanced filter.
self.pdapi = null; // Helper for API calls.
self.poller = null; // This points to the interval function so we can clear it if needed.
self.showBadgeUpdates = false; // Whether we show updates on the toolbar badge.
self.badgeLocation = null; // Which view should be linked to from the badge icon.
self.maxNotifAtOnce = 5;

// Ctor
self._construct = function _construct()
Expand All @@ -46,20 +49,7 @@ function PagerDutyNotifier()
// This loads any configuration we have stored with chrome.storage
self.loadConfiguration = function loadConfiguration(callback)
{
chrome.storage.sync.get(
{
pdAccountSubdomain: '',
pdAPIKey: null,
pdIncludeLowUrgency: false,
pdRemoveButtons: false,
pdOpenOnAck: false,
pdNotifSound: false,
pdRequireInteraction: false,
pdFilterServices: null,
pdFilterUsers: null,
pdShowBadgeUpdates: false,
pdBadgeLocation: 'triggered',
},
chrome.storage.sync.get( storageDefaults,
function(items)
{
self.account = items.pdAccountSubdomain;
Expand All @@ -69,10 +59,12 @@ function PagerDutyNotifier()
self.openOnAck = items.pdOpenOnAck;
self.notifSound = items.pdNotifSound;
self.requireInteraction = items.pdRequireInteraction;
self.filterServices = items.pdFilterServices;
self.filterUsers = items.pdFilterUsers;
for (var filter in basicFilters) {
self.basicFilters[filter] = items[basicFilters[filter]];
}
self.showBadgeUpdates = items.pdShowBadgeUpdates;
self.badgeLocation = items.pdBadgeLocation;
self.advancedFilter = compileAdvancedFilter(items.pdAdvancedFilter);
callback(true);
});
}
Expand Down Expand Up @@ -134,12 +126,16 @@ function PagerDutyNotifier()
// Construct the URL
var url = 'https://' + self.account + '.pagerduty.com/api/v1/incidents?'
+ 'statuses[]=triggered&'
+ 'since=' + since.toISOString() + '&'
+ 'limit=5&'; // More than this would be silly to show notifications for.
+ 'since=' + since.toISOString() + '&';
url = self.includeFilters(url);

// Make the request.
self.pdapi.GET(url, self.parseIncidents);
var counter = 0;
self.pdapi.Paginate(url, 'incidents',
function (incident) {
if (self.parseIncident(incident))
counter += 1;
return counter < self.maxNotifAtOnce;
}
)
}

// Adds filters to a URL we'll be using in a request
Expand All @@ -148,31 +144,36 @@ function PagerDutyNotifier()
// Limit to high urgency if that's all the user wants.
if (!self.includeLowUgency) { url = url + 'urgencies[]=high&'; }

// Add a service filter if we have one.
if (self.filterServices && self.filterServices != null && self.filterServices != "")
{
self.filterServices.split(',').forEach(function(s)
// Limit to users/services/teams
for (var filter in basicFilters) {
var value = self.basicFilters[filter]
if (value && value != null && value != "")
{
url = url + 'service_ids[]=' + s + '&';
});
}

// Add a user filter if we have one.
if (self.filterUsers && self.filterUsers != null && self.filterUsers != "")
{
self.filterUsers.split(',').forEach(function(s)
{
url = url + 'user_ids[]=' + s + '&';
});
value.split(',').forEach(function(s)
{
url = url + filter + '_ids[]=' + s + '&';
});
}
}

return url;
}

// This will parse the AJAX response and trigger notifications for each incident.
self.parseIncidents = function parseIncidents(data)
// This checks if incident should be displayed as notification.
// Returns true if notification was triggered.
self.parseIncident = function parseIncidents(incident)
{
for (var i in data.incidents) { self.triggerNotification(data.incidents[i]); }
if (self.advancedFilter) {
try {
if (! self.advancedFilter(incident) )
return false;
} catch (e) {
console.log("Advanced filter exception:", e)
// Filter failed, assume that incident should be displayed
}
}
self.triggerNotification(incident);
return true;
}

// This will update the icon badge in the toolbar.
Expand Down
230 changes: 230 additions & 0 deletions pd-notifier/lib/advanced-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
"use strict";
function AdvancedFilterCompiler()
{
var self = this;
this.debug = 0;
this.compilers = {
bool_op: function compile_bool_op(kwdef, stack) {
if (stack.nextUnexpected('bool_op')) return false;
stack.push({
next: { property: true, paren_left: true, bool_un: true },
bool_op: true,
code: '(' + stack.pop().code + ') ' + kwdef.code + ' ',
});
return true;
},
bool_un: function compile_bool_un(kwdef, stack) {
if (stack.top() && stack.nextUnexpected('paren_left')) return false;
stack.push({
next: { property: true, paren_left: true, bool_un: true },
bool_op: true,
code: kwdef.code + ' ',
});
return true;
},
property: function compile_property(kwdef, stack) {
if (stack.top() && stack.nextUnexpected('property')) return false;
stack.push( {
next: { prop_op: true},
code: kwdef.code
});
return true;
},
prop_op: function compile_prop_op(kwdef, stack) {
if (stack.nextUnexpected('prop_op')) return false;
stack.push({
next: { string: true},
lhs: stack.pop(),
code: kwdef.code,
add_rhs_and_push: function(rhs, stack) {
var op = this;
return stack.pushBool(
'( ((lhs,rhs) => lhs.reduce( (acc, item) => acc || (' + op.code + ')(item, rhs), false )) (' + op.lhs.code + ', ' + rhs + ') )'
);
}
});
return true;
},
string: function compile_string(strdef, stack) {
if (stack.nextUnexpected('string')) return false;
return stack.pop().add_rhs_and_push(strdef.code, stack);
},
paren_left: function compile_paren_left(_unused, stack) {
if (stack.top() && stack.nextUnexpected('paren_left')) return false;
stack.push({
next:{ property: true, paren_left: true, bool_un: true },
paren_left: true
})
return true;
},
paren_right: function compile_paren_right(_unused, stack) {
if (stack.top() && stack.nextUnexpected('paren_right')) return false;
var code = '(' + stack.pop().code + ')';
if (! stack.safetop().paren_left) {
console.log("Expected left parenthesis on stack, got:", stack.top());
return false;
}
stack.pop();
return stack.pushBool(code);
},
};
this.tokens = {
whitespace:{
rx: /^\s+/,
insert: (matchObject, stack) => true,
},
keyword:{
rx: /^([A-Za-z][_A-Za-z0-9]+|=)/,
insert: function insert_keyword(matchObject, stack) {
var kwdef = self.keywords[matchObject[0]];
var parser = kwdef && self.compilers[kwdef.type];
if (parser)
return parser(kwdef, stack);
if (self.debug) console.log("Unknown parser for:", matchObject);
return false;
},
},
string:{
rx: /^('[^\\']*'|"[^\\"]*")/, //"',
insert: (matchObject, stack) => self.compilers.string({code: matchObject[0]}, stack),
},
paren_left: {
rx: /^\(/,
insert: self.compilers.paren_left,
},
paren_right: {
rx: /^\)/,
insert: self.compilers.paren_right,
},
};
this.keywords = {
'AND': { type:'bool_op', code: '&&' },
'OR': { type:'bool_op', code: '||' },
'NOT': { type:'bool_un', code: '!' },

'service_id': { type:'property', code: '[ incident.service.id ]'},
'team_id': { type:'property', code: 'incident.teams.map( x => x && x.id )'},
'user_id': { type:'property',
code: 'incident.assignments.map( x => x && x.assignee && x.assignee.id )'},

'=': { type:'prop_op', code: '( (prop, value) => (prop == value) )' },
};

this.compileAdvancedFilter = function compileAdvancedFilter(filter) {
var stack = [];
stack.top = function () { return this[this.length - 1 ]; };
stack.safetop = function () { if (this.length == 0) return {}; else return this[this.length - 1]; };
stack.next = function () { var n = stack.safetop().next; if (n) return n; else return {}; };
stack.nextUnexpected = function (name) {
if (stack.next()[name]) return false;
if (self.debug) console.log("[" + name + "] object on stack does not want me!", stack.top());
return true;
};
stack.pushBool = function (code) {
if (self.debug > 3) console.log("[Pushing bool]", code);
if (this.safetop().bool_op) {
var op = this.pop();
return this.pushBool( op.code + '(' + code + ')' );
} else {
this.push({
next: { bool_op: true, paren_right: true },
code: code
});
if (self.debug > 3) console.log("Stack:", this);
return true;
}
};
var pos = 0;

while (filter.slice(pos).length > 0) {
var match = false;
for (var token in self.tokens) {
var match = self.tokens[token].rx.exec(filter.slice(pos));
if (match) {
if (self.debug > 3)
console.log("\nToken:", token, " match:", match);
if (!self.tokens[token].insert(match, stack))
// expression error
return null;
if (self.debug > 3)
console.log("Stack:", stack);
pos += match[0].length;
break;
}
}
if (! match) {
if (self.debug) console.log("Invalid token: '" + filter.slice(pos) + "'");
return null;
}
}
if (self.debug > 3) console.log("Input end, stack:", stack);
if (stack.length == 1 && stack.next().paren_right) { //Expression may be enclosed in parentheses
if (self.debug > 1)
return stack[0].code;
try {
return Function('incident', 'return (' + stack[0].code + ');');
} catch (err) {
// Internal error
console.log(err);
return null;
}
} else if (stack.length == 0) {
// empty expression
return false;
} else {
// invalid expression
return null;
}
}
};


var compileAdvancedFilter = (function () {
var compiler = new AdvancedFilterCompiler();
return x => compiler.compileAdvancedFilter(x);
})();

// SELF TEST in Node.js
if ((typeof process === 'object') && (typeof process.release === 'object') && process.release.name === 'node') {
var compiler = new AdvancedFilterCompiler();
var incident = {
assignments:[ { assignee:{ id:"PUSER1" } }, { assignee:{ id:"PUSER2" } } ],
teams:[ { id:"PTEAM1" }, { id:"PTEAM2" } ],
service: { id: "PSERVICE1" }
};
var tests = {
'':"empty",
' user_id = "P123456" ':false,
' user_id = "PUSER1" ':true,
' team_id = "P123456" ':false,
' team_id = "PTEAM1" ':true,
' service_id = "P123456" ':false,
' service_id = "PSERVICE1" ':true,
' NOT service_id = "PSERVICE1" ':false,
' NOT service_id = "P123456" ':true,
' user_id = "PUSER1" AND service_id = "POFF" OR team_id = "PTEAM1" OR user_id = "POOH" ':true,
' (user_id = "PUSER1") AND service_id = "POFF" OR team_id = "PTEAM1" OR user_id = "POOH" ':true,
' (user_id = "PUSER1" AND service_id = "POFF") OR ( ( team_id = "PTEAM1" OR user_id = "POOH") ) ':true,
' (user_id = "PUSER1" AND service_id = "POFF") OR (team_id = "PTEAM1" AND user_id = "POOH") ': false,
};

var logResult = function(test, value) {
if (tests[test] == value)
console.log(" [ok] , Result:", value);
else {
console.log("!FAIL!, Result:", value);
compiler.debug = 5;
console.log(compiler.compileAdvancedFilter(filter));
}
};

for (var filter in tests) {
console.log("\nCompiling:", filter);
compiler.debug = 1;
var f = compiler.compileAdvancedFilter(filter);
if (!f) {
if (f == false) logResult(filter, "empty")
else logResult(filter, "invalid")
} else logResult(filter, f(incident));
};
}
31 changes: 31 additions & 0 deletions pd-notifier/lib/pd-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,35 @@ function PDAPI(apiKey, version = 2)
req.setRequestHeader("Content-Type", "application/json");
req.send(data);
}

// Perform a paginated GET request.
this.Paginate = function Paginate(url, list_name, item_callback, error_callback = null)
{
const items_limit = 50;
var pages_limit = 20; // 20*50 = 1000 should be enough. Do not overload servers with big data.
var offset = 0;
var parseResponse = function(data) {
if (data.offset != offset) {
error_callback(-999, "Invalid offset in response")
return null;
}
for (var i in data[list_name]) {
try {
if (item_callback(data[list_name][i]) === false) {
error_callback(-998, "Item callback returned false");
return null;
}
} catch (e) {
error_callback(-997, "Item callback raised an error");
return null;
}
}
if (data.more && data[list_name].length > 0 && pages_limit > 0) {
pages_limit -= 1;
offset += data[list_name].length;
self.GET(url + 'offset=' + offset + '&limit=' + items_limit, parseResponse, error_callback);
}
}
self.GET(url + 'offset=' + offset + '&limit=' + items_limit, parseResponse, error_callback);
}
}
Loading