I'm building an iOS messenger app using Swift, Firebase and Nodejs.
My Goal:
Whenever a user sends a message and writes message data (such as senderId, receiverId, messageText) into a Firebase database inside node (/messages/{pushId}/), I want to make a message count increment by 1 using a transaction method that Firebase provides and display a notification to a receiver user.
Progress I've made so far and Problem I'm facing:
I've successfully increment message count (totalCount) using transaction method but I can't get value inside transaction result (Here's image of functions log )
I want to get "value_: 1"( this is the incremented message count) inside snapshot and put it to a badge parameter.
exports.observeMessages = functions.database.ref('/messages/{pushId}/')
.onCreate((snapshot, context) => {
const fromId = snapshot.val().fromId;
const toId = snapshot.val().toId;
const messageText = snapshot.val().messageText;
console.log('User: ', fromId, 'is sending to', toId);
return admin.database().ref('/users/' + toId).once('value').then((snap) => {
return snap.val();
}).then((recipientId) => {
return admin.database().ref('/users/' + fromId).once('value').then((snaps) => {
return snaps.val();
}).then((senderId) => {
return admin.database().ref('/user-messages/' + toId + '/totalCount').transaction((current) => {
return (current || 0) + 1
}).then((readCount) => {
console.log('check readCount:', readCount);
var message = {
data: {
fromId: fromId,
badge: //I want to display the message count here
},
apns: {
payload: {
aps: {
alert: {
title: 'You got a message from ' + senderId.username,
body: messageText
},
"content-available": 1
}
}
},
token: recipientId.fcmToken
};
return admin.messaging().send(message)
}).then((response) => {
console.log('Successfully sent message:', response);
return response;
})
.catch((error) => {
console.log('Error sending message:', error);
//throw new error('Error sending message:', error);
})
})
})
})
Does anyone know how to do this?
Thanks in advance.
The API documentation for transaction() suggests that the promise from the transaction will receive an object with a property snapshot with the snapshot of the data that was written at the location of the transaction. So:
admin.database.ref("path/to/count")
.transaction(current => {
// do what you want with the value
})
.then(result => {
const count = result.snapshot.val(); // the value of the count written
})
Related
I initially had a simple firebase cloud function that sent out a push notification to a topic when a new message child was created in my real-time database. But I wanted to add message filtering where notifications for messages from some filtered users would be sent only to admin users. For this, I have created user groups in my real-time database of the format {userName: FIRToken}, which gets written to from my iOS App every time it launches and I get a FIRToken. So now I will have to load 2 lists 1) Admin Users, 2) Filtered Users before I can actually decide where to send the notification.
So I looked into ways to do this and async/await seemed better than doing a promise inside a promise for loading my 2 user lists. I then saw a firestore video tutorial where a similar usecase function was converted to use async/await instead of promises in promises. Following that, I refactored my code to await on the 2 snapshots for admin and filtered users, before going on to decide where to send the notification and return a promise. My refactoring seems correct. But unfortunately, my old iPhone is stuck on <DeviceName> is busy: Copying cache files from device. Hence I can't physically login from 2 different devices and test if the notifications are going only to my admin user account. Which is why I am posting my function here to see if I have refactored my code correctly or missed something. Please let me know if I will get the intended results or I should fix something in the code.
Edit: Updated code to fix these issues:
Also, the methods to send messages are very confusing. send needs topic name to be defined in the payload but does not support apns. sendToTopic needs a topic name as an argument with the payload. sendMulticast fails to send messages to users whereas sendToDevice sends properly.
Finally sendToDevice supports sound field in notification field, but send does not.
functions.database
.ref("/discussionMessages/{autoId}/")
.onCreate(async (snapshot, context) => {
// console.log("Snapshot: ", snapshot);
try {
const groupsRef = admin.database().ref("people/groups");
const adminUsersRef = groupsRef.child("admin");
const filteredUsersRef = groupsRef.child("filtered");
const filteredUsersSnapshot = await filteredUsersRef.once("value");
const adminUsersSnapshot = await adminUsersRef.once("value");
var adminUsersFIRTokens = {};
var filteredUsersFIRTokens = {};
if (filteredUsersSnapshot.exists()) {
filteredUsersFIRTokens = filteredUsersSnapshot.val();
}
if (adminUsersSnapshot.exists()) {
adminUsersFIRTokens = adminUsersSnapshot.val();
}
// console.log(
// "Admin and Filtered Users: ",
// adminUsersFIRTokens,
// " ",
// filteredUsersFIRTokens
// );
const topicName = "SpeechDrillDiscussions";
const message = snapshot.val();
// console.log("Received new message: ", message);
const senderName = message.userName;
const senderCountry = message.userCountryEmoji;
const title = senderName + " " + senderCountry;
const messageText = message.message;
const messageTimestamp = message.messageTimestamp.toString();
const messageID = message.hasOwnProperty("messageID")
? message.messageID
: undefined;
const senderEmailId = message.userEmailAddress;
const senderUserName = getUserNameFromEmail(senderEmailId);
const isSenderFiltered = filteredUsersFIRTokens.hasOwnProperty(
senderUserName
);
console.log(
"Will attempt to send notification for message with message id: ",
messageID
);
var payload = {
notification: {
title: title,
body: messageText,
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
};
console.log("Is sender filtered? ", isSenderFiltered);
if (isSenderFiltered) {
adminFIRTokens = Object.values(adminUsersFIRTokens);
console.log("Sending filtered notification with sendMulticast()");
payload.tokens = adminFIRTokens; //Needed for sendMulticast
return admin
.messaging()
.sendMulticast(payload)
.then((response) => {
console.log(
"Sent filtered message (using sendMulticast) notification: ",
JSON.stringify(response)
);
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(adminFIRTokens[idx]);
}
});
console.log(
"List of tokens that caused failures: " + failedTokens
);
}
return true;
});
} else {
console.log("Sending topic message with send()");
payload.topic = topicName;
return admin
.messaging()
.send(payload)
.then((response) => {
console.log(
"Sent topic message (using send) notification: ",
JSON.stringify(response)
);
return true;
});
}
} catch (error) {
console.log("Notification sent failed:", error);
return false;
}
});
I am trying to send push notifications using cloud functions for a group chat system, but i keep getting this error in my terminal: Each then() should return a value or throw
Why is this happening?
Here's my code:
let functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.database.ref('/chatrooms/{chatroomId}/chatroom_messages/{chatmessageId}')
.onWrite((snap, context) => {
console.log("System: starting");
console.log("snapshot: ", snap);
console.log("snapshot.after: ", snap.after);
console.log("snapshot.after.val(): ", snap.after.val());
//get the message that was written
let message = snap.after.val().message;
let messageUserId = snap.after.val().user_id;
console.log("message: ", message);
console.log("user_id: ", messageUserId);
//get the chatroom id
let chatroomId = context.params.chatroomId;
console.log("chatroom_id: ", chatroomId);
return snap.after.ref.parent.parent.once('value').then(snap => {
let data = snap.child('users').val();
console.log("data: ", data);
//get the number of users in the chatroom
let length = 0;
for(value in data){
length++;
}
console.log("data length: ", length);
//loop through each user currently in the chatroom
let tokens = [];
let i = 0;
for(var user_id in data){
console.log("user_id: ", user_id);
//get the token and add it to the array
let reference = admin.database().ref("/users/" + user_id);
return reference.once('value').then(snap => {
//get the token
let token = snap.child('messaging_token').val();
console.log('token: ', token);
tokens.push(token);
i++;
//also check to see if the user_id we're viewing is the user who posted the message
//if it is, then save that name so we can pre-pend it to the message
let messageUserName = "";
if(snap.child('user_id').val() === messageUserId){
messageUserName = snap.child('name').val();
console.log("message user name: " , messageUserName);
message = messageUserName + ": " + message;
}
//Once the last user in the list has been added we can continue
if(i === length){
console.log("Construction the notification message.");
let payload = {
data: {
data_type: "data_type_chat_message",
title: "Tabian Consulting",
message: message,
chatroom_id: chatroomId
}
};
return admin.messaging().sendToDevice(tokens, payload)
.then(function(response) {
// See the MessagingDevicesResponse reference documentation for
// the contents of response.
console.log("Successfully sent message:", response);
return response;
})
.catch(function(error) {
console.log("Error sending message:", error);
});
}
});
}
});
});
The message is caused by eslint detecting that you have a then() handler that can finish without returning a value or throwing an error.
It is caused by your for loop on the following line because you do not return a value or throw if data is empty:
for (var user_id in data) {
As others have commented, your for-loop won't execute correctly because you are returning a promise and finishing the handler on only the first iteration.
for (var user_id in data) {
// ...
return reference.once('value').then(snap => {
// ...
}
Arrays in Firebase RTDB
Based on your code, you encountered some of the issues with working with arrays in the RTDB as detailed in this blog post.
Instead of using an array to keep track of a chat room's members (below), it would be best to use a key-value pair approach instead. The value stored in the key-value pair can be a simple true value; or it can be given meaning (true for admins, false for others).
// Array-based list
"chatrooms/chatroomId1": {
"chatroom_messages": { ... },
"users": [
"userId1",
"userId2",
"userId3"
]
}
// RTDB stores above data as:
"chatrooms/chatroomId1": {
"chatroom_messages": { ... },
"users": {
"0": "userId1",
"1": "userId2",
"2": "userId3"
}
}
// Recommeneded: key-value pairs
"chatrooms/chatroomId1": {
"chatroom_messages": { ... },
"users": {
"userId1": true,
"userId2": false,
"userId3": false
}
}
The main benefit of such an approach is that delete a user from a room is simpler which will help with cleaning spam users/messages. To delete a user, you just call
firebase.database().ref("chatrooms/chatroomId1/users/userId1").delete();
rather than
firebase.database().ref("chatrooms/chatroomId1/users").orderByValue().equalTo("userId1").once('value')
.then((snap) => snap.delete());
Furthermore, sending a notification/message that a user was added or removed could be easily implemented using Cloud Functions defined using:
functions.database.ref('/chatrooms/{chatroomId}/users/{userId}').onCreate(...)
functions.database.ref('/chatrooms/{chatroomId}/users/{userId}').onDelete(...)
Chaining promises
When working with asynchronous tasks, avoid using for loops entirely because they are prone to causing undetectable mistakes and modern Javascript provides better alternatives. One such method is using the Promise.all(someArray.map(value => {...})) idiom covered in this answer.
It was also suggested in the question comments to flatten your promise chain, due to the number of changes needed to do the task efficiently, I decided to just make them and note each change in the code itself. The code below relies on the restructure of the chatroom members list discussed above.
let functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(); // CHANGED: Cloud Functions provides the needed environment variables to initialize this for you when called without arguments.
exports.sendNotification = functions.database.ref('/chatrooms/{chatroomId}/chatroom_messages/{chatMessageId}') // CHANGED: renamed 'chatmessageId' to 'chatMessageId' (consistent camelCaseStyling)
.onWrite((change, context) => { // CHANGED: renamed 'snap' to 'change' (matches actual type & less ambiguous below)
if (!change.after.exists()) { // CHANGED: Handle when message was deleted
// message deleted. abort
console.log(`Message #${context.params.chatMessageId} in Room #${context.params.chatroomId} deleted. Aborting.`);
return;
}
let messageData = change.after.val(); // CHANGED: avoid calling change.after.val() multiple times
// console.log("New data written: ", messageData); // CHANGED: Removed verbose log commands.
let message = messageData.message;
let messageAuthorId = messageData.user_id; // CHANGED: renamed 'messageUserId' to 'messageAuthorId' (less ambiguous)
let chatroomId = context.params.chatroomId;
console.log("New message:", { // CHANGED: merged log commands (less StackDriver API overhead when deployed)
user_id: messageAuthorId,
chatroom_id: chatroomId,
message: message
});
let chatroomMembersRef = change.after.ref.parent.parent.child('users'); // CHANGED: only got needed data
return chatroomMembersRef.once('value')
.then(snap => {
// DATABASE STRUCTURE CHANGE: "/chatrooms/{chatroomId}/users" - change array (["userId1", "userId2", "userId3"]) to a userId keyed OBJECT (e.g. {"userId1": true, "userId2": true, "userId3": true})
let chatroomMemberList = Object.keys(snap.val()); // CHANGED: renamed 'data' to 'chatroomMemberList' (less ambiguous)
// console.log("Chatroom Members: ", {
// count: chatroomMemberList.length,
// members: chatroomMemberList
// });
// Asyncronously, in parallel, retrieve each member's messaging token
let chatroomMemberTokenPromises = chatroomMemberList.map((memberId) => { // CHANGED: renamed 'user_id' to 'memberId' (less ambiguous, consistent camelCaseStyling)
let memberDataRef = admin.database().ref("/users/" + memberId); // CHANGED: renamed 'reference' to 'memberDataRef' (less ambiguous)
// CHANGED: For each member, get only their registration token (rather than all of their user data)
let getMessagingTokenPromise = memberDataRef.child('messaging_token').once('value').then((memberTokenSnap) => {
console.log("Got messaging token for member #", memberId);
return memberTokenSnap.val();
});
// If this member is the message author, also get their name to prepend to the notification message.
if (memberId === messageAuthorId) {
let prependUserNamePromise = memberDataRef.child('name').once('value')
.then((memberNameSnap) => {
let messageAuthorName = memberNameSnap.val();
console.log("Message author's name: " , messageAuthorName);
message = messageAuthorName + ": " + message;
});
return Promise.all([getMessagingTokenPromise, prependUserNamePromise])
.then(results => results[0]); // only return result of getMessagingTokenPromise
} else {
return getMessagingTokenPromise;
}
});
// Wait for all of the messaging tokens
return Promise.all(chatroomMemberTokenPromises);
})
.then((chatroomMemberTokensArray) => {
console.log("Constructing the notification message...");
let payload = {
data: {
data_type: "data_type_chat_message",
title: "Tabian Consulting",
message: message,
chatroom_id: chatroomId
}
};
return admin.messaging().sendToDevice(chatroomMemberTokensArray, payload)
.then(function(response) {
// See the MessagingDevicesResponse reference documentation for
// the contents of response.
console.log("Successfully sent message:", response);
return response;
})
.catch(function(error) {
console.log("Error sending message:", error);
});
})
.catch((error) {
console.log("Unexpected error:", error)
});
});
I have deployed the following JavaScript to Firebase Cloud Function but I am constantly getting the following error even though I know that there is a token saved under the path specified.
The idea is to trigger a notification to device when a new message is written to the database, it then fetches the registration token which is then used to try and send a notification to the user.
Error: Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array.
JS Function:
exports.notifyNewMessage = functions.database.ref('/messages/{pushId}/{message}').onCreate((snap, context) => {
const message = snap.val();
const fromId = message['fromId'];
const toId = message['toId'];
const messageTxt = message ['message'];
const imageUrl = message ['imageUrl'];
return admin.database().ref('/fcmtokens/' + toId + '/registration-tokens').once('value').then((userTok) => {
const registrationTokens = userTok.val()
console.log(registrationTokens);
return admin.database().ref('/users' + fromId).once('value').then((userDoc) => {
const senderName = userDoc.firstName //get('firstName')
const notificationBody = (imageUrl === "") ? "You received a new image message." : messageTxt
//build media messages notification
const payload = {
notification: {
title: senderName + " sent you a message",
body: messageTxt
},
data: {
SENDER_NAME: senderName,
SENDER_ID: fromId
}//end data
}//end payload
//send message
return admin.messaging().sendToDevice(registrationTokens, payload).then( response => {
const stillRegisteredTokens = registrationTokens
response.results.forEach((result, index) => {
const error = result.error
if (error) {
const failedRegistrationToken = registrationTokens[index]
console.error('blah', failedRegistrationToken, error)
if (error.code === 'messaging/invalid-registration-token'
|| error.code === 'messaging/registration-token-not-registered') {
const failedIndex = stillRegisteredTokens.indexOf(failedRegistrationToken)
if (failedIndex > -1) {
stillRegisteredTokens.splice(failedIndex, 1)
}
}
}
})//end forEach
return admin.database().ref("fcmtokens/" + recipientId).update({
registrationTokens: stillRegisteredTokens
})//end update
})//end sendToDevice
})//end return-then
})//end return-then
});
This is my fcmtokens database structure node:
"fcmtokens" : {
"dBQdpR7l1WT2utKVxdX2" : {
"registration-tokens" : {
"c4PSCAUAg5s:Yw95DyVxwElE88LwX7" : true
}
}
}
Question update:
The update database part of my function above is creating a separate branch called registrationTokens of all active tokens. I want to overwrite and update the current token under registration-tokens instead?
return admin.database().ref("fcmtokens/" + recipientId).update({
registrationTokens: stillRegisteredTokens
})//end update
Image showing the new registrationTokens branch being created.
Given your data structure, this code:
admin.database().ref('/fcmtokens/' + toId + '/registration-tokens').once('value').then((userTok) => {
const registrationTokens = userTok.val()
Leads to registrationTokens being:
{
"c4PSCAUAg5s:Yw95DyVxwElE88LwX7" : true
}
You then call sendToDevice with:
return admin.messaging().sendToDevice(registrationTokens, payload).then( response => {
But if you look at the reference documentation for `sendToDevice, it says:
registrationTokens
Array of string
An array of registration tokens for the devices to which to send the message.
And clearly your registrationTokens is not an array, but just an object.
To convert it to an array use Object.keys():
const registrationTokens = Object.keys(userTok.val())
So I am attempting to use Firebase cloud messaging to send push notifications to my app. However, it seems to fail continuously for some odd reason. I am using the onCreate method to listen for the creation of a node under a certain path. It is getting the UID from that info gathered in the onCreate method but then when I try to get the user info from a different node to grab the fcmToken so I can send the notification. It always says undefined.
My index.js looks like this:
//listen for reply notification and then trigger a push notification
exports.observeNotification = functions.database.ref('/notifications/{uid}/{commentId}/')
.onCreate(event => {
// Grab the current value of what was written to the Realtime Database.
//const notification = event.data.val();
console.log('User ID is: ', event.params.uid);
console.log('Comment ID Is: ', event.params.commentId);
return admin.database().ref('/users/' + event.params.uid).once('value', snapshot => {
var userWeAreSendingTo = snapshot
console.log('User ID is: ', userWeAreSendingTo);
console.log('FCM Token of that user is: ', userWeAreSendingTo.fcmToken);
var payload = {
notification: {
title: "",
body: snapshot.content
}
};
admin.messaging().sendToDevice(userWeAreSendingTo.fcmToken, payload)
.then((res) => {
// Response is a message ID string.
console.log('Successfully sent message:', res);
return true
})
.catch((error) => {
console.log('Error sending message:', error);
return true
})
})
})
This is a snapshot of the child node under the user node in my database.
"9mzAHeX3lcdzriPdC4TfbRTkaUm2" : {
"fcmToken" : "coC8uorosgc:APA91bGur2vvH4fwIProh87pUzVw0jYTOOFW3KfqWRVk4WdX0x8M1iBFwg28wM3tFyB5iRrowTWCZ_45oGwo0_7BFD6YvULE30NNZXxvE2O2XLjlLd_fqYwMfkndOqSUem2HqO-qvNcZ",
"profilePic" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/profile_images%2F832F8156-C14A-4EF3-86EB-D8F4CFC784E5.PNG?alt=media&token=b9d11ea1-8cb3-4d0f-b111-b0baf567ac7b",
"username" : "lol123"
}
This screenshot of the console log
enter image description here
I'm having a strange problem with Parse-Server.
I get sometimes the following error:
ParseError { code: 101, message: 'Object not found.' }
It usually happen after an error in cloud code, in whatever function that use the object even if there is no update in it (like the following function that just doing a find but still create the bug if it fail for any reason like a bad code line). However I'm not sure this is the cause.
I have that simple function in Cloud Code:
const user = req.user
const card_id = cid(req, user)
const base_error = global.i18n.__('errors.default')
if(!card_id) return res.error(base_error)
console.log('CID: ' + card_id + ' | UID: ' + user.id)
new Parse.Query(global.m.Card).get(card_id)
.then((card) => {
if((new Date) - card.updatedAt > OSC.syncDelay)
return _CARD.sync(card.get('onum'))
return card
})
.then((card) => {
res.success(card)
})
.then(null, (error) => {
if(Array.isArray(error)) error = error[0]
console.log(error) // HERE is where the error is logged
res.error(base_error)
})
Here is what the console.log print:
CID: PqOMwi5y60 | UID: QldBflokJV
Here is how the Card ACLs are defined (with an update, not at creation):
const cardACL = card.getACL()
cardACL.setReadAccess(user, true)
cardACL.setWriteAccess(user, true)
if(memory.admin) {
cardACL.setReadAccess(memory.admin, true)
cardACL.setWriteAccess(memory.admin, true)
}
card.setACL(cardACL)
Finally, this is what I have in my Database for the card object Permissions/ACLs:
"_id" : "PqOMwi5y60",
"_wperm" : [
"role:Administrator",
"QldBflokJV"
],
"_rperm" : [
"role:Administrator",
"QldBflokJV"
],
"_acl" : {
"role:Administrator" : {
"w" : true,
"r" : true
}
}
Am I doing something wrong ?
Because your ACL contains the user (since only the user or admin can read and write from/to the object) in your query you need also to send the session token of the logged in user. So at the end your code should look like the following:
const user = req.user
const card_id = cid(req, user)
const base_error = global.i18n.__('errors.default')
if(!card_id) return res.error(base_error)
console.log('CID: ' + card_id + ' | UID: ' + user.id)
new Parse.Query(global.m.Card).get(card_id,{
sessionToken: request.user.get("sessionToken")
})
.then((card) => {
if((new Date) - card.updatedAt > OSC.syncDelay)
return _CARD.sync(card.get('onum'))
return card
})
.then((card) => {
res.success(card)
})
.then(null, (error) => {
if(Array.isArray(error)) error = error[0]
console.log(error) // HERE is where the error is logged
res.error(base_error)
})
Please notice that i sent also the session token in the query.