Send messages to specific devices using firebase functions - javascript

I tried sending a specific message to a client device using node js and firebase functions. But when I tried executing the function, it came back with an error saying:
Error. Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array.
The image is shown below.
I was guessing it's from my JS code. So I am posting that too. What I am actually do is retrieving a data from a specific node to be used when a totally different node is being written. So I am gonna post the JS code before the database screenshots.
exports.sendNotification8 = functions.database.ref('/Users/{user_id}/Notifications/')
.onWrite(( change,context) =>{
var user_id = context.params.user_id;
// Grab the current value of what was written to the Realtime Database.
var eventSnapshot = change.after.val();
var device_token = admin.database().ref('/Users/{user_id}/device_token').once('value');
return device_token.then(result => {
var token_id = result.val();
var str = eventSnapshot.from + " : " + eventSnapshot.message;
console.log(eventSnapshot.from);
var payload = {
data: {
name: str,
title: eventSnapshot.from,
click_action: "Chats"
}
};
// Send a message to devices subscribed to the provided topic.
return admin.messaging().sendToDevice(token_id, payload).then(function (response) {
// See the MessagingTopicResponse reference documentation for the
// contents of response.
console.log("Successfully sent message:", response);
return;
})
.catch(function (error) {
console.log("Error sending message:", error);
});
});
});
And below is my database screenshots...
So that's how I am retrieving the device_token node. From the user that had the newest data written to his/her notifications node. Please help. What am I doing wrong?

Wow. This has been torture. But it finally worked. I got something like this.
exports.sendNotification8 = functions.database.ref('/Users/{user_id}/Notifications/{notifications_id}')
.onWrite((change,context) =>{
var user_id = context.params.user_id;
console.log(user_id);
// Grab the current value of what was written to the Realtime Database.
var eventSnapshot = change.after.val();
var device_token = admin.database().ref('/Users/'+user_id+'/device_token').once('value');
return device_token.then(result => {
var token_id = result.val();
console.log(token_id);
var str = eventSnapshot.message;
console.log(eventSnapshot.from);
var payload = {
data: {
name: str,
title: eventSnapshot.from,
click_action: "Chats"
}
};
// Send a message to devices subscribed to the provided topic.
return admin.messaging().sendToDevice(token_id, payload).then(function (response) {
// See the MessagingTopicResponse reference documentation for the
// contents of response.
console.log("Successfully sent message:", response);
return;
})
.catch(function (error) {
console.log("Error sending message:", error);
});
});
});

Related

Retrieve an array from a Firestore document and store it to Node.Js then use it as tokens to send notifications

I've been trying to figure this out for hours and I just can't. I'm still a beginner with Node.js and Firebase. I need your help to be able to retrieve the tokens array in my "userdata" collection to Node.js and be able to use it to send notifications in the Cloud Function. So far this is what I've been working on. Here is what my database looks like:
The receiverId is gathered from when I have an onCreate function whenever a user sends a new message. Then I used it to access the userdata of a specific user which uses the receiverId as their uid.
In the cloud function, I was able to start the function and retrieve the receiverId and print the userToken[key]. However, when I try to push the token it doesnt go through and it results in an error that says that the token is empty. See the image:
Your help would mean a lot. Thank you!
newData = snapshot.data();
console.log("Retrieving Receiver Id");
console.log(newData.receiverId); //uid of the user
const tokens = [];
const docRef = db.collection('userdata').doc(newData.receiverId);
docRef.get().then((doc) => {
if (doc.exists) {
console.log("DocRef exist");
const userToken = doc.data().tokens;
for(var key in userToken){
console.log(userToken[key]);
tokens.push(userToken[key]);
}
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch((error) => {
console.log("Error getting document:", error);
});
//Notification Payload
var payload = {
notification: {
title: newData.sendBy,
body: 'Sent you a message',
sound: 'default',
},
data: {
click_action : 'FLUTTER_NOTIFICATION_CLICK',
route: '/telconsultinbox',
}
};
console.log("Sending Notification now.");
console.log(tokens);
try{
//send to device
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
console.log(newData.sendBy);
}catch(err){
console.log(err);
}
I think you should avoid using for..in to iterate through an array (you can read more about it in this answer). Try one of these 2 options:
You could use forEach(), which is more elegant:
userToken.forEach((token) => {
console.log(token);
tokens.push(token);
});
for-of statement:
for(const token of userToken){
console.log(token);
tokens.push(token);
}
Also, I would consider renaming userToken to userTokens, since it should contain multiple values. Makes the code a bit more readable.

Advice converting an onCreate firebase cloud function trigger with FCM Messaging to support async/await and database reads

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;
}
});

How to send notification using FCM?

I wrote a cloud function, to listen for document creation in a collection, in my database
here is the function,
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('FCM/{id}').onCreate(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = 'hello';
const deviceIdTokens = await admin
.firestore()
.collection('FCM')
.get();
var tokens = [];
var i=0;
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().ar1[i]);
i++;
}
var payload = {
notification: {
title: 'push title',
body: 'push body',
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
This function works weirdly,
For example, sometimes it sends notification, and sometimes it does not.
It throws errors like " TypeError: Cannot read property '0' of undefined".
I don't know how to resolve this issue,
In my arr1 field, i have an array of device tokens, to whom i want to send notifications to,
i want the function to send notifications only to the devices(using tokens) which are just created(in the newly created document ),then delete the document.
I think it's sending notifications to all the documents at once.
I'm pretty new at node..
please help me out.
UPDATE:-
Here is my document structure
Type error coming from this line:
tokens.push(token.data().arr1[i]);
So all I can say is that sometimes token.data() doesn't have an arr1 attribute.

Each then() should return a value or throw

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)
});
});

How do you access a DocumentReference object stored in Firestore from a Cloud Function?

In my Firestore database I store DocumentReferences to users so that I am always using up-to-date user data such as username, profile pictures, and auth tokens.
I am also implementing Cloud Functions to listen for database triggers so that I can send notifications to those specific users about activity related to their posts.
This is where I run into trouble, because I do not know how to use the stored reference object properly inside the Node.js function when I access it like all other database information.
The following is my function code:
exports.countNameChanges = functions.firestore
.document('posts/{postId}')
.onUpdate((change, context) => {
// Retrieve the current and previous value
const data = change.after.data();
const previousData = change.before.data();
var registrationToken = '';
var notification = '';
var postTitle = data.statement;
var userRef = data.userRef; //This is my `DocumentReference` object
if (data.interactionCount > previousData.interactionCount && data.postTypeId == 2131165321) notification = 'You recieved a new comment!';
if (data.interactionCount > previousData.interactionCount && data.postTypeId == 2131165335) notification = 'You recieved a new vote!';
if (data.likes > previousData.likes) notification = 'You have a new post like!' ;
if (data.dislikes > previousData.dislikes) notification = 'You have a new post dislike!' ;
admin.firestore()
.doc(userRef) //This is my `DocumentReference` object
.get()
.then(doc => {
registrationToken = doc.data().token;
var payload = {
data: {
title: postTitle,
body: notification
},
token: registrationToken
};
admin.messaging().send(payload)
.then((response) => {
console.log('Successfully sent message:', response);
})
.catch((error) => {
console.log('Error sending message:', error);
})
});
});
});
My Function Log
I would assume that the DocumentReference object would be easy to work with
inside a Cloud Function since the object is supported for direct storage into Firestore, but I can't figure it out.
If userRef is a DocumentReference type object, then just call get() on it directly. Don't pass it to doc(). You're only supposed to pass string type objects to doc().
userRef.get().then(...)

Categories