I'm trying to implement one time PUSH notification subscriptions in the Parse Cloud service. Here's my model:
Installation.user - pointer to User
User - team (String)
Subscription - email (String), status (String), user (pointer to User)
As a user, I can say - send PUSH notification(s), to all my devices, when user with email became status and this user is in my team. User can have many subscriptions. User can unsubscribe manually (via OS X, iOS, Android, ... apps) or automatically - when user receives PUSH notification, subscription should be automatically cancelled. He can subscribe again, no problem, but auto cancellation is a must. These subscriptions are user based, user can see them on devices.
I wrote Parse Cloud function which consists of two pieces.
Send PUSH
Parse.Push.send({
where : query,
data : pushData
},
{ success: function() {
response.success('OK');
}, error: function(err) {
console.log(err);
response.error({ error: err });
}});
This does work. My query works and PUSH received.
Delete Subscriptions
deleteSubscriptionQuery.find({
success: function(objects) {
Parse.Object.destroyAll(objects, {
success: function() {
response.success('OK');
},
error: function(err) {
console.log(err);
response.error(err);
}
});
},
error: function(err) {
console.log(err);
response.error(err);
},
});
This does work. My deleteSubscriptionQuery works and correct subscriptions deleted.
Combined Together
Parse.Push.send({
where : query,
data : pushData
},
{ success: function() {
deleteSubscriptionQuery.find({
success: function(objects) {
Parse.Object.destroyAll(objects, {
success: function() {
response.success('OK');
},
error: function(err) {
console.log(err);
response.error(err);
}
});
},
error: function(err) {
console.log(err);
response.error(err);
},
});
}, error: function(err) {
console.log(err);
response.error({ error: err });
}});
This doesn't work and success is reported. It seems that the problem lies in Parse.Push.send, ie. success is called too soon. I assume Parse.Push.send doesn't really send notifications, but just schedules them and success is called upon successful schedule. If so, should be renamed to Parse.Push.schedule. Also I assume that they are scheduled with my query (query saved, not really executed), so, I suspect this solution doesn't work because of:
Parse.Push.send - saves my query and calls success,
Subscription objects are deleted in success,
Parse started to process my pushes with saved query, executes the query and it returns zero objects, because I already deleted Subscription objects in success of Parse.Push.send.
Am I right? Does anyone know Parse internals? If I am right, what do you propose I should do to implement one time PUSH notifications based on custom queries?
Here's the solution based on #Wain's proposal.
Fetch Subscription objects based on request parameters.
User is included in this query, so, it's fetched along with Subscription objects.
Make separate Installation query based on User objects from Subscription objects.
Send PUSH to separate Installation query.
Delete already fetched Subscription objects upon success.
I can safely delete Subscription objects in this way and it doesn't interfere with Installation query for PUSH notifications. Does work as expected.
Parse.Cloud.define("status", function(request, response) {
//
// Input validation
//
if (!request.params.hasOwnProperty("status")) {
response.error("Missing status parameter");
return;
}
var statusQuo = request.params["status"]
if (!statusQuo.hasOwnProperty("email") || !statusQuo.hasOwnProperty("team") || !statusQuo.hasOwnProperty("status")) {
response.error("Invalid status dictionary");
return;
}
var status = statusQuo["status"]
if ( status != "Available" ) {
response.success('OK');
return;
}
var email = statusQuo["email"]
var team = statusQuo["team"]
Parse.Cloud.useMasterKey();
//
// Find subscriptions
//
var usersQuery = new Parse.Query(Parse.User);
usersQuery.equalTo('team', team);
var Subscription = Parse.Object.extend("Subscription");
var subscriptionsQuery = new Parse.Query(Subscription);
subscriptionsQuery.include('user');
subscriptionsQuery.equalTo('status', status);
subscriptionsQuery.equalTo('email', email);
subscriptionsQuery.matchesQuery('user', usersQuery);
subscriptionsQuery.find({
success: function(subscriptions) {
var users = subscriptions.map(function(subscription) {
return subscription.get('user');
});
//
// Query for notifications / installations
//
var query = new Parse.Query(Parse.Installation);
query.equalTo('channels', 'status');
query.containedIn('user', users);
//
// Push notifications data
//
var pushData = { 'status' : statusQuo };
var apsData = { 'sound' : '', 'alert' : email + ' became Available.' };
pushData['aps'] = apsData
Parse.Push.send({
where : query,
data : pushData
},
{ success: function() {
Parse.Object.destroyAll(subscriptions, {
success: function() {
response.success('OK');
},
error: function(err) {
console.log(err);
response.error(err);
}
});
}, error: function(err) {
console.log(err);
response.error({ error: err });
}});
}, error: function(err) {
console.log(err);
response.error(err);
}
});
});
Related
Hopefully someone can point out my error here.
In my app a user clicks on a button to insert a doc into the database. When they click on another button, a timestamp is added to an array.
Here's the code to create the doc (it works):
// Add User
function addUser(event) {
event.preventDefault();
ident = makeWords(2);
var newUser = {
'ident' : ident,
'group': '',
'timestamps': [],
'date_created': Date()
}
// Use AJAX to post the object to our adduser service
$.ajax({
type: 'POST',
data: newUser,
url: '/users/adduser',
dataType: 'JSON'
}).done(function( response ) {
if (response.msg === '') {
console.log('user added');
} else {
alert('Error');
}
});
};
And here's the route which handles it:
/*
* POST to adduser.
*/
router.post('/adduser', function(req, res) {
var db = req.db;
var collection = db.get('testcol'); //'testcol' is the name of my collection
collection.insert(req.body, function(err, result){
res.send(
(err === null) ? { msg: '' } : { msg: err }
);
});
});
I kind of thought that updating a doc would be just as easy. I'm grabbing the doc by the ident field, which will be unique to each user. However, I can't seem to make the client-side stuff pass to the server. Here's my client-side update:
function addError(event) {
event.preventDefault();
// If it is, compile all user info into one object
var errorUpdate = {
'$push': {'error_button': Date()}
}
// Use AJAX to post the object to our adduser service
$.ajax({
type: 'PUT',
data: errorUpdate,
url: '/users/errorUpdate',
dataType: 'JSON'
}).done(function( response ) {
if (response.msg === '') {
console.log("update sent, didn't receive an error");
}
else {
alert('Error');
}
});
};
This code executes, but the server-side just throws 500s. Here's that function:
/*
* update mongo doc
*/
router.put('/errorUpdate', function(req, res) {
var db = req.db;
var collection = db.get('testcol');
collection.update({'ident': ident},req.body, function(err, result){
if (err) {
console.log('Error updating menu: ' + err);
res.send({'users.js: error':'An error has occurred'});
} else {
console.log('doc has been updated');
res.send(item);
}
});
});
Any idea where I'm going wrong?
I solved this and it was a really really stupid mistake.
You might notice in my server-side code I use a variable called ident:
router.put('/errorUpdate', function(req, res) {
var db = req.db;
var collection = db.get('testcol');
collection.update({'ident': ident},req.body, function(err, result)...
ident is a global variable from my client-side stuff (global.js, which makes the ajax call), and it never made it to the server.
Further, I tried to send the Mongo update statement with the ident variable, which is totally unnecessary and just caused headaches.
Here's how I fixed it. This is client-side (where I only send the ident variable):
function addError(event) {
event.preventDefault();
// If it is, compile all user info into one object
var identifyMe = {
'ident': ident
}
// Use AJAX to post the object to our adduser service
$.ajax({
type: 'POST',
url: '/users/errors',
data: identifyMe,
dataType: 'JSON'
}).done(function( response ) {
// Check for successful (blank) response
if (response.msg === '') {
console.log('update sent, no errors received');
}
else {
console.log('Error detected. Response was: ' + response);
}
});
};
... and this is server-side, where I take that identifier and do the update (this works because all I'm doing is inserting a time stamp):
router.post('/errors', function(req, res) {
console.log(req.body);
var identifier = req.body.ident;
var db = req.db;
var collection = db.get('testcol');
collection.update({'ident': identifier}, {$push: {'error_button': Date()}}, function(err, result){
res.send(
(err === null) ? { msg: '' } : { msg: err }
);
});
});
You might notice that I'm pulling out that ident variable from the JSON that's being passed, with req.body.ident.
Hope this helps someone else struggling with updating a Mongo doc by posting to Express routes via Ajax with Node! :)
I've written a bit of Cloud Code which executes after every user is saved. Inside, I would like to add the user to two roles, Alpha and Free, but this code only successfully adds new users to the Alpha role; the Free role has no data in the users table. Is there a way in Parse to assign users multiple roles?
Here is my Cloud Code.
Parse.Cloud.afterSave(Parse.User, function(request, response) {
Parse.Cloud.useMasterKey(); // grant administrative access to write to roles
var user = request.object;
query = new Parse.Query(Parse.Role);
query.equalTo("name", "Alpha");
query.first ( {
success: function(object) {
object.relation("users").add(user);
object.save();
response.success("The user has been authorized.");
},
error: function(error) {
response.error("User authorization failed!");
}
});
query = new Parse.Query(Parse.Role);
query.equalTo("name", "Free");
query.first ( {
success: function(object) {
object.relation("users").add(user);
object.save();
response.success("The user has been authorized.");
},
error: function(error) {
response.error("User authorization failed!");
}
});
});
The problem is sequencing. We need all of the queries and saves to complete before response.success() is called. As it is now, the timing of actions in the code is not deterministic. Clean it up by using the promises returned by the parse sdk...
Parse.Cloud.afterSave(Parse.User, function(request, response) {
Parse.Cloud.useMasterKey(); // grant administrative access to write to roles
var user = request.object;
query = new Parse.Query(Parse.Role);
query.equalTo("name", "Alpha");
query.first().then(function(object) {
object.relation("users").add(user);
return object.save();
}).then(function() {
query = new Parse.Query(Parse.Role);
query.equalTo("name", "Free");
return query.first();
}).then(function(object) {
object.relation("users").add(user);
return object.save();
}).then(function() {
response.success("The user has been authorized.");
}, function(error) {
response.error("error: " + error.message);
});
});
I'm using Parse to represent the state of a beer keg (among other things). I'd like to check the user's notifications, stored in a "Notifications" table, to see if they'd like to receive a notification when the keg is filled.
I have all of the logic for setting the user's notification settings as well as sending notifications in cloud/notifications.js. All of the logic for updating the keg is in cloud/beer.js. I created an exported function called "sendKegRefillNotification" which performs a query.find() on the Notifications table and gets called from beer.js.
The problem is that it doesn't seem to be executing query.find() when I call the function from beer.js, however when I call the same function from a job within notifications.js, it works just fine.
main.js:
require("cloud/beer.js");
require("cloud/notifications.js");
beer.js:
var notify = require("cloud/notifications.js");
var Keg = Parse.Object.extend("Keg");
var fillKeg = function(beerName) {
var promise = new Parse.Promise();
var keg = new Keg();
keg.set("beerName", beerName)
keg.set("kickedReports", []);
keg.save(null, { useMasterKey: true }).then(function(keg) {
console.log("Keg updated to " + beerName + ".");
promise.resolve(keg);
notify.sendKegRefillNotification(keg);
},
function(keg, error) {
promise.reject(error);
});
return promise;
}
Parse.Cloud.define("beerFillKeg", function(request, response) {
var beerName = request.params.name;
if (!beerName) {
response.error("No beer was specified.");
return;
}
if (!util.isUserAdmin(request.user)) {
response.error("User does not have permission to update the keg.");
return;
}
fillKeg(beerName).then(function(keg) {
kegResponse(keg).then(function(result) {
response.success(result);
});
},
function(error) {
response.error(error);
});
});
function kegResponse(keg) {
var promise = new Parse.Promise();
var result = {
id: keg.id,
beer: {
name: keg.get("beerName")
},
filled: keg.createdAt,
kickedReports: []
};
var kickedReports = keg.get("kickedReports");
if (!kickedReports || kickedReports.length == 0) {
promise.resolve(result);
} else {
util.findUsers(kickedReports).then(function(users) {
result.kickedReports = util.infoForUsers(users);
promise.resolve(result);
}, function(users, error) {
console.log(error);
promise.resolve(result);
});
}
return promise;
}
notifications.js:
var Keg = Parse.Object.extend("Keg");
var Notification = Parse.Object.extend("Notifications");
exports.sendKegRefillNotification = function(keg) {
var beerName = keg.get("beerName");
console.log("Sending notifications that keg is refilled to '" + beerName + "'.");
var promise = new Parse.Promise();
var query = new Parse.Query(Notification);
query.include("user");
query.equalTo("keg_filled", true);
query.find({ useMasterKey: true }).then(function(notifications) {
console.log("Found notifications!");
promise.resolve("Found notifications!");
},
function(notifications, error) {
console.error("No notifications");
console.error(error);
promise.reject(error);
});
return promise;
}
Parse.Cloud.job("beerSendRefillNotification", function(request, status) {
var query = new Parse.Query(Keg);
query.descending("createdAt");
query.first().then(function(keg) {
if (!keg) {
status.error("No keg");
return;
}
exports.sendKegRefillNotification(keg);
},
function(keg, error) {
response.error(error);
});
});
When I run the job "beerSendRefillNotification" from the Parse dashboard, I can tell that query.find() is getting called because it prints "Found notifications!":
E2015-02-23T06:59:49.006Z]v1564 Ran job beerSendRefillNotification with:
Input: {}
Result: success/error was not called
I2015-02-23T06:59:49.055Z]false
I2015-02-23T06:59:49.190Z]Sending notifications that keg is refilled to 'test'.
I2015-02-23T06:59:49.243Z]Found notifications!
However, when I call the cloud function "beerFillKeg", it isn't because it's not printing "Found notifications!" or "No notifications":
I2015-02-23T07:00:17.414Z]v1564 Ran cloud function beerFillKeg for user HKePOEWZvC with:
Input: {"name":"Duff"}
Result: {"beer":{"name":"Duff"},"filled":{"__type":"Date","iso":"2015-02-23T07:00:17.485Z"},"id":"olLXh0F54E","kickedReports":[]}
I2015-02-23T07:00:17.438Z]false
I2015-02-23T07:00:17.523Z]Keg updated to Duff.
I2015-02-23T07:00:17.525Z]Sending notifications that keg is refilled to 'Duff'.
I finally understand it. In sendKegRefillNotification, you're calling query.find({...}), then returning an object. That find is asynchronous, and you're doing nothing to wait for the result. I think you need to return the find function call, rather than an object you set within that method.
In other words, you're running along, leaving some async running code behind you.
Edit: I understand what you tried to do. It sort of makes sense. You defined a promise, and thought the caller would wait for the promise. The problem is, the promise is defined in an asynchronous block. It doesn't yet have any meaning at the moment the caller gets it.
It looks like Parse doesn't allow you to run a query from inside a callback from save(). When I moved "notify.sendKegRefillNotification(keg);" to outside of the callback, it worked.
var fillKeg = function(beerName) {
var promise = new Parse.Promise();
var keg = new Keg();
keg.set("beerName", beerName)
keg.set("kickedReports", []);
keg.save(null, { useMasterKey: true }).then(function(keg) {
console.log("Keg updated to " + beerName + ".");
console.log("Send notifications.");
promise.resolve(keg);
},
function(keg, error) {
promise.reject(error);
});
notify.sendKegRefillNotification(keg); // Now this works
return promise;
}
Can anyone shed some more light on why this worked?
I have been trying to do a specific operation once I receive the submitAdapterAuthentication from the challenge handler and I could not do any operation because my code it does not even compile through it. I am using the submitAdapterAuthentication in one method of my angular service. The method looks like this:
login: function (user, pass) {
//promise
var deferred = $q.defer();
//tempuser
tempUser = {username: user, password: pass};
userObj.user = user;
checkOnline().then(function (onl) {
if (onl) { //online
console.log("attempting online login");
var auth = "Basic " + window.btoa(user + ":" + pass);
var invocationData = {
parameters: [auth, user],
adapter: "SingleStepAuthAdapter",
procedure: "submitLogin"
};
ch.submitAdapterAuthentication(invocationData, {
onFailure: function (error) {
console.log("ERROR ON FAIL: ", error);
},
onConnectionFailure: function (error) {
console.log("BAD CONNECTION - OMAR", error);
},
timeout: 10000,
fromChallengeRequest: true,
onSuccess: function () {
console.log("-> submitAdapterAuthentication onSuccess!");
//update user info, as somehow isUserAuthenticated return false without it
WL.Client.updateUserInfo({
onSuccess: function () {
//return promise
deferred.resolve(true);
}
});
}
});
} else { //offline
console.log("attempting offline login");
deferred.resolve(offlineLogin());
}
uiService.hideBusyIndicator();
});
uiService.hideBusyIndicator();
return deferred.promise;
}
where ch is
var ch = WL.Client.createChallengeHandler(securityTest);
and checkOnline is this function that checks whether the user is online or not:
function checkOnline() {
var deferred = $q.defer();
WL.Client.connect({
onSuccess: function () {
console.log("** User is online!");
deferred.resolve(true);
},
onFailure: function () {
console.log("** User is offline!");
deferred.resolve(false);
},
timeout: 1000
});
return deferred.promise;
}
Finally this is the "submitLogin" procedure that I have in my SingleStepAuthAdapter.js. SingleStepAuthAdapter is the name of the adapter.
//-- exposed methods --//
function submitLogin(auth, username){
WL.Server.setActiveUser("SingleStepAuthAdapter", null);
var input = {
method : 'get',
headers: {Authorization: auth},
path : "/",
returnedContentType : 'plain'
};
var response = "No response";
response = WL.Server.invokeHttp(input);
WL.Logger.info('Response: ' + response.isSuccessful);
WL.Logger.info('response.responseHeader: ' + response.responseHeader);
WL.Logger.info('response.statusCode: ' + response.statusCode);
if (response.isSuccessful === true && (response.statusCode === 200)){
var userIdentity = {
userId: username,
displayName: username,
attributes: {
foo: "bar"
}
};
WL.Server.setActiveUser("SingleStepAuthAdapter", userIdentity);
return {
authRequired: false
};
}
WL.Logger.error('Auth unsuccessful');
return onAuthRequired(null, "Invalid login credentials");
}
So I am trying to send a promise to my controller in order to redirect the user to another page but the promise is not being returned as the challenge handler is not even working.
And by the way, I have followed this tutorial: https://medium.com/#papasimons/worklight-authentication-done-right-with-angularjs-768aa933329c
Does anyone know what this is happening?
Your understanding of the Challenge Handler and mine are considerably different.
Although the
ch.submitAdapterAuthentication()
is similar in structure to the standard adapter invocation methods I have never used any callbacks with it.
I work from the IBM AdapteBasedAuthentication tutorial materials
The basic idea is that your challenge handler should have two callback methods:
isCustomResponse()
handleChallenge()
You will see these functions invoked in response to your submission.
I suggest that start by looking at those methods. I can't comment on the ionic example you reference, but I have myself used angular/ionic with the authentication framework and challenge handlers. My starting point was the IBM material I reference above.
I'm attempting to send a push notification to a specific user's installation.
In the user class I have a column which is a pointer to an installation instance. Here's my code:
Parse.Cloud.beforeSave("Notification", function(request, response) {
var user;
if (!request.object.get("reciever")) {
response.error("Please include a user");
} else {
user = request.object.get("reciever");
}
var message = request.object.get("message");
var query = new Parse.Query(Parse.User);
query.include("installation");
query.equalTo("objectID", user.get("objectID"));
query.find({
success: function(result) {
var obj = result[0];
var intst = obj.get("installation");
console.log(intst);
Parse.Push.send({
where: intst,
data: {
alert: message
}
}, {
success: function() {
console.log("Push was successful");
response.success();
},
error: function(error) {
console.error(error);
response.error(error);
}
});
},
error: function(error) {
response.error(error);
}
});
});
From looking at my logs, the installation instance was received correctly. When sending, I get the following error:
Failed to create new object, with error code:
{"code":115,"message":"Missing the push channels."}
What am I doing wrong?
The Parse documentation specifys this as an example for sending Push notificatons using Javascript
Parse.Push.send({
channels: [ "Giants", "Mets" ],
data: { alert: "The Giants won against the Mets 2-3." }
}, {
success: function() {
// Push was successful
}, error: function(error) {
// Handle error
}
});
What you are missing is the 'channels: ["Giants", "Mets"]' section of this push notifcation. If you go into your installation table in parse you will notice there is a Channels column and this is what defines who the pushes get sent to. In this example the push will go to anyone who has signed up for the channels 'Giants' and 'Mets'.