I have this node.js cloud function but it does not work? - node.js

I have this cloud function using node.js that listen every time a child is added on a specific node, then it sends a notification to the users. However when I added something on the database, it does not send anything. I am working on android studio java. Should I connect the function to the android studio, if it will only listen on the database and then send FCM messages on the device tokens.
also how to do debugging on this, I am using VS code.
This is my code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.listen = functions.database.ref("/Emergencies/{pushId}")
.onCreate(async (change, context) => {
change.after.val();
context.params.pushId;
// Get the list of device notification tokens. Note: There are more than 1 users in here
const getDeviceTokensPromise = admin.database()
.ref("/Registered Admins/{uid}/Token").once("value");
// The snapshot to the user's tokens.
let tokensSnapshot;
// The array containing all the user's tokens.
let tokens;
const results = await Promise.all([getDeviceTokensPromise]);
tokensSnapshot = results[0];
// Check if there are any device tokens.
if (!tokensSnapshot.hasChildren()) {
return functions.logger.log(
'There are no notification tokens to send to.'
);
}
functions.logger.log(
'There are',
tokensSnapshot.numChildren(),
'tokens to send notifications to.'
);
// Notification details.
const payload = {
notification: {
title: "New Emergency Request!",
body: "Someone needs help check Emergenie App now!",
}
};
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
functions.logger.error(
'Failure sending notification to',
tokens[index],
error
);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove);
});

This seems wring:
const getDeviceTokensPromise = admin.database()
.ref("/Registered Admins/{uid}/Token").once("value");
The {uid} in this string is not defined anywhere, and is also going to be treated as just a string, rather than the ID of a user - which is what I expect you want.
More likely, you'll need to:
Load all of /Registered Admins
Loop over the results you get from that
Get the Token value for each of them
If you are new to JavaScript, Cloud Functions for Firebase is not the easiest way to learn it. I recommend first using the Admin SDK in a local Node.js process or with the emulator suite, which can be debugged with a local debugger. After those you'll be much better equipped to port that code to your Cloud Functions.

Related

Notify Users when Child is added on specific node using Cloud function

Is it possible for cloud function to listen for specific node for when a child is added and then send a notification to users located on a different node, and if that is possible how so? I am using node.js with Firebase realtime database and not Firestore.
This is my database:
I want the cloud function to listen every time a child is added on "Emergencies", and then notify all the users in the "Registered Admins"
This is the contents of the users in "Registered Admins" node, it has a child "Notification" containing the message, and I want to send that message to all the users, when a child is added on "Emergencies" node.
This is my cloud function using node.js. I've deployed it however it does not work, does not send any notification at all.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.listen = functions.database.ref("/Emergencies")
.onWrite(async (change, context) => {
change.after.val();
context.params.pushId;
// Get the list of device notification tokens.
const getDeviceTokensPromise = admin.database()
.ref("/Registered Admins/{uid}/Token").once("value");
// The snapshot to the user's tokens.
let tokensSnapshot;
// The array containing all the user's tokens.
let tokens;
const results = await Promise.all([getDeviceTokensPromise]);
tokensSnapshot = results[0];
// Check if there are any device tokens.
if (!tokensSnapshot.hasChildren()) {
return functions.logger.log(
'There are no notification tokens to send to.'
);
}
functions.logger.log(
'There are',
tokensSnapshot.numChildren(),
'tokens to send notifications to.'
);
// Notification details.
const payload = {
notification: {
title: "New Emergency Request!",
body: "Someone needs help check Emergenie App now!",
}
};
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
functions.logger.error(
'Failure sending notification to',
tokens[index],
error
);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove);
});
Yes, that sounds possible and is in fact quite close to what the example on notifying users when something interesting happens does.
To send a message to a specific device, you 'll need to know the token for that device. If you want to broadcast a message to multiple users, you could subscribe those users to a topic. Just keep in mind that anyone can subscribe to a topic if they know its name, so you can't use that to send messages that only a certain group of users is allowed to see.

How can i get the values of the field in Firebase cloud function node

I want to send notifications to a specific device using the Firebase cloud function in nodejs. I successfully get the token that I store inside the firestore.
As in this screenshot, the name field and record field are in the same path. The problem is, I want to get the name's value and send it through the notification. Is there is possible way?
This is my NodeJS code:
const admin = require("firebase-admin");
const functions = require("firebase-functions");
admin.initializeApp(functions.config().firebase);
// get the token of the device
async function getToken() {
var instance = await admin.firestore().collection('fcm').doc('token').get();
let _token = instance.data()['token'];
console.log(_token);
return _token;
}
// Send notification when new record are created.
exports.timeInNotification = functions.firestore.document('employee/{employeeId}/record/{recordId}').onCreate(async (event) => {
// get the fcm token
var token = await getToken();
// get the name (the name field is on the same path with the record field)
var name;
let body = name + "has time in";
// time in message
let timeIn = "Time in";
var message = {
notification: {
title: timeIn,
body: body
}
};
// send the notification
let response = await admin.messaging().sendToDevice(token,message);
console.log(response);
});
// Send notification when the field inside record update.
exports.timeOutNotification = functions.firestore.document('employee/{employee}/record/{recordId}').onUpdate(async (event) => {
// get the fcm token
var token = await getToken();
// get the name (the name field is on the same path with the record field)
var name;
let body = name + " has time out";
// time out message
let timeOut = "Time out";
// notification message
var message = {
notification: {
title: timeOut,
body: body
}
}
// send the notification message
let response = await admin.messaging().sendToDevice(token,message);
console.log(response);
});
I tried to get it like the way i get the fcm token here. But there is a several employees that I would not know what employee will create a new documents or update the documents.
You are using the ID Token from firebase you can not use it to send notification.
You need to create a FCM Token for each device and put it in the database and use that token to send notification.
here is the difference between them :
-Firebase Authentication ID tokens identify a user. This means that if the same user is signed in on two different devices, they have ID tokens that identify the same user.
-FCM tokens (also called Instance ID tokens) identify an application installation. If you have two tokens from two different devices, there is nothing that is shared between those tokens.

Why doesn't firebase admin messaging send notifications to all device token in multicast?

I know there are lot of questions around this question, and I promise I've checked quiet a number of them but non seems to give me an exact answer
I'm using firebase cloud function's admin messaging SDK to send push notifications to an array of device token I put together from my users collection.
The code:
let deviceToken = [<device tokens>];
let payload = {
notification: {
title: main_title,
body: notf_body,
},
data: {
<data object>
},
};
await messaging
.sendToDevice(deviceToken, payload)
.then((response) => {
console.log("Successfully sent message to::", response.successCount);
})
.catch((error) => {
console.log("Error sending message:", error);
});
} else {
console.log("ARTICLE PUBLISHED BUT NOT BROADCASTED");
return;
}
seems to be working fine but the push notification is never sent to all the device token in the array...
Below is a log of the function triggered, where 49 device token are present in the array but only 32 notifications are successful
What could be the reason for this, as some clients have been complaining they aren't getting notification
There are quite some reasons why sending a message to a token might fail. The specific reason for each token that failed is specified in the response.results that you get back.
The most common reasons are that tokens get outdated/expired over time, meaning they won't work anymore. In a well working app, you'll register new tokens for the those same devices, but failure to clean up the old tokens from your database will result in more and more failures over time.
For a good example of how to deal with these errors and clean up outdated tokens, see this code from the example of sending notifications in Cloud Functions:
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
functions.logger.error(
'Failure sending notification to',
tokens[index],
error
);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove);

credentials used to authenticate does not have permission

I am trying to use firebase cloud messaging to deploy a http push notifications onto an actual device. When so, I am getting the error
"The credential used to authenticate this SDK does not have permission to send messages to the device corresponding to the provided registration token. Make sure the credential and registration token both belong to the same Firebase project."
I have checked the credentials on both the frontEnd and backEnd side and they all match up with my firebase correctly. I have tried to follow plenty of examples on here and I have missed on all of them. My node.js file looks like
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
export GOOGLE_APPLICATION_CREDENTIALS = "/Users/myname/mylocation/myfile.json"
exports.sendPushNotifications = functions.https.onRequest((req, res) => {
res.send("Attempting to send push notification")
console.log("LOGGER --- Trying to send push message..");
var uid = 'randomUIDString'
var fcmToken = 'myToken'
return admin.database().ref('/users/' + uid).once('value', snapshot => {
var user = snapshot.val();
console.log("username is " + user.name);
var payload = {
notification: {
title: 'Push Notification Title',
body: 'Test Notification Message'
}
}
admin.messaging().sendToDevice(fcmToken, payload)
.then(function(response) {
console.log('Succesfully sent message:', response);
console.log(response.results[0].error);
})
.catch(function(error) {
console.log('Error sending message', error);
});
})
})
The users name prints from calling the uid, but I am having trouble accessing the fcmToken.
I have checked my info.plist and the correct project is listed. Same with apple developer and appDelegate. is there something more that I am missing??

Is it possible to integrate bot-builder into an existing express app?

I have an existing node/express chatbot application that connects to several chat platforms using ExpressJS' next(), next() middleware design pattern. I send a 200 response at the very beginning to acknowledge the receipt of a message, and send a new POST request to send a message from my last middleware.
app.post("/bots", receiveMsg, doStuff, formatAndSendMsg, catchErrors);
Now I want to integrate Skype as a channel for my bot, but the NodeJS library for bot-framework has a totally different way of doing things, using events and such magic that I haven't fully understood yet:
var connector = new builder.ConsoleConnector();
app.post("/skype", connector.listen());
var bot = new builder.UniversalBot(connector, function (session) {
session.send("You said: %s", session.message.text);
});
It doesn't look like these are compatible ways to do things, so what is the best way to receive a message and then send a response to a user without having to change my express routing to fit bot-builder in? Can I get a Session object with Session.send() to respond to? Will I have to do all the addressing manually? Is there a method that resembles this:
app.post("/skype", (req, res, next) => {
const address = req.body.id;
const message = new builder.Message(address, messageBody).send()
}
Or:
app.post("/skype", connector.listen(), (req, res, next) => {
// (res.locals is available in every express middleware function)
const session = res.locals.botFrameworkSession;
// do stuff
session.send(message);
}
You can register bot application in your existing express applications. Bot builder SDK is also compatible in expressjs framework. You can refer to official sample which is also leveraging express.
Don't forget to modify the messsaging endpoint in your bot registration to your bot's endpoint, e.g.
https://yourdomain/stuff
in your scenario. Please refer to https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration for more info.
Building messages, addressing them, and sending those messages are all possible using the official bot framework NodeJS library. What I couldn't do with that library was receive messages and verify their authenticity on my routes without making major changes to my design (using request middleware - next() - to process the incoming request) which is already in production with other bots and not easy to change.
Here's my workaround: First is this BotFrameworkAuthenticator class that I create based on the Microsoft documentation here: https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication
It is instantiated with the appID and appPassword from your Bot Framework app.
import axios from "axios";
import * as jwt from "jsonwebtoken";
import * as jwkToPem from 'jwk-to-pem';
export class BotFrameworkAuthenticator {
private appId: string;
private appPassword: string;
private openIdMetadata: any;
// The response body specifies the document in the JWK format but also includes an additional property for each key: endorsements.
private validSigningKeys: any;
// The list of keys is relatively stable and may be cached for long periods of time (by default, 5 days within the Bot Builder SDK).
private signingKeyRefreshRate: number = 432000; // in seconds (432000 = 5 days)
constructor(appId, appPassword) {
this.appId = appId;
this.appPassword = appPassword;
this.getListOfSigningKeys();
}
// response data should contain "jwks_uri" property that contains address to request list of valid signing keys.
public async getOpenIdMetaData() {
// This is a static URL that you can hardcode into your application. - MS Bot Framework docs
await axios.get("https://login.botframework.com/v1/.well-known/openidconfiguration").then(response => {
this.openIdMetadata = response.data;
logger.info("OpenID metadata document recieved for Bot Framework.");
}).catch(err => {
logger.warn(err.message, "Could not get OpenID metadata document for Bot Framework. Retrying in 15 seconds...");
setTimeout(this.getListOfSigningKeys, 15000);
})
}
public async getListOfSigningKeys() {
await this.getOpenIdMetaData();
if (this.openIdMetadata && this.openIdMetadata.jwks_uri) {
// previous function getOpenIdMetaData() succeeded
await axios.get(this.openIdMetadata.jwks_uri).then(response => {
logger.info(`Signing keys recieved for Bot Framework. Caching for ${this.signingKeyRefreshRate / 86400} days.`);
this.validSigningKeys = response.data.keys;
setTimeout(this.getListOfSigningKeys, (this.signingKeyRefreshRate * 1000));
}).catch(err => {
logger.error(err.message, "Could not get list of valid signing keys for Bot Framework. Retrying in 15 seconds");
setTimeout(this.getListOfSigningKeys, 15000);
});
} else {
// previous function getOpenIdMetaData() failed, but has already queued this function to run again. Will continue until succeeds.
return;
}
}
/**
* Verifies that the message was sent from Bot Framework by checking values as specified in Bot Framework Documentation:
* https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication#step-4-verify-the-jwt-token
* Retrieves the Bearer token from the authorization header, decodes the token so we can match the key id (kid) to a key in the OpenID
* document, then converts that key to PEM format so that jwt/crypto can consume it to verify that the bearer token is
* cryptographically signed.
* If the serviceUrl property in the token doe not match the serviceUrl property in the message, it should also be rejected.
*/
public verifyMsgAuthenticity(serviceUrl: string, headers: any) {
try {
const token = headers.authorization.replace("Bearer ", "");
const decoded = jwt.decode(token, { complete: true }) as any;
const verifyOptions = {
issuer: "https://api.botframework.com",
audience: this.appId,
clockTolerance: 300, // (seconds) The token is within its validity period. Industry-standard clock-skew is 5 minutes. (Bot Framework documentation);
}
const jwk = this.lookupKey(decoded.header.kid)
const pem = jwkToPem(jwk);
const verified = jwt.verify(token, pem, verifyOptions) as any;
if (!serviceUrl || serviceUrl !== verified.serviceurl) {
logger.warn("Non-matching serviceUrl in Bot Framework verified token!")
return false;
}
return true;
} catch (err) {
logger.warn("Received invalid/unsigned message on Bot Framework endpoint!", err.message)
return false;
}
}
// Finds the relevant key from the openID list. Does not transform the key.
private lookupKey(kid) {
const jwk = this.validSigningKeys.find((key) => {
return (key.kid === kid);
});
return jwk;
}
}
Use the BotFrameworkAuthenticator class like this at the very beginning of your express route to verify that all incoming requests are valid.
const botFrameworkAuthenticator = new BotFrameworkAuthenticator(appID, appPassword);
router.post("/", (req: Request, res: Response, next: NextFunction) => {
if (botFrameworkAuthenticator.verifyMsgAuthenticity(req.body.serviceUrl, req.headers) === true) {
res.status(200).send();
next();
} else {
// unsafe to process
res.status(403).send();
return;
}
});
And to send messages using the regular Bot Framework library without having a Session object that would normally be created by the Bot Framework library when it receives an incoming message:
import * as builder from "botbuilder";
// instantiate the chatConnector (only once, not in the same function as the sending occurs)
const botFrameworkSender = new builder.ChatConnector({ appId, appPassword });
//---------------------------------------------
const skypeMsg = req.body;
const address = {
channelId: skypeMsg.channelId,
user: skypeMsg.from,
bot: skypeMsg.recipient,
conversation: skypeMsg.conversation
};
const response = new builder.Message().text(someText).address(address).toMessage();
const formattedResponses = [response];
botFrameworkSender.send(formattedResponses, logErrorsToConsole);
Note that all of the builder.Message() -- .attachment(), .images(), etc.. -- functions can be used, not just the text()

Resources