Skip to content

Commit

Permalink
Issue mikaelbr#193
Browse files Browse the repository at this point in the history
* Support SnoreToast "-b" flag for action buttons
* Communicate with SnoreToast using unique named pipes
* Parse result from pipe into data object before calling the actionJackerDecorator
* Sanitizing data now changes "timedout" to "timeout"
* Sample file
  • Loading branch information
yoavain committed Oct 28, 2019
1 parent 8f01da4 commit 3d4d86c
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 48 deletions.
34 changes: 34 additions & 0 deletions example/toaster-with-actions.js
@@ -0,0 +1,34 @@
const notifier = require('../index');
const path = require('path');

notifier.notify(
{
message: 'Are you sure you want to continue?',
icon: path.join(__dirname, 'coulson.jpg'),
actions: ['OK', 'Cancel']
},
(err, data) => {
// Will also wait until notification is closed.
console.log('Waited');
console.log(JSON.stringify({ err, data }, null, '\t'));
}
);

// Built-in actions:
notifier.on('timeout', () => {
console.log('Timed out!');
});
notifier.on('activate', () => {
console.log('Clicked!');
});
notifier.on('dismissed', () => {
console.log('Dismissed!');
});

// Buttons actions (lower-case):
notifier.on('ok', () => {
console.log('"Ok" was pressed');
});
notifier.on('cancel', () => {
console.log('"Cancel" was pressed');
});
10 changes: 5 additions & 5 deletions example/toaster.js
@@ -1,5 +1,5 @@
var notifier = require('../index');
var path = require('path');
const notifier = require('../index');
const path = require('path');

notifier.notify(
{
Expand All @@ -10,14 +10,14 @@ notifier.notify(
function(err, data) {
// Will also wait until notification is closed.
console.log('Waited');
console.log(err, data);
console.log(JSON.stringify({ err, data }));
}
);

notifier.on('timeout', function() {
notifier.on('timeout', () => {
console.log('Timed out!');
});

notifier.on('click', function() {
notifier.on('click', () => {
console.log('Clicked!');
});
37 changes: 36 additions & 1 deletion lib/utils.js
Expand Up @@ -6,6 +6,9 @@ var path = require('path');
var url = require('url');
var os = require('os');
var fs = require('fs');
var net = require('net');

const BUFFER_SIZE = 1024;

function clone(obj) {
return JSON.parse(JSON.stringify(obj));
Expand Down Expand Up @@ -225,6 +228,7 @@ module.exports.mapToMac = function(options) {
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
module.exports.isArray = isArray;

function noop() {}
module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
Expand Down Expand Up @@ -253,6 +257,9 @@ module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
if (resultantData.match(/^activate|clicked$/)) {
resultantData = 'activate';
}
if (resultantData.match(/^timedout$/)) {
resultantData = 'timeout';
}
}

fn.apply(emitter, [err, resultantData, metadata]);
Expand Down Expand Up @@ -318,25 +325,30 @@ function removeNewLines(str) {
---- Options ----
[-t] <title string> | Displayed on the first line of the toast.
[-m] <message string> | Displayed on the remaining lines, wrapped.
[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple buttons separated by ;
[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple buttons separated by ";"
[-tb] | Displayed a textbox on the bottom line, only if buttons are not presented.
[-p] <image URI> | Display toast with an image, local files only.
[-id] <id> | sets the id for a notification to be able to close it later.
[-s] <sound URI> | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx.
[-silent] | Don't play a sound file when showing the notifications.
[-appID] <App.ID> | Don't create a shortcut but use the provided app id.
[-pid] <pid> | Query the appid for the process <pid>, use -appID as fallback. (Only relevant for applications that might be packaged for the store)
[-pipeName] <\.\pipe\pipeName\> | Provide a name pipe which is used for callbacks.
[-application] <C:\foo.exe> | Provide a application that might be started if the pipe does not exist.
-close <id> | Closes a currently displayed notification.
*/
var allowedToasterFlags = [
't',
'm',
'b',
'tb',
'p',
'id',
's',
'silent',
'appID',
'pid',
'pipeName',
'close',
'install'
];
Expand Down Expand Up @@ -407,6 +419,11 @@ module.exports.mapToWin8 = function(options) {
options.s = toasterDefaultSound;
}

if (options.actions && isArray(options.actions)) {
options.b = options.actions.join(';');
delete options.actions;
}

for (var key in options) {
// Check if is allowed. If not, delete!
if (
Expand Down Expand Up @@ -518,3 +535,21 @@ function sanitizeNotifuTypeArgument(type) {

return 'info';
}

module.exports.createNamedPipe = namedPipe => {
const buf = Buffer.alloc(BUFFER_SIZE);

return new Promise(resolve => {
const server = net.createServer(stream => {
stream.on('data', c => {
buf.write(c.toString());
});
stream.on('end', () => {
server.close();
});
});
server.listen(namedPipe, () => {
resolve(buf);
});
});
};
97 changes: 63 additions & 34 deletions notifiers/toaster.js
Expand Up @@ -6,12 +6,16 @@ var notifier = path.resolve(__dirname, '../vendor/snoreToast/snoretoast');
var utils = require('../lib/utils');
var Balloon = require('./balloon');
var os = require('os');
const uuid = require('uuid/v4');

var EventEmitter = require('events').EventEmitter;
var util = require('util');

var fallback;

const PIPE_NAME = 'notifierPipe';
const PIPE_PATH_PREFIX = '\\\\.\\pipe\\';

module.exports = WindowsToaster;

function WindowsToaster(options) {
Expand All @@ -28,17 +32,29 @@ util.inherits(WindowsToaster, EventEmitter);

function noop() {}

var timeoutMessage = 'the toast has timed out';
var successMessage = 'user clicked on the toast';
function parseResult(data) {
if (!data) {
return {};
}
return data.split(';').reduce((acc, cur) => {
const split = cur.split('=');
if (split && split.length === 2) {
acc[split[0]] = split[1];
}
return acc;
}, {});
}

function hasText(str, txt) {
return str && str.indexOf(txt) !== -1;
function getPipeName() {
return `${PIPE_PATH_PREFIX}${PIPE_NAME}-${uuid()}`;
}

WindowsToaster.prototype.notify = function(options, callback) {
WindowsToaster.prototype.notify = async function(options, callback) {
options = utils.clone(options || {});
callback = callback || noop;
var is64Bit = os.arch() === 'x64';
var resultBuffer;
const namedPipe = getPipeName();

if (typeof options === 'string') {
options = { title: 'node-notifier', message: options };
Expand All @@ -51,36 +67,45 @@ WindowsToaster.prototype.notify = function(options, callback) {
);
}

var actionJackedCallback = utils.actionJackerDecorator(
this,
options,
function cb(err, data) {
/* Possible exit statuses from SnoreToast, we only want to include err if it's -1 code
Exit Status : Exit Code
Failed : -1
Success : 0
Hidden : 1
Dismissed : 2
TimedOut : 3
ButtonPressed : 4
TextEntered : 5
*/
if (err && err.code !== -1) {
return callback(null, data);
}
callback(err, data);
},
function mapper(data) {
if (hasText(data, successMessage)) {
return 'click';
}
if (hasText(data, timeoutMessage)) {
return 'timeout';
}
return false;
var snoreToastResultParser = (err, callback) => {
/* Possible exit statuses from SnoreToast, we only want to include err if it's -1 code
Exit Status : Exit Code
Failed : -1
Success : 0
Hidden : 1
Dismissed : 2
TimedOut : 3
ButtonPressed : 4
TextEntered : 5
*/
const result = parseResult(
resultBuffer && resultBuffer.toString('utf16le')
);

// parse action
if (result.action === 'buttonClicked' && result.button) {
result.activationType = result.button;
} else if (result.action) {
result.activationType = result.action;
}
);

if (err && err.code === -1) {
callback(err, result);
}
callback(null, result);
};

var actionJackedCallback = err =>
snoreToastResultParser(
err,
utils.actionJackerDecorator(
this,
options,
callback,
data => data || false
)
);

options.title = options.title || 'Node Notification:';
if (
Expand All @@ -96,6 +121,10 @@ WindowsToaster.prototype.notify = function(options, callback) {
return fallback.notify(options, callback);
}

// Add pipeName option, to get the output
resultBuffer = await utils.createNamedPipe(namedPipe);
options.pipeName = namedPipe;

options = utils.mapToWin8(options);
var argsList = utils.constructArgumentList(options, {
explicitTrue: true,
Expand Down
3 changes: 2 additions & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "node-notifier",
"version": "6.0.0",
"version": "7.0.0",
"description": "A Node.js module for sending notifications on native Mac, Windows (post and pre 8) and Linux (or Growl as fallback)",
"main": "index.js",
"scripts": {
Expand Down Expand Up @@ -54,6 +54,7 @@
"is-wsl": "^2.1.1",
"semver": "^6.3.0",
"shellwords": "^0.1.1",
"uuid": "^3.3.3",
"which": "^1.3.1"
},
"husky": {
Expand Down
6 changes: 3 additions & 3 deletions test/index.js
Expand Up @@ -10,16 +10,16 @@ describe('constructors', function() {
expect(notifier.notify({ title: 'My notification' }, cb)).toBeTruthy();
});

it('should throw error when second parameter is not a function', function() {
it('should throw error when second parameter is not a function', async () => {
var wrongParamOne = 200;
var wrongParamTwo = 'meaningless string';
var data = { title: 'My notification' };

var base = notifier.notify.bind(notifier, data);
expect(base.bind(notifier, wrongParamOne)).toThrowError(
await expect(base.bind(notifier, wrongParamOne)()).rejects.toThrowError(
/^The second argument/
);
expect(base.bind(notifier, wrongParamTwo)).toThrowError(
await expect(base.bind(notifier, wrongParamTwo)()).rejects.toThrowError(
/^The second argument/
);
});
Expand Down

0 comments on commit 3d4d86c

Please sign in to comment.