Nicolas GreniéMark CheshireManfred Bortenschlager
Published © CC BY-NC

Beer Buddy

The skill app for beer recommendation, and checking on your favorite beer social network.

AdvancedShowcase (no instructions)10 hours1,246
Beer Buddy

Things used in this project

Hardware components

Amazon Echo
Amazon Alexa Amazon Echo
Mainly using Echosim.io similator
×1

Software apps and online services

AWS Lambda
Amazon Web Services AWS Lambda
Powered by Serverless framework
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit

Story

Read more

Schematics

Conversation graph

It all starts with "Alexa, open BeerBuddy" :)

Code

lambda.js

JavaScript
The whole lambda funciton
'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;
  });

}

Credits

Nicolas Grenié

Nicolas Grenié

1 project • 4 followers
Mark Cheshire

Mark Cheshire

1 project • 1 follower
Manfred Bortenschlager

Manfred Bortenschlager

1 project • 1 follower

Comments