How to reduce email sending time (using nodemailer and firebase)? - node.js

We have written code that sends emails to a user and their contacts, when a new node is added to a specific path in Firebase realtime database.
The average time to send the emails is 4 minutes.
We think the problem is due to awaiting for some needed promises.
We would like to get the run time down.
Do you have any advice? Thanks in advance!
This is our code:
const functions = require("firebase-functions");
const nodemailer = require('nodemailer');
require('dotenv').config()
//for fire store
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
const { SENDER_EMAIL, SENDER_PASSWORD } = process.env;
exports.sendEmails = functions.database.ref("/devices/{device_ID}/history/{alert_ID}")
.onWrite(
(snapshot, context) => {
sendMail(snapshot, context);
return true;
}
);
async function sendMail(snapshot, context){
const { before, after } = snapshot;
// new alert created
if (before.val() == null) {
console.log('DEBUG:: NEW ALERT');
// get owners uID from device ID
const deviceRef = db.collection('deviceToUid').doc(context.params.device_ID);
const uidDoc = await deviceRef.get();
if(!uidDoc.exists){
functions.logger.info("No such document!");
return;
}
// get users email from uID
const userRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('user-info');
// get users contact
const contactRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('contacts');
const [userInfo, contactList] = await Promise.all([userRef.get(), contactRef.get()]);
if(userInfo.empty){
functions.logger.info("No such collection!");
return;
}
const email = userInfo.docs[0].id; // owners email
let contacts = []; // initialize contact list
contactList.forEach(
(doc) => {
if(doc.data().confirmed){
contacts.push(doc.id);
}
}
)
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: SENDER_EMAIL,
pass: SENDER_PASSWORD,
},
});
const mailOptions = {
from: 'ALERT <noreply#firebase.com>',
to: email,
bcc: contacts,
subject: `...Motion detected`,
html: `<p dir=ltr>New Alert...</p>`
};
mailTransport.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}
}
I'd also recommend learning a bit about list comprehensions, as this:
let contacts = []; // initialize contact list
contactList.forEach(
(doc) => {
if(doc.data().confirmed){
contacts.push(doc.id);
}
}
)
Can be reduced to a more concise:
let contacts = contactList.docs
.filter((doc) => doc.data().confirmed)
.map((doc) => doc.id);

You were getting pretty close, but were missing an await in the top-level function, and one inside sendMail for the call to mailTransport.sendMail.
I think this should be it:
exports.sendEmails = functions.database.ref("/devices/{device_ID}/history/{alert_ID}")
.onWrite(
async (snapshot, context) => {
await sendMail(snapshot, context);
return true;
}
);
async function sendMail(snapshot, context){
const { before, after } = snapshot;
// new alert created
if (before.val() == null) {
console.log('DEBUG:: NEW ALERT');
// get owners uID from device ID
const deviceRef = db.collection('deviceToUid').doc(context.params.device_ID);
const uidDoc = await deviceRef.get();
if(!uidDoc.exists){
functions.logger.info("No such document!");
return;
}
// get users email from uID
const userRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('user-info');
// get users contact
const contactRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('contacts');
const [userInfo, contactList] = await Promise.all([userRef.get(), contactRef.get()]);
if(userInfo.empty){
functions.logger.info("No such collection!");
return;
}
const email = userInfo.docs[0].id; // owners email
let contacts = []; // initialize contact list
contactList.forEach(
(doc) => {
if(doc.data().confirmed){
contacts.push(doc.id);
}
}
)
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: SENDER_EMAIL,
pass: SENDER_PASSWORD,
},
});
const mailOptions = {
from: 'ALERT <noreply#firebase.com>',
to: email,
bcc: contacts,
subject: `...Motion detected`,
html: `<p dir=ltr>New Alert...</p>`
};
await mailTransport.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
return true;
}
}
Since you were not using await in the top-level call, the Cloud Functions contains will/may shut down the container before the asynchronous calls have completed. For more on this, see the documentation on sync, async and promises - and how Cloud Functions are terminated.

Related

Payment Options View Controller (for Stripe) Not Showing Up

I am fairly new to this. I have used cloud functions and firebase to integrate stripe into my iOS project. Here is my code for the payment options (in node.js).
exports.addPaymentMethodDetails = functions.firestore
.document('/stripe_customers/{userId}/payment_methods/{pushId}')
.onCreate(async (snap, context) => {
try {
const paymentMethodId = snap.data().id;
const paymentMethod = await stripe.paymentMethods.retrieve(
paymentMethodId
);
await snap.ref.set(paymentMethod);
// Create a new SetupIntent so the customer can add a new method next time.
const intent = await stripe.setupIntents.create({
customer: paymentMethod.customer,
});
await snap.ref.parent.parent.set(
{
setup_secret: intent.client_secret,
},
{ merge: true }
);
return;
} catch (error) {
await snap.ref.set({ error: userFacingMessage(error) }, { merge: true });
await reportError(error, { user: context.params.userId });
}
})
Here is my code in Xcode:
``let customerContext = STPCustomerContext(keyProvider: StripeApi)
var paymentContext: STPPaymentContext!
init() {
self.paymentContext = STPPaymentContext(customerContext: customerContext)
super.init(nibName: nil, bundle: nil)
self.paymentContext.delegate = self
self.paymentContext.hostViewController = self
self.paymentContext.paymentAmount = 5000 // This is in cents, i.e. $50 USD
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#IBAction func paymentMethodClicked(_ sender: Any) {
paymentContext.pushPaymentOptionsViewController()
}
func setupStripeConfig() {
let config = STPPaymentConfiguration.shared()
config.additionalPaymentOptions = .default
config.requiredBillingAddressFields = .none
let customerContext = STPCustomerContext(keyProvider: StripeApi)
paymentContext = STPPaymentContext(customerContext: customerContext, configuration: config, theme: .default())
paymentContext.delegate = self
paymentContext.hostViewController = self
When I run the simulator of this and click the button to pull up payment options, nothing happens and my console (in Xcode) reads "INTERNAL". Can someone please show me how to bring up the Payment Options View Controller, or if there is something I need to or add/fix in order to do so? Also here is my node.js code for getting an ephemeral key:
exports.createEphemeralKey = functions.https.onCall(async (data, context) => {
const customerID = data.customer_id;
const stripeVersion = data.stripe_version;
const uid = context.auth.uid;
if (uid === null) {
console.log('Illegal access attempt due to unauthenticated user');
throw new functions.https.HtppsError('permission-denied', 'Illegal access attempt')
}
let key = await stripe.ephemeralKeys.create(
{customer: '{{CUSTOMER_ID}}'},
{stripe_version: '{{API_VERSION}}'}
);
return stripe.ephemeralKeys.create(
{customer: customerID},
{stripe_version: stripeVersion}).then((key) => {
return key
}).catch((err) => {
console.log(err)
throw new functions.https.HttpsError9('internal', 'Unable to create ephemeral key.')
});
});

Session expiring in Dialogflow

I came to know that context expires in 15 minutes but is there any way to solve it manually i.e by storing the previous conversation in dB so can we handle that session expiring issue or else the whole conversation(output context) under that session ID will get clear and need to start from the first.
exports.fulfillmenttext = functions.https.onRequest((req,res) =>{
const answer1 = req.body.Text;
console.log("Text said by the user",answer1);
const uid = answer1.substring(0,28);
console.log("uid1 is",uid);
const answer = answer1.substring(28);
console.log("answer is",answer);
const sessionId = uid;
var count,questvalue;
runSample();
async function runSample(projectId = 'xxxxxxx') {
const languageCode = 'en-US';
const credentials = {
client_email: 'xxxxxxxxxx',
private_key: 'xxxxxxxxx'
};
//Instantiate a DialogFlow client.
const dialogflow = require('dialogflow');
const sessionClient = new dialogflow.SessionsClient({
projectId,
credentials,
});
// Define session path
const sessionPath = sessionClient.sessionPath(projectId, sessionId);
// The text query request.
const request = {
session: sessionPath,
queryInput: {
text: {
text: answer,
languageCode,
},
},
};
const responses = await sessionClient.detectIntent(request);
console.log('Detected intent');
const result = responses[0].queryResult;
let action = result.action;
console.log("action is"+action);
console.log(` Query: ${result.queryText}`);
console.log(` Response: ${result.fulfillmentText}`);
if (result.intent) {
const question = result.fulfillmentText;
console.log("question is",question);
const actionHandlers = {
'early': () => {
console.log('earlyaction1', action);
let name1 = JSON.stringify(result.parameters.fields.Name.stringValue);
name1 = name1.toString().replace(/"/g,"");
var data1 = {
Name: name1
};
var setDoc1 = admin.firestore().collection('User').doc(uid).collection("Popop").doc(uid).collection('Answers').doc('Earlyyears').update(data1);
},
'family': () => {
console.log('familyaction1', action);
let mname1 = JSON.stringify(result.parameters.fields.M_Name.stringValue);
let mname_string = mname1.toString().replace(/"/g,"");
var data20 = {
MName: mname_string
};
var setDoc20 = admin.firestore().collection('User').doc(uid).collection("Popop").doc(uid).collection('Answers').doc('Family').update(data20);
}
};
if (action === 'early') {
console.log('1');
actionHandlers[action]();
}
else if (action === 'family') {
console.log('2');
actionHandlers[action]();
}
res.status(200).send({"question":result.fulfillmentText,"action":action});
} else {
console.log(` No intent matched.`);
res.status(400).send({"action":"empty"});
}
}
});
I stumbled upon this problem as well. My solution was to save the userID and save the contexts to Firestore.
UPDATE:
This is how I stored Dialogflow's contexts in Firestore:
function saveContexts(userId, contexts) {
let UID = userId;
//get all contexts + parameters
if (contexts === undefined) {
console.log("contexts are undefined! returning");
return false;
}
db.collection("user-contexts-prod").doc(UID).set({
dateCreated: new Date(),
contexts: JSON.stringify(contexts)
})
.then(function () {
console.log("success!");
return true;
})
.catch(function (error) {
console.log("error writing document..", error);
return false;
});
}
Retrieving user contexts:
async function getContexts(userId) {
let UID = userId;
let docRef = db.collection("user-contexts-prod").doc(UID);
return docRef.get()
.then(res => {
if (res.exists) {
let contexts = JSON.parse(res.data().contexts);
console.log("<><> parsed contexts <><>: ");
console.log(contexts);
return contexts;
} else {
console.log(" UID DOES NOT EXIST!");
return false;
}
})
}
You can set the contexts again by looping over them and using the contextClient to create new contexts. Or use this method to loop through the contexts and find the one you need:
contexts.forEach(function(context) {
if (context.name === 'projects/{DIALOGFLOWPROJECTID}/agent/sessions/' + senderId + '/contexts/{CONTEXTNAME}') {
sessionData = context.parameters;
// all data that you saved in CONTEXTNAME is now available in the sessionData variable
}
});
Original answer:
Whenever a user started talking that didn't have any active contexts I check if I had the userID stored in my Database. If this user existed in my DB I retrieved the user information with all his data like this:
knownUser = await db.isKnownUser(senderId);
if (knownUser) {
//knownUser
console.log("Known user");
let userData = db.getUserDataById(senderId)
//initialize contexts with data you need
payload = returningUser_useSameData();
messenger.send(payload, senderId);
dashbot.logBotMessage(payload.toString, sessionId, intentName);
break;
} else {
//newUser
console.log("new user");
createContext('await_fillInTogether', '', sessionPath, sessionId, 1);
createContext('session', '', sessionPath, sessionId, 500);
payload = fillInTogetherNewUser();
messenger.send(payload, senderId);
dashbot.logBotMessage(payload.toString, sessionId, intentName);
break;
}

Cannot update a Firestore's document after a purchase verification using Firebase functions

I've been working on a function to verify purchase in server side. Everything works well.
Only one issue, when the purchase is valid (the response.status === 200) the console shows (success) but the document hasn't been updated.
Here is my code. Did i miss something ?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {google} = require('googleapis');
const publisher = google.androidpublisher('v2');
const authClient = new google.auth.JWT({
email: 'my_email',
key: 'my_key',
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
admin.initializeApp();
admin.firestore().settings({timestampsInSnapshots: true});
exports.validatePurchases = functions.firestore
.document('purchases/{channel}')
.onCreate((docSnapshot, context) => {
const purchase = docSnapshot.data();
const order_id = purchase.orderId;
const package_name = purchase.packageName;
const sku = purchase.sku;
const purchase_token = purchase.purchaseToken;
const user_id = purchase.userId;
authClient.authorize((err, result) => {
if (err) {
console.log(err);
}
publisher.purchases.subscriptions.get({
auth: authClient,
packageName: package_name,
subscriptionId: sku,
token: purchase_token
}, (err, response) => {
if (err) {
console.log(err);
}
// Result Status must be equals to 200 so that the purchase is valid
if (response.status === 200) {
console.log('success');
return admin.firestore().collection('users').doc(user_id).update({
purchaseToken: purchase_token,
isPremiumS: 'true'
});
} else {
console.log('fail');
return admin.firestore().collection('users').doc(user_id).update({
purchaseToken: purchase_token,
isPremiumS: 'false'
});
}
});
});
return null;
});
Instead of returning null, your function should return the Promise from your Firestore update() calls.
Add return in two places:
return authClient.authorize((err, result) => {...
return publisher.purchases.subscriptions.get({...
Delete return null; at the end of the function

Getting document not a function

I am attempting to retrieve the boolean child (notificationsOn) of an object stored as a Firestore document to see if the rest of a function should be executed.
The overall function works to completion without this portion, but adding the portion from let threadDoc to the if statement presents a "threadDoc.get is not a function" error. I think my syntax is wrong but I don't know how, as a similar function works in a later part of the function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendDMNotification =functions.firestore.document('/dm_threads/{thread_id}/messages/{message_id}').onCreate((snapshot, context) => {
const newMessage = snapshot.data();
const senderName = newMessage.authorName;
const senderID = newMessage.authorUID;
const messageText = newMessage.message;
const recipientID = newMessage.recipientUID;
var notificationsOn = null;
let deviceTokenQuery = admin.firestore().collection(`/users/${recipientID}/device_tokens/`);
var idsToBeSorted = [senderID, recipientID];
idsToBeSorted.sort();
var threadID = idsToBeSorted[0] + idsToBeSorted[1];
console.log(recipientID);
console.log(threadID);
let threadDoc = admin.firestore().document(`users/${recipientID}/threads/${threadID}/`);
return threadDoc.get().then(doc => {
let notificationsOn = doc.data.notificationsOn;
console.log(notificationsOn);
if (notificationsOn !== false){
return deviceTokenQuery.get().then(querySnapshot => {
let tokenShapshot = querySnapshot.docs;
const notificationPromises = tokenShapshot.map(doc => {
let token_id = doc.data().tokenID;
const payload = {
data: {
title: senderName,
body: messageText,
senderID: senderID,
senderName: senderName
}
};
return admin.messaging().sendToDevice(token_id, payload).then(response => {
console.log("Notification sent: ", response);
})
.catch(error => {
console.log("Error sending message: ", error);
});
});
return Promise.all(notificationPromises);
});
}
return;
});
});
admin.firestore().document() was supposed to be admin.firestore().collection(...).doc(...)
This fixed my problem
I think you meant to say admin.firestore() instead of functions.firestore.

Firebase Node.JS Admin SDK send verification email

I am using the sdk to create a user and a db entry for the user which all works perfectly. Upon creation of the database entry I call a function to sendEmailVerification() but I am guessing this is a client side function as it returns null when being called.
What is the process to send the verify email directly from the admin sdk (if even possible). Currently what I do is send some JSON back to the client to say if the verification email sent successfully or not. But calling the function does not work so it doesn't get that far. Here is my function within node.
function verifiyEmail(email, res) {
var user = admin.auth().currentUser;
user.sendEmailVerification().then(function() {
// Email sent.
var jsonResponse = {
status: 'success',
alertText: '1',
email: email
}
res.send(jsonResponse);
}, function(error) {
// An error happened.
var jsonResponse = {
status: 'success',
alertText: '0',
email: email
}
res.send(jsonResponse);
});
}
UPDATE
I am guessing this isn't possible so I generate a custom token in node and send that back to the client. I then use the token I get back to try and sign the user in by calling the below however the signInWithCustomToken() fuction doesnt get called. Heres my code am I missing something. Seems like a lot of work just to send out the verification email!
function signInUserWithToken(token) {
firebase.auth().signInWithCustomToken(token).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
console.log(errorCode);
console.log(errorMessage);
verifiyEmail();
});
}
UPDATE 2
I scraped the token idea. All i do now is use the onAuthStateChanged() function and handle the email verification there in the client implementation. Its not perfect as this method gets called several times. However adding a flag seems to do the trick. Like the below.
function authListenerContractor() {
// Listening for auth state changes.
$('#not-verified').css('display','none');
var flag = true;
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is verified.
var displayName = user.displayName;
var email = user.email;
var emailVerified = user.emailVerified;
var photoURL = user.photoURL;
var isAnonymous = user.isAnonymous;
var uid = user.uid;
var providerData = user.providerData;
console.log("Email Verified?: " + emailVerified);
if(emailVerified) {
window.location.href = "http://www.my-redirect-url.com";
} else {
if (flag == true) {
$('#not-verified').css('display','inherit');
verifiyEmail();
flag = false;
}
}
} else {
console.log("User is signed out.");
}
});
}
function verifiyEmail() {
var user = firebase.auth().currentUser;
user.sendEmailVerification().then(function() {
// Email sent.
console.log("Verification email sent");
$('#not-verified').text('**Email verification sent. Please check your email now!**');
}, function(error) {
// An error happened.
console.log("Email verification not sent. An error has occurred! >>" + error);
});
}
This is a classic case to use Firebase Cloud Functions
Sending Welcome Mail Example
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const gmailEmail = encodeURIComponent(functions.config().gmail.email);
const gmailPassword = encodeURIComponent(functions.config().gmail.password);
const mailTransport = nodemailer.createTransport(
`smtps://${gmailEmail}:${gmailPassword}#smtp.gmail.com`);
const APP_NAME = 'My App';
exports.sendWelcomeEmail = functions.auth.user().onCreate(event => {
const user = event.data; // The Firebase user.
const email = user.email; // The email of the user.
const displayName = user.displayName; // The display name of the user.
return sendWelcomeEmail(email, displayName);
});
function sendWelcomeEmail(email, displayName) {
const mailOptions = {
from: `${APP_NAME} <noreply#firebase.com>`,
to: email
};
mailOptions.subject = `Welcome to ${APP_NAME}!`;
mailOptions.text = `Hey ${displayName || ''}! Welcome to ${APP_NAME}. I hope you will enjoy our service.`;
return mailTransport.sendMail(mailOptions).then(() => {
console.log('New welcome email sent to:', email);
});
}
Check this Link for More Info , used these functions to trigger a mail in this very app
UPDATE
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const mailTransport = nodemailer.createTransport(
`smtps://emailid:password#smtp.gmail.com`);
const APP_NAME = 'My App';
exports.sendPageCountEmail = functions.database.ref('/yournode').onWrite(event => { // here we are specifiying the node where data is created
const data = event.data;
return sendEmail('emailid',data);
});
// Sends a welcome email to the given user.
function sendEmail(email,body) {
const mailOptions = {
from:`${APP_NAME}noreply#firebase.com`,
to: email
};
mailOptions.subject = `Welcome to ${APP_NAME}!`;
mailOptions.text = `Welcome to ${APP_NAME}.\n
return mailTransport.sendMail(mailOptions).then(() => {
console.log('New welcome email sent to:', email);
});
}

Resources