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: