Is there a way to get email or text notifications each time data is written to my Google Cloud Firestore bucket? - firebase

I have a google cloud bucket and firebase writes my app data there. I would like to monitor my data, and have any new update (write) to my firebase database it sent via a text or email to me. I currently have Twilio set up on Nodejs to send texts on Firebase and my code is:
const functions = require('firebase-functions');
var twilio = require('twilio');
const admin = require('firebase-admin');
admin.initializeApp();
var accountSid = 'account id'; // Account SID from www.twilio.com/console
var authToken = 'account token'; // Auth Token from www.twilio.com/console
var client = new twilio(accountSid, authToken);
exports.useWildcard = functions.firestore
.document('comments/{commentContent}')
.onWrite((change, context) => {
client.messages.create({
body: context.params.commentContent,
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
Currently, I would like to build it out for just the comments document, which are organized inside firebase through comments/{commentContent}. Later, I would like to expand to other trees. I am however, unsure if the above will run each time there is a write to my comments tree. Does it require the firebase-admin module as I have put above? Thanks!

Yes, the onWrite method will not only run when there is a write to the comments tree, but will also be triggered by any change in any document and on the deletion of a document. This means that right now your code will responde in the same way to any of the above cases, and this could cause problems, especially in the case of a document being deleted since it will try to send a comment that doesent exist and will likely get some null exceptions.
Said that you have different solutions.
If you only want the function to react to a new comment, but not to an update or deletion you should use onCreate trigger instead of onWrite.
If you also want to handle a comment update notification you can use both onCreate and onUpdate, but sending different messages by doing something like:
exports.useWildcardCreate = functions.firestore
.document('comments/{commentContent}')
.onCreate((change, context) => {
client.messages.create({
body: context.params.commentContent,
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
exports.useWildcardUpdate = functions.firestore
.document('comments/{commentContent}')
.onUpdate((change, context) => {
const newComment = change.after.data();
const previuosComment = change.before.data();
client.messages.create({
body: 'The comment ${previuosComment} has been changed to ${newComment}',
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
At last if you also need to notify when a comment has been deleted you should use onWrite method but differentiating between the 3 different cases as shown below:
exports.useWildcard = functions.firestore
.document('comments/{commentContent}')
.onWrite((change, context) => {
var textBody;
const oldComment = change.before.data();
const newComment = change.after.data();
if (change.after.exists == false) { // comment has been deleted
textBody = 'The comment ${oldComment} has been deleted';
}
else if (oldComment != newComment) { // comment has been updated
textBody = 'The comment ${oldComment} has been changed to ${newComment}';
}
else { // if its not an update or a deletion its a new comment
textBody = newComment;
}
client.messages.create({
body: textBody,
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
Finally require('firebase-admin') is needed since it will allow you to interact with Firebase from privileged environments. Here you can find all the information to the Firebase Admin SDK

Related

cloud function using typrescript and if statment

hello I'm new to cloud functions and I want to ask if there is some why to get more than one topic in one if statement
here is my cloud function :
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
const fcm = admin.messaging();
export const sendToTopic = functions.firestore
.document("Doctor2019/{documentId}")
.onCreate(async snapshot => {
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'NEW POST!',
body: `Click here to see New Post`,
icon: 'your-icon-url',
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return fcm.sendToTopic('Doctor2019', payload);
});
the problem is that I have more than one topic what I want to do is to check the creation of document on other collections and send the notification based on that , I really don't know what to do , any help ?
I see that you want to send messages to different topics on FCM that are tied to different documents creations.
You cannot use one function to achieve that as the Function is tied to document creation on a specific collection. what you will need to do is to create different functions to different collections.
If I correctly understand your goal, you can use a {wildcard} in place of the document ID as well as in place of the collection ID. Then you use the context object to get the value of the collection ID as follows:
export const sendToTopic = functions.firestore
.document("{collectionId}/{documentId}")
.onCreate(async (snap, context) => { // Note the addition of context
const collectionId = context.params.collectionId;
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'NEW POST!',
body: `Click here to see New Post`,
icon: 'your-icon-url',
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return fcm.sendToTopic(collectionId, payload);
});
In case you have root collections that should not trigger a message, just adapt your data model and make the collections that need to trigger a message subcollections of a specific document. Something like:
export const sendToTopic = functions.firestore
.document("messagingTriggers/triggers/{collectionId}/{documentId}")
.onCreate(async (snap, context) => {...});
Then any document creation in, for example, a users collection will not trigger the Cloud Function.

How to load 2 different Firestore docs in one 'onUpdate' Cloud Function?

I am trying to make an "onUpdate" function that loads the document that has been updated. Then I want to load another document using the data received by the wildcards. So to summarize I want to access the document that was updated and one more that is in the same collection.
I want : /userProfiles/{doc1}/employees/{doc2} AND /userProfiles/{doc1}.
I can get them both but when I try to use the data from one, it doesn't read the previous data and gives me a ReferenceError.
The end goal is to use both these docs to send an email with nodemailer. Thanks for any help.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const nodemailer = require('nodemailer');
admin.initializeApp();
exports.testLog = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
var info = [];
const doc1 = context.params.doc1;
const doc2 = context.params.doc2;
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
var email = doc.data().email;
var phone = doc.data().phone;
info.push(doc.data());
console.log(email, phone); // sees and gets info
return email, phone;
}),
db
.collection("userProfiles")
.doc(`${doc1}`)
.collection(`employees`)
.doc(`${doc2}`)
.get()
.then(doc => {
info.push(doc.data());
var Status = doc.data().Status;
console.log(phone, `${Status}`); //phone is undefined
if (`${Status}` === "Alarm") {
// replace with variables from the users settings page
console.log(`${info.phone}`); // phone is undefined
let transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: "xxxxxx#gmail.com",
pass: "xxxxxxxxxx"
}
});
// send mail with defined transport object
let mailOptions = {
from: '"Fred Foo 👻" <foo#example.com>',
to: `${info.phone}`, // tried phone as well
subject: "Hello ✔",
text: "216+?",
};
transporter.sendMail(mailOptions, error => {
if (error) {
return console.log(error);
} else {
return console.log("message sent");
}
});
}
console.log(Status);
// return
return console.log("im after the if statement. No alarm triggered");
})
.then(message => console.log(message.sid, "success"))
.catch(err => console.log(err))
);
});
So I want to get the phone number and the Status in these 2 images
The error that is returned:
ReferenceError: phone is not defined
There are two things that aren't quite working the way you expect leading to your problem:
The handling of promises isn't really passing data the way you expect -- in particular, the variables phone and email exist only in one promise handler, they aren't global in scope, so phone and email aren't being passed down the promise chain.
You don't actually need to ever read the second document, as the content is passed to you in the function itself. This actually greatly simplifies the overall thing you are doing, and makes dealing with the first point nearly trivial, since you can skip the second database call.
Look at this code where I have omitted the messaging code for clarity and just left in place most of the log messages:
exports.firestoreOnUpdateTest = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
// var info = []; I have removed this list, it is not necessary
const doc1 = context.params.doc1;
// no need to get the doc2 parameter, as we are handed the doc itself by the function call.
const doc2content = change.after.data();
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
const doc1content = doc.data();
const email = doc1content.email;
const phone = doc1content.phone;
console.log(email, phone); // sees and gets info
console.log(`No need to fetch doc2, as I already have it: ${JSON.stringify(doc2content)}`);
const Status = doc2content.Status;
console.log(`email for user is still: ${email}`); // email is now defined
console.log(phone, `${Status}`); // phone is now defined
if (`${Status}` === "Alarm") {
console.log(`${phone}`); // phone is now defined
return console.log('message would be sent here - code omitted')
}
console.log(Status);
return console.log("im after the if statement. No alarm triggered");
})
.catch(err => console.error(err))
);
});
In the new version, we just store the content from the document that triggered us, including the Status parameter. We then fetch the document with the content we need -- at the higher level in the tree. Once that document is returned, we just process it and combine with the data from doc2. All the fields are now defined (assuming, of course, the database objects are well-formed).
Your messaging code would be re-inserted right were the obvious log message is.
Finally, the info list I don't think is necessary now, so I've removed it. Instead, I recommend you build what you need as you construct the message itself from the data already on hand. That said, your original code wasn't accessing it correctly (that is, as a list) anyway and may have been confusing you further.
Finally, I haven't addressed the use of the Nodemailer module as the question focused primarily on the undefined fields, but I suspect your original code may not be entirely correct either -- as it doesn't either return a promise back from sendMail() or perform an await on that call (and make the entire function async), so you will need to look at that more closely.

dialogflow chatbot how to go to firestore database and return with support relevant questions?

I am using "firestore" database for my "dialogflow" chat bot which I already created for an online grocery store. The problem is: I want my chatbot to initially ask questions from users to find the proper item title in my database and then return to user by asking 3-4 support relevant questions about that item. the questions must be the item attributes (Brand, color, size...) and will vary from one item to another. so the chatbot will stream down the user to find the best item.
can you please help me to find the answer? I already created the codes but they don't work and I do not know what is wrong with that. If you already created this and have the index.js file, I appreciate to propose me here.
index.js:
'use strict';
const functions = require('firebase-functions');
// Import admin SDK
const admin = require('firebase-admin');
const {
WebhookClient
} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
admin.initializeApp(functions.config().firebase);
// here we get the database in a variable
const db = admin.firestore();
const data = {...};
// Add a new document in collection "dialogflow" with document ID 'agent'
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').set(data);
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({
request,
response
});
function writeToDb(agent) {
// Get parameter from Dialogflow with the string to add to the database doc
const databaseEntry = agent.parameters.databaseEntry;
// Get the database collection 'dialogflow' and document 'agent' and store
// the document {entry: "<value of database entry>"} in the 'agent' document
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').where('title', '==', title);
return db.runTransaction(t => {
t.set(dialogflowAgentRef, {
entry: databaseEntry
});
return Promise.resolve('Write complete');
}).then(doc => {
agent.add(`Wrote "${databaseEntry}" to the Firestore database.`);
}).catch(err => {
console.log(`Error writing to Firestore: ${err}`);
agent.add(`Failed to write "${databaseEntry}" to the Firestore database.`);
});
}
function readFromDb(agent) {
// Get the database collection 'dialogflow' and document 'agent'
const dialogflowAgentDoc = db.collection('dialogflow/agent/rss/channel/item'); // .doc('agent')
// Get the value of 'entry' in the document and send it to the user
return dialogflowAgentDoc.get()
.then(doc => {
if (!doc.exists) {
agent.add('No data found in the database!');
} else {
agent.add(doc.data().entry);
}
return Promise.resolve('Read complete');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
agent.add('Please add a entry to the database first by saying, "Write <your phrase> to the database"');
});
}
// Map from Dialogflow intent names to functions to be run when the intent is matched
let intentMap = new Map();
intentMap.set('ReadFromFirestore', readFromDb);
intentMap.set('WriteToFirestore', writeToDb);
agent.handleRequest(intentMap);
});
There are a number of issues with your code as you've shown it that could cause problems reading and writing with the Firestore database.
It looks like you're trying to find an existing collection to write to with the line
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').where('title', '==', title);
but title isn't defined anywhere, which I suspect causes an error. Furthermore, doc() returns a DocumentReference, but there is no where() method in a DocumentReference.
Remember that you need to structure Firestore using alternating collections and documents. So your "firebase" collection can contain a document named "agent", and that document may have subcollections.
When you're trying to read with
const dialogflowAgentDoc = db.collection('dialogflow/agent/rss/channel/item');
You're getting a collection, but then trying to treat it as a document. The comment suggests that you're trying to read a specific doc from this collection (which makes sense), but you're loading that document by a hard-coded string "agent", rather than trying to get the agent from the parameters passed to you from Dialogflow.
Finally - the paths in the read and write sections don't match. Using hard-coded paths are fine when testing, but make sure you're using matching paths and that they reflect the collection/doc/collection/doc/... path requirement.
So in both cases, you might have a reference that looks something like
const docTitle = agent.parameters.title;
const docRef = db.collection('dialogflow').doc(title);
Which, if you have defined a "title" parameter in your Intents in Dialogflow, will use this to reference the doc, which you can then read or write.
thanks for the answer I already changed my database to real time firebase instead of firestore. still having problem with support relevant questions. I want to go to my real time database to find the item by search using "oederByChild" and "equalTo" methods as I found these in people questions and answer in this website. still cannot find and item title through my database child. here is the codes are written:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {
WebhookClient
} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
admin.initializeApp(functions.config().firebase);
const db = admin.database();
// const ref = db.ref('server/saving-data/fireblog');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({
request,
response
});
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
// Get the database collection 'dialogflow' and document 'agent' and store
// the document {entry: "<value of database entry>"} in the 'agent' document
function writeToDb(agent) {
const databaseEntry = agent.parameters.databaseEntry;
const acc = db.ref('rss/channel/item/4/title'); //**This worked! */
acc.set({
entry: databaseEntry
});
return Promise.resolve('write complete')
.then(_acc => {
agent.add(`Wrote ${databaseEntry} to the realtime database.`);
return false;
}).catch(err => {
console.log(`Error writing to Firestore: ${err}`);
agent.add(`Failed to write "${databaseEntry}" to the Firestore database.`);
});
}
// and this is when we want to write to in the same child, keeping the old values:
//const acc = db.ref('/rss/channel/item/5/color'); //**This worked! */
//const result = acc.child(databaseEntry).set({entry: databaseEntry});
//agent.add(`Wrote ${databaseEntry} to the realtime database.`);
//console.log(result.key);
//});
// to read data
function readFromDb(agent) {
const any = agent.parameters.any;
agent.add(`Thank you...`);
var rootRef = db.ref();
var childref = rootRef.child("rss/channel/item");
return childref.orderByChild("title").equalTo("Icebreaker").once("value").then(function(snapshot){ //has been taken from the bus example: https://stackoverflow.com/questions/51917390/dialogflow-how-do-i-pass-a-parameter-through-in-a-firebase-query
var colored = snapshot.child("color/__text").val();
var sized = snapshot.child("size/__text").val();
agent.add(`Your search result for ` + any + ` Throughout the database is ` + colored +
` Color and ` + sized + ` Size`);
return Promise.resolve('Read complete');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
agent.add('Please add a entry to the database first by saying, "Write <your phrase> to the database"');
});
}
// Map from Dialogflow intent names to functions to be run when the intent is matched
let intentMap = new Map();
intentMap.set('IWannaBuy', readFromDb);
intentMap.set('WriteToFirebase', writeToDb);
agent.handleRequest(intentMap);
});
enter code here
[this is how my database is][1]
[1]: https://i.stack.imgur.com/QdFy5.png

Firebase Functions - push notification when new document is created

I have the following Firestore DB structure:
users
$USER_ID
notifications
$DOC1
$DOC2
$DOC3
I want to push a new notification when a document is created at the user notification collection.
It should be something like this, but I don't know of any way to this for each $UID:
exports.newSubscriberNotification = functions.firestore
.document('users/$UID/notifications')
.onCreate(async event => {
How can I use Firebase Functions to do this? If there is no way, any suggestions for a workaround?
You should use the following code to trigger your Cloud Function:
exports.newSubscriberNotification = functions.firestore
.document('users/{userId}/notifications/{docId}')
.onCreate((snap, context) => {
//You get the values of the newly created doc as follows:
const newValue = snap.data();
console.log(newValue);
//You get the parameters as follows:
const userId = context.params.userId;
//console.log(userId);
const docId = context.params.docId;
//console.log(docId);
// You perform here the notification sending
});
For the code for the notification sending, have a look at this official Firebase Cloud Function sample: https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.js

Generating a custom auth token with a cloud function for firebase using the new 1.0 SDK

As of firebase-admin#5.11.0 and firebase-functions#1.0.0 firebase-admin no longer takes in an application config when the app initializes.
I had a firestore function that would generate a custom token using firebase-admin’s createCustomToken. Calling that function would generate a credential that I would pass into initializeApp in the credential attribute. How would I go about doing that now?
Do I need to edit process.env.FIREBASE_CONFIG somehow and put the serialized credential there before calling initializeApp?
Based on this issue in Github, it still works.
https://github.com/firebase/firebase-admin-node/issues/224
The following example worked for me:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://yourapplication.firebaseio.com/'
});
exports.createToken = functions.https.onCall((data, context) => {
const uid = context.auth.uid;
return admin.auth()
.createCustomToken(uid)
.then(customToken => {
console.log(`The customToken is: ${customToken}`);
return {status: 'success', customToken: customToken};
})
.catch(error => {
console.error(`Something happened buddy: ${error}`)
return {status: 'error'};
});
});
Michael Chen's cloud function appears to trigger from a HTTP request from somewhere (an external server?). My employee wrote a cloud function that triggers when the user logs in:
// this watches for any updates to the user document in the User's collection (not subcollections)
exports.userLogin = functions.firestore.document('Users/{userID}').onUpdate((change, context) => {
// save the userID ubtained from the wildcard match, which gets put into context.params
let uid = context.params.userID;
// initialize basic values for custom claims
let trusted = false;
let teaches = [];
// check the Trusted_Users doc
admin.firestore().collection('Users').doc('Trusted_Users').get()
.then(function(doc) {
if (doc.data().UIDs.includes(uid)) {
// if the userID is in the UIDs array of the document, set trusted to true.
trusted = true;
}
// Get docs for each language in our dictionary
admin.firestore().collection('Dictionaries').get()
.then(function(docs) {
// for each of those language docs
docs.forEach(function(doc) {
// check if the userID is included in the trustedUIDs array in the doc
if (doc.data().trustedUIDs.includes(uid)) {
// if it is, we push the 2-letter language abbreviation onto the array of what languages this user teaches
teaches.push(doc.data().shortLanguage);
}
});
// finally, set custom claims as we've parsed
admin.auth().setCustomUserClaims(uid, {'trusted': trusted, 'teaches': teaches}).then(() => {
console.log("custom claims set.");
});
});
});
});
First, we put in a lastLogin property on the user object, which runs Date.now when a user logs in and writes the time to the database location, triggering the cloud function.
Next, we get the userID from the cloud function response context.params.userID.
Two variables are then initialized. We assume that the user is not trusted until proven otherwise. The other variable is an array of subjects the user teaches. In a roles-based data security system, these are the collections that the user is allowed to access.
Next, we access a document listing the userIDs of trusted users. We then check if the recently logged in userID is in this array. If so, we set trusted to true.
Next, we go to the database and traverse a collection Dictionaries whose documents include arrays of trusted userIDs (i.e., users allowed to read and write those documents). If the user is in one or more of these arrays, he or she gets that document added to the teaches property on his or her user data, giving the user access to that document.
Finally, we're ready to run setCustomUserClaims to customize the token claims.
Here's a variation for a Callable Cloud Function, thanks to Thomas's answer
Once the custom claim is set, you can access the field in/from .. say, a firebase storage rule.
For example:
allow write: if request.auth.token.isAppAdmin == true;
With a Callable Cloud Function, as long as the admin.auth().setCustomUserClaims(..) function is returned somewhere along the promise chain, the claim field will be added to the request.auth.token object:
const functions = require('firebase-functions');
exports.setIsAdminClaim = functions.https.onCall((data, context) => {
var uid = context.auth.uid;
return admin.auth().setCustomUserClaims(
uid, {
isAppAdmin: true
}
)
.then(() => {
var msg = 'isAppAdmin custom claim set';
console.log(msg);
return new Promise(function (resolve, reject) {
var resolveObject = {
message : msg
};
resolve(resolveObject);
});
});
});

Resources