I have a function running on the creation of a document.
When I send this information to an external API Firebase returns on 'ok' message before the API call is complete.
const functions = require('firebase-functions');
const request = require('request');
const admin = require('firebase-admin');
const rp = require('request-promise');
const port = '****';
const ip = '***.***.***.***';
admin.initializeApp(functions.config().firebase);
exports.sendUser = functions.firestore
.document('user/{userId}')
.onCreate((snap, context) => {
const data = snap.data();
const options = {
method: 'POST',
uri: 'http://' + ip + ':' + port + '/user',
body: data,
json: true,
};
rp(options)
.then(function (parsedBody) {
console.log('TEN ', parsedBody);
return parsedBody;
})
.catch(function (err) {
console.log('ERR ', err);
return err;
});
});
As you can see from my function it is not doing anything special apart from sending the data to an external source.
The API look like the following:-
app.post('/user', function (req, res) {
fs.exists(path, function(exists) {
if (exists === true) {
console.log('Currently Printing Different User Info');
fs.unlinkSync(path);
res.status(404).json({errorCode: 404, errorMessage: 'Currently Printing Different User.'});
return;
} else {
fs.writeFile(path, '', () => { console.log('File Created'); });
fs.unlinkSync(path);
res.status(200).json({statusCode: 200, statusMessage: 'Here we go'});
return;
}
});
})
How can I get Firebase to recognise the returned 404 as a failed call, and also wait until the call is complete before returning ok or failed.
The API is behaving correctly with Postman but not when data is posted via Firebase.
Has anyone encountered this before, or can anybody see what I am doing wrong?
The data is being parse over to the serve but only once Firebase has returned with 'ok' even if I purposely trigger a fail.
I need this in place to be able to use the Firebase Cloud Function retry function.
Images can be seen # https://imgur.com/a/1qYxrci
The Cloud Function returns the result before the call is complete because you don't return the Promise returned by the request-promise call.
Changing your code as follows should do the trick (at least for this problem):
exports.sendUser = functions.firestore
.document('user/{userId}')
.onCreate((snap, context) => {
const data = snap.data();
const options = {
method: 'POST',
uri: 'http://' + ip + ':' + port + '/user',
body: data,
json: true,
};
return rp(options) // <-- See the change here
.then(function (parsedBody) {
console.log('TEN ', parsedBody);
return parsedBody;
})
.catch(function (err) {
console.log('ERR ', err);
return err;
});
});
I would suggest you watch the official Video Series (https://firebase.google.com/docs/functions/video-series/) which explain very well this point about returning Promises for background functions (in particular the ones titled "Learn JavaScript Promises").
Related
I am playing around with dialogflow action. I have one intent that asks for three questions and then I do REST call to my low-code platform with the responses from the users.
Everything looks fine and I get the result back nice and tidy to my own platform.
But I want to inform the user that I have accepted the call and created the object. Therefore in my REST response I have a QuoteName and a QuoteNr that I would like to send to the agent to display to the user as the end of conversation.
Before I do this I put this information into the console.log() and I can see that it is printed in the log.
Log from dialogflow
But my dialogflows response is "XXXXX can't respond right now, please try again later." even if I got the REST call and everything working as supposed???
What is going on here?
Here is my inline editor code:
// See https://github.com/dialogflow/dialogflow-fulfillment-nodejs
// for Dialogflow fulfillment library docs, samples, and to report issues
'use strict';
const axios = require('axios');
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
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));
function Auth() {
console.log("Getting the token");
axios.post('https://b4c5e131-eff9-44e0-a8dc-3764f17a1220.app.flowfactory.se/Auth/LoginAPI?username=api#test.se&password=APassword', {
params: {
}
})
.then((response) => {
let upload = indoor(response.data.AccessToken, agent);
})
.catch(function(error) {
console.log(error);
});
}
function indoor(token, agent){
console.log("Making the call againts indoor");
const config = {
headers: { Authorization: 'Bearer ' + token, }
};
const bodyParameters = {
Customer: agent.parameters.customer,
Value: agent.parameters.quotevalue,
Description: agent.parameters.description
};
axios
.post(
'https://b4c5e131-eff9-44e0-a8dc-3764f17a1220.app.flowfactory.se/externalapi/V1/QuickQuote/CreateQuickQuote',
bodyParameters,
config
).then((response) => {
console.log(response.data.QuoteName + " - " + response.data.QuoteNr);
let msgToUser = theResponseBack(response.data.QuoteName, response.data.QuoteNr, agent);
agent.add("Skapade ett utkast av en offert åt dig i system.");
})
.catch(function(error) {
console.log(error);
});
}
function theResponseBack(qName,qNr, agent) {
console.log("Sending message back to user");
agent.add("Skapade offert " + qName + " - " + qNr);
}
let intentMap = new Map();
intentMap.set('QuickQuote', Auth);
agent.handleRequest(intentMap);
});
I am making a Discord bot, and I want it to be able to use the YouTube API to fetch new uploads from a specific channel.
I have searched elsewhere, but they all say how to upload videos, not how to track uploads.
Is this possible, and how can I do it?
Edit: Tried PubSubHubbub but it was very confusing and I couldn't get it to work
Here an example built on top of Node.js (v12) and Fastify and published with ngrok:
I wrote some comments explaining what it is happening:
const fastify = require('fastify')({ logger: true })
const xmlParser = require('fast-xml-parser')
const { URLSearchParams } = require('url')
const fetch = require('node-fetch')
// add an xml parser
fastify.addContentTypeParser('application/atom+xml', { parseAs: 'string' }, function (req, xmlString, done) {
try {
const body = xmlParser.parse(xmlString, {
attributeNamePrefix: '',
ignoreAttributes: false
})
done(null, body)
} catch (error) {
done(error)
}
})
// this endpoint needs for authentication
fastify.get('/', (request, reply) => {
reply.send(request.query['hub.challenge'])
})
// this endpoint will get the updates
fastify.post('/', (request, reply) => {
console.log(JSON.stringify(request.body, null, 2))
reply.code(204)
reply.send('ok')
})
fastify.listen(8080)
.then(() => {
// after the server has started, subscribe to the hub
// Parameter list: https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.5.1
const params = new URLSearchParams()
params.append('hub.callback', 'https://1f3dd0c63e78.ngrok.io') // you must have a public endpoint. get it with "ngrok http 8080"
params.append('hub.mode', 'subscribe')
params.append('hub.topic', 'https://www.youtube.com/xml/feeds/videos.xml?channel_id=UCfWbGF64qBSVM2Wq9fwrfrg')
params.append('hub.lease_seconds', '')
params.append('hub.secret', '')
params.append('hub.verify', 'sync')
params.append('hub.verify_token', '')
return fetch('https://pubsubhubbub.appspot.com/subscribe', {
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: params,
method: 'POST'
})
})
.then((res) => {
console.log(`The status must be 204. Received ${res.status}`)
// shows the error if something went wrong
if (res.status !== 204) {
return res.text().then(txt => console.log(txt))
}
})
I used my channel id to do some testing, consider that the notification is not in real-time, the POSTs are triggered after several minutes usually.
Firstly, please note that I am very very new to JS and coding as a general :)
Desired behaviour:
I have written the following JS HTTPS Firebase function which, which takes in a query parameter locationId, it performs a GET API call and saves the response back to Firebase. The code correctly saves the data to Firebase as desired. I have come across similar issues but i'm struggling to adapt those solutions to my specific issue below. From what I see, I'm only sending the response once.
Specific error: The following is the console output
Cannot set headers after they are sent to the client
Unhandled rejection
My function:
exports.doshiiGetMenuForOnboardedVenue = functions.https.onRequest((req, res) => {
// Forbidding PUT requests.
if (req.method === 'PUT') {
return res.status(403).send('Forbidden!');
}
cors(req, res, () => {
const locationId = req.query.locationId;
console.log('locationId', locationId);
if (locationId){
console.log('locationId', locationId);
var token = jwttoken();
const options = {
headers: {
'content-type': 'application/json',
'authorization': 'Bearer ' + token}
};
const uri = 'https://sandbox.doshii.co/partner/v3/locations/' + locationId + '/menu?lastVersion=:lastVersion&filtered=true'
axios.get(uri, options)
.then(response => {
return admin.database().ref(`/venue-menus/${locationId}/`).set(response.data)
.then(response => {
return res.status(200).send(locationId)
})
.catch(err => {
return res.status(500).send({error: err})
})
})
.then(response => {
return res.status(200).send(locationId)
})
.catch(err => {
return res.status(500).send({error: err})
})//end axios
} else {
return res.status(500).send({error: 'locationId missing'})
}//end if-else (!locationId)
})//end cors
});
By flattening out your nested promises, you can see that your code is performing the following instructions (when the axios call doesn't throw an error):
admin.database().ref(`/venue-menus/${locationId}/`).set(response.data))
.then(response => res.status(200).send(locationId))
.catch(err => res.status(500).send({error: err})
.then(response => res.status(200).send(locationId)) // this line is always called after either of the above.
.catch(err => res.status(500).send({error: err})
As general practice, unless required, you should not nest promises with their own then() and catch() handlers as it will lead to bizarre effects like this.
Furthermore, if your code calls for using //end axios or //end cors messages, you should flatten out your code so it makes sense without those messages.
Adapting your code to "fail fast", correcting your API responses and appropriately hiding error stack traces gives:
const cors = require('cors')({
origin: true,
methods: ["GET"]
});
exports.doshiiGetMenuForOnboardedVenue = functions.https.onRequest((req, res) => {
cors(req, res, (err) => { // note: cors will handle OPTIONS method
if (err) {
// note: log full error at ERROR message level
console.error('Internal CORS error:', err);
// note: return only generic status message to client
return res.status(500).json({error: 'Internal Server Error'});
}
// Forbidding anything but GET requests.
if (req.method !== 'GET') {
// 405 METHOD_NOT_ALLOWED
return res.status(405)
.set('Allow', 'GET')
.json({error: 'Not Allowed!'});
}
const locationId = req.query.locationId;
console.log('locationId', locationId);
if (!locationId) {
// 400 BAD_REQUEST
return res.status(400).json({error: 'locationId missing'})
}
var token = jwttoken();
const options = {
headers: {
'content-type': 'application/json',
'authorization': 'Bearer ' + token
}
};
// note: Don't forget to enable billing for third-party APIs!
const uri = 'https://sandbox.doshii.co/partner/v3/locations/' + locationId + '/menu?lastVersion=:lastVersion&filtered=true'
axios.get(uri, options)
.then(response => admin.database().ref(`/venue-menus/${locationId}/`).set(response.data))
.then(() => {
// note: as locationId was already sent by the client, send new/useful
// information back or nothing but the right status code
res.status(200).json({ ref: `/venue-menus/${locationId}/` });
})
.catch(err => {
// note: log full error at ERROR message level
console.error('Failed to retrieve/save API data:', err);
// note: return only message to client
res.status(500).json({error: err.message || 'Internal Server Error'});
});
});
});
I have developed Google Cloud Function which calls an API hosted in AZURE.
However the function returns error
Error: function crashed.Details:
getaddrinfo ENOTFOUND https://bupanonproduction.azure-api.net https://bupanonproduction.azure-api.net:443
Below is the google cloud function
'use strict';
const http = require('https');
const host = 'https://bupanonproduction.azure-api.net';
exports.remaininglimits = (req, res) => {
// Call the API
callRemainingLimitsApi().then((output) => {
// Return the results from the API to API.AI
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ 'speech': output, 'displayText': output }));
}).catch((error) => {
// If there is an error let the user know
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ 'speech': error, 'displayText': error }));
});
};
function callRemainingLimitsApi () {
return new Promise((resolve, reject) => {
// Create the path for the HTTP request to get the weather
let path = '/api/Values';
console.log('API Request: ' + host + path);
// Make the HTTP request to get the weather
http.get({host: host, path: path, headers: {'Ocp-Apim-Subscription-Key':'0a6e2fa822ec4d7a821d7f286abb6990'}}, (res) => {
let body = ''; // var to store the response chunks
res.on('data', (d) => { body += d; }); // store each response chunk
res.on('end', () => {
// After all the data has been received parse the JSON for desired data
let response = JSON.parse(body);
let jasonString = JSON.stringify(response);
// Create response
let output = `Hi, your limit is ${jasonString}.`;
// Resolve the promise with the output text
console.log(output);
resolve(output);
});
res.on('error', (error) => {
reject(error);
});
});
});
}
When I use other public API such as below it returns correct result to the cloud function.
https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=MSFT&apikey=demo
Any idea why the cloud function not recognizing the AZURE API url?
-Alan-
I just found out that the host should be defined without the prefix "https". That fixed the problem. I am using Free Trial with $300 credit and I am not sure if this is considered a paid plan.
I'm making a bot that searches restaurants based on location. Can anyone help me why this doesnt show up in FB messenger?:
restaurants(result.getMemory('location').raw)
.then(res=>{
message.addReply(res);
message.reply();
});
}
The call to the restaurants function returns the results from a YELP API call (an array of restaurants) but when I add it as a reply to message, nothing happens in FB messenger.
Here is the full code for message.js:
const recastai = require('recastai');
const restaurants = require('./restaurants');
// This function is the core of the bot behaviour
const replyMessage = (message) => {
// Instantiate Recast.AI SDK, just for request service
const request = new recastai.request(process.env.REQUEST_TOKEN,
process.env.LANGUAGE);
// Get text from message received
const text = message.content;
console.log('I receive: ', text);
// Get senderId to catch unique conversation_token
const senderId = message.senderId;
// Call Recast.AI SDK, through /converse route
request.converseText(text, { conversationToken: senderId })
.then(result => {
//Recast takes text analyses that, returns a result object, generates replies adds messages to reply stack and then sends the replies
//Call Yelp API with when the intent is Location. When Yelp returns result we add it to the result.replies array.
//Then we add everything in result.replies to the messaging queue that sends the responses to FB
if (result.action) {
console.log('The conversation action is: ', result.action.slug);
}
// If there is not any message return by Recast.AI for this current conversation
if (!result.replies.length) {
message.addReply({ type: 'text', content: 'I don\'t have the reply to this yet :)' });
} else {
// Add each reply received from API to replies stack
result.replies.forEach(replyContent => message.addReply({ type: 'text', content: replyContent }));
}
// Send all replies
message.reply()
//send initial reply generated by Recast first
.then(() => {
//call restaurant function that returns a list of results from API
//if the action is location and done
if(result.action && result.action.slug === 'location' && result.action.done){
restaurants(result.getMemory('location').raw)
.then(res=>{
console.log(res);
message.addReply(res);
message.reply();
});
}
})
.catch(err => {
console.error('Error while sending message to channel', err);
});
})
.catch(err => {
console.error('Error while sending message to Recast.AI', err);
});
};
module.exports = replyMessage;
And here is my restaurants.js code that is imported into the message.js file for the bot behavior:
const rp = require('request-promise');
// Load configuration
require('./config');
const restaurants = (location) => {
return Promise.all([
yelpCall(location)
]).then(result => {
//result contains the return value from Yelp call
return result;
});
};
const yelpCall = (location) => {
const auth = {
method: 'POST',
url: 'https://api.yelp.com/oauth2/token?grant_type=client_credentials&client_id='+ process.env.YELP_APP_ID +'&client_secret='+process.env.APP_SECRET
};
return rp(auth)
.then(result => {
const tokens = JSON.parse(result);
return tokens;
})
.then(result=>{
const options = {
url: 'https://api.yelp.com/v3/businesses/search?location=' + location + "&term=thai",
headers: {Authorization: "Bearer " + result.access_token}
};
return rp(options).then(findings =>{
return findings;
});
});
};
module.exports = restaurants;
A few thoughts :
message.reply is thenable, therefore return message.reply() in two places.
request.converseText() is thenable, therefore return request.converseText(...).
restaurants is thenable, therefore return restaurants(...).
in message.js, message.addReply() is passed object of the form {type:..., content:...} in two places but finally just res. Is that correct?
in restaurants.js, Promise.all() appears to be unnecessary. It will cause its result to be wrapped in an array. module.exports = location => yelpCall(location); seems more appropriate.