1
0
mirror of https://github.com/node-red/node-red-nodes.git synced 2023-10-10 13:36:58 +02:00

Updated pushbullet node with all push types, added new "pushbullet in" that emits pushes. Added unit tests.

This commit is contained in:
dsundberg 2015-01-27 08:08:54 +01:00
parent 5982da8495
commit 425858d94d
6 changed files with 1872 additions and 180 deletions

View File

@ -1,5 +1,5 @@
<!--
Copyright 2013 IBM Corp.
Copyright 2013,2015 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,94 +14,353 @@
limitations under the License.
-->
<!-- PUSHBULLET CONFIG -->
<script type="text/x-red" data-template-name="pushbullet-config">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-apikey"><i class="fa fa-lock"></i> API-key</label>
<input type="password" id="node-config-input-apikey">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('pushbullet-config', {
category: 'config',
color: "rgb(218, 196, 180)",
defaults: {
name: {value: ""}
},
credentials: {
apikey: {type: "password"}
},
label: function() {
return this.name || "Pushbullet config";
}
});
</script>
<!-- PUSHBULLET OUT -->
<script type="text/x-red" data-template-name="pushbullet">
<div class="form-row">
<label for="node-input-title"><i class="fa fa-flag"></i> Title</label>
<input type="text" id="node-input-title" placeholder="Node-RED">
<label for="node-input-config"><i class="fa fa-user"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-input-deviceid"><i class="fa fa-mobile"></i> Device ID</label>
<select id="node-input-deviceid">
<option value="">All</option>
<option value="_msg_">- set from msg.deviceid -</option>
</select>
</div>
<div class="form-row">
<label for="node-input-chan"><i class="fa fa-random"></i> Channel</label>
<input type="text" id="node-input-chan" placeholder="channel name">
</div>
<div class="form-row">
<label for="node-config-input-deviceid"><i class="fa fa-user"></i> Device ID</label>
<input type="text" id="node-config-input-deviceid">
<label for="node-input-pushtype"><i class="fa fa-dot-circle-o"></i> Type</label>
<select id="node-input-pushtype">
<optgroup label="Push types">
<option value="address">Address</option>
<option value="file">File</option>
<option value="link">Link</option>
<option value="list">List</option>
<option value="note">Note</option>
<option value="">- set from msg.pushtype -</option>
</optgroup>
<optgroup label="Actions">
<option value="delete">Delete push</option>
<option value="dismissal">Dismiss push</option>
<option value="updatelist">Update list</option>
</optgroup>
</select>
</div>
<div class="form-row">
<label for="node-config-input-pushkey"><i class="fa fa-lock"></i> API Key</label>
<input type="password" id="node-config-input-pushkey">
<label for="node-input-title"><i class="fa fa-random"></i> Title</label>
<input type="text" id="node-input-title" placeholder="Node-RED">
</div>
<br/>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips" id="node-tip"><b>Note:</b> Using credentials from global pushkey.js file.</div>
<div class="form-tips" id="pushbullet-migration-info" style="display: none;">
<p><i class="fa fa-random"></i> Configuration node has been migrated, please click Ok and then re-deploy flow to save settings.</p>
</div>
</script>
<script type="text/x-red" data-help-name="pushbullet">
<p>Uses PushBullet to push the <b>msg.payload</b> to a device that has the PushBullet app installed.</p>
<p>Optionally uses <b>msg.topic</b> to set the title, if not already set in the properties.</p>
<p>You need to configure both your <i>API key</i> and the target <i>device ID</i>.
The device ID may be passed in as <b>msg.deviceid</b>.</p>
<p>You can also push to any channels that you own either configured or via <b>msg.channel</b>.</p>
The old method of storing your credentials in the pushkey.js file in the directory <b>above</b> /node-red is deprecated.</p>
<p>The deviceid can be found by clicking on your required device on the <a href="https://www.pushbullet.com/">PushBullet website</a>, once you have logged in.</p>
<p>Uses PushBullet to push <code>msg.payload</code> to a device that has the PushBullet app installed.</p>
<p>Optionally uses <code>msg.topic</code> to set the title, if not already set in the properties.</p>
<p>Optionally uses <code>msg.pushtype</code> to set the type of the push, if not already set in the properties.</p>
<p>Optionally uses <code>msg.deviceid</code> to set the device ID, if not already set in the properties.</p>
<p>You can also push to any channels that you own either configured or via <code>msg.channel</code>.</p>
<p>The node can also <i>dismiss</i> and <i>delete</i> and <i>update</i> items in a pushed list. In this case <code>msg.data.iden</code> must be set to a valid push id, if <code>msg</code> originates from the Pushbullet input node this value is already set.</p>
<p>The old method of storing your credentials in the pushkey.js file in the directory above /node-red is deprecated.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('pushbullet',{
category: 'output',
category: 'social-output',
defaults: {
config: {type: "pushbullet-config", required: true},
pushtype: {value:"note"},
title: {value:""},
chan: {value:"" },
name: {value:""}
},
credentials: {
deviceid: {value: ""},
pushkey: {value: ""}
},
color:"#a7c9a0",
inputs:1,
outputs:0,
icon: "bullet.png",
align: "right",
label: function() {
return this.name||this.title||"pushbullet";
var self = this;
function getName(deviceid) {
if(!self.devicename && deviceid && self.config) {
$.getJSON('pushbullet/'+self.config+'/devices', function(data) {
for(var i=0; i<data.length; i++) {
if(data[i].iden === deviceid) {
self.devicename = data[i].nickname;
self.dirty = true;
RED.view.redraw();
return;
}
}
});
}
}
try {
getName(this.credentials.deviceid);
}
catch(err) {
$.getJSON('credentials/pushbullet/'+this.id, function(data) {
getName(data.deviceid);
});
}
return this.name||this.devicename||this.title||"pushbullet";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditsave: function() {
this.devicename = undefined;
if(this.migrationData) {
$.ajax('pushbullet/'+this.id+'/migrate?save=true');
}
},
oneditprepare: function() {
var node = this, ddConfig = $('#node-input-config'), ddDevice = $('#node-input-deviceid'), ddPushtype = $('#node-input-pushtype');
function updateDeviceList() {
if(!ddConfig.val()) {
ddDevice.children().remove();
ddDevice.append('<option value="">All</option>')
ddDevice.append('<option value="_msg_">- set from msg.deviceid -</option>')
var currentDevice;
if(node.credentials) {
currentDevice = node.credentials.deviceid;
}
if(currentDevice && currentDevice !== "_msg_") {
ddDevice.append('<option value="'+currentDevice+'">'+currentDevice+'</option>');
ddDevice.val(currentDevice);
}
}
else {
var config = RED.nodes.node(ddConfig.val()),
url = 'pushbullet/'+ddConfig.val()+'/devices';
if(config && config.credentials && config.credentials.apikey) {
url += '?apikey='+config.credentials.apikey;
}
$.getJSON(url, function(data) {
ddDevice.children().remove();
ddDevice.append('<option value="">All</option>')
ddDevice.append('<option value="_msg_">- set from msg.deviceid -</option>')
var currentDevice, addCurrent = true;
if(node.credentials) {
currentDevice = node.credentials.deviceid;
}
for(var i=0; i<data.length; i++) {
var dev = data[i];
ddDevice.append('<option value="'+dev.iden+'">'+dev.nickname+' ('+dev.kind+')</option>');
if(dev.iden === currentDevice) {
addCurrent = false;
}
}
if(currentDevice) {
if(addCurrent && currentDevice !== "_msg_") {
ddDevice.append('<option value="'+currentDevice+'">'+currentDevice+'</option>');
}
ddDevice.val(currentDevice);
}
});
}
}
function checkMigration(cb) {
$.getJSON('pushbullet/'+node.id+'/migrate', function(data) {
var showMigration = false;
node.migrationData = data.config;
if(data.migrated) {
if(data.config) {
var configId = data.config;
var configType = 'pushbullet-config';
var configTypeDef = RED.nodes.getType(configType);
RED.nodes.add({
type: configType,
id: configId,
name: "Imported",
users: [node.id],
label: configTypeDef.label,
_def: configTypeDef,
});
}
if(node.credentials) {
if(node.credentials.pushkey) {
if(ddConfig.find('option[value="'+data.config+'"]').length === 0) {
ddConfig.append('<option value="'+data.config+'">Imported</option>');
}
ddConfig.val(data.config);
showMigration = true;
}
else {
ddConfig.val("_ADD_");
}
if(node.credentials.deviceid) {
ddDevice.append('<option value="'+node.credentials.deviceid+'">Imported</option>');
ddDevice.val(node.credentials.deviceid);
showMigration = true;
}
else {
ddDevice.val("");
}
}
}
if(showMigration) {
ddPushtype.val("note");
node.dirty = true;
$('#pushbullet-migration-info').show();
}
if(cb) {
cb(showMigration);
}
});
}
checkMigration(function(migrated) {
if(!migrated) {
ddConfig.change(function() {
if(ddConfig.val() && ddConfig.val() !== "_ADD_") {
updateDeviceList();
}
});
}
});
}
});
</script>
<!-- PUSHBULLET IN -->
<script type="text/x-red" data-template-name="pushbullet in">
<div class="form-row">
<label for="node-input-config"><i class="fa fa-user"></i> Config</label>
<input type="text" id="node-input-config" placeholder="Node-RED">
</div>
<div class="form-row">
<label for="node-input-filters"><i class="fa fa-mobile"></i> Device filter</label>
<select id="node-input-filters" style="width: 60%;" multiple="multiple"></select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips">
<i class="fa fa-mobile"></i> Device filter list is multi-selectable. No selections means no filtering, i.e. all pushes are emitted.
</div>
</script>
<script type="text/x-red" data-help-name="pushbullet in">
<p>Receives Pushbullets from all devices. Messages contain the following data:</p>
<p><code>msg.pushtype</code>: type of message</p>
<p><code>msg.topic</code>: topic information from the push</p>
<p><code>msg.payload</code>: main content of the push</p>
<p><code>msg.data</code>: original object from the pushbullet API containing e.g. sender, receiver and message ids.</p>
<p>Pushes of type <i>link</i> and <i>file</i> will also have <code>msg.message</code> containing the message associated with the push.</p>
<p>For further details of see <a href="https://docs.pushbullet.com/stream/">Pushbullet Stream API</a> and <a href="https://docs.pushbullet.com/v2/pushes/">Pushbullet Push API</a>.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('pushbullet in',{
category: 'social-input',
defaults: {
config: {type: "pushbullet-config", required: true},
name: {value:""}
},
credentials: {
filters: {value: []}
},
color:"#a7c9a0",
inputs:0,
outputs:1,
icon: "bullet.png",
label: function() {
return this.name||"pushbullet";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
$.getJSON('pushbullet/'+this.id,function(data) {
if (data.deviceid) {
$('#node-config-input-deviceid').val(data.deviceid);
var node = this, ddConfig = $('#node-input-config'), ddDevice = $('#node-input-filters');
function updateDeviceList() {
var config = RED.nodes.node(ddConfig.val()),
url = 'pushbullet/'+ddConfig.val()+'/devices';
if(config && config.credentials && config.credentials.apikey) {
url += '?apikey='+config.credentials.apikey;
}
if (data.hasPassword) {
$('#node-config-input-pushkey').val('__PWRD__');
} else {
$('#node-config-input-pushkey').val('');
$.getJSON(url, function(data) {
var currentDevices = [], addDevices = [];
if(node.credentials && node.credentials.filters) {
currentDevices = node.credentials.filters;
addDevices = node.credentials.filters.splice();
}
if (data.global) $('#node-tip').show();
else $('#node-tip').hide();
var idx;
ddDevice.children().remove();
for(var i=0; i<data.length; i++) {
var dev = data[i];
ddDevice.append('<option value="'+dev.iden+'">'+dev.nickname+' ('+dev.kind+')</option>');
idx = addDevices.indexOf(dev.iden);
if (idx > -1) {
addDevices.splice(idx, 1);
}
}
for(var j=0;j<addDevices.length;j++) {
ddDevice.append('<option value="'+addDevices[j]+'">'+addDevices[j]+'</option>');
}
ddDevice.val(currentDevices);
});
},
oneditsave: function() {
var newUser = $('#node-config-input-deviceid').val();
var newPass = $('#node-config-input-pushkey').val();
var credentials = {};
credentials.deviceid = newUser;
if (newPass != '__PWRD__') {
credentials.pushkey = newPass;
}
$.ajax({
url: 'pushbullet/'+this.id,
type: 'POST',
data: credentials,
success:function(result){}
});
},
ondelete: function() {
$.ajax({
url: 'pushbullet/'+this.id,
type: 'DELETE',
success: function(result) {}
ddConfig.change(function() {
if(ddConfig.val() && ddConfig.val() !== "_ADD_") {
updateDeviceList();
}
});
}
});

View File

@ -1,5 +1,5 @@
/**
* Copyright 2013 IBM Corp.
* Copyright 2013,2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,109 +17,527 @@
module.exports = function(RED) {
"use strict";
var PushBullet = require('pushbullet');
var fs = require('fs');
var util = require('util');
var when = require('when');
var nodefn = require('when/node');
var EventEmitter = require('events').EventEmitter;
// Either create pushkey.js in dir ABOVE node-red, it just needs to be like
// module.exports = {pushbullet:'My-API-KEY', deviceid:'12345'}
// or set them per node in the edit dialog
try {
var pushkeys = RED.settings.pushbullet || require(process.env.NODE_RED_HOME+"/../pushkey.js");
}
catch(err) {
//util.log("[57-pushbullet.js] Warning: Failed to load global PushBullet credentials");
}
function PushbulletNode(n) {
RED.nodes.createNode(this,n);
this.title = n.title;
this.chan = n.chan;
var credentials = RED.nodes.getCredentials(n.id);
if ((credentials) && (credentials.hasOwnProperty("pushkey"))) { this.pushkey = credentials.pushkey; }
else {
if (pushkeys) { this.pushkey = pushkeys.pushbullet; }
else { this.error("No Pushbullet API key set"); }
}
if ((credentials) && (credentials.hasOwnProperty("deviceid"))) { this.deviceid = credentials.deviceid; }
else {
if (pushkeys) { this.deviceid = pushkeys.deviceid; }
else { this.warn("No deviceid set"); }
}
this.pusher = new PushBullet(this.pushkey);
var node = this;
this.on("input",function(msg) {
var titl = node.title || msg.topic || "Node-RED";
var dev = node.deviceid || msg.deviceid;
var channel = node.chan || msg.chan;
if (channel != undefined) {
dev = { channel_tag : channel };
} else {
if (!isNaN(dev)) { dev = Number(dev); }
}
if (typeof(msg.payload) === 'object') {
msg.payload = JSON.stringify(msg.payload);
}
else { msg.payload = msg.payload.toString(); }
if (node.pushkey && dev) {
try {
node.pusher.note(dev, titl, msg.payload, function(err, response) {
if (err) { node.error("Pushbullet error"); }
});
}
catch (err) {
function onError(err, node) {
if(err && node) {
if(node.emitter) {
if(!node.emitter.emit('error', err)) {
node.error(err);
}
}
else {
node.warn("Pushbullet credentials not set/found. See node info.");
node.error(err);
}
}
}
function PushbulletConfig(n) {
RED.nodes.createNode(this, n);
this.name = n.name;
this._inputNodes = [];
this.emitter = new EventEmitter();
var self = this;
// sort migration from old node
var apikey;
if(n._migrate) {
apikey = n._apikey;
this.credentials = {apikey:apikey};
}
else if(this.credentials) {
apikey = this.credentials.apikey;
}
if (apikey) {
try {
var pusher = new PushBullet(apikey);
// get 'me' info
this.me = when.promise(function(resolve, reject) {
pusher.me(function(err, me) {
if(err) {
reject(err);
return onError(err, self);
}
resolve(me);
});
});
// get latest timestamp
this.last = when.promise(function(resolve) {
pusher.history({limit:1}, function(err, res) {
if(err) {
resolve(0);
return onError(err, self);
}
try {
resolve(res.pushes[0].modified);
}
catch(ex){
self.warn('Unable to get history.');
resolve(0);
}
});
});
this.pusher = pusher;
}
RED.nodes.registerType("pushbullet",PushbulletNode);
var querystring = require('querystring');
RED.httpAdmin.get('/pushbullet/:id',function(req,res) {
var credentials = RED.nodes.getCredentials(req.params.id);
if (credentials) {
res.send(JSON.stringify({deviceid:credentials.deviceid,hasPassword:(credentials.pushkey&&credentials.pushkey!=="")}));
catch(err) {
onError(err, this);
}
else if (pushkeys && pushkeys.pushbullet && pushkeys.deviceid) {
RED.nodes.addCredentials(req.params.id,{pushkey:pushkeys.pushbullet,deviceid:pushkeys.deviceid,global:true});
credentials = RED.nodes.getCredentials(req.params.id);
res.send(JSON.stringify({deviceid:credentials.deviceid,global:credentials.global,hasPassword:(credentials.pushkey&&credentials.pushkey!=="")}));
}
else {
res.send(JSON.stringify({}));
this.error("No credentials set for pushbullet config.");
}
this.on("close", function() {
if(self.stream) {
self.stream.close();
}
self._inputNodes.length = 0;
});
}
RED.nodes.registerType("pushbullet-config", PushbulletConfig, {
credentials: {
apikey: {type: "password"}
}
});
RED.httpAdmin.delete('/pushbullet/:id',function(req,res) {
RED.nodes.deleteCredentials(req.params.id);
res.send(200);
PushbulletConfig.prototype.onConfig = function(type, cb) {
this.emitter.on(type, cb);
}
PushbulletConfig.prototype.setupStream = function() {
var self = this;
if(this.pusher) {
var stream = this.pusher.stream();
stream.on('message', function(res) {
if(res.type === 'tickle') {
self.handleTickle(res);
}
else if(res.type === 'push') {
self.pushMsg(res.push);
}
});
stream.on('connect', function() {
self.emitter.emit('stream_connected');
});
stream.on('close', function() {
self.emitter.emit('stream_disconnected');
});
stream.on('error', function(err) {
self.emitter.emit('stream_error', err);
});
stream.connect();
this.stream = stream;
}
};
PushbulletConfig.prototype.handleTickle = function(ticklemsg) {
var self = this;
if(this.pusher && ticklemsg.subtype === "push") {
var lastprom = this.last;
this.last = when.promise(function(resolve) {
when(lastprom).then(function(last) {
self.pusher.history({modified_after: last}, function(err, res) {
if(err) {
resolve(last);
return onError(err);
}
for(var i=0;i<res.pushes.length; i++) {
self.pushMsg(res.pushes[i]);
}
try {
resolve(res.pushes[0].modified);
} catch(ex) {
resolve(last);
}
});
});
});
}
};
PushbulletConfig.prototype.pushMsg = function(incoming) {
if(this._inputNodes.length === 0) {
return;
}
var msg = {
pushtype: incoming.type,
data: incoming
}
if(incoming.dismissed === true) {
msg.pushtype = 'dismissal';
msg.topic = 'Push dismissed';
msg.payload = incoming.iden;
}
else if(incoming.active === false && incoming.type === undefined) {
msg.pushtype = 'delete';
msg.topic = 'Push deleted';
msg.payload = incoming.iden;
}
else if(incoming.type === 'clip') {
msg.topic = 'Clipboard content';
msg.payload = incoming.body;
}
else if(incoming.type === 'note') {
msg.topic = incoming.title;
msg.payload = incoming.body;
}
else if(incoming.type === 'link') {
msg.topic = incoming.title;
msg.payload = incoming.url;
msg.message = incoming.body;
}
else if(incoming.type === 'address') {
msg.topic = incoming.name;
msg.payload = incoming.address;
}
else if(incoming.type === 'list') {
msg.topic = incoming.title;
msg.payload = incoming.items;
}
else if(incoming.type === 'file') {
msg.topic = incoming.file_name;
msg.payload = incoming.file_url;
msg.message = incoming.body;
}
// Android specific, untested
else if(incoming.type === 'mirror') {
msg.topic = incoming.title;
msg.payload = incoming.body;
}
else if(incoming.type === 'dismissal') {
msg.topic = "dismissal";
msg.topic = "Push dismissed";
msg.payload = incoming.iden;
}
else {
this.error("unknown push type: " + incoming.type + " content: " + JSON.stringify(incoming));
return;
}
for (var i = 0; i < this._inputNodes.length; i++) {
this._inputNodes[i].emitPush(msg);
}
};
PushbulletConfig.prototype.registerInputNode = function(/*Node*/handler) {
if(!this.stream) {
this.setupStream();
}
this._inputNodes.push(handler);
};
function migrateOldSettings(n) {
if(n.config === undefined) {
var newid, config, apikey, deviceid, pushkeys;
try {
pushkeys = RED.settings.pushbullet || require(process.env.NODE_RED_HOME+"/../pushkey.js");
}
catch(err) {
}
var cred = RED.nodes.getCredentials(n.id);
// get old apikey
if(cred && cred.hasOwnProperty("pushkey")) {
apikey = cred.pushkey;
}
else if(pushkeys) {
apikey = pushkeys.pushbullet;
}
// get old device
if (cred && cred.hasOwnProperty("deviceid")) {
deviceid = cred.deviceid;
}
else if (pushkeys) {
deviceid = pushkeys.deviceid;
}
if(apikey) {
newid = (1+Math.random()*4294967295).toString(16);
config = new PushbulletConfig({
id: newid,
type: 'pushbullet-config',
name: n.name,
_migrate: true,
_apikey: apikey,
});
}
if(!(apikey || deviceid)) {
return false;
}
// override configuration properties to compatible migrated ones
n.pushtype = "note";
n.deviceid = deviceid;
return {
deviceid: deviceid,
apikey: apikey,
config: config,
id: newid
};
}
return false;
}
function PushbulletOut(n) {
RED.nodes.createNode(this, n);
var self = this;
this.migrated = migrateOldSettings(n);
this.title = n.title;
this.chan = n.chan;
this.pushtype = n.pushtype;
this.pusher = null;
var configNode;
if(this.migrated) {
this.warn('Settings migrated from previous version of Pushbullet Node, please edit node to update settings.');
this.status({fill: 'yellow', shape: 'ring', text: 'Node migrated'});
this.deviceid = this.migrated.deviceid;
configNode = this.migrated.config;
}
else {
this.status({});
configNode = RED.nodes.getNode(n.config);
try {
this.deviceid = this.credentials.deviceid;
}
catch(err){}
}
if(configNode) {
this.pusher = configNode.pusher;
configNode.onConfig('error', function(err) {
self.error(err);
});
}
this.on("input", function(msg) {
var title = self.title || msg.topic || "Node-RED";
var deviceid = (self.deviceid === '_msg_')? (msg.deviceid || ""): (self.deviceid || "");
var pushtype = self.pushtype || msg.pushtype || "note";
var channel = self.chan || msg.channel;
if (typeof(msg.payload) === 'object') {
msg.payload = JSON.stringify(msg.payload);
}
else if(msg.payload) {
msg.payload = msg.payload.toString();
}
if(['delete', 'dismissal', 'updatelist', '_rawupdate_'].indexOf(pushtype) === -1) {
if (channel) {
deviceid = { channel_tag : channel };
}
else if(deviceid === "") {
try {
when(configNode.me).then(function(me) {
deviceid = me.email;
self.pushMsg(pushtype, deviceid, title, msg);
});
return;
}
catch(err) {
self.error('Unable to push to "all".');
}
}
else if (!isNaN(deviceid)) {
deviceid = Number(deviceid);
}
}
self.pushMsg(pushtype, deviceid, title, msg);
});
}
RED.nodes.registerType("pushbullet", PushbulletOut, {
credentials: {
deviceid: {value: ""},
pushkey: {value: ""}
}
});
RED.httpAdmin.post('/pushbullet/:id',function(req,res) {
var body = "";
req.on('data', function(chunk) {
body+=chunk;
});
req.on('end', function(){
var newCreds = querystring.parse(body);
var credentials = RED.nodes.getCredentials(req.params.id)||{};
if (newCreds.deviceid === null || newCreds.deviceid === "") {
delete credentials.deviceid;
} else {
credentials.deviceid = newCreds.deviceid;
PushbulletOut.prototype.pushMsg = function(pushtype, deviceid, title, msg) {
var self = this;
if (this.pusher) {
var handleErr = function(msg){
return function(err) {
if(err) {
self.error(msg);
onError(err, self);
}
if (newCreds.pushkey === "") {
delete credentials.pushkey;
} else {
credentials.pushkey = newCreds.pushkey||credentials.pushkey;
}
RED.nodes.addCredentials(req.params.id,credentials);
res.send(200);
}
if(deviceid) {
if(pushtype === 'note') {
this.pusher.note(deviceid, title, msg.payload, handleErr('Unable to push note'));
}
else if(pushtype === 'address') {
this.pusher.address(deviceid, title, msg.payload, handleErr('Unable to push address'));
}
else if(pushtype === 'list') {
this.pusher.list(deviceid, title, JSON.parse(msg.payload), handleErr('Unable to push list'));
}
else if(pushtype === 'link') {
this.pusher.push(deviceid, {
type: 'link',
title: title,
body: msg.message,
url: msg.payload
}, handleErr('Unable to push link'));
}
else if(pushtype === 'file') {
// Workaround for Pushbullet dep not handling error on file open
if(fs.existsSync(msg.payload)) {
this.pusher.file(deviceid, msg.payload, title, handleErr('Unable to push file'));
}
else {
this.error('File does not exist!');
}
}
else if(pushtype === '_raw_') {
this.pusher.push(deviceid, msg.raw, handleErr('Unable to push raw data'));
}
}
if(msg.data && msg.data.iden) {
if(pushtype === 'delete') {
this.pusher.deletePush(msg.data.iden, handleErr('Unable to delete push'));
}
else if(pushtype === 'dismissal') {
this.pusher.updatePush(msg.data.iden, {dismissed: true}, handleErr('Unable to dismiss push'));
}
else if(pushtype === 'updatelist') {
try {
var data = JSON.parse(msg.payload);
if(msg.data.type && msg.data.type !== 'list') {
this.warn('Trying to update list items in non list push');
}
this.pusher.updatePush(msg.data.iden, {items: data}, handleErr('Unable to update list'));
}
catch(err) {
this.warn("Invalid list");
}
}
else if(pushtype === '_rawupdate_') {
this.pusher.updatePush(msg.data.iden, msg.raw, handleErr('Unable to update raw data'));
}
}
}
else {
self.error("Pushbullet credentials not set/found.");
}
};
RED.httpAdmin.get('/pushbullet/:id/migrate', function(req, res) {
var node = RED.nodes.getNode(req.params.id);
if(node && node.migrated) {
if(req.query.save) {
var promise;
if(node.migrated.apikey) {
promise = RED.nodes.addCredentials(node.migrated.id, {apikey: node.migrated.apikey});
}
if(node.migrated.deviceid) {
when(promise).then(function() {
RED.nodes.addCredentials(req.params.id, {deviceid: node.migrated.deviceid});
});
}
}
res.send(JSON.stringify({migrated: true, config: node.migrated.id}));
}
else {
res.send("{}");
}
});
RED.httpAdmin.get('/pushbullet/:id/devices', function(req, res) {
var config = RED.nodes.getNode(req.params.id);
var cred = RED.nodes.getCredentials(req.params.id);
if(config && config.pusher) {
config.pusher.devices(function(err, chans) {
if(err) {
res.send("[]");
return onError(err, config);
}
res.send(JSON.stringify(chans.devices));
});
}
else if(cred && cred.apikey) {
var pb = new PushBullet(cred.apikey);
pb.devices(function(err, chans) {
if(err) {
res.send("[]");
return onError(err, config);
}
res.send(JSON.stringify(chans.devices));
});
}
else if(req.query.apikey) {
var pb = new PushBullet(req.query.apikey);
pb.devices(function(err, chans) {
if(err) {
res.send("[]");
return onError(err, config);
}
res.send(JSON.stringify(chans.devices));
});
}
else {
res.send("[]");
}
});
function PushbulletIn(n) {
RED.nodes.createNode(this, n);
var self = this;
var config = RED.nodes.getNode(n.config);
if(config) {
config.registerInputNode(this);
config.onConfig('error', function(err) {
self.error(err);
});
config.onConfig('stream_connected', function() {
self.status({fill: 'green', shape: 'ring', text: 'connected'});
});
config.onConfig('stream_disconnected', function(err) {
self.status({fill: 'red', shape: 'ring', text: 'disconnected'});
});
config.onConfig('stream_error', function(err) {
self.status({fill: 'red', shape: 'ring', text: 'error, see log'});
self.error(err);
});
}
}
RED.nodes.registerType("pushbullet in", PushbulletIn, {
credentials: {
filters: {value: []}
}
});
PushbulletIn.prototype.emitPush = function(msg) {
try {
if(this.credentials.filters.length > 0) {
if( (this.credentials.filters.indexOf(msg.data.source_device_iden) > -1) ||
(this.credentials.filters.indexOf(msg.data.target_device_iden) > -1) ||
(!msg.data.target_device_iden && !msg.data.source_device_iden)) { /* All */
this.send(msg);
}
}
else {
this.send(msg);
}
}
catch(err) {
this.send(msg);
}
}
}

View File

@ -13,9 +13,23 @@ Run the following command in the root directory of your Node-RED install
Usage
-----
### Pushbullet output node
Uses PushBullet to push <code>msg.payload</code> to a device that has the PushBullet app installed.
* Optionally uses <code>msg.topic</code> to set the title, if not already set in the properties.
* Optionally uses <code>msg.pushtype</code> to set the type of the push, if not already set in the properties.
* Optionally uses <code>msg.deviceid</code> to set the device ID, if not already set in the properties.
Uses PushBullet to push the <b>msg.payload</b> to a device that has the PushBullet app installed.
You can also push to any channels that you own either configured or via <code>msg.channel</code>.
Optionally uses <b>msg.topic</b> to set the title, if not already set in the properties.
The node can also *dismiss* and *delete* any push and *update* items in a pushed list. In this case <code>msg.data.iden</code> must be set to a valid push id, if <code>msg</code> originates from the Pushbullet input node this value is already set.
You need to configure both your <i>API key</i> and the target <i>device ID</i>. The device ID may be passed in as <b>msg.deviceid</b>. You can set these per node in the edit dialog.
### Pushbullet input node
Receives Pushbullets from all devices. Messages contain the following data:
* <code>msg.pushtype</code>: type of message
* <code>msg.topic</code>: topic information from the push
* <code>msg.payload</code>: main content of the push
* <code>msg.data</code>: original object from the pushbullet API containing e.g. sender, receiver and message ids.
Pushes of type <i>link</i> and <i>file</i> will also have <code>msg.message</code> containing the message associated with the push.
For further details of see <a href="https://docs.pushbullet.com/stream/">Pushbullet Stream API</a> and <a href="https://docs.pushbullet.com/v2/pushes/">Pushbullet Push API</a>.

View File

@ -1,13 +1,14 @@
{
"name" : "node-red-node-pushbullet",
"version" : "0.0.2",
"version" : "0.0.3",
"description" : "A Node-RED node to send alerts via Pushbullet",
"dependencies" : {
"pushbullet" : "1.4.*"
"pushbullet": "1.4.*",
"when": "^3.6.4"
},
"repository" : {
"type":"git",
"url":"https://github.com/node-red/node-red-nodes/tree/master/hardware/pushbullet"
"url":"https://github.com/node-red/node-red-nodes/tree/master/social/pushbullet"
},
"license": "Apache",
"keywords": [ "node-red", "pushbullet" ],

View File

@ -0,0 +1,779 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/*jshint expr: true*/
var sinon = require('sinon');
var should = require("should");
var proxyquire = require("proxyquire");
var EventEmitter = require('events').EventEmitter
var data = require("./data");
var path = require("path");
// Mute "Starting/Stopping flows to stdout"
var util = require('util');
var flows = proxyquire('../../../.node-red/red/nodes/flows.js', {
util: {
log: function(msg) {
if(!/ing flows/.test(msg)) {
util.log(msg);
}
}
}
});
var helper = require('../../../.node-red/test/nodes/helper.js');
var currentPB = null;
var pushbulletStub = function() {
var self = this;
currentPB = this;
this.streamEmitter = new EventEmitter();
this.streamObj = {
on: function(key, cb){ self.streamEmitter.on(key, cb);},
close: function(){},
connect: function(){}
}
// funcs
this.me = function(cb){ cb(null, data.me); };
this.history = function(opts, cb){ cb(null, getPushReply('note')); };
this.devices = function(){};
// pushes
this.note = function(){};
this.file = function(){};
this.link = function(){};
this.list = function(){};
this.address = function(){};
this.push = function(){};
// websocket
this.stream = function() {
return this.streamObj;
};
// modifiers
this.deletePush = function(){};
this.updatePush = function(){};
};
function getReply() {
return JSON.parse(JSON.stringify(data.base));
}
function getPushReply(name) {
var r = JSON.parse(JSON.stringify(data.base));
r.pushes.push(JSON.parse(JSON.stringify(data[name])))
return r;
}
var pushbulletNode = proxyquire("../../../social/pushbullet/57-pushbullet.js", {
"pushbullet": function(apikey) {
currentPB.apikey = apikey;
return currentPB;
}
});
describe('pushbullet node', function() {
beforeEach(function(done) {
currentPB = new pushbulletStub();
helper.startServer(done);
});
afterEach(function(done) {
helper.unload();
helper.stopServer(done);
});
describe('loading and settings', function() {
it('loads', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
done();
});
});
it('gets user info', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"}];
var func = sinon.stub(currentPB, "me");
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
func.yield(false, data.me);
helper.getNode("n1").me.then(function(me) {
me.should.have.property("email", "john.doe@noma.il");
done();
});
});
});
it('gets user info, fail', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"},
{id:"n3", type:"pushbullet in", config: "n1"}];
var func = sinon.spy(currentPB, "me");
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var warn1 = sinon.spy(helper.getNode("n2"), "error");
var warn2 = sinon.spy(helper.getNode("n3"), "error");
func.yield(true, null);
func.callCount.should.be.above(0);
warn1.callCount.should.be.above(0);
warn2.callCount.should.be.above(0);
//helper.getNode("n1").me.should.have.property("email", "john.doe@noma.il");
done();
});
});
it('list devices', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"}];
var func = sinon.stub(currentPB, "devices");
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.request().get("/pushbullet/n1/devices").end(function(err, res) {
var devs = JSON.parse(res.text);
devs.should.have.length(2);
done();
});
var ret = getReply();
ret.devices = data.devices;
func.yields(false, ret);
});
});
it('gets last modified', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"}];
var func = sinon.stub(currentPB, "history");
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
func.yield(false, getPushReply('modified'));
helper.getNode("n1").last.then(function(last) {
last.should.equal(1234);
done();
});
});
});
it('gets last modified with no history', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"}];
var func = sinon.stub(currentPB, "history");
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
func.yield(false, getReply());
helper.getNode("n1").last.then(function(last) {
last.should.equal(0);
done();
});
});
});
});
describe('backward compatiblity', function() {
it('should be valid', function(done) {
var flow = [{"id": "n1", "type": "pushbullet", "chan": "", "title": "topic"},
{id:"n3", type:"helper", wires: [["n1"]]}];
helper.load(pushbulletNode, flow, {n1:{pushkey:"key", deviceid: "dev"}}, function() {
var func = sinon.spy(currentPB, "note");
helper.getNode("n3").send({
payload: "my note",
});
sinon.assert.calledWith(func, "dev", "topic", "my note");
done();
});
});
});
describe('defaults and msg optionals', function() {
it('defaults', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"},
{id:"n3", type:"helper", wires: [["n2"]]}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var func = sinon.spy(currentPB, "note");
helper.getNode("n3").send({
payload: "my note",
});
helper.getNode("n1").me.then(function(){
sinon.assert.calledWith(func, "john.doe@noma.il", "Node-RED", "my note");
done();
});
});
});
it('no overrides', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "note", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "note");
helper.getNode("n3").send({
payload: "my note",
});
sinon.assert.calledWith(func, "id", "title", "my note");
done();
});
});
it('channel', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "note", title: "title", chan: "mychannel"},
{id:"n3", type:"helper", wires: [["n2"]]}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "note");
helper.getNode("n3").send({
payload: "my note",
});
sinon.assert.calledWith(func, {channel_tag: "mychannel"}, "title", "my note");
done();
});
});
it('channel from msg', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "note", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "note");
helper.getNode("n3").send({
payload: "my note",
channel: "mychannel"
});
sinon.assert.calledWith(func, {channel_tag: "mychannel"}, "title", "my note");
done();
});
});
it('all optionals but defined in node', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "link", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "push");
helper.getNode("n3").send({
deviceid: "over1",
topic: "over2",
pushtype: "note",
payload: "payload"
});
sinon.assert.calledWith(func, "id", {type: "link", body: undefined, title:"title", url: "payload"});
done();
});
});
it('all optionals', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "", title: ""},
{id:"n3", type:"helper", wires: [["n2"]]}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "_msg_"}}, function() {
var func = sinon.spy(currentPB, "push");
helper.getNode("n3").send({
deviceid: "over1",
topic: "over2",
payload: "over3",
pushtype: "link"
});
sinon.assert.calledWith(func, "over1", {type: "link", body: undefined, title: "over2", url: "over3"});
done();
});
});
});
describe('output', function() {
it('note', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "note", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "note");
helper.getNode("n3").send({
payload: "my note",
});
sinon.assert.calledWith(func, "id", "title", "my note");
done();
});
});
it('link', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "link", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "push");
helper.getNode("n3").send({
payload: "http://link",
message: "message"
});
sinon.assert.calledWith(func, "id", {type: "link", title: "title", url: "http://link", body: "message"});
done();
});
});
it('list', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "list", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "list");
helper.getNode("n3").send({
payload: ["a", "b", "c"],
});
sinon.assert.calledWith(func, "id", "title");
func.getCall(0).args[2].should.have.length(3);
done();
});
});
it('list string', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "list", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "list");
helper.getNode("n3").send({
payload: '["a", "b", "c"]',
});
sinon.assert.calledWith(func, "id", "title");
func.getCall(0).args[2].should.have.length(3);
done();
});
});
it('address', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "address", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "address");
helper.getNode("n3").send({
payload: "My Street 4",
});
sinon.assert.calledWith(func, "id", "title", "My Street 4");
done();
});
});
it('file', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "file", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "file");
var fn = path.join(__dirname, "data.js")
helper.getNode("n3").send({
payload: fn,
});
sinon.assert.calledWith(func, "id", fn, "title");
done();
});
});
it('should fail if file don\'t exist', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "file", title: "title"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "file");
var errfn = sinon.spy(helper.getNode("n2"), "error");
helper.getNode("n3").send({
payload: "hello",
});
func.called.should.fail;
errfn.callCount.should.be.above(0);
done();
});
});
it('raw', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "push");
helper.getNode("n3").send({
pushtype: '_raw_',
raw: {type: 'note', title: 'title', body: 'this is the note'}
});
sinon.assert.calledWith(func, "id", {type: 'note', title: 'title', body: 'this is the note'});
done();
});
});
it('raw update', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2: {deviceid: "id"}}, function() {
var func = sinon.spy(currentPB, "updatePush");
helper.getNode("n3").send({
pushtype: '_rawupdate_',
raw: {items: [{checked:false, text: 'a'}]},
data: {iden: 'id'}
});
sinon.assert.calledWith(func, "id", {items: [{checked:false, text: 'a'}]});
done();
});
});
});
describe('actions', function() {
it('delete', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "delete"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var func = sinon.spy(currentPB, "deletePush");
helper.getNode("n3").send({
payload: "my note",
data: { iden: "delid"}
});
sinon.assert.calledWith(func, "delid");
done();
});
});
it('dismiss', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "dismissal"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var func = sinon.spy(currentPB, "updatePush");
helper.getNode("n3").send({
payload: "my note",
data: { iden: "delid", dismissed: false }
});
sinon.assert.calledWith(func, "delid");
done();
});
});
it('update list', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet", config: "n1", pushtype: "updatelist"},
{id:"n3", type:"helper", wires: [["n2"]]}
];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var func = sinon.spy(currentPB, "updatePush");
helper.getNode("n3").send({
payload: [{}, {}, {}],
data: { iden: "delid", type: "list" }
});
sinon.assert.calledWith(func, "delid");
func.getCall(0).args[1].items.should.have.length(3);
done();
});
});
});
describe('input', function() {
it('handles stream push', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
done();
});
currentPB.streamEmitter.emit("message", {type: "push", push: {type: "clip", body: "clipboard"}})
});
});
it('handles status', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var func = sinon.spy(helper.getNode("n2"), 'status');
currentPB.streamEmitter.emit("connect");
currentPB.streamEmitter.emit("close");
currentPB.streamEmitter.emit("error");
func.callCount.should.equal(3);
done();
});
});
it('clipboard', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "clip");
msg.should.have.property("payload", "hello");
msg.should.have.property("data");
done();
});
currentPB.streamEmitter.emit("message", {type: "push", push: {type: "clip", body: "hello"}});
});
});
it('mirror', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "mirror");
msg.should.have.property("payload", "If you see this on your computer, mirroring is working!\n");
msg.should.have.property("data");
done();
});
currentPB.streamEmitter.emit("message", {type: "push", push: data.mirror});
});
});
it('dismissal', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "dismissal");
msg.should.have.property("payload", "pjgzwwocCCy");
msg.should.have.property("data");
done();
});
currentPB.streamEmitter.emit("message", {type: "push", push: data.dismissal});
});
});
it('unknown type', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
var err = sinon.spy(helper.getNode("n1"), "error");
currentPB.streamEmitter.emit("message", {type: "push", push: {type: "push", push: {type: "unknown", data: "test"}}});
err.called.should.be.ok;
done();
});
});
describe('tickle', function() {
it('note', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "note");
msg.should.have.property("payload", "body");
msg.should.have.property("topic", "title");
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
func.yields(false, getPushReply('note'));
});
});
it('link', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "link");
msg.should.have.property("payload", "url");
msg.should.have.property("topic", "title");
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
func.yields(false, getPushReply('link'));
});
});
it('address', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "address");
msg.should.have.property("payload", "address");
msg.should.have.property("topic", "title");
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
func.yields(false, getPushReply('address'));
});
});
it('file', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "file");
msg.should.have.property("payload", "fileurl");
msg.should.have.property("topic", "filename");
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
func.yields(false, getPushReply('file'));
});
});
it('list', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "list");
msg.should.have.property("topic", "title");
msg.should.have.property("payload").with.length(3);
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
func.yields(false, getPushReply('list'));
});
});
it('delete', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "delete");
msg.should.have.property("payload", "pjgzwwocCCy");
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
func.yields(false, getPushReply('delete'));
});
});
it('dismissed', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}}, function() {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("pushtype", "dismissal");
msg.should.have.property("payload", "xXxXxXxXxXxsjArqXRsaZM");
msg.should.have.property("data");
done();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
var rep = getPushReply('note');
rep.pushes[0].dismissed = true;
func.yields(false, rep);
});
});
it('filter', function(done) {
var flow = [{id:"n1", type:"pushbullet-config"},
{id:"n2", type:"pushbullet in", config: "n1", wires: [["n3"]]},
{id:"n3", type:"helper"}];
helper.load(pushbulletNode, flow, {n1:{apikey:"invalid"}, n2:{filters:['a', 'b']}}, function() {
var counter = sinon.spy();
helper.getNode("n3").on("input", function(msg) {
counter();
});
var func = sinon.stub(currentPB, "history");
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
var msg0 = getPushReply('link'); msg0.pushes[0].source_device_iden = 'a';
func.onCall(0).yields(false, msg0);
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
var msg1 = getPushReply('link'); msg1.pushes[0].source_device_iden = 'b';
func.onCall(1).yields(false, msg1);
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
var msg2 = getPushReply('link'); msg2.pushes[0].source_device_iden = 'c';
func.onCall(2).yields(false, msg2);
currentPB.streamEmitter.emit("message", {type: "tickle", subtype: "push"});
var msg3 = getPushReply('link');
delete msg3.pushes[0].source_device_iden;
delete msg3.pushes[0].target_device_iden;
func.onCall(3).yields(false, msg3);
setTimeout(function() {
counter.callCount.should.equal(3);
done();
}, 100);
});
});
});
});
});

View File

@ -0,0 +1,221 @@
module.exports = {
base: {
"accounts": [],
"aliases": [],
"channels": [],
"clients": [],
"contacts": [],
"devices": [],
"grants": [],
"pushes": [],
"subscriptions": []
},
modified: {
"active": true,
"iden": "xXxXxXxXxXxsjArqXRsaZM",
"created": 1421834169.9435499,
"modified": 1234,
"type": "note",
"dismissed": false,
"sender_iden": "xXxXxXxXxXx",
"sender_email": "john.doe@noma.il",
"sender_email_normalized": "johndoe@noma.il",
"sender_name": "John Doe",
"receiver_iden": "xXxXxXxXxXx",
"receiver_email": "john.doe@noma.il",
"receiver_email_normalized": "johndoe@noma.il",
"title": "title",
"body": "body"
},
devices: [
{
"active": true,
"iden": "xXxXxXxXxXxsjAnBdJA0yG",
"created": 1418045447.298193,
"modified": 1420698259.7963278,
"type": "chrome",
"kind": "chrome",
"nickname": "Chrome",
"manufacturer": "Google",
"model": "Chrome",
"app_version": 157,
"pushable": true
},
{
"active": true,
"iden": "xXxXxXxXxXxsjAiVsKnSTs",
"created": 1418045076.665757,
"modified": 1418047786.564935,
"type": "ios",
"kind": "ios",
"nickname": "myphone",
"manufacturer": "Apple",
"model": "iPhone 5s (Global)",
"app_version": 3671,
"push_token": "production",
"pushable": true
}
],
me: {
"iden": "xXxXxXxXxXx",
"created": 1418045075.2908669,
"modified": 1418504323.166974,
"email": "john.doe@noma.il",
"email_normalized": "johndoe@noma.il",
"name": "John Doe",
"image_url": "https://photo.jpg",
"google_userinfo": {
"name": "John Doe"
},
"preferences": {
"onboarding": {
"app": false,
"extension": false,
"friends": true
},
"social": false
}
},
note: {
"active": true,
"iden": "xXxXxXxXxXxsjArqXRsaZM",
"created": 1421834169.9435499,
"modified": 1421850263.303394,
"type": "note",
"dismissed": false,
"sender_iden": "xXxXxXxXxXx",
"sender_email": "john.doe@noma.il",
"sender_email_normalized": "johndoe@noma.il",
"sender_name": "John Doe",
"receiver_iden": "xXxXxXxXxXx",
"receiver_email": "john.doe@noma.il",
"receiver_email_normalized": "johndoe@noma.il",
"title": "title",
"body": "body"
},
link: {
"active": true,
"iden": "xXxXxXxXxXxsjxQymYT0qy",
"created": 1421834198.374012,
"modified": 1421850263.181358,
"type": "link",
"dismissed": false,
"sender_iden": "xXxXxXxXxXx",
"sender_email": "john.doe@noma.il",
"sender_email_normalized": "johndoe@noma.il",
"sender_name": "John Doe",
"receiver_iden": "xXxXxXxXxXx",
"receiver_email": "john.doe@noma.il",
"receiver_email_normalized": "johndoe@noma.il",
"title": "title",
"url": "url"
},
file: {
"active": true,
"iden": "xXxXxXxXxXxsjAjKAwqc5k",
"created": 1421842821.5911932,
"modified": 1421850262.446063,
"type": "file",
"dismissed": false,
"sender_iden": "xXxXxXxXxXx",
"sender_email": "john.doe@noma.il",
"sender_email_normalized": "johndoe@noma.il",
"sender_name": "John Doe",
"receiver_iden": "xXxXxXxXxXx",
"receiver_email": "john.doe@noma.il",
"receiver_email_normalized": "johndoe@noma.il",
"body": "title",
"file_name": "filename",
"file_type": "text/plain",
"file_url": "fileurl"
},
address: {
"active": true,
"iden": "xXxXxXxXxXxsjAlHSjtjcO",
"created": 1421834217.764878,
"modified": 1421850261.531033,
"type": "address",
"dismissed": false,
"sender_iden": "xXxXxXxXxXx",
"sender_email": "john.doe@noma.il",
"sender_email_normalized": "johndoe@noma.il",
"sender_name": "John Doe",
"receiver_iden": "xXxXxXxXxXx",
"receiver_email": "john.doe@noma.il",
"receiver_email_normalized": "johndoe@noma.il",
"name": "title",
"address": "address"
},
list: {
"active": true,
"iden": "xXxXxXxXxXxsjAdWmbcF3Y",
"created": 1421834433.677966,
"modified": 1421834444.256159,
"type": "list",
"dismissed": false,
"sender_iden": "xXxXxXxXxXx",
"sender_email": "john.doe@noma.il",
"sender_email_normalized": "johndoe@noma.il",
"sender_name": "John Doe",
"receiver_iden": "xXxXxXxXxXx",
"receiver_email": "john.doe@noma.il",
"receiver_email_normalized": "johndoe@noma.il",
"title": "title",
"items": [
{
"checked": true,
"text": "a"
},
{
"checked": true,
"text": "b"
},
{
"checked": false,
"text": "c"
}
]
},
mirror: {
"type": "mirror",
"iden": "pjgzwwn3YPQ",
"source_device_iden": "ubddjAzSDVLgrI",
"package_name": "com.pushbullet.android",
"application_name": "Pushbullet",
"notification_id": "-8",
"notification_tag": null,
"dismissable": true,
"title": "Mirroring test",
"body": "If you see this on your computer, mirroring is working!\n",
"icon": "iVBORw0KGgoAAAANSUhEBgUzC42AAAADNElEQVRo\ng==\n",
"created": 1399350964.1649699,
"sender_iden": "ubd",
"sender_email": "ryan@pushbullet.com",
"sender_email_normalized": "ryan@pushbullet.com",
"receiver_iden": "ubd",
"receiver_email": "ryan@pushbullet.com",
"receiver_email_normalized": "ryan@pushbullet.com"
},
dismissal: {
"type": "dismissal",
"iden": "pjgzwwocCCy",
"source_device_iden": "ubddjAzSDVLgrI",
"package_name": "com.pushbullet.android",
"notification_id": "-8",
"notification_tag":null,
"created":1399350966.22458,
"sender_iden": "ubd",
"sender_email": "ryan@pushbullet.com",
"sender_email_normalized": "ryan@pushbullet.com",
"receiver_iden": "ubd",
"receiver_email": "ryan@pushbullet.com",
"receiver_email_normalized": "ryan@pushbullet.com"
},
"delete": {
"active": false,
"iden": "pjgzwwocCCy",
"created": 1422525494.4456809,
"modified": 1422526124.538504,
"dismissed": false
}
};