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

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

Related

How to await a write function inside a get function with Firebase Cloud Function for Flutter app

So, I don't really know how to write JS, I am developing a mobile app in Flutter, and I would be grateful for some help and clarifications regarding Future/Promises in JS.
I got a collection of posts for each user, and I want to create an .onCreate function which when a user posts a new post (a new document is created inside the 'posts/userId/user_posts' collection), then it gets all the user's followers (from a collection 'user_followers/userUid') and for each follower, it writes the postUid and postOwnerUid to that follower's newsFeed collection ('user_news_feed/followerId').
This is what I got right now, but I am walking blind, since I really don't know JS and I don't know how can I await a write function while inside a get function.
And how do I prevent Cloud Timeouts? If for instance the user has 1000 followers, how can I prevent Firebase from shutting down my function and making sure all the followers are notified?
exports.writeToUserNewsFeed = functions.firestore
.document('posts/{userId}/user_posts/{postId}')
.onCreate((snap, context) => {
const postData = snap.data();
const postUid = postData['post_uid'];
const userUid = postData['user_uid'];
const postCreationDate = postData['post_creation_date'];
var docRef = db.collection('user_followers').doc(userUid).collection('followers');
docRef.get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
db.collection('user_news_feed')
.doc(doc.data['uid'])
.collection('feed')
.document(postUid)
.set({
'post_uid': postUid,
'user_uid': userUid,
'post_uid': postCreationDate,
});
});
});
});
As explained in the doc, in a background Cloud Function like an onCreate() for Firestore, you need to return a Promise when all the asynchronous work is completed. So in your case, one possibility is to use Promise.all() because you don't know upfront how many documents are in the followers subcollection. Since Promise.all() returns a single Promise you can include it in the Promise chain that you need to return in the Cloud Function.
exports.writeToUserNewsFeed = functions.firestore
.document('posts/{userId}/user_posts/{postId}')
.onCreate((snap, context) => {
const postData = snap.data();
const postUid = postData['post_uid'];
const userUid = postData['user_uid'];
const postCreationDate = postData['post_creation_date'];
var followersColRef = db.collection('user_followers').doc(userUid).collection('followers');
return followersColRef.get().then((querySnapshot) => { // <= See return here
const promises = [];
querySnapshot.forEach((doc) => {
promises.push(
db.collection('user_news_feed')
.doc(doc.data['uid'])
.collection('feed')
.doc(postUid)
.set({
'post_uid': postUid,
'user_uid': userUid,
'post_uid': postCreationDate,
})
);
});
return Promise.all(promises); // <= See return here
})
.catch(error => {
console.log(error);
return null;
})
});
Note that instead of using Promise.all() you could also use a batched write but there is a limit of 500 operations for a batched write.

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

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

How to delete document collection and all nested data from auth.user.onDelete trigger

Currently, the logic for deleting user data is the following:
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
const firestore_tools = require('firebase-tools');
admin.initializeApp();
const Auth = admin.auth();
const UsersCollection = admin.firestore().collection(`users`);
exports.deleteUserDocuments = functions.auth.user().onDelete((user) => {
const userID = user.uid;
UsersCollection.doc(userID)
.delete({})
.catch(error => {
return error
});
});
But since the user document record contains nested collections that contain other documents and collections they are still preserved due to the fact:
When you delete a document, Cloud Firestore does not automatically delete the documents within its sub-collections
I've researched a bit and found a documentation on how to create a callable function:
https://firebase.google.com/docs/firestore/solutions/delete-collections
But I wonder is it possible to have this logic instead executed from the auth.user.onDelete trigger?
Update with the Solution
const firestore_tools = require('firebase-tools');
exports.deleteUserDocuments = functions.auth.user().onDelete((user) => {
const userID = user.uid;
const project = process.env.GCLOUD_PROJECT;
const token = functions.config().ci_token;
const path = `/users/${userID}`;
console.log(`User ${userID} has requested to delete path ${path}`);
return firestore_tools.firestore
.delete(path, {
project,
token,
recursive: true,
yes: true,
})
.then(() => {
console.log(`User data with ${userID} was deleted`);
})
});
You can run whatever code you want in whatever trigger you want. The type of the trigger doesn't have any bearing on the type of code you can run.

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.

Cloud Functions for Firebase Time Out w/ wrong response

Newbie question: Cloud Function times out every single time I run it.
In addition, it only returns ONE value, which is the first userId, in the Functions Log and none of its children. Im assuming this is because it's calling the .once however, it's in a forEach loop, so I'm not sure what it wants.
Firebase database
-items
---- userId0123456789
---- randomKey987654321
-- itemName
-- itemDate
-- itemType
---- userId987654321
---- randomKey012345678
-- itemName
-- itemDate
-- itemType
And here is the function code...
const key = req.query.key;
**let userID = 'xxxxx';
let ikey = 'xxx';**
var dbRef = admin.database().ref('/items/{userID}/{ikey}');
dbRef.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(child.key+": "+child.val());
});
});
UPDATE: here is the entire function and now it's just timing out with no response.
'use strict';
// Firebase Functions
const functions = require('firebase-functions');
// Firebase Admin
const admin = require('firebase-admin');
// Default admin firebase configuration
admin.initializeApp(functions.config().firebase);
const rp = require('request-promise');
const promisePool = require('es6-promise-pool');
const PromisePool = promisePool.PromisePool;
const secureCompare = require('secure-compare');
const MAX_CONCURRENT = 3;
//Initial function call:
exports.CheckItemTypeinFB = functions.https.onRequest((req, res) => {
const key = req.query.key;
// Exit if the keys don't match
if (!secureCompare(key, functions.config().cron.key)) {
console.log('The key provided in the request does not match the key set in the environment. Check that', key,
'matches the cron.key attribute in `firebase env:get`');
res.status(403).send('Security key does not match. Make sure your "key" URL query parameter matches the ' +
'cron.key environment variable.');
return;
}
// Try the database here...
let userID = 'xxx';
let ikey = 'xxxxx
//create database ref
let ref = admin.database().ref(`/items/${userID}/${ikey}`);
//do a bunch of stuff
ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
res.send(200, {/* response data */});
});
//send back response
// res.redirect(200);
}) // END THE MAJJOR CONTAINER THINGS
// Returns an access token using the Google Cloud metadata server. */
function getAccessToken(accessToken) {
// If we have an accessToken in cache to re-use we pass it directly.
if (accessToken) {
return Promise.resolve(accessToken);
}
const options = {
uri: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token',
headers: {'Metadata-Flavor': 'Google'},
json: true
};
return rp(options).then(resp => resp.access_token);
}
Help is much appreciated.
Update:. Timeout is fixed and it returns the userId's that are in the database under "/items". HOWEVER, if I use ${userId}/${key} I get nothing. I'm still not able to tell how to get the children under random userId's in the database and none of the other posts I read explain it. Firebase's docs state to use {userId} to get all under that wildcard but its not working. What am I missing?
You're not returning the result of the once function or returning at all, so the function doesn't know when to finish hence the timeout.
let userID = 'xxxxxxxxx';
let key = 'xxxxxxxx';
let ref = admin.database().ref(`/items/${userID}/${key}`);
return ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
});
Also please be aware that the reference you are observing will give you the children of a particular user item (itemName, itemDate, itemType). If you want the items belonging to a particular user, adjust your reference path to be /items/${userID}.
When inside a HTTP trigger, you can send a response after observing the value.
exports.CheckItemTypeinFB = functions.https.onRequest((req, res) => {
...
ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
res.send(200, {/* response data */});
});
});

Resources