Firebase Cloud Function to send notification at very specific time [duplicate] - node.js

Inside the Firebase Console, under the Cloud Messaging view, users are able to create test notifications. This functionality also allows you to schedule the time at which the notification will send to a device or set of devices.
Is it possible to create and send scheduled FCM notifications to specific devices by using firebase cloud functions and the Firebase Admin SDK? Is there an alternative way to solving this?
The current way that I send scheduled messages to users is like so:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const schedule = require('node-schedule');
admin.initializeApp();
exports.setScheduledNotification = functions.https.onRequest(async (req, res) => {
const key = req.query.notification_key;
const message = {
notification: {
title: 'Test Notification',
body: 'Test Notification body.'
}
};
var currentDate = new Date();
var laterDate = new Date(currentDate.getTime() + (1 * 60000));
var job = schedule.scheduleJob(key, laterDate, () => {
const snapshot = admin.messaging().sendToDevice(key, message);
});
return res.status(200).send(`Message has been scheduled.`);
});
First of all, I am unsure how node-schedule interacts with firebase cloud functions. The logs appear that the function terminates very quickly which I would think would be correct. The longer the operation runs the more costly it is in our firebase bills. The notification does still run on a scheduled time though. I'm confused on how that all is working behind the scenes.
Secondly, I am having issues canceling these scheduled notifications. The notifications will most likely be on a 2hr timed schedule from the point it gets created. Before the 2hrs is up, I'd like the have the ability to cancel/overwrite the notification with an updated scheduled time.
I tried this to cancel the notification and it failed to find the previously created notification. Here is the code for that:
exports.cancelScheduledNotification = functions.https.onRequest(async (req, res) => {
const key = req.query.notification_key;
var job = schedule.scheduledJobs[key];
job.cancel();
return res.status(200).send(`Message has been canceled.`);
});
Is it possible to tap into the scheduling functionality of firebase cloud messaging outside of the firebase console? Or am I stuck with hacking my way around this issue?

A Cloud Function can run for a maximum of 9 minutes. So unless you're using node-schedule for periods shorter than that, your current approach won't work. Even if it would work, or if you are scheduling for less than 9 minutes in advance, using this approach is very uneconomic as you'll be paying for the Cloud Functions for all this time while it's waiting.
A more common approach is to store information about what message you want to be delivered to whom at what time in a database, and then use regular scheduled functions to periodically check what messages to send. For more on this, see these previous questions:
Firebase scheduled notification in android
How to schedule push notifcations for react native expo?
Schedule jobs in Firebase
Ionic: Is it possible to delay incoming push FCM push notification from showing to my device until a specific time
Cloud Functions for Firebase trigger on time?
How to create cron jobs dynamically in firebase
A recent improvement on this is to use the Cloud Tasks API to programmatically schedule Cloud Functions to be called at a specific time with a specific payload, and then use that to send the message through FCM. Doug Stevenson wrote a great blog post about this here: How to schedule a Cloud Function to run in the future with Cloud Tasks (to build a Firestore document TTL). While the post is about deleting documents at a certain time, you can combine it with the previous approach to schedule FCM messages too.
Scheduling of tasks is now also described in the documentation on enqueueing functions with Cloud Tasks
A final option, and one I'd actually recommend nowadays, is to separate the delivery of the message from the display of the notification.
Display of data messages (unlike notification messages) is never handled by the system, and always left to your application. So you can deliver the FCM data message straight away that then contains the time to display the message, and then wake the device up to display the message (often called a local notification) at that time.

To make Frank's answer more tangible, I am including some sample code below for scheduled cloud functions, that can help you achieve the 'scheduled FCM notifications'.
You should store the information required to send your notification(s) in Firestore (e.g. the when-to-notify parameter and the FCM token(s) of the users you want to send the notification to) and run a cloud function every minute to evaluate if there is any notification that needs to be delivered.
The function checks what Firestore documents have a WhenToNofity parameter that is due, and send the notifications to the receiver tokens immediately. Once sent, the function sets the boolean 'notificationSent' to true, to avoid that the users receive the same notification again on the next iteration.
The code below achieves just that:
const admin = require('firebase-admin');
admin.initializeApp();
const database = admin.firestore();
exports.sendNotification = functions.pubsub.schedule('* * * * *').onRun(async (context) => {
//check whether notification should be sent
//send it if yes
const query = await database.collection("experiences")
.where("whenToNotify", '<=', admin.firestore.Timestamp.now())
.where("notificationSent", "==", false).get();
query.forEach(async snapshot => {
sendNotification(snapshot.data().tokens);
await database.doc('experiences/' + snapshot.id).update({
"notificationSent": true,
});
});
function sendNotification(tokens) {
let title = "INSERT YOUR TITLE";
let body = "INSERT YOUR BODY";
const message = {
notification: { title: title, body: body},
tokens: tokens,
android: {
notification: {
sound: "default"
}
},
apns: {
payload: {
aps: {
sound: "default"
}
}
}
};
admin.messaging().sendMulticast(message).then(response => {
return console.log("Successful Message Sent");
}).catch(error => {
console.log(error);
return console.log("Error Sending Message");
});
}
return console.log('End Of Function');
});
If you're unfamiliar with setting up cloud functions, you can check how to set them up here. Cloud functions require a billing account, but you get 1M cloud invocations per month for free, which is more than enough to cover the costs of this approach.
Once done, you can insert your function in the index.js file.

Related

How to send a large amount of push notifications using FCM with firebase admin SDK?

I have a cron job function running on Firebase functions, which fetches all documents from my User collection in Firestore, and sends notification using FCM to their devices. Due to limitations on how many tokens you can send to in one go, I'm splitting all my users tokens up in chunks of 100, and sending it in batches.
const admin = require("firebase-admin");
const fcm = admin.messaging();
const _ = require("lodash");
....
const deviceTokens = [.....] // <- flat array with all device tokens
const chunkedList = _.chunk(deviceTokens, 100); // [[...], [...], ...]
const message = "some message";
const sendAll = async () => {
const sendInChunks = chunkedList.map(async (tokenArr) => {
await fcm.sendToDevice(tokenArr, message);
});
await Promise.all(sendInChunks);
};
await sendAll();
I'm trying to understand from the documentation if this would be a safe way of doing it. For example, if one of the device tokens is stale or for some other reason fails, will that whole call to fcm.sendToDevice fail with along with the other tokens that was passed in, or will just that single device not recieve it? Or is there anything else I'm missing here?
Having one or more invalid/outdated tokens in the API call does not cause that call to fail.
Instead the FCM API (and its Admin SDK wrappers) will try to deliver each message separately and report back which specific tokens that you passed are unknown/outdated. You'll want to process those results and use them to prune your own token registry, similar to what this code sample in our Cloud Functions samples repro shows.

Better approach to store FCM tokens array in a collection/doc and update it on token refresh

I am building an application where people can host events. It allows for users to follow the events and all the event followers can chat in a group. I am storing the FCM token for push notifications in a user collection in Firestore. But when I send the notification my current logic is not quite optimal and it takes several minutes for the notification to send to each user as I am first getting all user's tokens for each user's document and then combing those tokens in a list and send push notification to each token using a loop in my cloud function. I think it takes time for sending the messages and the group chat does not seem to be real-time because of that computation.
What I thought is to store the FCM tokens of each user inside every event he follows. So that when there is a chat message the list of tokens is fetched from the specific event and they are sent a multicast notification. But here again, I am not sure if it is a good approach because I will need to keep track of refreshed tokens and update the tokens in each document where it exists.
Let's say a user has joined 50 events and on app launch, his FCM token got refreshed now I will have to run a cloud function that will see if the token is renewed it should loop through all of those 50 events and update his token.
I want to know what can be the best approach for this use case.
Below is the code that I am using:
exports.sendNotification = functions.firestore
.document("event/{eventid}/{chat}/{chatid}")
.onCreate((snap, context) => {
processData(snap, context);
return null;
});
async function processData(snap, context) {
const doc = snap.data();
const eventID = context.params.eventid;
const senderID = doc.sender;
var senderName = doc.senderName;
await admin
.firestore()
.collection("event")
.doc(eventID)
.get()
.then(value => {
var joinedBy = value.data()["joinedBy"];
joinedBy.forEach(item => {
getTokens(uid, senderID, doc, eventName, senderName);
});
});
}
async function getTokens(uid, senderID, doc, eventName, senderName) {
await admin
.firestore()
.collection("people")
.doc(uid)
.get()
.then(value => {
var token = value.data()["pushToken"];
if (uid != senderID) {
if (token) {
const payload = {
notification: {
title: `${eventName} : ${senderName}`,
body: doc.text,
badge: "1",
sound: "default"
},
};
sendMessage(token, payload);
} else {
console.log("Can not find pushToken target user");
}
} else {
console.log("uid and sender id is same so notification not sent");
}
});
}
One thing to consider in your current approach is whether you really first need to read all tokens for the message and only then starting calling FCM. Might things get faster if you call FCM right away on each set of tokens you read, and thus perform some of the reads and FCM calls in parallel?
Another consideration is that you say that FCM's built-in topics work really fast for you. Internally FCM's topic subsystem associates a bulk of device IDs with the topic, and then reads the tokens for the topic as needed.
Is this something you can mimic for your app? For example, can you store the tokens in larger groups inside fewer documents, thus reducing the number of read operations you need to do.

Can I schedule a Google Cloud Task on the client side to call a cloud function with a payload?

I want to make sure I'm thinking about Cloud Tasks right conceptually, and not sure that I am.
The examples I've been looking at seem to trigger a cloud function first that then schedules a task, that then calls a cloud function again.
(Or at least this is what I'm understanding, I could be wrong).
I'd like to set up something so that when a user clicks a button, it schedules a cloud task for some time in the future (anywhere from 1 minute to an hour and half). The cloud task then triggers the cloud function to upload the payload to the db.
I tried to set this up client side but I've been getting the error "You need to pass auth instance to use gRPC-fallback client in browser or other non-Node.js environments."
I don't want the user to have to authenticate if that's what this is saying (not sure why I'd have to do that for my use case).
This is the code that gives that error.
const {CloudTasksClient} = require('#google-cloud/tasks');
const client = new CloudTasksClient();
// import { Plugins } from '#capacitor/core';
// const { RemotePlugin } = Plugins;
const scheduleTask = async(seconds) => {
async function createHttpTask() {
const project = 'spiral-productivity';
const queue = 'spiral';
const location = 'us-west2';
const url = 'https://example.com/taskhandler';
const payload = 'Hello, World!';
const inSeconds = 5;
// Construct the fully qualified queue name.
const parent = client.queuePath(project, location, queue);
const task = {
httpRequest: {
httpMethod: 'POST',
url,
},
};
if (payload) {
task.httpRequest.body = Buffer.from(payload).toString('base64');
}
if (inSeconds) {
// The time when the task is scheduled to be attempted.
task.scheduleTime = {
seconds: inSeconds + Date.now() / 1000,
};
}
// Send create task request.
console.log('Sending task:');
console.log(task);
const request = {parent: parent, task: task};
const [response] = await client.createTask(request);
console.log(`Created task ${response.name}`);
}
createHttpTask();
// [END cloud_tasks_create_http_task]
}
More recently I set up a service account and download a .json file and all of that. But doesn't this mean my users will have to authenticate?
That's why I stopped. Maybe I'm on the wrong track, but if anyone wants to answer what I need to do to schedule a cloud task from the client side without making the user authenticate, it would be a big help.
As always, I'm happy to improve the question if anything isn't clear. Just let me know, thanks!
Yes.
Your understanding is mostly accurate. Cloud Tasks is a way to queue "tasks". The examples are likely using Cloud Functions as an analog for "some app" (a web app) that would be analogous to your Node.js (web) app, i.e. your Node.js app can submit tasks to Cloud Tasks. To access Google Cloud Platform services (e.g. Cloud Tasks), you need to authenticate and authorize.
Since your app is the "user" of the GCP services, you're correct in using a Service Account.
See Application Default Credentials to understand authenticating (code) as a service account.
Additionally, see Controlling access to webapps.

Sending Notification With Firebase Cloud Functions to Specific users

I am using firebase cloud function in my firebase group chat app, Setup is already done but problem is when some one send message in any group then all user get notification for that message including non members of group.
I want to send notification to group specific users only, below is my code for firebase cloud function -
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const _ = require('lodash');
admin.initializeApp(functions.config().firebase);
exports.sendNewMessageNotification = functions.database.ref('/{pushId}').onWrite(event => {
const getValuePromise = admin.database()
.ref('messages')
.orderByKey()
.limitToLast(1)
.once('value');
return getValuePromise.then(snapshot => {
const { text, author } = _.values(snapshot.val())[0];
const payload = {
notification: {
title: text,
body: author,
icon: ''
}
};
return admin.messaging()
.sendToTopic('my-groupchat', payload);
});
});
This will be really help full, if anyway some one can suggest on this.
As per our conversation on the comments I believe that the issue is that you are using a topic that contains all the users, which is the my-groupchat topic. The solution would be to create a new topic with the users that are part of this subtopic.
As for how to create such topics, there are a couple of examples in this documentation, in there you can see that you could do it server side, or client side. In your case, you could follow the examples for the server side, since you would need to add them in bulk, but as new users are added it could be interesting to implement the client side approach.

Azure BotFramework Directline polling interval causes events to continuously trigger

I am currently developing a prototype to communicate data between a chatbot and website elements. I am using the Azure Bot Services BotFrameworkAdapter and DirectLine in order to communicate between the two applications.
I am having a problem which I have narrowed down to the 'pollingInterval' property of the DirectLine object. The documentation says:
Clients that poll using HTTP GET should choose a polling interval that matches their intended use.
Service-to-service applications often use a polling interval of 5s or 10s.
Client-facing applications often use a polling interval of 1s, and issue a single additional request shortly after every message that the client sends (to rapidly retrieve a bot's response). This delay can be as short at 300ms but should be tuned based on the bot's speed and transit time. Polling should not be more frequent than once per second for any extended period of time.
To my understanding this is how the DirectLine object receives events from the bot, however I only need the event to trigger once, and not on the polling interval. It seems like there is no way to say "I am finished with this event" and move on. As soon as it is triggered once, it is continuously triggered which is causing issues with the functionality of the application.
I have this BotConnection object that is used to create a DirectLine instance and subscribe event handlers to the received events:
import { DirectLine } from 'botframework-directlinejs';
export default class BotConnection {
constructor() {
//Connect to directline object hosted by the bot
this.connection = new DirectLine({
domain: 'http://localhost:3001/directline',
pollingInterval: 1000,
webSocket: false
})
}
getConnection() {
return this.connection;
}
subscribeEventHandler(name, handle) {
console.log('subscribeEventHandler');
this.connection.activity$
.filter(activity => activity.type === "event" && activity.name === name)
.subscribe(activity => handle(activity));
}
}
I am implementing the botConnection class in my App file like so:
props.botConnection.subscribeEventHandler('changePage', () => console.log('test'));
My Bot file takes a message and sends an event to the page that should be handled once on the client application:
const { ActivityHandler } = require('botbuilder');
class MyBot extends ActivityHandler {
constructor() {
super();
this.onMessage(async (context, next) => {
//Testing directline back-channel functionality
if(context.activity.text === "Change page") {
await context.sendActivity({type: 'event', name: 'changePage', value: '/test'});
}
await next();
}
}
Any help with this issue would be fantastic. I am unsure if there is something supported by Azure or if there is some custom magic that I need to do.
Thanks

Resources