I am trying to make an assistant app and was using the cloud firestore service of firebase to send the response back to my app using webhook as fulfilment. I have used 'session' parameter in request JSON according to this documentation and sending fulfilmentText as response to the user. But whenever user launches the app, a new session is created which I don't want. I simply want, just a single entry for each user in my database so how to achieve that using dialogflow.
In Alexa Skill, we have deviceId as parameter by which we can uniquely identify the user irrespective of the session id but is there any parameter in the dialogflow request JSON. If not, then how to achieve this task without it.
The request JSON I am getting from Dialogflow has a userID in it, so can I use the userId or should I go with userStorage provided the userStorage parameter is not available in the request JSON.
request.body.originalDetectIntentRequest { source: 'google', version: '2', payload: { surface: { capabilities: [Object] },
inputs: [ [Object] ],
user:
{ locale: 'en-US',
userId: 'ABwppHG5OfRf2qquWWjI-Uy-MwfiE1DQlCCeoDrGhG8b0fHVg7GsPmaKehtxAcP-_ycf_9IQVtUISgfKhZzawL7spA' },
conversation:
{ conversationId: '1528790005269',
type: 'ACTIVE',
conversationToken: '["generate-number-followup"]' },
availableSurfaces: [ [Object] ] } }
EDIT : Thank You #Prisoner for the answer but I am unable to send the random ID generated in the response and set in in the payload. Below is the code where I am generating the uuid and storing it in firestore. What I am doing wrong in the below code due to which new uuid is generated for returning user and therefore response is shown as No document found in the database. I suppose I am not sending uuid appropriately. Please help.
exports.webhook = functions.https.onRequest((request, response) => {
console.log("request.body.queryResult.parameters", request.body.queryResult.parameters);
console.log("request.body.originalDetectIntentRequest.payload", request.body.originalDetectIntentRequest.payload);
let userStorage = request.body.originalDetectIntentRequest.payload.user.userStorage || {};
let userId;
console.log("userStorage", userStorage);
if (userId in userStorage) {
userId = userStorage.userId;
} else {
var uuid = require('uuid/v4');
userId = uuid();
userStorage.userId = userId
}
console.log("userID", userId);
switch (request.body.queryResult.action) {
case 'FeedbackAction': {
let params = request.body.queryResult.parameters;
firestore.collection('users').doc(userId).set(params)
.then(() => {
response.send({
'fulfillmentText' : `Thank You for visiting our ${params.resortLocation} hotel branch and giving us ${params.rating} and your comment as ${params.comments}.` ,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
return console.log("resort location", params.resortLocation);
})
.catch((e => {
console.log('error: ', e);
response.send({
'fulfillmentText' : `something went wrong when writing to database`,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
}))
break;
}
case 'countFeedbacks':{
var docRef = firestore.collection('users').doc(userId);
docRef.get().then(doc => {
if (doc.exists) {
// console.log("Document data:", doc.data());
var dat = doc.data();
response.send({
'fulfillmentText' : `You have given feedback for ${dat.resortLocation} and rating as ${dat.rating}`,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
response.send({
'fulfillmentText' : `No feedback found in our database`,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
}
return console.log("userStorage_then_wala", userStorage);
}).catch((e => {
console.log("Error getting document:", error);
response.send({
'fulfillmentText' : `something went wrong while reading from the database`,
'payload': {
'google': {
'userStorage': userStorage
}
}
})
}));
break;
}
You have a couple of options, depending on your exact needs.
Simple: userStorage
Google provides a userStorage object which is persisted across conversations when it can identify a user. This lets you store your own identifier when you need to track when a user returns.
The easiest way to do this is to check the userStorage object for the identifier when your webhook is called. If it doesn't exist, create one using something like a v4 UUID and save it in the userStorage object.
If you are using the actions-on-google library, the code might look something like this:
let userId;
// if a value for userID exists un user storage, it's a returning user so we can
// just read the value and use it. If a value for userId does not exist in user storage,
// it's a new user, so we need to generate a new ID and save it in user storage.
if (userId in conv.user.storage) {
userId = conv.user.storage.userId;
} else {
// Uses the "uuid" package. You can get this with "npm install --save uuid"
var uuid = require('uuid/v4');
userId = uuid();
conv.user.storage.userId = userId
}
If you are using the dialogflow library, you can use the above, but you'll need this line first:
let conv = agent.conv();
If you're using the multivocal library, it does all of the above for you and will provide a UserID in the environment under the path User/Id.
If you're handling the JSON directly, and you are using the Dialogflow v2 protocol, you can get the userStorage object by examining originalDetectIntentRequest.payload.user.userStorage in the JSON request object. You'll set the payload.google.userStorage object in the JSON response. The code is similar to the above and might look something like this:
let userStorage = body.originalDetectIntentRequest.payload.user.userStorage || {};
let userId;
// if a value for userID exists un user storage, it's a returning user so we can
// just read the value and use it. If a value for userId does not exist in user storage,
// it's a new user, so we need to generate a new ID and save it in user storage.
if (userId in userStorage) {
userId = userStorage.userId;
} else {
// Uses the "uuid" package. You can get this with "npm install --save uuid"
var uuid = require('uuid/v4');
userId = uuid();
userStorage.userId = userId
}
// ... Do stuff with the userID
// Make sure you include the userStorage as part of the response
var responseBody = {
payload: {
google: {
userStorage: JSON.stringify(userStorage),
// ...
}
}
};
Note the first line of the code - if userStorage doesn't exist, use an empty object. It won't exist until you send a response that includes storing something in it for the first time, which will happen in the last few lines of this code.
Advanced: Account Linking
You can request users to sign in to your Action using Google Sign In. This can be done just using voice for the simplest of cases and would only interrupt the flow the first time.
After this, your Action is given a JWT which contains their Google ID which you can use as their identifier.
If you're using the actions-on-google library, you can get the ID from the decoded JWT with a line such as:
const userId = conv.user.profile.payload.sub;
In the multivocal library, the ID from the decoded JWT is available in the environment under the path User/Profile/sub
Deprecated: Anonymous User ID
You'll see some answers here on StackOverflow that reference an Anonymous User ID. Google has deprecated this identifier, which was not always a reliable way to verify returning users, and will be removing it 1 Jun 2019.
This code is currently still being sent, but this will be removed starting 1 Jun 2019.
Related
I,m learning MongoDB and mongoose and now I have a problem in defining a 404 status for my route handler. Here is the code:
app.get('/users/:id', async (req, res) => {
const _id = req.params.id
try {
const user = await User.findById(_id)
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
Now if I give it an id that doesn't exist, it doesn't give me 404 Not Found status. it only executes the catch block which is not what I want.
I would appreciate it if you tell me where I made mistake or tell me a way to get error handling for that.
Thanks
The problem
As you can see in the log
CastError: Cast to ObjectId failed for value "6082d50a2c89db3164" at path "_id" for model "User"
It means : the value you provide to findById function ("6082d50a2c89db3164") is not a valid ObjectId.Then the catch block is executed.
Suggestion
1. Validate the parameter before query in database
I understand that you're trying to provide some id that doesn't exist in the database to test. But IMHO, there a difference between 2 cases :
you provide a valid id, and this id cannot be found in the database. It should return 404 in this case
you provide an invalid id in the request, it could be a string like "6082d50a2c89db3164", or even "#Q*&$(##*" or anything we could imagine. For this case, it could be better if we validate the input (req.params._id) to ensure that the format is valid. The code will be something like this:
app.get('/users/:id', async (req, res) => {
const _id = req.params.id;
// validate params
if(!isValidateObjectId(_id)) { // the function we need to write
res.status(200).send("Invalid params"); // you can define your status and message
return;
}
// good params, get user from database
try {
const user = await User.findById(_id)
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
2. Use findOne() method instead of findById
If you want a simpler solution, don't use findById because the function expects a valid ObjectId. We can use findOne() method :
app.get('/users/:id', async (req, res) => {
const _id = req.params.id
try {
const user = await User.findOne({_id : _id})
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
(IMHO, the first solution is better though..)
Some helpful link :
https://docs.mongodb.com/manual/reference/method/ObjectId/
Can I determine if a string is a MongoDB ObjectID?
https://mongoosejs.com/docs/api.html#model_Model.findOne
I wrote a small application that subscribes to DB changes using AWS Amplify CLI / AppSync. All amplify api calls work perfectly (mutations, queries) but unfortunately the observer does not receive events. I can see that the MQTT socket receives periodically binaries but I can't obtain changed objects.
I configured Amplify for amplify use. I can see in the debugger that the AppSyncProvider has been initisalised. Also tried API and PubSub but makes no difference.
const awsmobile = {
"aws_appsync_graphqlEndpoint": "https://[...].appsync-api.[...]/graphql",
"aws_appsync_region": "[...]",
"aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
};
Amplify.configure(awsmobile);
ngOnInit()
{
try {
this.apiService.OnUpdateA.subscribe(
{
next: (x) => {[...]},
error: (e) => {[...]},
complete: () => {[...]}
});
}
catch (error) {[...] }
}
***Schema***
type A
#model
#auth(rules: [
{allow: owner},
{allow: groups, groups: ["A"], operations: [create, update, read]},
{allow: groups, groups: ["B"], operations: [read]},
])
{
id: ID!
entry: AnotherType! #connection(name: "AnotherConnection")
[...]
}
OnUpdateAListener: Observable<
OnUpdateASubscription
> = API.graphql(
graphqlOperation(
`subscription OnUpdateA($owner: String) {
onUpdateA(owner: $owner) {
__typename
id
owner
[...]
}
}`
)
) as Observable<OnUpdateASubscription>;
Anyone for any ideas?
**Logs:**
{mqttConnections: Array(1), newSubscriptions: {…}, provider: Symbol(INTERNAL_AWS_APPSYNC_PUBSUB_PROVIDER)}
mqttConnections: Array(1)
0: {url: "wss://[...]-ats.iot.[...].amazonaws…[...]%3D%3D", topics: Array(2), client: "[...]"}
length: 1
__proto__: Array(0)
newSubscriptions:
onUpdate:
expireTime: 1573313050000
topic: "[....]/22tmaezjv5555h4o7yreu24f7u/onUpdate/1cd033bad555ba55555a20690d3e04e901145776d3b8d8ac95a0aea447b273c3"
__proto__: Object
__proto__: Object
provider: Symbol(INTERNAL_AWS_APPSYNC_PUBSUB_PROVIDER)
__proto__: Object
However, not sure whether it is suspicious that the subscription Object has no queue?
Subscription {_observer: {…}, _queue: undefined, _state: "ready", _cleanup: ƒ}
_cleanup: ƒ ()
_observer:
next: (x) => {…}
__proto__: Object
_queue: ***undefined***
_state: "ready"
closed: (...)
__proto__: Object
Many thanks in advance.
For those who are experiencing the same behaviour. It was due to the fact that I had the owner in the auth section of the schema. Deleted the {allow: owner}, part and the subscriptions started to work immediately.
here is a working example of AWS Amplify Subscriptions:
import Amplify from 'aws-amplify';
import API from '#aws-amplify/api';
import PubSub from '#aws-amplify/pubsub';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
API.configure(awsconfig);
PubSub.configure(awsconfig);
// put above in root
// below is example
import { API, graphqlOperation } from 'aws-amplify';
var onAddNote = `subscription OnCreateNote {
onCreateNote {
id
patient {
id
organization {
id
}
}
}
}
`;
listenForNoteAdd() {
return API.graphql(graphqlOperation(onAddNote) ).subscribe({next: (noteData) => {
console.log("new note so reload consider reload")
let note = noteData.value.data.onCreateNote
console.log(JSON.stringify(note))
// now that you have indication of something happening
// do what you must next
}})
}
I had the same problem with GraphQL. Thing is: we have to return Owner in mutation response in order to let Subscription know to whom to send this event.
removing {allow: owner} did worked for me but thats not the right way since we require it in order to have owner based access to data.
so the correct way i found is:
if subscription is:
subscription MySubscription {
onCreateuser(owner: "$userName") {
id
name
number
}
}
mutation should be:
mutation MyMutation {
createUser(input: {name: "xyz", id: "user123", number: "1234567890"}) {
id
name
number
owner
}
}
we must return owner in mutation's response in order to get the subscription to that event and all other properties as same as mutation response.
For those coming here experiencing this error, do not delete {allow: owner}. Allow owner ensures only a user authenticated with Cognito User Pool can run queries, mutations, etc.
It looks like the OP is using amplify codegen to generate his API service, and if you look the listener has param for owner. It's optional, but if your #auth is {allow: owner} it is required.
Additional note: Not sure if he is using the correct owner field stored in his datastore or not. If there is not already an owner field created (or a different field specified), it will create one with a unique uuid. So he could be passing the incorrect owner or none at all.
Run a simple call to get the owner...
export const GetOwner = async (id: string): Promise<GetOwnerQuery> => {
const statement = `query GetAppData($id: ID!) {
getAppData(id: $id) {
owner
}
}`;
const gqlAPIServiceArguments: any = {
id,
};
const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any;
return <GetOwnerQuery>response.data.getAppData;
};
...and pass that in your subscription.
const { owner } = await GetOwner(id);
if (owner) {
this.apiUpdateListener$ = this.api.OnUpdateAppDataListener(owner).subscribe((data) => {
console.log('api update listener ===> ', data);
});
}
I am currently able to sign in just fine with previously created user credentials and use the app as normal, but am unable to create a new user. I am using React.js on the client side and an Express api on the backend. I am getting a mongoose validation error. All of the authentication came with the template the course has us use and I haven't touched any of those files. I even went back and compared commit history trees to ensure that nothing was changed.
Here is my user schema and sign-up route. I tried eliminating uniqueness from the model and that didn't impact it. I know there is a lot of potential places something could be going wrong, but if anyone has any suggestions on potential issues I would be forever grateful! I console logged the req.body.credentials within sign up and the data being sent over looks good.
Error code: 422 Unprocessable Entity
Server side error: 'The received params failed a Mongoose validation'
const mongoose = require('mongoose')
const { petSchema } = require('./pet.js')
const { pictureSchema } = require('./picture.js')
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
hashedPassword: {
type: String,
required: true
},
token: String,
pets: [petSchema],
pictures: [pictureSchema]
}, {
timestamps: true,
toObject: {
// remove `hashedPassword` field when we call `.toObject`
transform: (_doc, user) => {
delete user.hashedPassword
return user
}
}
})
module.exports = mongoose.model('User', userSchema)
// SIGN UP
// POST /sign-up
router.post('/sign-up', (req, res) => {
// start a promise chain, so that any errors will pass to `handle`
console.log(req.body.credentials)
Promise.resolve(req.body.credentials)
// reject any requests where `credentials.password` is not present, or where
// the password is an empty string
.then(credentials => {
if (!credentials ||
!credentials.password ||
credentials.password !== credentials.password_confirmation) {
throw new BadParamsError()
}
})
// generate a hash from the provided password, returning a promise
.then(() => bcrypt.hash(req.body.credentials.password, bcryptSaltRounds))
.then(hash => {
// return necessary params to create a user
return {
email: req.body.credentials.email,
hashedPassword: hash
}
})
// create user with provided email and hashed password
.then(user => User.create(user))
// send the new user object back with status 201, but `hashedPassword`
// won't be sent because of the `transform` in the User model
.then(user => res.status(201).json({ user: user.toObject() }))
// pass any errors along to the error handler
.catch(err => handle(err, res))
})
Solved. One of the subdocuments I had within user had a key with a value set to unique. I needed to eliminate that because my database was indexing users with a null value and throwing a duplicate error. I then needed to reset my database (I just renamed it to test it out) so that it didn't have any saved indexes with that configuration. I just deleted my collections within Heroku as well (luckily I didn't have significant amounts of data in there and this solution was perfectly fine for my situation). I am now able to sign up users again without any duplicate key errors.
I'm having trouble communicating between the frontend and backend for a selected GET request.
I am using a React frontend with an express/mongoose setup out in the backend.
In the frontend, I do a GET call using axios for:
axios.get('/api/orders/', {
params : {
name: this.props.user.name // user name can be Bob
}
})
And in the backend I'm having a hard time understanding the correct method I would need to do to query the database (example below doesn't work). I found stuff with .select but even then I still can't get it to work:
router.get('/orders', function(req, res) {
Order.find({}).select(req.params).then(function (order) {
res.send(req.params);
})
});
I also tried doing this to see if I can even get the params to send properly and to no demise:
router.get('/orders/:name', function(req, res) {
res.send('client sent :',req.query.name);
});
The orders document model holds objects that house an ordered array and a name (type: String) attached to the object. The Mongoose scheme for the order:
const orderScheme = new Schema({
name : { type : String },
orders : { type : Array}
});
In my MongoDB, I can see all the "Master Orders" send back. Each master order has the name of who submitted it, plus all the orders within (there can be a ton of orders).
What I'm trying to exactly do is pull up all orders that have a certain name. So if I search "TestAccount", I'll get all of bob's orders. I've included an image below:
Any pointers?
Client-side:
axios.get('/api/orders/' + this.props.user.name)
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
You need to handle the Promise when resolved/rejected.
Server-side:
router.get('/orders/:name', function(req, res) {
return Order.find({name: req.params.name}).then(function(orders) {
// return orders when resolved
res.send(orders);
})
.catch(function (error) {
// handle error
console.log(error);
})
});
You did not specify a named route parameter in your route path.
You also aren't accessing the name property by using req.params only.
You should use Model.find() conditions parameter to specify which document[s] you're trying to find. Query.prototype.select() is for filtering document fields.
I have a PostSchema which has a property array called votedBy:[],this is of type votedBySchema.Like below:
var votedBySchema = new Schema({
voteType:{
type:Boolean,default:false
},
voter:{
type:Schema.Types.ObjectId
}
});
const PostSchema = new Schema({
_authorId:{
type:Schema.Types.ObjectId,
required:true
},
title:{
type:String,
required:true
},
body:{
type:String,
},
date:{
type:Date
},
hidden:{
type:Boolean,
default:false
},
popularity:{
type:Boolean
},
votedBy:[{
type:votedBySchema,
}]
});
Now I fire a patch request from postman with to url /post/:postId.Here I run a middleware which checks if user requesting has a valid token.And put userObject and token on req object.
Next I want that if user is author of the post then he can change everything about the post but if he is not the author then he can only change popularity i.e upVote or downVote.
My logic was that every voting person must be registered in votedBy array.
like:
votedBy:[
{voteType:true,voter:'abcd#gmail.com'}, //upvote
{voteType:false,voter:'pqrs#gmail.com'}//downvote
]
As Email is unique one must only be able to upVote or downVote so unique entry on basis of email in votedBy.
my route logic code:
app.patch('/post/:postId',authenticate,(req,res)=>{
console.log("[server.js]=>request ${req.method} to ${req.url}")
let body = _.pick(req.body,["title","body","popularity","hidden"])
let user = req.user;
let userId = user._id;
let postId = req.params.postId;
Post.findById(postId).then((post)=>{
let oldPost = post;//No use in code
console.log(`Found post ${post}`);
let postAuthorId = post._authorId.toString();
if(postAuthorId == userId){
post.title = body.title || post.title;//new,old
post.body = body.body || post.body;
post.hidden = body.hidden ||post.hidden;
post.update(post,{new:true}).then((postUpdated)=>{
console.log("Post Updated");
res.send(postUpdated);
});
}
else{
let voter = user.email;
console.log("\n voter is \n",voter)
let voteType = body.popularity;
//below code needs to be changed
/* post.update(
{
$set:{
'votedBy.$.voteType':voteType,
'votedBy.$.voter':voter
}
},
{
new:true
}
).then((iDontKnow)=>{
console.log('I dont know',iDontKnow);
res.send(post);
})*///Update or push the votedBy array for voter
}
});
});
Note I want to give a voting feature same as fb/youtube.
Also suggest if there is any other way around.
Idea of doing this is fine but if I would have implemented this will have kept userId for mapping rather than user email.
Eventually you will have that post from front end so you can send value from front end, but that will not be a that good a method.
post.findOneAndUpdate( { userId : 'id of user from session' }, { "votedBy.$. voteType" : "true/false"}, { upsert : true }, callback );
So you need to perform find operation first to get value of vote type for particular user and then update it
Route code will be something like
dbOperation.getPostData(request.body.postId, userId , (err,result)=>{
if(err){ //log the error}
else if(result!=undefined){
dbOperation.updatePost(request.body, (err,result)=>{
if(err){ //log the error}
else{
response.json({message:'update',code:200,success:true});
}
});
else{
response.json({message:'post not found',code:404,success:false});
}
});