All My Friends Are Superheroes

A few weeks back I created an incredibly practical and not silly at all application that went through your device's contact list and "fixed" those contacts that didn't have a proper picture. The "fix" was to simply give them a random cat picture. That seems totally sensible, right?

This is practical

I was thinking about this during the weekend and it occured to me that there is an even cooler way we could fix our friends - by turning them all into superheros with the Marvel API. I've built a few apps with this API in the past (I'll link to them at the end) and I knew it had an API for returning characters. I thought - why not simply select a random character from the API and assign it to each of my contacts without a picture?

The Character endpoint of the Marvel API does not allow for random selections so I hacked up my own solution. First, I did a generic GET call on the API to get the first page of results. In that test, I was able to see the total number of characters:

Interactive tester

Given that I assume, but certainly can't verify, that they have IDs from 1 to 1485, I decided to simply select a random number between them. (I ended up going a bit below 1485 just to feel a bit safer.) I figured this would be an excellent use of OpenWhisk, so I wrote up a quick, and simple, action:


var request = require('request');
var crypto = require('crypto');

const publicKey = 'my api brings all the boys to the yard';
const privateKey = 'damn right its better than yours';

/*
This number came from searching for characters and doing no filter. The
API said there was 1485 total results. I dropped it down to 1400 to allow
for possible deletes in the future.
*/
const total = 1400;

function getRandomInt (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function main() {

    return new Promise(function(resolve, reject) {

        let url = 'https://gateway.marvel.com:443/v1/public/characters?limit=1&apikey='
        +publicKey+'&offset=';
        
        let selected = getRandomInt(0, total);
        url += selected;

        // add hash
        let ts = new Date().getTime();
        let hash = crypto.createHash('md5').update(ts + privateKey + publicKey).digest('hex');

        url += '&hash='+encodeURIComponent(hash)+'&ts='+ts;

        request.get(url, function(error, response, body) {
            if(error) return reject(error);
            let result = JSON.parse(body).data.results[0];

            let character = {
                name:result.name,
                description:result.description,
                picture:result.thumbnail.path + '.' + result.thumbnail.extension,
                url:''
            };
            if(result.urls && result.urls.length) {
                result.urls.forEach(function(e) {
                    if(e.type === 'detail') character.url = e.url;
                });
            }
            resolve(character);
        });

    });
}

exports.main = main;

I deployed this to OpenWhisk as a zipped action since crypo wasn't supported out of the box. (As an aside, that's wrong, but it's a long story, so don't worry about it now.) I then used one more wsk code to create the GET API, and I was done. And literally, that's it. 55 lines of code or so and the only real complex aspect is the hash. I do remove quite a bit of the Character record just because I didn't think it was necessary. I'm returning just the name, description, picture, and possibly a URL.

You can see this in action here: https://3b1fd5b1-e8cc-4871-a7d8-cc599e3ef852-gws.api-gw.mybluemix.net/api/getRandom

So yeah, I'm building something totally stupid and impractical here, but I freaking love how easy it was to deploy the API to Bluemix. As I said, I've got 50ish lines of code and I'm done, and as a developer, I think that royally kicks ass.

Ok, so what about the app? I'm not going to go through all the code since I shared it in the earlier post. The basics were - get all contacts, loop over each, and if they don't have a picture, "fix it", so let's focus on that code block.


Contacts.find(["name"]).then((res) => {

    res.forEach( (contact:Contact) => {

    if(!contact.photos) {
        console.log('FIXING '+contact.name.formatted);
        //console.log(contact);

        proms.push(new Promise( (resolve, reject) => {

        this.superHero.getSuperHero().subscribe((res)=>{
            console.log('super hero is '+JSON.stringify(res));              
            this.toDataUrl(res.picture, function(s) {

            var f = new ContactField('base64',s,true);

            contact.photos = [];
            contact.photos.push(f);
            contact.nickname = res.name;

            if(!contact.urls) contact.urls = [];
            contact.urls.push({type:"other",value:res.url});

            console.log('FIXED '+contact.name.formatted);
            contact.save();
            fixed++;
            resolve();
            
            });

        });
        

        }));
    }

    });

    Promise.all(proms).then( (res) => {
    
        loader.dismissAll();

        console.log('all done, fixed is  '+fixed);
        let subTitle, button;

        if(fixed === 0) {
            subTitle = "Sorry, but every single one of your contacts had a picture. I did nothing.";
            button = "Sad Face";
        } else {
            subTitle = `I've updated ${fixed} contact(s). Enjoy!`;
            button = "Awesome";      
        }

        this.alertCtrl.create({
            title:'Contacts Updated',
            subTitle:subTitle,
            buttons:[button]
        }).present();

    });

});

Previously the logic to handle finding a random cat was synchronous, but now we've got an asynch call out to my service so I had to properly handle that in my loop. Everything is still wrapped in a Promise though because I'm still converting the image to base64 for storage on the phone. (And that's probably a violation of the API, but I'm not releasing this to the market, so, yeah.) Outside of that, the code is the same. I call the API in this simple provider:


import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';

@Injectable()
export class SuperHero {

  apiUrl:string = 'https://3b1fd5b1-e8cc-4871-a7d8-cc599e3ef852-gws.api-gw.mybluemix.net/api/getRandom';

  constructor(public http: Http) {
  }

  getSuperHero() {
    return this.http.get(this.apiUrl + '?safaricanbiteme='+Math.random()).map(res => res.json());
  }

}

So what's with the random code at the end? See this post about a stupid Safari caching bug that impacts it. If you want to see the rest of the Ionic code, you can find it here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/fixcontacts2a

And the result?

Awesome

Pure awesomeness. (Ok, maybe just to me.) If your curious about my other uses of the Marvel API, here are a few links:

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.

See Also