Rebuilding a Flex Mobile App as an Alexa Skill

Many, many moons ago I created a ridiculous Flex Mobile app called TBS Horoscope. This was for the Nook platform, eventually moved to Amazon Android App store (where it still sits actually) and one of my few commercial apps. The app was fairly simple. It generated a fake horoscope when an astrological sign was requested and persisted it for the day. That way it would be a bit more "realistic".

I thought it would be fun to rebuild this for the Amazon Alexa. So I did it. Because I have a great job and I'm very lucky. Before I talk about how I built it, here's a video of it in action.

Ok, so how did I build this? I began by creating a service just for the horoscope generation itself. Initially this was going to be its own OpenWhisk action, but I ran into some issues where that wouldn't make sense. I'll explain why later. Here's the code.

const signs = ["Aries","Taurus","Gemini","Cancer","Leo","Virgo","Libra","Scorpio","Sagittarius","Capricorn","Aquarius","Pisces"];

function getAdjective() {
    return adjectives[randRange(0,adjectives.length-1)];
}

function getNoun() {
    return nouns[randRange(0,nouns.length-1)];
}

function getSign() {
    return signs[randRange(0,signs.length-1)];
}


function getFinancialString() {
    let options = [
        "Today is a good day to invest. Stock prices will change. ",
        "Today is a bad day to invest. Stock prices will change. ",
        "Investments are a good idea today. Spend wisely before the " + getAdjective() + " " + getNoun() + " turns your luck! ",
        "Save your pennies! Your " + getNoun() + " is not a safe investment today. ",
        "Consider selling your " + getNoun() + " for a good return today. ",
        "You can buy a lottery ticket or a " + getNoun() + ". Either is a good investment. "
    ];
    return options[randRange(1,options.length-1)];
}

function getRomanticString() {
    let options = [
        "Follow your heart like you would follow a "+getAdjective() + " " + getNoun() + ". It won't lead you astray. ",
        "You will fall in love with a " + getSign() + " but they are in love with their " + getNoun() + ". ",
        "Romance is not in your future today. Avoid it like a " + getAdjective() + " " + getNoun() + ". ",
        "Romance is blossoming like a " + getAdjective() + " " + getNoun() + "! ",
        "Avoid romantic engagements today. Wait for a sign - it will resemble a " +getAdjective() + " " + getNoun() + ". ",
        "Love is in the air. Unfortunately not the air you will be breathing. "
    ];
    return options[randRange(1,options.length-1)];
}

function getRandomString() {
    var options = [
        "Avoid talking to a " + getSign() + " today. They will vex you and bring you a " + getNoun() + ". ",
        "Spend time talking to a " + getSign() + " today. They think you are a " + getNoun() + "! ",
        "Dont listen to people who give you vague advice about life or your " + getNoun() + ". ",
        "Today you need to practice your patience. And your piano. ",
        "Meet new people today. Show them your " + getNoun() + ". ",
        "Your spirits are high today - but watch our for a " + getAdjective() + " " + getNoun() + ". ",
        "Your sign is in the third phase today. This is important. ",
        "Your sign is in the second phase today. This is critical. ",
        "Something big is going to happen today. Or tomorrow. ",
        "Something something you're special and important something something." ,
        "A " + getAdjective() + " " + getNoun() + " will give you important advice today. ",
        "A " + getAdjective() + " " + getNoun() + " has it out for you today. ",
        "Last Tuesday was a good day. Today - not so much. ",
        "On the next full moon, it will be full. ",
        "Today is a bad day for work - instead focus on your " + getNoun() + ". ",
        "Today is a good day for work - but don't forget your " + getNoun() + ". ",
        "A dark stranger will enter your life. They will have a " + getAdjective() + " " + getNoun() + ". "
    ];
    return options[randRange(1,options.length-1)];
}

function randRange(minNum, maxNum) {
    return (Math.floor(Math.random() * (maxNum - minNum + 1)) + minNum);
}

let nouns, adjectives;

function create(args) {

    //see if we have a cache noun list
    if(!nouns) {
        nouns = require('./nouns');
    }
    if(!adjectives) {
        adjectives = require('./adjectives');
    }

    let horoscope = "";
    horoscope += getRandomString();
    horoscope += getFinancialString();
    horoscope += getRomanticString();
    horoscope += "\n\n";
    horoscope += "Your lucky numbers are " + randRange(1,10) + ", " + randRange(1,10) + ", and " + getNoun() + ".";
    return horoscope;

}

exports.create = create;

A horoscope is generated by combining a few random strings. I realized many horoscopes focused on money and love, hence the specific functions for them. To make things super random, I used a noun list with over 4500 words and an adjective list nearly 1000 words long. I had to convert these to JSON files as I didn't know how to read text files in an OpenWhisk action. (Now I do.) If you're curious, I actually copied the entire word list into my clipboard, pasted it into Chrome Dev tools with backticks before and after, then converted it into an array. Finally I did copy(s) to copy it back to my clipboard and then I saved it to the file system.

Oh - and rewriting ActionScript to JavaScript was simple as heck - all I did mainly was remove the types. That made me feel a bit sad and I thought about maybe using TypeScript for my code, but I'd have to transpile it before sending it to OpenWhisk and I didn't want to worry about that.

So that's that. The Alexa part is pretty simple. Once again I'll say I'm not happy with how I write my skills. It works. It isn't difficult. I just think I can do better.

function main(args) {


    if(args.request.type === 'IntentRequest' && ((args.request.intent.name === 'AMAZON.StopIntent') || (args.request.intent.name === 'AMAZON.CancelIntent'))) {
        let response = {
        "version": "1.0",
        "response" :{
            "shouldEndSession": true,
            "outputSpeech": {
                "type": "PlainText",
                "text": "Bye!"
                }
            }
        }

        return {response:response};
    }

    //Default response object
    let response = {
        "version":"1.0",
        "response":{
            "outputSpeech": {
                "type":"PlainText"
            },
            "shouldEndSession":true
        }
    };
    
    //treat launch like help
    let intent;
    if(args.request.type === 'LaunchRequest') {
        intent = 'AMAZON.HelpIntent';
    } else {
        intent = args.request.intent.name;
    }

    // two options, help or do horo
    if(intent === "AMAZON.HelpIntent") {
        
        response.response.outputSpeech.text = "I give you a completely scientifically driven, 100% accurate horoscope. Honest.";
    
    } else {
        let horoscope = require('./getHoroscope').create();
        response.response.outputSpeech.text  = horoscope;
        //console.log('Response is '+JSON.stringify(response));
    }

    return response;

}

exports.main = main;

Basically this boils down to two things - help and the horoscope. You'll notice I don't actually persist the horoscope for the day. I was going to make this skill be a bit more advanced. It would ask you for your sign if you didn't give it, and heck, even let you give it a birthday and it would tell you your sign. I'd then persist the horoscope in Cloudant and everyone (of the same sign) would get the same horoscope.

Then I remember horoscopes are bull shit and this is for fun and as a user, I'd rather have it just always be random.

So you might be wondering - where's the skill verification stuff? Back in August I released a package for that so you no longer have to write the code yourself.

Given that my Alexa skill action is called tbshoroscope/doHoroscope, I made my public API like so:

wsk action update tbshoroscope/getHoroscope --sequence "/rcamden@us.ibm.com_My Space/alexa/verifier",tbshoroscope/doHoroscope --web raw

I then grabbed the URL (wsk action get tbshoroscope/getHoroscope --url) and supplied that to Amazon. I'm currently waiting for verification from Amazon and when it goes live, you'll be able to find it via its skill name, "TBS Horoscope". Note - the name may change as "TBS" is a television channel here. Also, if they find a bug, obviously the code may change a bit too. If the name changes, I'll add a comment below so please check it, and if the code changes, the GitHub repo will always have the latest. You can find this demo here: https://github.com/cfjedimaster/Serverless-Examples/tree/master/tbshoroscope

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

Want to read more like this?