/********************************************************************
* Libraries - Why we need 'em
********************************************************************/
// Centralized Configs
var config = require('../config/config.js');
// Controllable Log Levels
var log = require('iphb-logs');
// To create random strings
var crypto = require('crypto');
// KV Store
var kvstore = require('./kvstore.js');
/********************************************************************
* Guards
********************************************************************/
if (!config.kvstoreKeyPrefix) {
var _error = [
"Environment Variables Required:",
" KV_KEY_PREFIX"
].join('\n');
throw new Error(_error);
}
/********************************************************************
* Privates
********************************************************************/
/**
* @function _getUniqueKey
* @description Generate a unique key not already in use by the KV Store
* @private
* @param {string} prefix The prefix we use for this app
* when storing unique keys in the
* kv store for this url. this
* prefix needs to be unique for each
* domain we might use with this shortner
* code.
* @param {int} length The approximate length we want
* our unique url tokens to be.
* http://glg.link/$token
* @param {Function} callback Callback format (err,res)
*/
var _getUniqueKey = function(prefix, length, callback) {
/************
* Guards
***********/
if (typeof(callback) !== "function") {
return log.error("_getUniqueKey didn't get a callback");
}
if (!prefix) {
prefix = "";
}
// Let's begin
try {
// Create a random string at the specified length,
// then yoink out the equals sign (=)
var _randomString = (crypto.randomBytes(length - 1)).toString('base64');
var _keyWithoutEquals = _randomString.replace(/=/g, "").replace(/\+/g, "");
// Now we double check if this key already exists in the KV store and
// if it doesn't we have a winner. We wrap this in an enclosure incase
// we are called from a service or something that would possibly rapid fire
var _testKey = function(key) {
// We store the key with a prefix so we need to test it with the prefix
// By doing this it allows us to use relatively small tokens lengths per domain
// but guarantees uniqueness in the KV store
kvstore.get([prefix, key].join(''), function(err, res) {
// Errors are still no good. Even though we don't want the key
// to exist we should get a valid response from the KV store
if (err) {
throw new Error(err);
}
// We didn't get an error but there's already 'something' for this key
// so we just try our entire process again
if (res && res.message !== "no data for key") {
return _getUniqueKey(prefix, length, callback);
}
// We must have a decent key
callback(null, key);
});
};
// Test that the key isn't already used
_testKey(_keyWithoutEquals);
} catch (e) {
callback(e.toString());
}
};
/********************************************************************
* Main Exports
********************************************************************/
/**
* @module Api
* @description Public Exports for this module
* @type {Object}
*/
var api = {
/**
* @function getUrlByKey
* @description Get the URL for a key
* @param {string} key A key for the URL you wish to retrieve
* @param {Function} callback callback(err,url); the $url is a string
*/
getUrlByKey: function(urlKey, callback) {
/************
* Guards
***********/
// Callback must be a function
if (typeof(callback) !== "function") {
return log.error("getUrlByKey didn't get a callback");
}
try {
// Prefix the keys we generate in the KV store with 'something' unique
// to prevent collisions with any other app. It doesn't matter what the
// prefix is as long as its supported in the KV Store
var _kvstoreKey = [config.kvstoreKeyPrefix, urlKey].join('');
log.verbose("KVK:", _kvstoreKey);
kvstore.get(_kvstoreKey, function(err, urlObject) {
if (err || !urlObject || !urlObject.url) {
var _error = ["Something went wrong getting urlKey:", err, urlObject.url].join(' ');
log.verbose("Something went wrong getting urlKey:", err, urlObject.url);
return callback(_error);
}
// return the url
callback(null, urlObject.url);
});
} catch (e) {
callback(e.toString());
}
},
/**
* @function getKeyByUrl
* @description Get a key for a URL. When the shortener is presented with
* the returned key it will redirect to the URL passed here.
* @param {string} url The full URL you intend to shorten
* @param {Function} callback callback(err,key); the $key is a string
* that should be passed to the shortner url.
* For example:
* http://glg.link/$key
*/
getKeyByUrl: function(url, callback) {
/************
* Guards
***********/
if (typeof(callback) !== "function") {
return log.error("getKeyByUrl didn't get a callback");
}
try {
// Get a unique key. The token length returned here will
// be $prefix+$key.length. IE, if you set a length of 4
// the unique part will be approx 4 chars. The prefix is in addition
// to the unique part. Also, the key length isn't exact. We generate
// a random string of the specified length and then base64 encode it. Then,
// we strip weird chars. SO, it will be approx the length specified. For
// the purposes of this app.. meh
var _keyLength = 4;
_getUniqueKey(config.kvstoreKeyPrefix, _keyLength, function(err, key) {
if (err) {
throw new Error(err);
}
// Set our new key in the KV Store with the URL passed to us. We
// - Join the unique key returned with the 'prefix'
// Example:
// _uniqueKey = "$prefix_$key"
// - Use the above key as a token in the kvstore
// - Pass an object to be posted into the KV store that
// contains the URL we want associated with this key
// - Callback with our result
kvstore.set([config.kvstoreKeyPrefix, key].join(''), {
url: url
}, function(err, result) {
if (err) {
throw new Error(err);
}
callback(null, key);
});
});
} catch (e) {
callback(e.toString());
}
},
/**
* @function shorten
* @description A drop-in replacement for bit.ly's "shorten"
* method with no guarantees it works correctly. =)
* @param {string} url The URL we want to shorten
* @param {Function} callback callback(err,res):
* res = {
* data: {
* url: $short_url
* }
* }
*/
shorten: function(url, callback) {
/************
* Guards
***********/
if (!config.glgShortUrl) {
var _error = [
"You must specify environment var:",
" GLG_SHORT_URL"
].join('\n');
return callback(_error);
}
// Call our normal method but format the result in such a way
// that it would be compatible with the Bitly node module
try {
api.getKeyByUrl(url, function(err, key) {
if (err) {
throw new Error(err);
}
var _dataObject = {};
_dataObject.data = {};
_dataObject.data.url = [config.glgShortUrl, '/', key].join('');
return callback(null, _dataObject);
});
} catch (e) {
return callback(e.toString());
}
}
};
// Export our api
module.exports = api;
/********************************************************************
* Tests or Run Examples
********************************************************************/
if (module.parent === null) {
log.enable.tests = true;
api.getKeyByUrl("https://glg.it", function(err, key) {
if (err) {
return log.fail("getKeyByUrl:", err);
}
api.getUrlByKey(key, function(err, url) {
if (err) {
return log.fail("getUrlByKey:", err);
}
return log.success("getUrlByKey:", url);
});
return log.success("getKeyByUrl:", key);
});
api.shorten("https://www.google.com", function(err, res) {
if (err) {
return log.fail("shorten:", err);
}
log.success("shorten:", res.data.url);
});
}