Can't send Firebase Cloud Messages if function is called by Google Tasks - firebase

I have an .onCreate cloud function, that once triggered, creates a task and runs it.
If I simply run admin.messaging().sendToDevice(payload.tokens, payload.message); inside the exports.createScheduledNotification function, then it runs fine, but I wanted to utilise Cloud Tasks so that I can schedule the task a little further into the future.
However, even if the entire payload is inside the scheduledNotification function, it still doesn't run. I have a feeling that it's actually not being called at all. However, I've checked in the Google Cloud Tasks console, and it shows that it has run, but I get no response from the Firebase Messaging at all, so from that I can deduce it's not running.
Is there something wrong with how I'm sending the task request? Is the cloud function that receives the call written incorrectly?
Cloud Functions & Tasks Set up and Imports
'use strict';
const functions = require('firebase-functions');
var admin = require("firebase-admin");
const { CloudTasksClient } = require('#google-cloud/tasks')
var serviceAccount = require("./secret_file.json");
admin.initializeApp({
credential: admin.credential.cert(placeholder_for_account_credentials)
});
Function that is meant to create and send a Google Task, which is meant to call an .onRequest function, to send a Firebase Message
exports.createScheduledNotification = functions.firestore.document('/followers/{followedUid}')
.onCreate(async (snapshot) => {
const data = snapshot.data();
const tasksClient = new CloudTasksClient();
const queuePath = tasksClient.queuePath(project, location, queue);
const url = `https://{placeholder_for_location}-{placeholder_for_project_id}.cloudfunctions.net/scheduledNotification`;
const docPath = snapshot.ref.path;
const task = {
httpRequest: {
httpMethod: 'POST',
url,
body: Buffer.from(JSON.stringify({})).toString('base64'),
headers: {
'Content-Type': 'application/json',
},
},
scheduleTime: {
seconds: 10 + Date.now() / 1000
}
};
try {
await tasksClient.createTask({ parent: queuePath, task });
} catch (error) {
console.log(error);
}
});
Function to accept a call from Google Tasks, and then sends a message to a selected device_id
exports.scheduledNotification = functions.https.onRequest((req, res) => {
const payload = {
message: {
notification: {
title: 'You have a new follower!',
body: `Moe is now following you.`,
sound: 'default'
},
},
tokens: ["placeholder_for_device_id"]
};
admin.messaging().sendToDevice(payload.tokens, payload.message);
});
I would also like to mention that I saw in another question I needed to allow the principal firebase account to be able to create tasks, and I've added Cloud Task Admin and Cloud Task Enqeuer to every single account, and that the task are showing up in the Cloud Tasks Console.

Related

How to get the autogenerated task name from Google Cloud Tasks when adding Firebase Task queue functions?

Following the Cloud Functions documentation about how to Enqueue functions with Cloud Tasks it is demonstrated how to create a Firebase task queue function. This works all fine.
However, if I call the enqueue to enqueue the function a Promise without any information whatsoever on my queued task is returned.
→ How can I get the task name or any reference from the just created task that is added to my queue?
I need this name to store, so I can possibly cancel my task before execution.
As it seems to be, Firestore Cloud functions have a limited feature set in comparison to the "real" Google Cloud Functions.
To get more data on the task, do not use the enqueue function from firebase-admin/functions, but make use of the #google-cloud/tasks library:
// Imports the Google Cloud Tasks library.
import { protos, CloudTasksClient } from "#google-cloud/tasks";
// Set all the details for the function to call
const serviceAccountEmail = "PROJECT_ID#appspot.gserviceaccount.com";
const project = "PROJECT_ID";
const queue = "my-queue";
const location = "europe-west1";
const url =
"https://europe-west1-PROJECT_ID.cloudfunctions.net/yourFunctionToCall";
const client = new CloudTasksClient();
const formattedParent = client.queuePath(project, location, queue);
const payload = {
data: {
your: "data"
},
};
const task = {
httpRequest: {
httpMethod: "POST",
url: url,
body: Buffer.from(JSON.stringify(payload)).toString("base64"),
headers: {
"Content-Type": "application/json",
},
oidcToken: {
serviceAccountEmail,
},
},
scheduleTime: {
seconds: Date.now() / 1000, // Any epcoch time in seconds, or do not set for immediate execution
},
} as protos.google.cloud.tasks.v2beta3.ITask;
const request = {
parent: formattedParent,
task: task,
};
const [response] = await client.createTask(request);
functions.logger.info(`Created task ${response.name}`);
Calling client.createTask will return data on the Task. This allows you to also remove the Task if necessary.

Firebase functions cloud messaging notification not being recieved

When I send a notification from the Firebase cloud messaging console, my device receives it without a problem, but when I send it via a cloud functions, the function log says it was successfully sent but my device does not receive it. I tried switching to type script, sending the notification with different conditions but nothing works. The app is written in flutter.
My function code:
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
db.collection("users")
.where("bananas", "==", 1666).get().then(
(result) => {
if (result.size > 0) {
result.forEach((doc) => {
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
fcm.send(payload).then((response) => {
// Response is a message ID string.
console.log("Successfully sent message: "+
doc.data().NotToken+ " ", response);
return {success: true};
}).catch((error) => {
return {error: error.code};
});
});
}
});
response.send("Notification sent !");
functions.logger.info("Notification sent!");
return null;
});
cloud log
Any ideas?
Did you notice how your code never logs this message?
Successfully sent message
That's because both loading from Firestore, and sending messaging through Cloud Messaging are asynchronous calls. So your response.send("Notification sent !") runs before the data is ever retrieved from the database, and Cloud Functions at that point terminates your code to prevent charging after you say that you are done.
If you have asynchronous operations in your code, you need to return a promise from the top-level of your code that resolves/rejects when all asynchronous code has completed. So in your case that means the promise should only resolve once you've loaded the data from Firestore, and sent the messages.
Let's start with a simple example. Say that you want to only send a single message, no matter how many documents are in the database.
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
return db.collection("users") // 👈 Add return here
.where("bananas", "==", 1666).get().then((result) => {
if (result.size > 0) {
const doc = result.docs[0]; // 👈 Get the first result
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
return fcm.send(payload).then((response) => { // 👈 Add return here
console.log("Successfully sent message: "+
doc.data().NotToken+ " ", response);
response.send("Notification sent !"); // 👈 Move this call here
return {success: true};
}).catch((error) => {
// TODO: Send an error back to the caller
return {error: error.code};
});
}
});
});
So the top-level code now returns the result from loading data from Firestore, and in there, we return the call from calling FCM, which then in turn returns return {success: true};. When returning promises, the results bubble up - so you can typically just keep returning the nested results.
You'll also not that we've moved the response.send into the code that runs after calling FCM, as we don't want to send a result back to the caller until the FCM call is done.
The above is the simple variant, because in reality you have multiple documents, and you are only done once all of them are done.
For that we are going to use Promise.all(), which takes an array of promises and resolves once all those promises resolve. So we're going to capture all the calls to FCM (which returns a promise) and collection them in an array, that we then pass to Promise.all().
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
return db.collection("users")
.where("bananas", "==", 1666).get().then((result) => {
if (result.size > 0) {
let promises = [];
result.forEach((doc) => {
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
promises.push(fcm.send(payload))
});
return Promise.al(promises).then((results) => {
console.log("Successfully sent messages");
response.send("Notification sent !");
return {success: true};
});
}
});
});
While this may be a lot to grok all at once, handling asynchronous behavior is quite well covered in the Firebase documentation on terminating functions, in this video series on Learn JavaScript Promises with Cloud Functions, and in quite a few tutorials out there - so I recommend spending some time on those to get to grips with asynchronous code.

How to have Cloud Tasks run only once?

I've written a cloud task and it works perfectly and triggers the link I gave without any problems, but it won't stop retrying running the link.
How can I make it run it only once?
What I'm trying to do is run a Firestore Function once in the future, on a document write in a collection. I found this tutorial for it.
So far my task creation code works perfectly, and delivers correct payload to the function it's going to call. And the called function works correctly too the first time it runs and exits with status 200. But on the retries I have to exit with error 500 since there's no data to access anymore.
I can see the 200 and 500 logs in firestore function's logs, but Cloud Tasks' logs is empty, even if a method has been run 50 times!
This is the full code
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
const { CloudTasksClient } = require('#google-cloud/tasks')
exports.moveActivityFromPlanToRecord = () =>
functions
.region('europe-west1')
.firestore.document('Users/{userId}/Activities/{activityId}')
.onCreate(async snapshot => {
const moveTime = snapshot.data()! as MoveTime
if (!moveTime || !moveTime.dueTime) {
console.log("DueTime is empty or null: \n" + moveTime)
return
}
// Get the project ID from the FIREBASE_CONFIG env var
const project = JSON.parse(process.env.FIREBASE_CONFIG!).projectId
const location = 'europe-west1'
const queue = 'activityDateEventChecker'
//queuePath is going to be a string that uniquely identifes the task
const tasksClient = new CloudTasksClient()
const queuePath: string =
tasksClient.queuePath(project, location, queue)
// URL to my callback function and the contents of the payload to deliver
const url = `https://${location}-${project}.cloudfunctions.net/activityDateEventCheckerCallback`
const docPath = snapshot.ref.path
const dueTime = moveTime.dueTime
const payload: MoveTaskPayload = { docPath, dueTime }
console.log(payload)
// build up the configuration for the Cloud Task
const task = {
httpRequest: {
httpMethod: 'POST',
url: url,
body: Buffer.from(JSON.stringify(payload)).toString('base64'),
headers: {
'Content-Type': 'application/json',
},
},
scheduleTime: {
seconds: moveTime.dueTime / 1000
}
}
// enqueue the task in the queue
return tasksClient.createTask({ parent: queuePath, task: task })
})
interface MoveTime extends admin.firestore.DocumentData {
dueTime?: number
}
interface MoveTaskPayload {
docPath: string,
dueTime: number
}
exports.activityDateEventCheckerCallback = () =>
functions
.region('europe-west1')
.https.onRequest(async (req, res) => {
const payload = req.body as MoveTaskPayload
try {
// getting the item
const activity = await admin.firestore().doc(payload.docPath).get()
// if time is up for it
if (Date.now() >= payload.dueTime && activity.data() != undefined) {
// getting path to activity to be in record
const pathUser = activity.ref.parent.parent?.path
const pathDocRecord = admin.firestore().doc(`${pathUser}/Record/${activity.id}`)
console.log("RECORD-- ", (await (await pathDocRecord.get()).data())?.subject)
// moving activity into record
await pathDocRecord.set(activity.data()!)
await activity.ref.delete()
// sending notif to user
const fcmPayload = {
notification: {
title: `${activity.data()?.subject}`,
body: " Time for activity. Record how it goes!"
},
data: {
activityId: activity.id
}
}
const user = await admin.firestore().doc(pathUser!).get()
const fcmToken: string = user.data()?.fcmToken
return admin.messaging().sendToDevice(fcmToken, fcmPayload)
}
return null
} catch (error) {
console.error(error)
res.status(500).send(error)
return null
}
})
Tasks in Cloud Task retries when it does not get response code 2XX.
You can config the retry in Cloud Task Queue using maxAttempt paramtere.
Details are mentioned in the doc

How to use scheduler for Firebase Cloud Functions with Realtime Database/Analytics triggers?

I'm working on a Firebase Cloud Function, to send triggered push notifications.
Right now my function sends a push as soon as an user triggers the "IAP" event in my app.
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendIAPAnalytics = functions.analytics.event('IAP').onLog((event) => {
const user = event.user;
const uid = user.userId; // The user ID set via the setUserId API.
sendPushToUser();
return true;
});
function sendPushToUser(uid) {
// Fetching all the user's device tokens.
var ref = admin.database().ref(`/users/${uid}/tokens`);
return ref.once("value", function(snapshot){
const payload = {
notification: {
title: 'Hello',
body: 'Open the push'
}
};
console.log("sendPushToUser ready");
admin.messaging().sendToDevice(snapshot.val(), payload)
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
}
This functions works, push are sent and received.
I read some news about scheduling for Firebase Cloud Functions:
https://medium.com/#pascalluther/scheduling-firebase-cloud-functions-with-cloud-scheduler-b5ec22ace683
https://firebase.googleblog.com/2019/04/schedule-cloud-functions-firebase-cron.html
I understood, it's only for HTTP triggers ou PUB/SUB triggers.
So for now it's always impossible to trigger functions with delays, by writing in realtime database or when analytics events are triggered.
Am I right? or is there a trick?
I read nothing about this.
EDIT: Official documentation
https://firebase.google.com/docs/functions/schedule-functions
My syntax is wrong but I need something like this:
function sendPushToUser(uid) {
var ref = admin.database().ref(`/users/${uid}/tokens`);
return ref.once("value", function(snapshot){
const payload = {
notification: {
title: 'Hello',
body: 'Open the push'
}
};
functions.pubsub.schedule('at now + 10 mins').onRun((context) => {
admin.messaging().sendToDevice(snapshot.val(), payload)
})
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
}
There is no built-in way to retrigger Cloud Functions with a delay. If you want such functionality you will have to build that yourself, for example by scheduling a function to run periodically and then see what tasks need to be triggered. See my answer here: Delay Google Cloud Function
As Doug commented, you can use Cloud Tasks to schedule individual invocations. You'd dynamically create the task, and then have it call a HTTP function.

Unable to send SMS through Twilio and Google Functions

I am attempting to send a text (a one-time pass code) using Twilio, firebase and Google Functions, and using Postman.
I have run $ npm install --save twilio#3.0.0 -rc.13 in the functions directory.
When I run $ firebase deploy, it completes. But on Postman, when I do POST, Body and feed a JSON { "phone": "555-5555" }, I get an "Error: could not handle the request."
I am able to send a text in Twilio Programmable SMS from my Twilio number to an actual outside number direct to the mobile phone. I'm using live credentials for Sid and AuthToken.
Is this an issue with Twilio, Google Functions and some configurations?
Here are the logs on Functions:
// White flag sign//
Function execution took 1452 ms, finished with status: 'crash'
//Red Warning sign//
TypeError: handler is not a function
at cloudFunction (/user_code/node_modules/firebase-functions/lib/providers/https.js:26:41)
at /var/tmp/worker/worker.js:676:7
at /var/tmp/worker/worker.js:660:9
at _combinedTickCallback (internal/process/next_tick.js:73:7)
at process._tickDomainCallback (internal/process/next_tick.js:128:9)
Also, the google eslint forces consistent-return, which is why I put "return;" in the request-one-time-password.js. I cannot seem to turn it off by adding "consistent-return": 0 in eslintrc.
My code(with secret keys and phone numbers redacted):
//one-time-password/functions/service_account.js
has my keys copied and pasted.
//one-time-password/functions/twilio.js
const twilio = require('twilio');
const accountSid = 'redacted';
const authToken = 'redacted';
module.exports = new twilio.Twilio(accountSid, authToken);
//one-time-password/functions/request-one-time-password.js
const admin = require('firebase-admin');
const twilio = require('./twilio');
module.export = function(req, res) {
if(!req.body.phone) {
return res.status(422).send({ error: 'You must provide a phone number!'});
}
const phone = String(req.body.phone).replace(/[^\d]/g, '');
admin.auth().getUser(phone).then(userRecord => {
const code = Math.floor((Math.random() * 8999 + 1000));
// generate number between 1000 and 9999; drop off decimals
twilio.messages.create({
body: 'Your code is ' + code,
to: phone,
from: '+redacted'
}, (err) => {
if (err) { return res.status(422).send(err); }
admin.database().ref('users/' + phone).update({ code: code, codeValid: true }, () => {
res.send({ success: true });
})
});
return;
}).catch((err) => {
res.status(422).send({ error: err })
});
}
/////////////////////////////////
//one-time-password/functions/index.js
const admin = require('firebase-admin');
const functions = require('firebase-functions');
const createUser = require('./create_user');
const serviceAccount = require('./service_account.json')
const requestOneTimePassword = require('./request_one_time_password');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://one-time-password-650d2.firebaseio.com"
});
exports.createUser = functions.https.onRequest(createUser);
exports.requestOneTimePassword =
functions.https.onRequest(requestOneTimePassword);
You have
module.exports = new twilio.Twilio(accountSid, authToken);
on one line, and further down
module.export = function(req, res) { ... }.
Try changing export to exports.
One thing that tripped me up for a long time was how twilio sent the request body to the cloud function. It sends it in a body object so to access your request body it will look something like this
req.body.body
On top of that it passed it as a JSON string so I had to JSON.parse()
Example I got working:
export const functionName= functions.https.onRequest((req, res) => {
cors(req, res, () => {
let body = JSON.parse(req.body.body);
console.log(body);
console.log(body.service_id);
res.send();
});
});
This also may depend on the Twilio service you are using. I was using their Studio HTTP Request Module.
Hope this helps a little, not sure if it was your exact problem though :(

Resources