How to scale push notifications in Firebase Cloud functions for this use case? - javascript

In my app, I am sending push notifications to the followers of a user when that user creates a new post. As you can see in the code below, I have some additional settings that I need to query from each follower's profile to get their push token and check for some additional notification settings. I am afraid that this query of each user's profile might become a bottleneck if a user has a large number of followers i.e. 1000.
What is the best way to approach this?
// The cloud function to trigger when a post is created
exports.newPost = functions.database.ref('/posts/{postId}').onCreate(event => {
const postId = event.params.postId;
const post = event.data.val();
const userId = post.author;
let tokens = [];
let promises = [];
return admin.database().ref(`/followers/${userId}`).once('value', (followers) => {
followers.forEach((f) => {
let follower = f.key;
promises.push(
admin.database().ref(`users/${follower}`).once('value')
);
});
})
.then(() => {
return Promise.all(promises).then((users) => {
users.forEach((user) => {
const userDetails = user.val();
if (userDetails.post_notifications) {
if(userDetails.push_id != null) {
tokens.push(userDetails.push_id);
}
}
})
})
})
.then(() => {
if (tokens.length > 0) {
const payload = {
notification: {
title: 'New Post!',
body: 'A new post has been created'
}
};
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload);
}
});
})
EDIT:
We have thought about using topics. But we are not sure how we can still have our customized notifications settings working with topics. Here's our dilemma.
We have multiple actions that can create notifications and we provide individual switches for each type of notification in the app so a user can select which type of notifications they want to switch off.
Let's say when User A follows User B. We can subscribe User A to "User B's topic" so whenever User B performs an action which sends out notifications to his/her followers, I can send send out notifications to users subscribed to "User B topic".
Because we have multiple notification switches in the app and when user A changes his/her settings that they do not want notifications for new posts but still want other types of notifications from the users he/she follows, we have not been able to figure out how we can use topics in this case.

Instead of using tokens, you can use topics for this. So lets say a user started following someone, then he will register to that topic.
Lets say he followed someone called "Peter", then you can execute this:
FirebaseMessaging.getInstance().subscribeToTopic("Peter");
Now if you have this database:
posts
postid
postdetails: detailshere
author: Peter
then using onCreate():
exports.newPost = functions.database.ref('/posts/{postId}').onCreate(event => {
const postId = event.params.postId;
const post = event.data.val();
const authorname = post.author;
const details=post.postdetails;
const payload = {
data: {
title:userId,
body: details,
sound: "default"
},
};
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToTopic(authorname, payload, options);
});
You can use this, everytime the author creates a new post, onCreate() is triggered then you can add the details of the post and the author name in the notification (if you want) and sendToTopic() will send it to all users subscribed to the topic that is the authorname (ex: Peter)
After your edit, I think you want the user to be unsubscribed from a topic, but stay subscribed to other topics, then you have to use the admin sdk for this:
https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions
Using the admin sdk you can unsubscribe the user also from a topic, a simple example:
// These registration tokens come from the client FCM SDKs.
var registrationTokens = [
'YOUR_REGISTRATION_TOKEN_1',
// ...
'YOUR_REGISTRATION_TOKEN_n'
];
// Unsubscribe the devices corresponding to the registration tokens from
// the topic.
admin.messaging().unsubscribeFromTopic(registrationTokens, topic)
.then(function(response) {
// See the MessagingTopicManagementResponse reference documentation
// for the contents of response.
console.log('Successfully unsubscribed from topic:', response);
})
.catch(function(error) {
console.log('Error unsubscribing from topic:', error);
});

Related

Firebase Funtions push notifications

I'm new to firebase and there is something I can't do. I want to send a notification to the phone with firebase functions. I want to receive notifications on the phone when someone follows me. My Firebase collection is as in the photo. I want to access the Followers array and send its information with notification. The codes I could write are as follows. What do I need to add?
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.sendPushNotification = functions.firestore.document('/users/{uid}').onCreate((snap, context) => {
var values = snap.data();
var token = values.fcmTokens;
var payload = {
notification: {
title: values.title,
body: values.message
}
}
return admin.messaging().sendToDevice(token, payload);
});
First, onCreate() function is triggered when a document is created. I assume followers array will be updated everytime someone follows a user? In that case you should be using onUpdate() that'll trigger the function when the document is updated. You can just check if length of followers array has changed in the update, if yes then send the notification as shown below:
exports.sendPushNotification = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
const newValue = change.after.data();
const previousValue = change.before.data();
if (newValue.followers.length > previousValue.followers.length) {
// followers count increased, send notification
const token = newValue.fcmTokens;
const payload = {
notification: {
title: "New Follower",
body: "Someone followed you"
}
}
await admin.messaging().sendToDevice(token, payload);
}
return null;
});
Here, we send notification only if the followers field has changed since this function will trigger whenever any field in this user document is updated.
If you want to specify who followed the user, then you'll have to find the new UID added in followers array and query that user's data.
Firestore documents have a max size limit of 1 MB so if a user can have many followers then I'll recommend creating a followers sub-collection. Then you'll be able to use onCreate() on the sub-document path /users/{userId}/followers/{followerId}

How do I store and retrieve user chats in firebase?

I am writing an app which will have a chat function. I am using react native and firebase. My question is - how do I store a particular user chats in firebase and how do I later retrieve them? The current solution which I have is having two screens - Chat.js and ListChats.js. Chat.js is the screen which loads when I want to send message to another user. Once a user starts new chat with another user I am calling createChat:
const createChat = async () => {
await firebase
.firestore()
.collection("chats")
.add({
//this is the part which bothers me the most
users: [currentUser.uid, user.uid],
lastMessage: "Send the first message",
lastMessageTimestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
};
I am storing the two user ids in the users field. Later when a user press the Send button:
const onSend = () => {
firebase
.firestore()
.collection("chats")
.doc(chat.id)
.collection("messages")
.add({
creator: firebase.auth().currentUser.uid,
text: textToSend,
creation: firebase.firestore.FieldValue.serverTimestamp(),
});
};
So far so good. However, I have performance concerns when I go to the ListChats.js page. On this page I want to list all chats, which the current user has. So I am doing a call to firebase to fetch these chats in the following way:
function fetchUserChats() {
return (dispatch) => {
firebase
.firestore()
.collection("chats")
.where("users", "array-contains", currentUser.uid)
.onSnapshot((snapshot) => {
let chats = snapshot.docs.map((doc) => {
//do something
});
});
};
}
My problem is that I am going through all the chats, then checking the users field, which is array containing two values (user ids) and then searching for my current user id. Is there a smarter/better way of fetching a particular user chats in terms of performance?

Show user invoices for simultaneously logged in users using Expressjs

I have created a simple invoice application using the MERN stack. The application is great at handling data for the logged in user as long as one user is logged in, but if another user logs in then the invoices for the user that first logged in is shown.
I am currently using app.set() and app.get() to pass data between endpoints and send to my frontend client. Auth0 handles the authentication layer, but would express-session solve this issue? And if it is how would I go about implementing this? Or is there a better solution?
Below is the code that sends the invoices currently to the frontend:
var express = require('express');
var app = express();
var userInvoices = express.Router();
const axios = require('axios');
const InvoiceModel = require('../models/Invoice');
const UserModel = require('../models/User');
//Functions//
async function clientCall() {
const url = `${process.env.REACT_APP_SAVE_USER}`;
const axiosConfig = {
method: 'get',
url
};
await axios(axiosConfig).catch((e) => {console.log(e)});
};
async function fetchUsersFromDB() {
const usersFromDB = await UserModel.find().catch((e) => {console.log(e)});
return usersFromDB;
};
async function saveUser(User) {
const condition = {email: User.email};
const query = {
nickname: User.nickname,
name: User.name,
picture: User.picture,
email: User.email,
email_verified: User.email_verified,
sub: User.sub,
};
const options = { upsert: true };
const update = await UserModel.updateMany(condition, query, options).catch((e) => {console.log(e)});
// Log the amount of documents that where updated in the DB.
if(update.nModified > 0 ) {
console.log('Number of Users added or updated to DB:', update.nModified)
}
};
function findCommonUser(dbUser, User) {
if(dbUser.length <= 0) {
UserModel.create(User, () => {console.log('Users saved to database')});
console.log('An Error has occured with Database')
} else {
dbUser.forEach((DBElement) => {
if(User.email !== DBElement.email) {
saveUser(User);
}
})
}
console.log(' Users match')
};
function matchUserAndInvoice(dbInvoices, loggedInUser) {
let newArray = [];
dbInvoices.forEach(async (DBElement) => {
if(DBElement.customer_email === loggedInUser.email){
newArray.push(DBElement);
app.set('userInvoices', newArray);
}
})
}
// prevents user from having to refresh to get data.
clientCall();
userInvoices.post('/saveUser', async (req, res) => {
try {
const User = req.body;
const usersFromDB = await fetchUsersFromDB().catch((e) => {console.log(e)});
findCommonUser(usersFromDB, User);
app.set('Users', User)
} catch (error) {
console.log(error)
}
})
userInvoices.get('/fetchUserInvoices', async (req,res) => {
try {
const invoices = await InvoiceModel.find().catch((e) => {console.log(e)});
const user = await app.get('Users');
await matchUserAndInvoice(invoices,user);
const userInvoices = await app.get('userInvoices')
res.json(userInvoices);
} catch (error) {
console.log(error)
}
});
;
module.exports = userInvoices;
app.get() is essentially global to your server instance so putting data there to use between requests for individual users will (as you have discovered) get data confused between different users as all users are trying to store data in the same place.
The usual way to solve a problem like this is to use express-session. This cookies the end-user's connection the first time they connect to your server and then creates a server-side object that is automatically associated with that cookie. Then, inside of any request handler, you can read or set data in req.session and that data will uniquely belong to just that user.
If the user changes devices or clears their cookies, then the association with the session object for that user will be lost (creating a new session object for them upon their next connection). But, if you have a persistent data store and you use some sort of login that allows you to populate the session from the user's persistent store, you can even make the session persistent across devices for the same user (though often times this is not required).
In the specific application you describe in your question (tracking invoices), it seems like your invoice data should be tagged with a specific user when it is stored in your database and then any future requests to display invoices should query based on the particular user that is making the request. This requires a login and login cookie so that you can uniquely identify each user and thus show them only the invoices that pertain to them.
The above-described session object should only be used for temporal session state, not for persistent storage such as invoices. If your server implementation is truly RESTFUL, you may not even need any data stored in the session object other than user's logged in userID.

How to send notification to multiple device token using firebase cloud functions

I am new in using Firebase Cloud Functions and JavaScript and I was able to send notification to a single device using Firebase Cloud Functions(JavaScript). Now I am trying to send push notification to multiple device token and I think I have a problem on it.
I want to send the notification to these device tokens in my firebase database:
/receivers
/event1
/uid1: device_token1
/uid2: device_token2
/uid3: device_token3
/uid4: device_token4
This is my code so far but it doesn't work..
exports.sendSOSNotif = functions.database.ref('/SOSNotifs/{sosId}').onWrite((data, context) => {
const eventId=data.after.child("eventId").val();
const uid=data.after.child("uid").val();
const displayName=data.after.child("displayName").val();
const photoUrl=data.after.child("photoUrl").val();
const status=data.after.child("status").val();
console.log("eventId:", eventId);
console.log("displayName:", displayName);
console.log("uid", uid);
const payload = {
notification: {
title: "SOS Alert",
body: displayName + " sent an alert!",
sound: "default"
},
data: {
eventId: eventId,
displayName: displayName
}
};
return Promise.all([admin.database().ref("/receivers/event1").once('value')]).then(results => {
const tokens = results[0];
if (!tokens.hasChildren()) return null;
const tokensList = Object.keys(tokens.val());
return admin.messaging().sendToDevice(tokensList, payload);
});
});
First of all, you shouldn't be adding tokens like below, if that's how you've organised your DB. There might be multiple token for a single uid
/receivers
/event1
/uid1: device_token1
/uid2: device_token2
/uid3: device_token3
/uid4: device_token4
And for sending notifications to multiple UIDs, I've written a script here
Also, update your question about what exactly the problem you are facing.

How do I query Firebase using Typescript?

I have push notifications set up for my app using Firebase Cloud Functions. It works well. Now I want to update the app's badge count as part of the push notification. I've read that the only way to do that is via server-side code; I can't do it locally.
So I'm trying to get the number of new users from the server and then use that number as the badge count when I send the push notification, but I can't figure out how to go about it. I've spent three days on this and now I'm hoping someone can point me in the right direction.
I'm using Firebase functions and Typescript (with VSCode). My course of action is to:
get list of userIDs from 'admin' node
iterate over those userIDs on 'user' node to query if user's 'newUser' parameter is true
append those results to an array
count the array and then send that to the badge on push notification
My 'users' database structure is like so:
"users": {
"2NBvNgdNRVe3nccTEDts2Xseboma": {
"email": "someone#someone.com"
"newUser": "true",
"referral": "none",
...
},
"hjC6os6wzIV1FyULmGxalU3fM7ef": {
"email": "someoneElse#someone.com"
"newUser": "false",
"referral": "Bennett",
...
}
And my 'admin' database is structured like so:
"admin": {
"2NBvNgdNRVe3nccTEDts2Xseboma": {
"email": "someone#someone.com"
"familyName": "Someone",
"memberSince": "1529119893",
},
"hjC6os6wzIV1FyULmGxalU3fM7ef": {
"email": "someoneElse#someone.com"
"familyName": "Someone Else",
"memberSince": "1529125722",
...
}
Here is my feeble attempt to code this:
exports.getNewUserCount =
functions.database.ref('/users/{userID}/newUser')
.onUpdate((snapshot, _context) => {
console.log('test 2')
// Get a database reference
const db = admin.database();
const ref = db.ref('admin');
return ref.once('value', function(adminSnap) {
const userData = adminSnap.val()
console.log('admin key:', adminSnap.key)
console.log('user data:', userData)
})
});
Right now I'm stuck on retrieving the list of users from the admin node (my step #1 above).
UPDATE
I finally got a list of the users as a snapshot, but I can't figure out how to iterate over them. How do I turn the snapshot into an array of the user keys?
And then once I get the list of user keys, then how do I use that to iterate over the 'users' node to get the list of new users (my step #2 above)?
And then how to put those new users into an array (my step #3 above), and then get the number of new users for the 'badge' parameter when I send my push notification (my step #4 above)?
The problem is that this seems really inefficient. There has to be a better way to simply get a list of new users. There has to be some sort of query I can perform that will go over my 'users' node, see which ones have 'true' for their 'newUser' node, and get a count of those--instead of my roundabout way of getting a list of user from 'admin' node, then using that list to get a list of 'new users' from the 'users' node, then creating an array and then counting that array, then using that number to send to the 'badge' parameter on my push notification.
Any thoughts? I've been at this for days.
If it helps, I know Swift and the app is iOS. Thanks!!
UPDATE #2
So I opted to try and just get a snapshot of all users and bypass the 'admin' node altogether. Here is the code:
const db = admin.database();
const ref = db.ref('users');
return ref.once('value').then((adminSnap) => {
console.log('admin key:', adminSnap.key)
// create blank array to store
let newUserCount = 0;
// iterate over adminSnap to get each individual snap
adminSnap.forEach(function (userSnap) {
const userData = userSnap.val();
const userKey = userSnap.key
// console.log('email?', userData.email, 'user key:', userKey, 'new user?', userData.newUser)
if (userData.newUser === true) {
newUserCount++
console.log('new user:', userKey, userData.newUser, userData.email)
}
});
console.log(newUserCount)
})
This new code works and gives me the number for my badge parameter for when I perform my push notification, but I'm wondering if it's the most efficient way to do things. Plus, as my database grows in size, won't it tax the server / slow way down? And won't it cost me a lot of bandwidth for my Firebase account?
I thought this would be a simple thing to do, but it's turning into a bit of a hassle. I'm open to a different way to complete this. Thanks!
After even more research, I ended up abandoning my original approach. I decided to just create a new node on my Firebase database with the new user count and then update it via code from elsewhere. It's the simplest approach and will use the least amount of bandwidth.
Here is my final code:
function sendAlertToiPhone() {
console.log('test E')
// Get a database reference
const db = admin.database();
const ref = db.ref('stats');
ref.child('newUserCount').once('value').then((snapshot) => {
const newUserCount = snapshot.val()
console.log('new user count:', newUserCount)
// send to Phontaine's iPhone 6
const FCMToken = "blahbehtyblahblah"
const payload = {
notification: {
title: 'New User',
body: 'Moneypants has a new download.',
sound: 'default',
badge: String(newUserCount)
}
};
return admin.messaging().sendToDevice(FCMToken, payload)
.then(function (response) {
console.log("Successfully sent message:", response);
})
.catch(function (error) {
console.log("Error sending message:", error);
});
}).catch(function (err) {
console.log('new user count error:', err);
})
}

Categories

Resources