diff --git a/pd-notifier/audio/notification.html b/pd-notifier/audio/notification.html new file mode 100644 index 0000000..d7afa8a --- /dev/null +++ b/pd-notifier/audio/notification.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pd-notifier/background/pd-notifier.js b/pd-notifier/background/pd-notifier.js deleted file mode 100644 index 7aa7309..0000000 --- a/pd-notifier/background/pd-notifier.js +++ /dev/null @@ -1,389 +0,0 @@ -// Simple script to poll PagerDuty API for new incidents, and trigger a Chrome notification for -// any it finds. Will also give user ability to ack/resolve incidents right from the notifs. - -// Will poll continually at the pollInterval until it's destroyed (_destruct() is called). -function PagerDutyNotifier() -{ - // Members - var self = this; // Self-reference - self.account = null; // The PagerDuty account subdomain to check. - self.apiKey = null; // Optional API key to not require active session. - self.pollInterval = 15; // Number of seconds between checking for new notifications. - self.includeLowUgency = false; // Whether to include low urgency incidents. - self.removeButtons = false; // Whether or not to unclude the action buttons. - 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.filterTeams = null; // TeamID's of teams to only show alerts for. - 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. - - // Ctor - self._construct = function _construct() - { - // Load in configuration, and then set up everything we need. - self.loadConfiguration(function() - { - // If no account set up (first install), then do nothing. User will need to add - // config. Once they save, a reload will be triggered and things will kick off. - if (self.account == null || self.account == '') { return; } - - self.pdapi = new PDAPI(self.apiKey); - self.setupPoller(); - }); - } - - // Dtor - self._destruct = function _destruct() - { - clearInterval(self.poller); - self = null; - } - - // 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, - pdFilterTeams: null, - pdShowBadgeUpdates: false, - pdBadgeLocation: 'triggered', - }, - function(items) - { - self.account = items.pdAccountSubdomain; - self.apiKey = items.pdAPIKey; - self.includeLowUgency = items.pdIncludeLowUrgency; - self.removeButtons = items.pdRemoveButtons; - self.openOnAck = items.pdOpenOnAck; - self.notifSound = items.pdNotifSound; - self.requireInteraction = items.pdRequireInteraction; - self.filterServices = items.pdFilterServices; - self.filterUsers = items.pdFilterUsers; - self.filterTeams = items.pdFilterTeams; - self.showBadgeUpdates = items.pdShowBadgeUpdates; - self.badgeLocation = items.pdBadgeLocation; - callback(true); - }); - } - - // This will set up the poller process. - self.setupPoller = function setupPoller() - { - self.poller = setInterval(function() { self.polled(); }, self.pollInterval * 1000); - self.polled(); - } - - // This is the method that's executed on each poll. - self.polled = function polled() - { - self.pollNewIncidents(); - self.updateToolbarBadge(); - } - - // This will handle the event triggered from clicking one of the notification's buttons. - self.handlerButtonClicked = function handlerButtonClicked(notificationId, buttonIndex) - { - if (notificationId == 'test') { return; } // Ignore for test notifications. - - switch (buttonIndex) - { - case 0: // Acknowledge - self.pdapi.PUT( - 'https://' + self.account + '.pagerduty.com/api/v1/incidents/' + notificationId, - '{"incident":{"type":"incident_reference","status":"acknowledged"}}' - ); - if (self.openOnAck) { self.handlerNotificationClicked(notificationId); } - break; - - case 1: // Resolve - self.pdapi.PUT( - 'https://' + self.account + '.pagerduty.com/api/v1/incidents/' + notificationId, - '{"incident":{"type":"incident_reference","status":"resolved"}}' - ); - break; - } - setTimeout(function() { self.updateToolbarBadge(); }, 200); // Force a badge update, so it changes quickly. - } - - // This will handle the event triggered when clicking on the main notification area. - self.handlerNotificationClicked = function handlerNotificationClicked(notificationId) - { - if (notificationId == 'test') { return; } // Ignore for test notifications. - window.open('https://' + self.account + '.pagerduty.com/incidents/' + notificationId); - } - - // This is the poller action, which will trigger an API request and then pass any incidents - // it gets to the parsing function. - self.pollNewIncidents = function pollNewIncidents() - { - // Sanity check that an account has been set. - if (self.account == '') { return; } - - // We only want events triggered since we last polled. - var since = new Date(); - since.setSeconds(since.getSeconds() - self.pollInterval); - - // 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. - url = self.includeFilters(url); - - // Make the request. - self.pdapi.GET(url, self.parseIncidents); - } - - // Adds filters to a URL we'll be using in a request - self.includeFilters = function includeFilters(url) - { - // 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) - { - 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 + '&'; - }); - } - - // Add a team filter if we have one. - if (self.filterTeams && self.filterTeams != null && self.filterTeams != "") - { - self.filterTeams.split(',').forEach(function(s) - { - url = url + 'team_ids[]=' + s + '&'; - }); - } - - return url; - } - - // This will parse the AJAX response and trigger notifications for each incident. - self.parseIncidents = function parseIncidents(data) - { - for (var i in data.incidents) { self.triggerNotification(data.incidents[i]); } - } - - // This will update the icon badge in the toolbar. - self.updateToolbarBadge = function updateToolbarBadge() - { - if (!self.showBadgeUpdates) - { - chrome.browserAction.setBadgeText({ text: '' }); - return; - } - - // Check for any triggered incidents at all that follow our filters. - var url = self.includeFilters('https://' + self.account + '.pagerduty.com/api/v1/incidents?statuses[]=triggered&total=true&') - self.pdapi.GET(url, function(data) - { - // If there was an error in the response, show "Err" and log it. - if (data.error != null) - { - console.error("PagerDuty API returned an error while getting the incident count for the toolbar icon.", { - api_url: url, - error_returned: data.error - }); - chrome.browserAction.setBadgeText({ text: 'Err.' }); - chrome.browserAction.setBadgeBackgroundColor({ color: [90, 90, 90, 255] }); - return; - } - - // If there are no incidents, or an error in the response, show nothing on badge. - if (data.total == null || data.total == 0) - { - chrome.browserAction.setBadgeText({ text: '' }); - return; - } - - // Otherwise, we have incidents, show the count. - chrome.browserAction.setBadgeText({ text: '' + data.total }); - chrome.browserAction.setBadgeBackgroundColor({ color: [189, 0, 0, 255] }); - }); - } - - // This will open a tab to the dashboard using the relevant user settings. - self.openDashboard = function openDashboard() - { - // Determine the correct URL based on user's options. - statuses = ''; - switch(self.badgeLocation) - { - case 'open': statuses = '?status=triggered,acknowledged'; break; - case 'triggered': statuses = '?status=triggered'; break; - case 'acknowledged': statuses = '?status=acknowledged'; break; - case 'any': statuses = '?status=acknowledged,triggered,resolved'; break; - } - - // Open the tab - chrome.tabs.create({ 'url': 'https://' + self.account + '.pagerduty.com/incidents' + statuses }) - } - - // This will trigger the actual notification based on an incident object. - self.triggerNotification = function triggerNotification(incident) - { - // Define the buttons to show in the notification. Will be empty if user asked to remove. - var buttons = self.removeButtons ? [] : [ - { - title: "Acknowledge", - iconUrl: chrome.extension.getURL("images/icon-acknowledge.png") - }, - { - title: "Resolve", - iconUrl: chrome.extension.getURL("images/icon-resolve.png") - } - ]; - - chrome.notifications.create(incident.id, - { - type: "basic", - iconUrl: chrome.extension.getURL("images/icon-256.png"), - title: incident.summary, - message: "Service: " + incident.service.summary, - contextMessage: incident.urgency.charAt(0).toUpperCase() + incident.urgency.slice(1) + " Urgency", - priority: 0, - isClickable: true, - buttons: buttons, - requireInteraction: self.requireInteraction - }); - - // Trigger notification sound if user wants it. - if (self.notifSound) - { - var notifSound = new Audio("audio/notification.mp3"); - notifSound.play(); - } - } - - self._construct(); -} - -// Add event handlers for button/notification clicks, and delegate to the currently active notifier object. -chrome.notifications.onButtonClicked.addListener(function(notificationId, buttonIndex) -{ - chrome.runtime.getBackgroundPage(function(bgpg) - { - bgpg.getNotifier().handlerButtonClicked(notificationId, buttonIndex); - chrome.notifications.clear(notificationId); - }); -}); -chrome.notifications.onClicked.addListener(function(notificationId) -{ - chrome.runtime.getBackgroundPage(function(bgpg) - { - bgpg.getNotifier().handlerNotificationClicked(notificationId); - chrome.notifications.clear(notificationId); - }); -}); - -// Add event handler for the toolbar icon click. -chrome.browserAction.onClicked.addListener(function(tab) -{ - chrome.runtime.getBackgroundPage(function(bgpg) - { - bgpg.getNotifier().openDashboard(); - }); -}); - -// If this is the first installation, show the options page so user can set up their settings. -chrome.runtime.onInstalled.addListener(function(details) -{ - if (details.reason == 'install') - { - chrome.tabs.create({ 'url': 'chrome://extensions/?options=' + chrome.runtime.id }); - } -}); - -// The currently active notifier object, and accessor. -var _pdNotifier = null; -function getNotifier() { return _pdNotifier; } - -// This will reload/trigger the the notifier (and pick up any new configuration options). -function reloadNotifier() -{ - if (_pdNotifier != null) { _pdNotifier._destruct(); } - _pdNotifier = new PagerDutyNotifier(); -} - -// Add option to clear all notifications to icon context-menu. -chrome.contextMenus.create({ - title: "Clear all notifications", - id: "pd_clear_all", - contexts: ["browser_action"], - visible: true -}); - -chrome.contextMenus.onClicked.addListener(function(info, tab) -{ - if (info.menuItemId === "pd_clear_all") - { - chrome.notifications.getAll(function(notifs) - { - for (var i in notifs) { chrome.notifications.clear(i); } - }); - } -}); - -// Add option to trigger a test notification popup. -chrome.contextMenus.create({ - title: "Show test notification", - id: "pd_test_notification", - contexts: ["browser_action"], - visible: true -}); - -chrome.contextMenus.onClicked.addListener(function(info, tab) -{ - if (info.menuItemId === "pd_test_notification") - { - _pdNotifier.triggerNotification({ - 'id': 'test', - 'summary': 'Test Notification', - 'service': { - 'summary': 'Test Service' - }, - 'urgency': 'high' - }) - } -}); - -// Listen for Chrome Alarms and retrigger the notifier when one is caught. -chrome.alarms.onAlarm.addListener(function(alarm) -{ - chrome.runtime.getBackgroundPage(function(bgpg) - { - bgpg.reloadNotifier(); - }); -}); - -// Sets up a Chrome Alarm to retrigger the notifier every so often, to make sure it's always running. -chrome.alarms.create("pagerduty-notifier", { periodInMinutes: 1 }); - -// Initial run, as alarm won't trigger immediately. -reloadNotifier(); diff --git a/pd-notifier/lib/pd-api.js b/pd-notifier/lib/pd-api.js index ceaf284..2a3dd16 100644 --- a/pd-notifier/lib/pd-api.js +++ b/pd-notifier/lib/pd-api.js @@ -1,55 +1,72 @@ // Helper wrappers for PagerDuty API methods. -function PDAPI(apiKey, version = 2) -{ +export default function PDAPI(apiKey, version = 2) { // Members - var self = this; // Self-reference - self.apiKey = apiKey; // API key used for requests. - self.userAgent = "pd-chrome-notifier-" + chrome.app.getDetails().version; // Will be in the X-Requested-With header of requests. - - // Wrapper for generic XMLHttpRequest stuff - this.prepareRequest = function prepareRequest(method, url) - { - var xhr = new XMLHttpRequest(); - xhr.open(method, url, true); - xhr.setRequestHeader("X-Requested-With", self.userAgent); - xhr.setRequestHeader("X-PagerDuty-Api-Local", 1); - xhr.setRequestHeader("Accept", "application/vnd.pagerduty+json;version=" + version); + var self = this; // Self-reference + self.apiKey = apiKey; // API key used for requests. + self.userAgent = "pd-chrome-notifier-" + chrome.runtime.getManifest().version; // Will be in the X-Requested-With header of requests. + + // Perform a GET request, and trigger the callback with the result. + this.GET = function GET(url, callback, error_callback = null) { + const headers = new Headers(); + headers.append('X-Requested-With', self.userAgent); + headers.append('X-PagerDuty-Api-Local', 1); + headers.append('Accept', "application/vnd.pagerduty+json;version=" + version); // If we have a valid API key, authenticate using that. - if (self.apiKey != null && self.apiKey.length == 20) - { - xhr.setRequestHeader("Authorization", "Token token=" + self.apiKey); + if (self.apiKey != null && self.apiKey.length == 20) { + headers.append('Authorization', 'Token token=' + self.apiKey); } - return xhr; - } + const init = { + method: "GET", + headers: headers, + mode: "cors", + cache: "default" + }; - // Perform a GET request, and trigger the callback with the result. - this.GET = function GET(url, callback, error_callback = null) - { - var req = self.prepareRequest("GET", url); - req.onreadystatechange = function() - { - if (req.readyState == 4) - { - try - { - callback(JSON.parse(req.responseText)); - } - catch(e) - { - if (error_callback != null) { error_callback(req.status, req.responseText); } - } + fetch(url, init).then((res) => { + if (res.ok) { + res.json().then((data) => { + try { + callback(data); + } + catch (e) { + if (error_callback != null) { error_callback(res.statusText, res.statusText); } + } + }); + } else { + console.error(res); + if (error_callback != null) { error_callback(res.statusText, res.statusText); } } - }; - req.send(); - } + }); + }; // Fire and forget a PUT request. - this.PUT = function PUT(url, data) - { - var req = self.prepareRequest("PUT", url); - req.setRequestHeader("Content-Type", "application/json"); - req.send(data); - } + this.PUT = function PUT(url, data) { + const headers = new Headers(); + headers.append('X-Requested-With', self.userAgent); + headers.append('X-PagerDuty-Api-Local', 1); + headers.append('Accept', "application/vnd.pagerduty+json;version=" + version); + + // If we have a valid API key, authenticate using that. + if (self.apiKey != null && self.apiKey.length == 20) { + headers.append('Authorization', 'Token token=' + self.apiKey); + } + + headers.append('Content-Type', 'application/json'); + + const init = { + method: "PUT", + headers: headers, + mode: "cors", + cache: "default", + body: data + }; + + fetch(url, init).then((res) => { + if (!res.ok) { + console.error(res.status); + } + }); + }; } diff --git a/pd-notifier/lib/pd-notifier.js b/pd-notifier/lib/pd-notifier.js new file mode 100644 index 0000000..ea0c4bf --- /dev/null +++ b/pd-notifier/lib/pd-notifier.js @@ -0,0 +1,280 @@ +import PDAPI from "./pd-api.js"; + +// Simple script to poll PagerDuty API for new incidents, and trigger a Chrome notification for +// any it finds. Will also give user ability to ack/resolve incidents right from the notifs. + +// Will poll continually at the pollInterval until it's destroyed (_destruct() is called). +export default function PagerDutyNotifier() { + // Members + var self = this; // Self-reference + self.account = null; // The PagerDuty account subdomain to check. + self.apiKey = null; // Optional API key to not require active session. + self.pollInterval = 15; // Number of seconds between checking for new notifications. + self.includeLowUgency = false; // Whether to include low urgency incidents. + self.removeButtons = false; // Whether or not to unclude the action buttons. + 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.filterTeams = null; // TeamID's of teams to only show alerts for. + 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. + + // Ctor + self._construct = function _construct() { + // Load in configuration, and then set up everything we need. + self.loadConfiguration(function () { + // If no account set up (first install), then do nothing. User will need to add + // config. Once they save, a reload will be triggered and things will kick off. + if (self.account == null || self.account == '') { return; } + + self.pdapi = new PDAPI(self.apiKey); + self.setupPoller(); + }); + }; + + // Dtor + self._destruct = function _destruct() { + clearInterval(self.poller); + self = null; + }; + + // 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, + pdFilterTeams: null, + pdShowBadgeUpdates: false, + pdBadgeLocation: 'triggered', + }, + function (items) { + self.account = items.pdAccountSubdomain; + self.apiKey = items.pdAPIKey; + self.includeLowUgency = items.pdIncludeLowUrgency; + self.removeButtons = items.pdRemoveButtons; + self.openOnAck = items.pdOpenOnAck; + self.notifSound = items.pdNotifSound; + self.requireInteraction = items.pdRequireInteraction; + self.filterServices = items.pdFilterServices; + self.filterUsers = items.pdFilterUsers; + self.filterTeams = items.pdFilterTeams; + self.showBadgeUpdates = items.pdShowBadgeUpdates; + self.badgeLocation = items.pdBadgeLocation; + callback(true); + }); + }; + + // This will set up the poller process. + self.setupPoller = function setupPoller() { + self.poller = setInterval(function () { self.polled(); }, self.pollInterval * 1000); + self.polled(); + }; + + // This is the method that's executed on each poll. + self.polled = function polled() { + self.pollNewIncidents(); + self.updateToolbarBadge(); + }; + + // This will handle the event triggered from clicking one of the notification's buttons. + self.handlerButtonClicked = function handlerButtonClicked(notificationId, buttonIndex) { + if (notificationId == 'test') { return; } // Ignore for test notifications. + + switch (buttonIndex) { + case 0: // Acknowledge + self.pdapi.PUT( + 'https://' + self.account + '.pagerduty.com/api/v1/incidents/' + notificationId, + '{"incident":{"type":"incident_reference","status":"acknowledged"}}' + ); + if (self.openOnAck) { self.handlerNotificationClicked(notificationId); } + break; + + case 1: // Resolve + self.pdapi.PUT( + 'https://' + self.account + '.pagerduty.com/api/v1/incidents/' + notificationId, + '{"incident":{"type":"incident_reference","status":"resolved"}}' + ); + break; + } + setTimeout(function () { self.updateToolbarBadge(); }, 200); // Force a badge update, so it changes quickly. + }; + + // This will handle the event triggered when clicking on the main notification area. + self.handlerNotificationClicked = function handlerNotificationClicked(notificationId) { + if (notificationId == 'test') { return; } // Ignore for test notifications. + // Open the tab + chrome.tabs.create({ 'url': 'https://' + self.account + '.pagerduty.com/incidents/' + notificationId }); + }; + + // This is the poller action, which will trigger an API request and then pass any incidents + // it gets to the parsing function. + self.pollNewIncidents = function pollNewIncidents() { + // Sanity check that an account has been set. + if (self.account == '') { return; } + + // We only want events triggered since we last polled. + var since = new Date(); + since.setSeconds(since.getSeconds() - self.pollInterval); + + // 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. + url = self.includeFilters(url); + + // Make the request. + self.pdapi.GET(url, self.parseIncidents); + }; + + // Adds filters to a URL we'll be using in a request + self.includeFilters = function includeFilters(url) { + // 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) { + 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 + '&'; + }); + } + + // Add a team filter if we have one. + if (self.filterTeams && self.filterTeams != null && self.filterTeams != "") { + self.filterTeams.split(',').forEach(function (s) { + url = url + 'team_ids[]=' + s + '&'; + }); + } + + return url; + }; + + // This will parse the AJAX response and trigger notifications for each incident. + self.parseIncidents = function parseIncidents(data) { + for (var i in data.incidents) { + self.triggerNotification(data.incidents[i]); + } + }; + + // This will update the icon badge in the toolbar. + self.updateToolbarBadge = function updateToolbarBadge() { + if (!self.showBadgeUpdates) { + chrome.action.setBadgeText({ text: '' }); + return; + } + + // Check for any triggered incidents at all that follow our filters. + var url = self.includeFilters('https://' + self.account + '.pagerduty.com/api/v1/incidents?statuses[]=triggered&total=true&'); + self.pdapi.GET(url, function (data) { + // If there was an error in the response, show "Err" and log it. + if (data.error != null) { + console.error("PagerDuty API returned an error while getting the incident count for the toolbar icon.", { + api_url: url, + error_returned: data.error + }); + chrome.action.setBadgeText({ text: 'Err.' }); + chrome.action.setBadgeBackgroundColor({ color: [90, 90, 90, 255] }); + return; + } + + // If there are no incidents, or an error in the response, show nothing on badge. + if (data.total == null || data.total == 0) { + chrome.action.setBadgeText({ text: '' }); + return; + } + + // Otherwise, we have incidents, show the count. + chrome.action.setBadgeText({ text: '' + data.total }); + chrome.action.setBadgeBackgroundColor({ color: [189, 0, 0, 255] }); + }); + }; + + // This will open a tab to the dashboard using the relevant user settings. + self.openDashboard = function openDashboard() { + // Determine the correct URL based on user's options. + let statuses = ''; + switch (self.badgeLocation) { + case 'open': statuses = '?status=triggered,acknowledged'; break; + case 'triggered': statuses = '?status=triggered'; break; + case 'acknowledged': statuses = '?status=acknowledged'; break; + case 'any': statuses = '?status=acknowledged,triggered,resolved'; break; + } + + // Open the tab + chrome.tabs.create({ 'url': 'https://' + self.account + '.pagerduty.com/incidents' + statuses }); + }; + + // This will trigger the actual notification based on an incident object. + self.triggerNotification = function triggerNotification(incident) { + // Define the buttons to show in the notification. Will be empty if user asked to remove. + var buttons = self.removeButtons ? [] : [ + { + title: "Acknowledge", + iconUrl: chrome.runtime.getURL("images/icon-acknowledge.png") + }, + { + title: "Resolve", + iconUrl: chrome.runtime.getURL("images/icon-resolve.png") + } + ]; + + chrome.notifications.create(incident.id, + { + type: "basic", + iconUrl: chrome.runtime.getURL("images/icon-256.png"), + title: incident.summary, + message: "Service: " + incident.service.summary, + contextMessage: incident.urgency.charAt(0).toUpperCase() + incident.urgency.slice(1) + " Urgency", + priority: 0, + isClickable: true, + buttons: buttons, + requireInteraction: self.requireInteraction + }); + + // Trigger notification sound if user wants it. + if (self.notifSound) { + self.playNotification(); + } + }; + + // Creates an offscreen document to play the audio notification + self.playNotification = async function () { + try { + if (await chrome.offscreen.hasDocument()) { + await chrome.offscreen.closeDocument(); + self.playNotification(); + return; + } + await chrome.offscreen.createDocument({ + url: chrome.runtime.getURL('../audio/notification.html'), + reasons: ['AUDIO_PLAYBACK'], + justification: 'Play PagerDuty notification sound', + }); + } catch (e) { + // When lots of notifications come in at once + // they try to create lots of offscreen documents + // but we can only create one, and that's all we need + } + }; + + self._construct(); +} diff --git a/pd-notifier/manifest.json b/pd-notifier/manifest.json index f9771df..cb95cbf 100644 --- a/pd-notifier/manifest.json +++ b/pd-notifier/manifest.json @@ -1,37 +1,35 @@ { - "manifest_version": 2, - "name": "PagerDuty Notifier", - "short_name": "PD Notifier", - "description": "Desktop notifications for your PagerDuty incidents.", - "version": "0.23", - "author": "Rich Adams (https://richadams.me)", - "icons": { - "16": "images/icon-16.png", - "32": "images/icon-32.png", - "64": "images/icon-64.png", - "128": "images/icon-128.png", - "256": "images/icon-256.png" - }, - "permissions": [ - "notifications", - "background", - "storage", - "alarms", - "https://*.pagerduty.com/api/v1/*", - "contextMenus" - ], - "options_ui": { - "open_in_tab": true, - "page": "options/options.html" - }, - "background": { - "scripts": [ - "lib/pd-api.js", - "background/pd-notifier.js" - ], - "persistent": true - }, - "browser_action": { - "default_icon": "images/browser-icon-32.png" - } + "manifest_version": 3, + "name": "PagerDuty Notifier", + "short_name": "PD Notifier", + "description": "Desktop notifications for your PagerDuty incidents.", + "version": "0.23", + "author": "Rich Adams (https://richadams.me)", + "icons": { + "16": "images/icon-16.png", + "32": "images/icon-32.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "256": "images/icon-256.png" + }, + "permissions": [ + "notifications", + "background", + "storage", + "alarms", + "contextMenus", + "offscreen" + ], + "host_permissions": ["https://*.pagerduty.com/api/v1/*"], + "options_ui": { + "open_in_tab": true, + "page": "options/options.html" + }, + "background": { + "service_worker": "service_worker.js", + "type": "module" + }, + "action": { + "default_icon": "images/browser-icon-32.png" + } } diff --git a/pd-notifier/options/api-validate.js b/pd-notifier/options/api-validate.js index 93151e8..378ba25 100644 --- a/pd-notifier/options/api-validate.js +++ b/pd-notifier/options/api-validate.js @@ -1,100 +1,86 @@ +import PDAPI from '../lib/pd-api.js'; // These functions will allow us to validate the API access, so people can self-debug any incorrect credentials. // Should be exactly 20 chars long. -function isKeyFormatValid() -{ +function isKeyFormatValid() { e = getElement('api-key'); return (e.value == "" || e.value.length == 20); } // Status message updater helpers. -function statusError(message = '') -{ +function statusError(message = '') { getElement('access-status').className = 'ko'; getElement('access-status').innerHTML = '✗ ' + message; } -function statusOK(message = '') -{ +function statusOK(message = '') { getElement('access-status').className = 'ok'; getElement('access-status').innerHTML = '✓ ' + message; } -function statusWarning(message = '') -{ +function statusWarning(message = '') { getElement('access-status').className = 'warn'; getElement('access-status').innerHTML = '⚠ ' + message; } -function validateAPIKeyAccess(apiKey) -{ +function validateAPIKeyAccess(apiKey) { // Validate if they can access v2 API endpoint using API Key. var v2api = new PDAPI(apiKey); - try - { + try { v2api.GET('https://api.pagerduty.com/users?limit=1', - // If so, the API key is valid (for either v1 or v2) - function success(data) - { - statusOK(); - }, + // If so, the API key is valid (for either v1 or v2) + function success(data) { + statusOK(); + }, - // If we get to here, then API key is invalid (or not supplied). Validate if cookie auth is working. - function error(status, data) - { - // Don't pass an API key, will attempt cookie auth using their subdomain. - var v1api = new PDAPI('', 1); - v1api.GET('https://' + getElement('account-subdomain').value + '.pagerduty.com/api/v1/users?limit=1', - function success(data) - { - // If HTTP200 and 2007 error code returned, then subdomain is invalid. Flag that up. - try - { - if (data.error.code == 2007) { statusError("Account not valid, check subdomain"); return; } - } - catch(e) {} + // If we get to here, then API key is invalid (or not supplied). Validate if cookie auth is working. + function error(status, data) { + // Don't pass an API key, will attempt cookie auth using their subdomain. + var v1api = new PDAPI('', 1); + v1api.GET('https://' + getElement('account-subdomain').value + '.pagerduty.com/api/v1/users?limit=1', + function success(data) { + // If HTTP200 and 2007 error code returned, then subdomain is invalid. Flag that up. + try { + if (data.error.code == 2007) { statusError("Account not valid, check subdomain"); return; } + } + catch (e) { } - // Otherwise, cookie auth is good. - statusWarning("Invalid API key! (Cookie auth is OK)"); - }, - function error(status, data) - { - // Cookie auth was bad, enumerate the reasons. - if (status == 401) { statusError("Invalid API key! (Not logged in for cookie auth)"); return; } - if (status == 403) { statusError("API key valid, but you are not authorized to access the API"); return; } - if (status == 404) { statusError("Account not valid, check subdomain"); return; } - statusError("Access Denied (" + status + "). Unable to access using API key or cookie auth"); + // Otherwise, cookie auth is good. + statusWarning("Invalid API key! (Cookie auth is OK)"); + }, + function error(status, data) { + // Cookie auth was bad, enumerate the reasons. + if (status == 401) { statusError("Invalid API key! (Not logged in for cookie auth)"); return; } + if (status == 403) { statusError("API key valid, but you are not authorized to access the API"); return; } + if (status == 404) { statusError("Account not valid, check subdomain"); return; } + statusError("Access Denied (" + status + "). Unable to access using API key or cookie auth"); + }); }); - }); } - catch(e) - { + catch (e) { statusError("Invalid API key! (Contains invalid chars?)"); } } // Add ability to validate API key access, so people can test themselves. -document.getElementById('api-validate').addEventListener('click', function () -{ +document.getElementById('api-validate').addEventListener('click', function () { // Clear any old status getElement('access-status').className = 'loading'; getElement('access-status').innerHTML = ''; // If API key in field is new, use that. - if (!isAPIKeyObfuscated(getElement('api-key').value)) - { + if (!isAPIKeyObfuscated(getElement('api-key').value)) { validateAPIKeyAccess(getElement('api-key').value); return; } // Otherwise, use the API key we have in storage. chrome.storage.sync.get( - { - pdAPIKey: '', - }, - function(items) - { - validateAPIKeyAccess(items.pdAPIKey); - }); + { + pdAPIKey: '', + }, + function (items) { + validateAPIKeyAccess(items.pdAPIKey); + }); }); diff --git a/pd-notifier/options/options.html b/pd-notifier/options/options.html index 842a360..56c988b 100644 --- a/pd-notifier/options/options.html +++ b/pd-notifier/options/options.html @@ -1,17 +1,19 @@ + PagerDuty Notifier Configuration +

Notifier Configuration

Account and Authentication

- + https://.pagerduty.com
@@ -20,43 +22,51 @@

Account and Authentication

- Optionally provide an API key for your PagerDuty account. [20 characters] + Optionally provide an API + key for your PagerDuty account. [20 characters]
- If you don't provide an API key, you will only receive notifications when your website session is active. + If you don't provide an API key, you will only receive notifications when your website session is active.
- If you provide a read-only API key, you will get notifications, but you will be unable to acknowledge/resolve them from the notification. Enable the "Remove Acknowledge & Resolve buttons?" customization option below in that case. + If you provide a read-only API key, you will get notifications, but you will be unable to + acknowledge/resolve them from the notification. Enable the "Remove Acknowledge & Resolve buttons?" + customization option below in that case.
-
+

Notification Filters

- Only show notifications for incidents currently assigned to these user(s). Should be one or more user IDs (comma separated, no spaces). + Only show notifications for incidents currently assigned to these user(s). Should be one + or more user IDs (comma separated, no spaces).
- Only show notifications for incidents in escalation policies that belong to these team(s). Should be one or more team IDs (comma separated, no spaces). + Only show notifications for incidents in escalation policies that belong to these + team(s). Should be one or more team IDs (comma separated, no spaces).
- Only show notifications for incidents from these services. Should be one or more service IDs (comma separated, no spaces). + Only show notifications for incidents from these services. Should be one or more service + IDs (comma separated, no spaces).
- All filters must be satisfied for you to get a notification. So if you filter by both a user and service, the incident must be in that service and belong to that user for you to get the notification. + All filters must be satisfied for you to get a notification. So if you filter by both a user and service, + the incident must be in that service and belong to that user for you to get the notification.
-
+

Customizations

@@ -64,7 +74,8 @@

Customizations

- +
@@ -80,29 +91,32 @@

Customizations

- +
-
-

This project is open-source on GitHub and we appreciate all friendly contributions.

+
+

This project is open-source on GitHub and we + appreciate all friendly contributions.

✓ Changes saved!

- + + - - - + + + \ No newline at end of file diff --git a/pd-notifier/options/options.js b/pd-notifier/options/options.js index 2fbfe75..222887f 100644 --- a/pd-notifier/options/options.js +++ b/pd-notifier/options/options.js @@ -1,70 +1,62 @@ // This provides all of the logic for handling user configuration of the extension. // Helper functions to obfuscate the API key on the UI, and detect if it's obfuscated. -function obfuscateAPIKey(key) -{ +function obfuscateAPIKey(key) { if (key == "") { return ""; } return "••••••••••••••••" + key.slice(16); } -function isAPIKeyObfuscated(key) -{ +function isAPIKeyObfuscated(key) { return key.slice(0, 16) == "••••••••••••••••"; } // Add an event listener to restore previously save configuration. -document.addEventListener('DOMContentLoaded', function () -{ +document.addEventListener('DOMContentLoaded', function () { chrome.storage.sync.get( - { - // Defaults - pdAccountSubdomain: '', - pdAPIKey: '', - pdIncludeLowUrgency: false, - pdRemoveButtons: false, - pdOpenOnAck: false, - pdNotifSound: false, - pdRequireInteraction: false, - pdFilterServices: '', - pdFilterUsers: '', - pdFilterTeams: '', - pdShowBadgeUpdates: false, - pdBadgeLocation: '' - }, - function(items) - { - // Update the page elements appropriately. - getElement('account-subdomain').value = items.pdAccountSubdomain; - getElement('api-key').value = obfuscateAPIKey(items.pdAPIKey); - getElement('low-urgency').checked = items.pdIncludeLowUrgency; - getElement('remove-buttons').checked = items.pdRemoveButtons; - getElement('open-on-ack').checked = items.pdOpenOnAck; - getElement('notif-sound').checked = items.pdNotifSound; - getElement('require-interaction').checked = items.pdRequireInteraction; - getElement('filter-services').value = items.pdFilterServices; - getElement('filter-users').value = items.pdFilterUsers; - getElement('filter-teams').value = items.pdFilterTeams; - getElement('show-badge').checked = items.pdShowBadgeUpdates; - - // Default to "Triggered" for badgeLocation. - if (items.pdBadgeLocation) { - getElement('option-' + items.pdBadgeLocation).selected = true; - } - else - { - getElement('option-triggered').selected = true; - } - }); + // Defaults + pdAccountSubdomain: '', + pdAPIKey: '', + pdIncludeLowUrgency: false, + pdRemoveButtons: false, + pdOpenOnAck: false, + pdNotifSound: false, + pdRequireInteraction: false, + pdFilterServices: '', + pdFilterUsers: '', + pdFilterTeams: '', + pdShowBadgeUpdates: false, + pdBadgeLocation: '' + }, + function (items) { + // Update the page elements appropriately. + getElement('account-subdomain').value = items.pdAccountSubdomain; + getElement('api-key').value = obfuscateAPIKey(items.pdAPIKey); + getElement('low-urgency').checked = items.pdIncludeLowUrgency; + getElement('remove-buttons').checked = items.pdRemoveButtons; + getElement('open-on-ack').checked = items.pdOpenOnAck; + getElement('notif-sound').checked = items.pdNotifSound; + getElement('require-interaction').checked = items.pdRequireInteraction; + getElement('filter-services').value = items.pdFilterServices; + getElement('filter-users').value = items.pdFilterUsers; + getElement('filter-teams').value = items.pdFilterTeams; + getElement('show-badge').checked = items.pdShowBadgeUpdates; + + // Default to "Triggered" for badgeLocation. + if (items.pdBadgeLocation) { + getElement('option-' + items.pdBadgeLocation).selected = true; + } + else { + getElement('option-triggered').selected = true; + } + }); }); // Add an event listener to save the current configuration, will validate it first. -document.getElementById('save').addEventListener('click', function () -{ +document.getElementById('save').addEventListener('click', function () { if (!validateConfiguration()) { return; } // If API key was updated, set it. If still obfuscated, don't. - if (!isAPIKeyObfuscated(getElement('api-key').value)) - { + if (!isAPIKeyObfuscated(getElement('api-key').value)) { chrome.storage.sync.set({ pdAPIKey: getElement('api-key').value }); } @@ -73,59 +65,48 @@ document.getElementById('save').addEventListener('click', function () badgeLocation = badgeLocation.options[badgeLocation.selectedIndex].value; chrome.storage.sync.set( - { - pdAccountSubdomain: getElement('account-subdomain').value, - pdIncludeLowUrgency: getElement('low-urgency').checked, - pdRemoveButtons: getElement('remove-buttons').checked, - pdOpenOnAck: getElement('open-on-ack').checked, - pdNotifSound: getElement('notif-sound').checked, - pdRequireInteraction: getElement('require-interaction').checked, - pdFilterServices: getElement('filter-services').value, - pdFilterUsers: getElement('filter-users').value, - pdFilterTeams: getElement('filter-teams').value, - pdShowBadgeUpdates: getElement('show-badge').checked, - pdBadgeLocation: badgeLocation - }, - function() - { - // Tell the notifier to reload itself with the latest configuration. - chrome.runtime.getBackgroundPage(function(bgpg) { - bgpg.reloadNotifier(); + pdAccountSubdomain: getElement('account-subdomain').value, + pdIncludeLowUrgency: getElement('low-urgency').checked, + pdRemoveButtons: getElement('remove-buttons').checked, + pdOpenOnAck: getElement('open-on-ack').checked, + pdNotifSound: getElement('notif-sound').checked, + pdRequireInteraction: getElement('require-interaction').checked, + pdFilterServices: getElement('filter-services').value, + pdFilterUsers: getElement('filter-users').value, + pdFilterTeams: getElement('filter-teams').value, + pdShowBadgeUpdates: getElement('show-badge').checked, + pdBadgeLocation: badgeLocation + }, + function () { + // Let the user know things saved properly. + getElement('saved').className = 'saved'; + setTimeout(function () { getElement('saved').className = ''; }, 3000); + + // Remove badge icon if it was previously set. + if (!getElement('show-badge').checked) { chrome.action.setBadgeText({ text: '' }); } }); - - // Let the user know things saved properly. - getElement('saved').className = 'saved'; - setTimeout(function() { getElement('saved').className = ''; }, 3000); - - // Remove badge icon if it was previously set. - if (!getElement('show-badge').checked) { chrome.browserAction.setBadgeText({ text: '' }); } - }); }); // Wrapper method to get the element of a field from the page. -function getElement(elementId) -{ +function getElement(elementId) { return document.getElementById(elementId); } // Helper to determine if value is an integer. -function isInteger(value) -{ +function isInteger(value) { var x; return isNaN(value) ? !1 : (x = parseFloat(value), (0 | x) === x); } // This will validate that our configuration is usable. -function validateConfiguration() -{ +function validateConfiguration() { var isValid = true; var e = null; // Subdomain is required. e = getElement('account-subdomain'); - if (e.value == "") - { + if (e.value == "") { e.className = "bad"; isValid = false; } @@ -133,8 +114,7 @@ function validateConfiguration() // API Key should be exactly 20 chars long. e = getElement('api-key'); if (e.value !== "" - && e.value.length != 20) - { + && e.value.length != 20) { e.className = "bad"; isValid = false; } @@ -143,8 +123,7 @@ function validateConfiguration() e = getElement('filter-services'); e.value = e.value.replace(/\s+/g, ''); if (e.value !== "" - && e.value.indexOf(" ") > -1) - { + && e.value.indexOf(" ") > -1) { e.className = "bad"; isValid = false; } @@ -153,8 +132,7 @@ function validateConfiguration() e = getElement('filter-users'); e.value = e.value.replace(/\s+/g, ''); if (e.value !== "" - && e.value.indexOf(" ") > -1) - { + && e.value.indexOf(" ") > -1) { e.className = "bad"; isValid = false; } @@ -163,8 +141,7 @@ function validateConfiguration() e = getElement('filter-teams'); e.value = e.value.replace(/\s+/g, ''); if (e.value !== "" - && e.value.indexOf(" ") > -1) - { + && e.value.indexOf(" ") > -1) { e.className = "bad"; isValid = false; } diff --git a/pd-notifier/service_worker.js b/pd-notifier/service_worker.js new file mode 100644 index 0000000..bcbbd2f --- /dev/null +++ b/pd-notifier/service_worker.js @@ -0,0 +1,86 @@ +import PagerDutyNotifier from './lib/pd-notifier.js'; + +// The currently active notifier object, and accessor. +var _pdNotifier = null; +function getNotifier() { return _pdNotifier; } + +// This will reload/trigger the the notifier (and pick up any new configuration options). +function reloadNotifier() { + if (_pdNotifier != null) { _pdNotifier._destruct(); } + _pdNotifier = new PagerDutyNotifier(); +} + +// Listen for Chrome Alarms and retrigger the notifier when one is caught. +chrome.alarms.onAlarm.addListener(function (alarm) { + reloadNotifier(); +}); + +// Tell the notifier to reload itself with the latest configuration when options changed. +chrome.storage.onChanged.addListener((changes, namespace) => { + reloadNotifier(); +}); + +// Sets up a Chrome Alarm to retrigger the notifier every so often, to make sure it's always running. +chrome.alarms.create("pagerduty-notifier", { periodInMinutes: 1 }); + +// Initial run, as alarm won't trigger immediately. +reloadNotifier(); + +// If this is the first installation, show the options page so user can set up their settings. +chrome.runtime.onInstalled.addListener(function (details) { + if (details.reason == 'install') { + chrome.tabs.create({ 'url': 'chrome://extensions/?options=' + chrome.runtime.id }); + } + + // Add option to clear all notifications to icon context-menu. + chrome.contextMenus.create({ + title: "Clear all notifications", + id: "pd_clear_all", + contexts: ["action"], + visible: true + }); + + chrome.contextMenus.onClicked.addListener(function (info, tab) { + if (info.menuItemId === "pd_clear_all") { + chrome.notifications.getAll(function (notifs) { + for (var i in notifs) { chrome.notifications.clear(i); } + }); + } + }); +}); + +// Add event handlers for button/notification clicks, and delegate to the currently active notifier object. +chrome.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) { + getNotifier().handlerButtonClicked(notificationId, buttonIndex); + chrome.notifications.clear(notificationId); +}); +chrome.notifications.onClicked.addListener(function (notificationId) { + getNotifier().handlerNotificationClicked(notificationId); + chrome.notifications.clear(notificationId); +}); + +// Add event handler for the toolbar icon click. +chrome.action.onClicked.addListener(function (tab) { + getNotifier().openDashboard(); +}); + +// Add option to trigger a test notification popup. +chrome.contextMenus.create({ + title: "Show test notification", + id: "pd_test_notification", + contexts: ["action"], + visible: true +}); + +chrome.contextMenus.onClicked.addListener(function (info) { + if (info.menuItemId === "pd_test_notification") { + _pdNotifier.triggerNotification({ + 'id': 'test', + 'summary': 'Test Notification', + 'service': { + 'summary': 'Test Service' + }, + 'urgency': 'high' + }); + } +}); \ No newline at end of file