Hardware components | ||||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
|
I have a 26 years old young man at home. He is a beer aficionado, and likes to discover new beers. He likes to use applications like Untappd to find new things to try and gather feedback from other beer fans.
I've built a skill for him :)
With BeerBuddy you get recommendation on beers depending on what type you like. You discover new flavors and hear the story from the brewers. Finally you call also checkin directly on your favorite beer social network.
Once you have selected a new beer to try, would not be cool to have it deliver directly at your door? Using APIs from Postmates and Drizly (WIP) it's totally doable.
From your coach you can now discover lovely beers and taste them just 20 minutes after.
To build this skill we've used AWS Lambda with the Serverless framework.
Data comes from the Untappd API.
Skill id: amzn1.ask.skill.82ef4b63-5285-4909-9cd2-e9c1d347e2c5
DemoGetting Started
BeerBuddy has 4 intents.
- DrinkBeer
- Check in
- Surprise
- LinkUntappd
DrinkBeer intent makes you discover new beers by asking which type of beers you like. You can trigger it with sentences like "I am thirsty" or "Suggest me a beer"
Check in intent lets you add a checkin on Untappd beer social network. It will ask for the beer you are drinking and the rating you want to give. Use sentences like "Check in" or "Check in a beer"
Surprise intent will suggest you a random beer to try. Try it by saying "Surprise me"
LinkUntappd intent will let you add your Untappd account. I had some issues with account linking so it's not properly working yet.
Enhancements- Make the account linking with Untappd working
- Use Postmates or Drizly API to actually deliver beers to users.
- Create intent to check other users profile
'use strict';
var Q = require("q");
var request = Q.denodeify(require("request"));
module.exports.hello = (event, context, callback) => {
try {
console.log("event.session.application.applicationId=" + event.session.application.applicationId);
/**
* Uncomment this if statement and populate with your skill's application ID to
* prevent someone else from configuring a skill that sends requests to this function.
*/
// if (event.session.application.applicationId !== "amzn1.echo-sdk-ams.app.05aecccb3-1461-48fb-a008-822ddrt6b516") {
// context.fail("Invalid Application ID");
// }
if (event.session.new) {
onSessionStarted({requestId: event.request.requestId}, event.session);
}
if (event.request.type === "LaunchRequest") {
onLaunch(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "IntentRequest") {
onIntent(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "SessionEndedRequest") {
onSessionEnded(event.request, event.session);
context.succeed();
}
} catch (e) {
context.fail("Exception: " + e);
}
};
/**
* Called when the session starts.
*/
function onSessionStarted(sessionStartedRequest, session) {
console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId
+ ", sessionId=" + session.sessionId);
// add any session init logic here
}
/**
* Called when the user invokes the skill without specifying what they want.
*/
function onLaunch(launchRequest, session, callback) {
console.log("onLaunch requestId=" + launchRequest.requestId
+ ", sessionId=" + session.sessionId);
getWelcomeResponse(callback);
}
/**
* Called when the user specifies an intent for this skill.
*/
function onIntent(intentRequest, session, callback) {
console.log("onIntent requestId=" + intentRequest.requestId
+ ", sessionId=" + session.sessionId);
var intent = intentRequest.intent,
intentName = intentRequest.intent.name;
console.log("INNTENT",intent)
// handle yes/no intent after the user has been prompted
if (session.attributes && session.attributes.userPromptedToContinue) {
delete session.attributes.userPromptedToContinue;
if ("AMAZON.NoIntent" === intentName) {
handleFinishSessionRequest(intent, session, callback);
} else if ("AMAZON.YesIntent" === intentName) {
handleRepeatRequest(intent, session, callback);
}
}
// dispatch custom intents to handlers here
if ("DrinkBeerIntent" === intentName) {
handleDrinkBeerRequest(intent, session, callback);
}else if ("BeerStyleIntent" === intentName) {
handleBeerStyleRequest(intent, session, callback);
} else if ("LinkUntappdIntent" === intentName) {
handleLinkUntappdRequest(intent, session, callback);
} else if ("CheckingIntent" === intentName) {
handleCheckingRequest(intent, session, callback);
}else if ("OpenIntent" === intentName) {
handleOpenIntentRequest(intent, session, callback);
}else if ("BeerRatingIntent" === intentName) {
handleBeerRatingRequest(intent, session, callback);
}else if ("SurpriseIntent" === intentName) {
handleSurpriseRequest(intent, session, callback);
}else if ("AMAZON.StartOverIntent" === intentName) {
getWelcomeResponse(callback);
} else if ("AMAZON.YesIntent" === intentName) {
handleYesRequest(intent, session, callback);
} else if ("AMAZON.NoIntent" === intentName) {
handleNoRequest(intent, session, callback);
} else if ("AMAZON.RepeatIntent" === intentName) {
handleRepeatRequest(intent, session, callback);
} else if ("AMAZON.HelpIntent" === intentName) {
handleGetHelpRequest(intent, session, callback);
} else if ("AMAZON.StopIntent" === intentName) {
handleFinishSessionRequest(intent, session, callback);
} else if ("AMAZON.CancelIntent" === intentName) {
handleFinishSessionRequest(intent, session, callback);
}
}
/**
* Called when the user ends the session.
* Is not called when the skill returns shouldEndSession=true.
*/
function onSessionEnded(sessionEndedRequest, session) {
console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId
+ ", sessionId=" + session.sessionId);
// Add any cleanup logic here
}
// ------- Skill specific business logic -------
var ANSWER_COUNT = 1;
var GAME_LENGTH = 5;
// Be sure to change this for your skill.
var CARD_TITLE = "Beer guide 🍻";
function getWelcomeResponse(callback) {
// Be sure to change this for your skill.
var speechOutput = "Welcome to Beer Recommendation, tell me about your taste I will recommend you some tasty beers.",
shouldEndSession = false,
repromptText = speechOutput,
sessionAttributes = {
"speechOutput": repromptText,
"repromptText": repromptText,
};
callback(sessionAttributes,
buildSpeechletResponse(CARD_TITLE, speechOutput, repromptText, shouldEndSession));
}
function handleYesRequest(intent, session, callback) {
console.log("HAnldeYES")
var speechOutput = "";
var sessionAttributes = session.attributes;
console.log("sessionAttributes",session.attributes);
console.log("HANDLING YES intent")
var shouldEndSession = false;
if(session.attributes.question_type == "want_to_order"){
speechOutput += "Great, I just ordered it on Drizly. It should be delivered at your door in the next half hour. Cheers! "
shouldEndSession = true
}else if(session.attributes.question_type == "list_beer_of_styles"){
speechOutput += "Here are my favorite beers of type "+session.attributes.beer_type +". "
var offset = session.attributes.offset || 0;
// if(!offset){
// offset = 0
// }
for(var i=0;i<5;i++){
speechOutput += session.attributes.beers[offset+i]+", "
}
speechOutput += "Do you want to hear more?"
sessionAttributes.offset = offset+4;
shouldEndSession = false
}
callback(sessionAttributes, buildSpeechletResponse(CARD_TITLE, speechOutput, "", shouldEndSession));
}
function handleNoRequest(intent, session, callback) {
console.log("HAnldeNO")
var speechOutput = "";
var sessionAttributes = session.attributes;
console.log("sessionAttributes",session.attributes);
var shouldEndSession = false;
if(session.attributes.question_type == "want_to_order"){
speechOutput += "No worries, we will have plenty of other opportunities to enjoy a drink together."
}else if(session.attributes.question_type == "list_beer_of_styles"){
speechOutput += "Sounds good. Let me know next time you are thirsty and I will recommend you by favorite ones and surprise you."
}
callback(sessionAttributes, buildSpeechletResponse(CARD_TITLE, speechOutput, "", true));
}
function handleSurpriseRequest(intent, session, callback){
var beers_id = [6407,109679,489155,529831,462780,807944,3095,6511,7318]
var speechOutput = "";
var imageURL = "";
var cardContent = ""
var sessionAttributes = {};
var shouldEndSession = true;
console.log("HERE SURPRISE")
findBeerByID(beers_id[Math.floor(beers_id.length * Math.random())]).then(function(beer){
speechOutput += "You like adventure, right? "
speechOutput += "I have selected the best beer to surprise your palet. "
speechOutput += "I recommend you try "+beer.beer_name+" it's a "+beer.beer_style+". Untappd users have rate it "+Math.trunc(beer.rating_score)+" out of 5. "
// speechOutput += "Here what they say about it: "+beer.beer_description
speechOutput += "Do you want me to order it for you using Drizly?"
console.log(speechOutput);
imageURL = beer.beer_label;
sessionAttributes.question_type = "want_to_order"
callback(sessionAttributes,buildSpeechletWithImageCardResponse("I selected "+beer.beer_name+" for you!", speechOutput,speechOutput,imageURL, "", false))
});
}
function handleBeerRatingRequest(intent, session, callback) {
var speechOutput = "";
var imageURL = "";
var cardContent = ""
var sessionAttributes = {};
var shouldEndSession = false;
if(session.attributes.question_type == "get_beer_rating" && intent.slots.Rating.value != ""){
console.log(session.attributes)
speechOutput += "Perfect, I will checkin your rating of "+intent.slots.Rating.value+" for "+session.attributes.beer_name +" by "+session.attributes.brewery_name+"."
return findBeerByNameAndBrewery(session.attributes.beer_name,session.attributes.brewery_name).then(function(res){
console.log("RRRRR",res)
return res.beer
}).then(function(beer){
imageURL = beer.beer_label
cardContent = "I've checked "+beer.beer_name+" on Untappd for you. Enjoy it!"
cardContent+= "\n Here are more info about it: \n"
cardContent += beer.beer_description.replace(/\n/g, "\n");
console.log("HHHHHH",cardContent)
return checkInBeer(beer.bid,intent.slots.Rating.value)
}).then(function(res){
console.log("AAQHQQHR",res);
console.log("IMAGEURL",imageURL)
setTimeout(callback(sessionAttributes, buildSpeechletWithImageCardResponse(CARD_TITLE, speechOutput,cardContent,imageURL, "", false)), 5000)
// (title, speechOutput, cardContent, imageURL, repromptText, shouldEndSession)
//
// callback(sessionAttributes, buildSpeechletWithImageCardResponse(CARD_TITLE, cardContent,imageURL,speechOutput, "", true));
});
}else{
speechOutput = "Sadly you can't rate a beer without telling me more about it. You can start by saying 'Check in a beer'."
shouldEndSession = true;
callback(sessionAttributes, buildSpeechletResponse(CARD_TITLE, speechOutput, "", false));
}
}
function handleOpenIntentRequest(intent, session, callback) {
var speechOutput = "";
var sessionAttributes = {};
var shouldEndSession = false;
var card_title =""
console.log("OPEN INTENT handling")
console.log(session.attributes)
if(session.attributes.question_type == "get_brewery"){
speechOutput += "Awesome I love "+intent.slots.Brewery_name.value + " beers. What's the name of the beer?"
sessionAttributes.brewery_name = intent.slots.Brewery_name.value;
sessionAttributes.question_type = "get_beer_name"
card_title = "What's the name of the beer?"
callback(sessionAttributes, buildSpeechletResponse("🍻"+card_title, speechOutput, "", false));
}else if (session.attributes.question_type == "get_beer_name"){
speechOutput += "That's a great choice "+intent.slots.Beer_name.value + " from "+ session.attributes.brewery_name +" is my favorite. On a scale of 0 to 5 how did you like it?";
sessionAttributes.beer_name = intent.slots.Beer_name.value;
sessionAttributes.brewery_name = session.attributes.brewery_name;
sessionAttributes.question_type = "get_beer_rating"
card_title = "How would you rate "+intent.slots.Beer_name.value+"?"
callback(sessionAttributes, buildSpeechletResponse("🍻"+card_title, speechOutput, "", false));
} else if (session.attributes.question_type == "get_type_beers"){
handleBeerStyleRequest(intent, session, callback);
}
}
function handleCheckingRequest(intent, session, callback) {
var speechOutput = "";
var sessionAttributes = {};
var shouldEndSession = false;
speechOutput += "I am glad you are having a good time. From which brewery is the beer you are drinking right now?"
sessionAttributes.question_type ="get_brewery"
callback(sessionAttributes, buildSpeechletResponse("From which brewery is the beer you are drinking right now?", speechOutput, "", false));
}
function handleLinkUntappdRequest(intent, session, callback) {
// var r = {
// "version": "1.0",
// "sessionAttributes": {},
// "response":
// }
// callback({},r)
// function callback(sessionAttributes, speechletResponse) {
// context.succeed(buildResponse(sessionAttributes, speechletResponse));
// });
console.log("HERE on untappd")
callback({},{
"outputSpeech": {
"type": "PlainText",
"text": "You must have an untappd account to use this skill. Please use the Alexa app to link your Amazon account with your untappd Account."
},
"card": {
"type": "LinkAccount"
},
"shouldEndSession": true
})
}
function handleBeerStyleRequest(intent, session, callback) {
var speechOutput = "";
var sessionAttributes = {};
var beer_type = ""
if(intent.slots.Beer_name)
beer_type = intent.slots.Beer_name.value;
if(intent.slots.Beerstyle)
beer_type = intent.slots.Beerstyle.value;
var shouldEndSession = false;
var beersArr = []
findBeerStyles(beer_type).then(function(beers){
var speechOutput = "I have found " + beers.length + " beers of type "+beer_type+". ";
speechOutput += "Do you want me to list some?"
sessionAttributes.question_type = "list_beer_of_styles"
for(var i=0;i<beers.length;i++){
var b = beers[i]
beersArr.push(b.beer.beer_name);
}
sessionAttributes.beers = beersArr;
sessionAttributes.beer_type = beer_type;
callback(sessionAttributes, buildSpeechletResponse(CARD_TITLE, speechOutput, "", false));
});
}
function handleDrinkBeerRequest(intent, session, callback) {
var speechOutput = "";
var sessionAttributes = {};
// var answerSlotValid = isAnswerSlotValid(intent);
// var userGaveUp = intent.name === "DontKnowIntent";
speechOutput = "Of course, I am sure it's 5 o'clock somewhere in the world. Which type of beer would you like to taste?";
sessionAttributes.question_type = "get_type_beers"
callback(sessionAttributes,
buildSpeechletResponse(CARD_TITLE, speechOutput, "", false));
}
function handleRepeatRequest(intent, session, callback) {
// Repeat the previous speechOutput and repromptText from the session attributes if available
// else start a new game session
if (!session.attributes || !session.attributes.speechOutput) {
getWelcomeResponse(callback);
} else {
callback(session.attributes,
buildSpeechletResponseWithoutCard(session.attributes.speechOutput, session.attributes.repromptText, false));
}
}
function handleGetHelpRequest(intent, session, callback) {
// Set a flag to track that we're in the Help state.
if (session.attributes) {
session.attributes.userPromptedToContinue = true;
} else {
// In case user invokes and asks for help simultaneously.
session.attributes = { userPromptedToContinue: true };
}
// Do not edit the help dialogue. This has been created by the Alexa team to demonstrate best practices.
var speechOutput = "To start a new game at any time, say, start new game. "
+ "To repeat the last element, say, repeat. "
+ "Would you like to keep playing?",
repromptText = "Try to get the right answer. "
+ "Would you like to keep playing?";
var shouldEndSession = false;
callback(session.attributes,
buildSpeechletResponseWithoutCard(speechOutput, repromptText, shouldEndSession));
}
function handleFinishSessionRequest(intent, session, callback) {
// End the session with a custom closing statment when the user wants to quit the game
callback(session.attributes,
buildSpeechletResponseWithoutCard("Thanks for playing Flash Cards!", "", true));
}
function isAnswerSlotValid(intent) {
var answerSlotFilled = intent.slots && intent.slots.Answer && intent.slots.Answer.value;
var answerSlotIsInt = answerSlotFilled && !isNaN(parseInt(intent.slots.Answer.value));
return 1;
}
// ------- Helper functions to build responses -------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
card: {
type: "Simple",
title: title,
content: output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
// function buildSpeechletWithImageCardResponse(title, cardContent,imageURL, speechOutput, repromptText, shouldEndSession) {
// return {
// outputSpeech: {
// type: "PlainText",
// text: output
// },
// card: {
// type: "Standard",
// title: title,
// text: title,
// image: {
// smallImageUrl: imageURL,
// largeImageUrl: imageURL
// }
// },
// reprompt: {
// outputSpeech: {
// type: "PlainText",
// text: repromptText
// }
// },
// shouldEndSession: shouldEndSession
// };
// }
function buildSpeechletWithImageCardResponse(title, speechOutput, cardContent, imageURL, repromptText, shouldEndSession) {
console.log("RESPONSE");
return {
outputSpeech: {
type: "PlainText",
text: speechOutput
},
card: {
type: "Standard",
title: title,
text: cardContent,
image:{
smallImageUrl:imageURL,
largeImageUrl:imageURL
}
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
function buildSpeechletResponseWithoutCard(output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: "1.0",
sessionAttributes: sessionAttributes,
response: speechletResponse
};
}
const getContent = function(url) {
// return new pending promise
return new Promise((resolve, reject) => {
// select http or https module, depending on reqested url
const lib = url.startsWith('https') ? require('https') : require('http');
const request = lib.get(url, (response) => {
// handle http errors
if (response.statusCode < 200 || response.statusCode > 299) {
reject(new Error('Failed to load page, status code: ' + response.statusCode));
}
// temporary data holder
const body = [];
// on every content chunk, push it to the data array
response.on('data', (chunk) => body.push(chunk));
// we are done, resolve promise with those joined chunks
response.on('end', () => resolve(body.join('')));
});
// handle connection errors of the request
request.on('error', (err) => reject(err))
})
};
const postContent = function(params, postData) {
// return new Promise(function(resolve, reject) {
var req = https.request(params, function(res) {
// reject on bad status
if (res.statusCode < 200 || res.statusCode >= 300) {
return (new Error('statusCode=' + res.statusCode));
}
// cumulate data
var body = [];
res.on('data', function(chunk) {
body.push(chunk);
});
// resolve on end
res.on('end', function() {
try {
console.log("BOOODY",body)
body = JSON.parse(Buffer.concat(body).toString());
} catch(e) {
return (e);
}
});
});
// reject on request error
req.on('error', function(err) {
// This is not a "Second reject", just a different sort of failure
return (err);
});
if (postData) {
req.write(postData);
}
// IMPORTANT
req.end();
// });
};
function findBeerByID(beer_id){
var url ='https://api.untappd.com//v4/beer/info/'+beer_id+'?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET'
var options = {
url: url,
method: "GET",
json:true,
};
var response = request(options);
return response.then(function (r){
var res = r[0].req.res;
var body = r[0].body;
console.log(body);
if (res.statusCode >= 300) {
throw new Error("Server responded with status code" + res[0].req.res.statusCode,body.errors);
} else {
return body.response.beer
}
return body;
});
}
function findBeerStyles(beer_type){
var url ='https://api.untappd.com/v4/search/beer?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&sort=checkin&q='+encodeURIComponent(beer_type)
var options = {
url: url,
method: "GET",
json:true,
};
var response = request(options);
console.log("JOJOJOOJO");
return response.then(function (r){
console.log("kiki");
var res = r[0].req.res;
var body = r[0].body;
console.log(r)
console.log(body);
if (res.statusCode >= 300) {
throw new Error("Server responded with status code" + res[0].req.res.statusCode,body.errors);
} else {
return body.response.beers.items
// console.log("BEER",body.response.beers.items[0])
}
return body;
});
}
function findBeerByNameAndBrewery(beer_name, brewery_name){
console.log("FINDBEER",beer_name, brewery_name);
var url ='https://api.untappd.com/v4/search/beer?client_id=7C1AF3B97E751A2C86D750A1EB1182AD8A8AE3A5&client_secret=49024C7705D43CDFCC565FE7C61F33C2D5C635D3&sort=checkin&q='+encodeURIComponent(brewery_name+" "+beer_name)
var options = {
url: url,
method: "GET",
json:true,
};
var response = request(options);
return response.then(function (r){
var res = r[0].req.res;
var body = r[0].body;
console.log(body);
if (res.statusCode >= 300) {
throw new Error("Server responded with status code" + res[0].req.res.statusCode,body.errors);
} else {
return body.response.beers.items[0]
// console.log("BEER",body.response.beers.items[0])
}
return body;
});
};
function checkInBeer(bid,rating){
console.log("CHECKIN",bid);
var access_token = "YOUR_TOKEN"
var url = 'https://api.untappd.com/v4/checkin/add?access_token='+access_token;
var options = {
url: url,
port: 443,
method: "POST",
body: "bid="+bid+"&rating="+rating+"&gmt_offset=-5&timezone=PST",
headers: {'content-type' : 'application/x-www-form-urlencoded'}
};
var response = request(options);
return response.then(function (r){
var res = r[0].req.res;
var body = r[0].body;
console.log("BODY",body)
if (res.statusCode >= 300) {
throw new Error("Server responded with status code" + res[0].req.res.statusCode,body.errors);
} else {
console.log("REESULT",JSON.parse(body));
return true
}
return body;
});
}
Comments