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?
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:
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?
Pure awesomeness. (Ok, maybe just to me.) If your curious about my other uses of the Marvel API, here are a few links: