Firebase functions promises not firing correctly - node.js

I'm having a hard time understanding promises in Firebase functions. I have a function that listens for new files in a storage bucket and then emails the user as well as sending them a Discord message. I'm getting inconsistent results and I'm pretty sure its to do with promises and callbacks being setup incorrectly.
exports.sendSetup = functions.storage.bucket('*the-bucket-id*').object().onFinalize((object) => {
// Get a URL for the new config file
console.log('New conf file: ' + object.name);
const { Storage } = require('#google-cloud/storage');
const storage = new Storage({
projectId: '*the-project-id*',
keyFilename: 'googleServiceAccountKey.json'
});
var bucket = storage.bucket('*the-bucket-name*');
const file = bucket.file(object.name);
console.log('Generating download url for file ' + object.name);
return file.getSignedUrl({
promptSaveAs: '*the-file-name*',
action: 'read',
expires: '03-09-2491'
}).then(signedUrls => {
var split = object.name.split("/");
var env = split[0];
var customer_id = split[1];
getCustomerDetails(customer_id, signedUrls[0], env);
});
});
function getCustomerDetails(customer_id, fileUrl, env) {
console.log('Retrieving customer details for customer id ' + customer_id + ' from Stripe');
var stripe = stripeLive;
if (env == 'test') {
stripe = stripeTest;
}
stripe.customers.retrieve(
customer_id,
function (err, customer) {
if (err == null) {
sendMail(fileUrl, customer.email, customer_id, customer.metadata.name);
console.log('discordId= ' + customer.metadata.discordId);
if (customer.metadata.discordId != 'undefined') {
sendDiscord(fileUrl, customer.metadata.discordId, customer.metadata.discordName);
}
console.log('Finished');
} else {
console.log(err);
}
}
);
}
function sendDiscord(fileUrl, discordId, discordName) {
console.log('Attempting to send a discord message to Discord id ' + discordId);
const Discord = require('discord.js');
const client = new Discord.Client();
client.login('*discord-api-key*');
client.once('ready', () => {
console.log('Discord client ready');
client.fetchUser(discordId)
.then((User) => {
console.log('Got Discord user object. Attempting to send message');
return User.send({
embed: {
color: 3447003,
fields: [
{
name: 'Hey ' + discordName + '!',
value: 'Below are the instructions to get you up and running'
},
{
name: '**Step 1**',
value: 'some instructions'
}
]
}
});
})
.catch((err) => {
console.log(err);
})
});
}
function sendMail(fileUrl, customer_email, customer_id, customer_name) {
console.log('customer_name in sendMail function = ' + customer_name);
var firstName = customer_name.substring(0, customer_name.indexOf(' '));
console.log(firstName);
const sgMail = require('#sendgrid/mail');
sgMail.setApiKey(*sendGridApiKey*);
sgMail.setSubstitutionWrappers('{{', '}}'); // Configure the substitution tag wrappers globally
const msg = {
to: customer_email,
subject: 'Welcome!',
from: {
email: 'noreply#example.com.au',
name: 'me'
},
text: 'Let\'s get you setup...',
html: '<p></p>',
templateId: '*template-id*',
substitutions: {
first_name: firstName,
file_url: fileUrl
},
};
console.log('Sending email to ' + customer_email + ' customer id:' + customer_id);
sgMail.send(msg);
}
I've read a heap of articles about promises and callbacks but can't seem to wrap my head around it. The "sendSetup" function actually returns OK but appears to stop right at the start of the getCustomerDetails function. Appreciate any assistance! I'm a bit lost!

Related

How to update a variable NodeJs

I am trying to update "documentCopyId" variable, but print undefined
function listFiles(auth) {
const drive = google.drive({ version: 'v3', auth });
const apiKey = 'xxxxx';
const paragraph = "Hello World";
let documentCopyId;
var copyTitle = "Copy Title";
let request = {
name: copyTitle,
};
drive.files.copy({
fileId: 'xxxx',
resource: request,
}, (err, driveResponse) => {
console.log('document from drive copy ' + driveResponse.data.id) //Print id
documentCopyId = driveResponse.data.id;
});
console.log('document from listFiles '+ documentCopyId); // print undefined
}
complete Log:
document from listFiles undefined
document from drive copy 1IjXkk5QgTNVT85xxxxxx
It is because the code
console.log('document from listFiles '+ documentCopyId); // print undefined
doesn't wait until this code completing
drive.files.copy({
fileId: 'xxxx',
resource: request,
}, (err, driveResponse) => {
console.log('document from drive copy ' + driveResponse.data.id) //Print id
documentCopyId = driveResponse.data.id;
});
which means that
console.log('document from listFiles '+ documentCopyId)
executing before
documentCopyId = driveResponse.data.id;
And in that case, documentCopyId is undefined.
As a solution, you can to promisify driver.files.copy part, and resolve the needed value. Or do need manipulations in a callback of drive.files.copy.
For example, you can do something like this
const listFiles = async (auth) => {
let documentCopyId;
const driveResponse = await copyPromise('Copy Title');
documentCopyId = driveResponse.data.id;
console.log('document from listFiles ' + documentCopyId);
};
const copyPromise = (name) => {
return new Promise((resolve, reject) => {
try {
const drive = google.drive({ version: 'v3', auth });
const apiKey = 'xxxxx';
const paragraph = 'Hello World';
let request = {
name
};
drive.files.copy(
{
fileId: 'xxxx',
resource: request,
},
(err, driveResponse) => {
if (err) throw new Error(err);
console.log('document from drive copy ' + driveResponse.data.id);
resolve(driveResponse);
}
);
} catch (error) {
console.log('Error in copyPromise:', error);
reject(error);
}
});
};

How to reduce email sending time (using nodemailer and firebase)?

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.

Microsoft bot framework save separately conversations or sessions

I got a Microsoft bot framework chatbot deployed on Azure and I´m using Tedious to save my conversations, thing is, bot it's being used on a web and many persons can open it to interact simultaneosly, but when I save a conversation from an user, it saves all the other interactions that have been made by other users at the same time, I need that each user has it's own conversation saved separately even if they are interacting with the chatbot at the same time...
Here's my code, maybe I'm missing something:
Bot.js
//SQL Connection
var Connection = require('tedious').Connection;
var config = {
server: 'myserver',
authentication: {
type: 'default',
options: {
userName: 'myuser',
password: 'mypass'
}
},
options: {
encrypt: true,
database: 'mydatabase'
}
};
const connection = new Connection(config);
connection.on('connect', function(err) {
console.log("Connected");
});
var Request = require('tedious').Request
var TYPES = require('tedious').TYPES;
// Function to save the conversation and bot ids
function executeConversationStatement(bot, cid, ulg ) {
request = new Request("INSERT INTO table (bot_id, conversationID, conversation_date, userlogged) VALUES (#bot, #cid, CURRENT_TIMESTAMP, #ulg); SELECT ##IDENTITY AS ID",function(err) {
if (err) {
console.log(err);}
});
request.addParameter('bot', TYPES.Int, bot);
request.addParameter('cid', TYPES.NVarChar, cid);
request.addParameter('ulg', TYPES.NVarChar, ulg);
request.on('row', function(columns) {
insertedcid = columns[0].value; // This is the id I pass later
columns.forEach(function(column) {
if (column.value === null) {
console.log('NULL');
} else {
console.log("Conversation id of inserted item is " + column.value);
}
});
});
connection.execSql(request);
}
// Here on members added I save the conversation id generated by the framework
class BOT extends ActivityHandler {
constructor(conversationState,userState,telemetryClient) {
super();
this.conversationState = conversationState;
this.userState = userState;
this.dialogState = conversationState.createProperty("dialogState");
this.previousIntent = this.conversationState.createProperty("previousIntent");
this.conversationData = this.conversationState.createProperty('conservationData');
const qnaMaker = new QnAMaker({
knowledgeBaseId: process.env.QnAKnowledgebaseId,
endpointKey: process.env.QnAEndpointKey,
host: process.env.QnAEndpointHostName
});
this.qnaMaker = qnaMaker;
this.onMessage(async (context, next) => {
await this.dispatchToIntentAsync(context);
await next();
});
this.onDialog(async (context, next) => {
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
await next();
});
this.onMembersAdded(async (context, next) => {
const { channelId, membersAdded } = context.activity;
actid = context._activity.id;
if (channelId === 'directline' || channelId === 'webchat') {
for (let member of membersAdded) {
if (member.id === context.activity.recipient.id) {
await context.sendActivity("Hi, I´m a chatbot to guide You");
try{
var saveqna = new executeConversationStatement(context._activity.id , 'Invitado');
}
catch{
console.log('Not saved');
}
}
}
}
await next();
});
}
//Finally, here I save the interaction:
async dispatchToIntentAsync(context) {
var result = await this.qnaMaker.getAnswers(context);
// Statement to save interaction with the insertedcid obtained above
var saveqnaint = new executeInteractionStatement(insertedcid, context._activity.text, result);
}
No matter if I use thet generated Id or the databse pk, I always keep the same identifier when multiple users are chatting, how can I got a separately Id for each session ?

Why am I Getting Duplicate Notifications?

I'm trying to use Firebase functions to automatically send push notifications to iOS devices. More specifically, right now when a user creates a post, it will contain an ID, which is actually the user's FCM token, from which the push notification will be sent to that user.
Why does it happen that, upon creating a post, my iOS device doesn't necessarily receive a single push notification, but many? Why is the getResult function being triggered potentially more than once for a given post?
Please see my code below. Thanks!
const functions = require('firebase-functions');
var admin = require('firebase-admin');
var firebase = require('firebase');
admin.initializeApp(functions.config().firebase);
var config = {
apiKey: "XXXXX",
authDomain: "YYYYY",
databaseURL: "ZZZZZ"
}
firebase.initializeApp(config);
firebase.database().ref('posts').on('child_added', function(snapshot1) {
snapshot1.forEach((child) => {
if (child.key == 'id') {
var token = child.val();
admin.database().ref('users').on('value', function(snapshot2) {
snapshot2.forEach(function(user) {
if (user.val().fcmToken == token) {
var newBadgeCount = user.val().badge + 1;
const payload = {
notification: {
title: 'Hello, World!',
body: 'Test Message!',
badge: '' + newBadgeCount,
sound: 'default'
}
};
function getResult(token) {
return result = admin.database().ref('fcmToken/' + token).once('value').then(allToken => {
if (allToken.val()) {
const token = Object.keys(allToken.val());
return admin.messaging().sendToDevice(token, payload).then(function (response) {
console.log("Successfully sent message: ", response.results[0].error);
}).catch(function (error) {
console.log("Error sending message: ", error);
});
};
});
}
function updateBadgeCount(badgeCount, userID) {
firebase.database().ref('users/' + userID + '/badge').set(badgeCount);
}
Promise.all([getResult(token)]).then(function(snapshots) {
updateBadgeCount(newBadgeCount, user.key);
});
};
});
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
};
});
});
Firebase triggers for each record of all records with 'child_added' option once and will re-trigger after each new child added. So, If you like to trigger a function on new write options, you need use other ways.
exports.sendNotifycation = functions.database.ref('/posts/{postId}')
.onWrite(event => {
Your new re-designed codes. (in case I will complete remain)
})

How to send push notifications to specific users with Cloud Functions for Firebase

I am using Firebase as my back end for my Android app and am very new to using Cloud Functions for Firebase and I was wondering how I would send specific users push notification when an event occurs.
For example how would I send the user with uId in the below code a push notification when a write occurs at adminName node on the database:
exports.sendNotification = functions.database.ref('/users/{uId}/groups/{adminName}')
.onWrite(event => {
// Grab the current value of what was written to the Realtime Database.
var eventSnapshot = event.data;
var str1 = "Author is ";
var str = str1.concat(eventSnapshot.child("author").val());
console.log(str);
var topic = "android";
var payload = {
data: {
title: eventSnapshot.child("title").val(),
author: eventSnapshot.child("author").val()
}
};
// Send a message to devices subscribed to the provided topic.
return admin.messaging().sendToTopic(topic, payload)
.then(function (response) {
// See the MessagingTopicResponse reference documentation for the
// contents of response.
console.log("Successfully sent message:", response);
})
.catch(function (error) {
console.log("Error sending message:", error);
});
});
Make the below changes. it works for me
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('TestCollection/{id}').onWrite(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = snapshot.data();
const deviceIdTokens = await admin
.firestore()
.collection('DeviceIDTokens')
.get();
var tokens = [];
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().device_token);
}
var payload = {
notification: {
title: 'Push Title',
body: 'Push Body',
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData.data,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});

Resources